Feature/docs (#67)

This commit is contained in:
Daniel Lautzenheiser
2022-11-04 17:41:12 -04:00
committed by GitHub
parent 7a1ec55a5c
commit 81ca566b5e
152 changed files with 1862 additions and 3832 deletions

View File

@ -68,7 +68,6 @@ export const DRAFT_CREATE_EMPTY = 'DRAFT_CREATE_EMPTY';
export const DRAFT_DISCARD = 'DRAFT_DISCARD';
export const DRAFT_CHANGE_FIELD = 'DRAFT_CHANGE_FIELD';
export const DRAFT_VALIDATION_ERRORS = 'DRAFT_VALIDATION_ERRORS';
export const DRAFT_CLEAR_ERRORS = 'DRAFT_CLEAR_ERRORS';
export const DRAFT_LOCAL_BACKUP_RETRIEVED = 'DRAFT_LOCAL_BACKUP_RETRIEVED';
export const DRAFT_LOCAL_BACKUP_DELETE = 'DRAFT_LOCAL_BACKUP_DELETE';
export const DRAFT_CREATE_FROM_LOCAL_BACKUP = 'DRAFT_CREATE_FROM_LOCAL_BACKUP';
@ -279,7 +278,7 @@ async function getAllEntries(state: RootState, collection: Collection) {
const backend = currentBackend(configState.config);
const integration = selectIntegration(state, collection.name, 'listEntries');
const provider = integration
? getSearchIntegrationProvider(state.integrations, backend.getToken, integration)
? getSearchIntegrationProvider(state.integrations, integration)
: backend;
if (!provider) {
@ -497,10 +496,6 @@ export function changeDraftFieldValidation(path: string, errors: FieldError[]) {
} as const;
}
export function clearFieldErrors() {
return { type: DRAFT_CLEAR_ERRORS } as const;
}
export function localBackupRetrieved(entry: Entry) {
return {
type: DRAFT_LOCAL_BACKUP_RETRIEVED,
@ -688,7 +683,7 @@ export function loadEntries(collection: Collection, page = 0) {
const backend = currentBackend(configState.config);
const integration = selectIntegration(state, collection.name, 'listEntries');
const provider = integration
? getSearchIntegrationProvider(state.integrations, backend.getToken, integration)
? getSearchIntegrationProvider(state.integrations, integration)
: backend;
if (!provider) {
@ -1142,7 +1137,6 @@ export type EntriesAction = ReturnType<
| typeof discardDraft
| typeof changeDraftField
| typeof changeDraftFieldValidation
| typeof clearFieldErrors
| typeof localBackupRetrieved
| typeof loadLocalBackup
| typeof deleteDraftLocalBackup

View File

@ -1,9 +1,8 @@
import { currentBackend } from '../backend';
import confirm from '../components/UI/Confirm';
import { getMediaIntegrationProvider } from '../integrations';
import { sanitizeSlug } from '../lib/urlHelper';
import { basename, getBlobSHA } from '../lib/util';
import { selectIntegration } from '../reducers';
import { selectMediaFilePath, selectMediaFilePublicPath } from '../lib/util/media.util';
import { selectEditingDraft } from '../reducers/entries';
import { selectMediaDisplayURL, selectMediaFiles } from '../reducers/mediaLibrary';
import { addSnackbar } from '../store/slices/snackbars';
@ -11,13 +10,12 @@ import { createAssetProxy } from '../valueObjects/AssetProxy';
import { addDraftEntryMediaFile, removeDraftEntryMediaFile } from './entries';
import { addAsset, removeAsset } from './media';
import { waitUntilWithTimeout } from './waitUntil';
import { selectMediaFilePath, selectMediaFilePublicPath } from '../lib/util/media.util';
import type { AnyAction } from 'redux';
import type { ThunkDispatch } from 'redux-thunk';
import type {
Field,
DisplayURLState,
Field,
ImplementationMediaFile,
MediaFile,
MediaLibraryInstance,
@ -78,7 +76,6 @@ export function openMediaLibrary(
payload: {
controlID?: string;
forImage?: boolean;
privateUpload?: boolean;
value?: string | string[];
allowMultiple?: boolean;
replaceIndex?: number;
@ -134,10 +131,8 @@ export function removeInsertedMedia(controlID: string) {
return { type: MEDIA_REMOVE_INSERTED, payload: { controlID } } as const;
}
export function loadMedia(
opts: { delay?: number; query?: string; page?: number; privateUpload?: boolean } = {},
) {
const { delay = 0, query = '', page = 1, privateUpload = false } = opts;
export function loadMedia(opts: { delay?: number; query?: string; page?: number } = {}) {
const { delay = 0, page = 1 } = opts;
return async (dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => {
const state = getState();
const config = state.config.config;
@ -146,32 +141,6 @@ export function loadMedia(
}
const backend = currentBackend(config);
const integration = selectIntegration(state, null, 'assetStore');
if (integration) {
const provider = getMediaIntegrationProvider(
state.integrations,
backend.getToken,
integration,
);
if (!provider) {
throw new Error('Provider not found');
}
dispatch(mediaLoading(page));
try {
const files = await provider.retrieve(query, page, privateUpload);
const mediaLoadedOpts = {
page,
canPaginate: true,
dynamicSearch: true,
dynamicSearchQuery: query,
privateUpload,
};
return dispatch(mediaLoaded(files, mediaLoadedOpts));
} catch (error) {
return dispatch(mediaLoadFailed({ privateUpload }));
}
}
dispatch(mediaLoading(page));
function loadFunction() {
@ -225,7 +194,7 @@ function createMediaFileFromAsset({
}
export function persistMedia(file: File, opts: MediaOptions = {}) {
const { privateUpload, field } = opts;
const { field } = opts;
return async (dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => {
const state = getState();
const config = state.config.config;
@ -234,7 +203,6 @@ export function persistMedia(file: File, opts: MediaOptions = {}) {
}
const backend = currentBackend(config);
const integration = selectIntegration(state, null, 'assetStore');
const files: MediaFile[] = selectMediaFiles(state, field);
const fileName = sanitizeSlug(file.name.toLowerCase(), config.slug);
const existingFile = files.find(existingFile => existingFile.name.toLowerCase() === fileName);
@ -247,7 +215,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}) {
* expect file names to be unique. If an asset store is in use, file names
* may not be unique, so we forego this check.
*/
if (!integration && existingFile) {
if (existingFile) {
if (
!(await confirm({
title: 'mediaLibrary.mediaLibrary.alreadyExistsTitle',
@ -260,60 +228,28 @@ export function persistMedia(file: File, opts: MediaOptions = {}) {
) {
return;
} else {
await dispatch(deleteMedia(existingFile, { privateUpload }));
await dispatch(deleteMedia(existingFile));
}
}
if (integration || !editingDraft) {
if (!editingDraft) {
dispatch(mediaPersisting());
}
try {
let assetProxy: AssetProxy;
if (integration) {
try {
const provider = getMediaIntegrationProvider(
state.integrations,
backend.getToken,
integration,
);
if (!provider) {
throw new Error('Provider not found');
}
const response = await provider.upload(file, privateUpload);
assetProxy = createAssetProxy({
url: response.asset.url,
path: response.asset.url,
});
} catch (error) {
assetProxy = createAssetProxy({
file,
path: fileName,
});
}
} else if (privateUpload) {
console.error('The Private Upload option is only available for Asset Store Integration')
throw new Error('The Private Upload option is only available for Asset Store Integration');
} else {
const entry = state.entryDraft.entry;
const collection = entry?.collection ? state.collections[entry.collection] : null;
const path = selectMediaFilePath(config, collection, entry, fileName, field);
assetProxy = createAssetProxy({
file,
path,
field,
});
}
const entry = state.entryDraft.entry;
const collection = entry?.collection ? state.collections[entry.collection] : null;
const path = selectMediaFilePath(config, collection, entry, fileName, field);
const assetProxy = createAssetProxy({
file,
path,
field,
});
dispatch(addAsset(assetProxy));
let mediaFile: ImplementationMediaFile;
if (integration) {
const id = await getBlobSHA(file);
// integration assets are persisted immediately, thus draft is false
mediaFile = createMediaFileFromAsset({ id, file, assetProxy, draft: false });
} else if (editingDraft) {
if (editingDraft) {
const id = await getBlobSHA(file);
mediaFile = createMediaFileFromAsset({
id,
@ -326,7 +262,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}) {
mediaFile = await backend.persistMedia(config, assetProxy);
}
return dispatch(mediaPersisted(mediaFile, { privateUpload }));
return dispatch(mediaPersisted(mediaFile));
} catch (error) {
console.error(error);
dispatch(
@ -340,13 +276,12 @@ export function persistMedia(file: File, opts: MediaOptions = {}) {
},
}),
);
return dispatch(mediaPersistFailed({ privateUpload }));
return dispatch(mediaPersistFailed());
}
};
}
export function deleteMedia(file: MediaFile, opts: MediaOptions = {}) {
const { privateUpload } = opts;
export function deleteMedia(file: MediaFile) {
return async (dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => {
const state = getState();
const config = state.config.config;
@ -355,42 +290,6 @@ export function deleteMedia(file: MediaFile, opts: MediaOptions = {}) {
}
const backend = currentBackend(config);
const integration = selectIntegration(state, null, 'assetStore');
if (integration) {
const provider = getMediaIntegrationProvider(
state.integrations,
backend.getToken,
integration,
);
if (!provider) {
throw new Error('Provider not found');
}
dispatch(mediaDeleting());
try {
await provider.delete(file.id);
return dispatch(mediaDeleted(file, { privateUpload }));
} catch (error: unknown) {
console.error(error);
if (error instanceof Error) {
dispatch(
addSnackbar({
type: 'error',
message: {
key: 'ui.toast.onFailToDeleteMedia',
options: {
details: error.message,
},
},
}),
);
}
return dispatch(mediaDeleteFailed({ privateUpload }));
}
}
try {
if (file.draft) {
@ -490,7 +389,6 @@ export function loadMediaDisplayURL(file: MediaFile) {
function mediaLibraryOpened(payload: {
controlID?: string;
forImage?: boolean;
privateUpload?: boolean;
value?: string | string[];
replaceIndex?: number;
allowMultiple?: boolean;
@ -516,7 +414,6 @@ export function mediaLoading(page: number) {
}
export interface MediaOptions {
privateUpload?: boolean;
field?: Field;
page?: number;
canPaginate?: boolean;
@ -531,43 +428,38 @@ export function mediaLoaded(files: ImplementationMediaFile[], opts: MediaOptions
} as const;
}
export function mediaLoadFailed(opts: MediaOptions = {}) {
const { privateUpload } = opts;
return { type: MEDIA_LOAD_FAILURE, payload: { privateUpload } } as const;
export function mediaLoadFailed() {
return { type: MEDIA_LOAD_FAILURE } as const;
}
export function mediaPersisting() {
return { type: MEDIA_PERSIST_REQUEST } as const;
}
export function mediaPersisted(file: ImplementationMediaFile, opts: MediaOptions = {}) {
const { privateUpload } = opts;
export function mediaPersisted(file: ImplementationMediaFile) {
return {
type: MEDIA_PERSIST_SUCCESS,
payload: { file, privateUpload },
payload: { file },
} as const;
}
export function mediaPersistFailed(opts: MediaOptions = {}) {
const { privateUpload } = opts;
return { type: MEDIA_PERSIST_FAILURE, payload: { privateUpload } } as const;
export function mediaPersistFailed() {
return { type: MEDIA_PERSIST_FAILURE } as const;
}
export function mediaDeleting() {
return { type: MEDIA_DELETE_REQUEST } as const;
}
export function mediaDeleted(file: MediaFile, opts: MediaOptions = {}) {
const { privateUpload } = opts;
export function mediaDeleted(file: MediaFile) {
return {
type: MEDIA_DELETE_SUCCESS,
payload: { file, privateUpload },
payload: { file },
} as const;
}
export function mediaDeleteFailed(opts: MediaOptions = {}) {
const { privateUpload } = opts;
return { type: MEDIA_DELETE_FAILURE, payload: { privateUpload } } as const;
export function mediaDeleteFailed() {
return { type: MEDIA_DELETE_FAILURE } as const;
}
export function mediaDisplayURLRequest(key: string) {

View File

@ -122,7 +122,7 @@ export function searchEntries(searchTerm: string, searchCollections: string[], p
dispatch(searchingEntries(searchTerm, allCollections, page));
const searchPromise = integration
? getSearchIntegrationProvider(state.integrations, backend.getToken, integration)?.search(
? getSearchIntegrationProvider(state.integrations, integration)?.search(
collections,
searchTerm,
page,
@ -179,7 +179,7 @@ export function query(
}
const queryPromise = integration
? getSearchIntegrationProvider(state.integrations, backend.getToken, integration)?.searchBy(
? getSearchIntegrationProvider(state.integrations, integration)?.searchBy(
JSON.stringify(searchFields.map(f => `data.${f}`)),
collectionName,
searchTerm,

View File

@ -41,9 +41,8 @@ import {
} from './lib/util/collection.util';
import { selectMediaFilePath } from './lib/util/media.util';
import { set } from './lib/util/object.util';
import { selectIntegration } from './reducers/integrations';
import { createEntry } from './valueObjects/Entry';
import { dateParsers, expandPath, extractTemplateVars } from './lib/widgets/stringTemplate';
import { createEntry } from './valueObjects/Entry';
import type {
BackendClass,
@ -778,9 +777,8 @@ export class Backend {
throw new Error('Config not loaded');
}
const integration = selectIntegration(state.integrations, null, 'assetStore');
const mediaFolders = selectMediaFolders(configState.config, collection, entry);
if (mediaFolders.length > 0 && !integration) {
if (mediaFolders.length > 0) {
const files = await Promise.all(
mediaFolders.map(folder => this.implementation.getMedia(folder)),
);

View File

@ -1,425 +0,0 @@
import { Base64 } from 'js-base64';
import partial from 'lodash/partial';
import result from 'lodash/result';
import trim from 'lodash/trim';
import trimStart from 'lodash/trimStart';
import { basename, dirname } from 'path';
import {
APIError,
localForage,
readFile,
readFileMetadata,
requestWithBackoff,
responseParser,
unsentRequest,
} from '../../lib/util';
import type { DataFile, PersistOptions } from '../../interface';
import type { ApiRequest } from '../../lib/util';
import type { ApiRequestObject } from '../../lib/util/API';
import type AssetProxy from '../../valueObjects/AssetProxy';
export const API_NAME = 'Azure DevOps';
const API_VERSION = 'api-version';
type AzureUser = {
coreAttributes?: {
Avatar?: { value?: { value?: string } };
DisplayName?: { value?: string };
EmailAddress?: { value?: string };
};
};
type AzureGitItem = {
objectId: string;
gitObjectType: AzureObjectType;
path: string;
};
// This does not match Azure documentation, but it is what comes back from some calls
// PullRequest as an example is documented as returning PullRequest[], but it actually
// returns that inside of this value prop in the json
interface AzureArray<T> {
value: T[];
}
enum AzureCommitChangeType {
ADD = 'add',
DELETE = 'delete',
RENAME = 'rename',
EDIT = 'edit',
}
enum AzureItemContentType {
BASE64 = 'base64encoded',
}
enum AzureObjectType {
BLOB = 'blob',
TREE = 'tree',
}
type AzureRef = {
name: string;
objectId: string;
};
type AzureCommit = {
author: {
date: string;
email: string;
name: string;
};
};
function getChangeItem(item: AzureCommitItem) {
switch (item.action) {
case AzureCommitChangeType.ADD:
return {
changeType: AzureCommitChangeType.ADD,
item: { path: item.path },
newContent: {
content: item.base64Content,
contentType: AzureItemContentType.BASE64,
},
};
case AzureCommitChangeType.EDIT:
return {
changeType: AzureCommitChangeType.EDIT,
item: { path: item.path },
newContent: {
content: item.base64Content,
contentType: AzureItemContentType.BASE64,
},
};
case AzureCommitChangeType.DELETE:
return {
changeType: AzureCommitChangeType.DELETE,
item: { path: item.path },
};
case AzureCommitChangeType.RENAME:
return {
changeType: AzureCommitChangeType.RENAME,
item: { path: item.path },
sourceServerItem: item.oldPath,
};
default:
return {};
}
}
type AzureCommitItem = {
action: AzureCommitChangeType;
base64Content?: string;
text?: string;
path: string;
oldPath?: string;
};
interface AzureApiConfig {
apiRoot: string;
repo: { org: string; project: string; repoName: string };
branch: string;
apiVersion: string;
}
export default class API {
apiVersion: string;
token: string;
branch: string;
endpointUrl: string;
constructor(config: AzureApiConfig, token: string) {
const { repo } = config;
const apiRoot = trim(config.apiRoot, '/');
this.endpointUrl = `${apiRoot}/${repo.org}/${repo.project}/_apis/git/repositories/${repo.repoName}`;
this.token = token;
this.branch = config.branch;
this.apiVersion = config.apiVersion;
}
withHeaders = (req: ApiRequest) => {
const withHeaders = unsentRequest.withHeaders(
{
Authorization: `Bearer ${this.token}`,
'Content-Type': 'application/json; charset=utf-8',
},
req,
);
return withHeaders;
};
withAzureFeatures = (req: ApiRequestObject) => {
if (API_VERSION in (req.params ?? {})) {
return req;
}
const withParams = unsentRequest.withParams(
{
[API_VERSION]: `${this.apiVersion}`,
},
req,
);
return withParams;
};
buildRequest = (req: ApiRequest) => {
const withHeaders = this.withHeaders(req);
const withAzureFeatures = this.withAzureFeatures(withHeaders);
if ('cache' in withAzureFeatures) {
return withAzureFeatures;
} else {
const withNoCache = unsentRequest.withNoCache(withAzureFeatures);
return withNoCache;
}
};
request = (req: ApiRequest): Promise<Response> => {
try {
return requestWithBackoff(this, req);
} catch (error: unknown) {
if (error instanceof Error) {
throw new APIError(error.message, null, API_NAME);
}
throw new APIError('Unknown api error', null, API_NAME);
}
};
responseToJSON = responseParser({ format: 'json', apiName: API_NAME });
responseToBlob = responseParser({ format: 'blob', apiName: API_NAME });
responseToText = responseParser({ format: 'text', apiName: API_NAME });
requestJSON = <T>(req: ApiRequest) => this.request(req).then(this.responseToJSON) as Promise<T>;
requestText = (req: ApiRequest) => this.request(req).then(this.responseToText) as Promise<string>;
toBase64 = (str: string) => Promise.resolve(Base64.encode(str));
fromBase64 = (str: string) => Base64.decode(str);
branchToRef = (branch: string): string => `refs/heads/${branch}`;
refToBranch = (ref: string): string => ref.slice('refs/heads/'.length);
user = async () => {
const result = await this.requestJSON<AzureUser>({
url: 'https://app.vssps.visualstudio.com/_apis/profile/profiles/me',
params: { [API_VERSION]: '6.1-preview.2' },
});
const name = result.coreAttributes?.DisplayName?.value;
const email = result.coreAttributes?.EmailAddress?.value;
const url = result.coreAttributes?.Avatar?.value?.value;
const user = {
name: name || email || '',
avatar_url: `data:image/png;base64,${url}`,
email,
};
return user;
};
async readFileMetadata(
path: string,
sha: string | null | undefined,
{ branch = this.branch } = {},
) {
const fetchFileMetadata = async () => {
try {
const { value } = await this.requestJSON<AzureArray<AzureCommit>>({
url: `${this.endpointUrl}/commits/`,
params: {
'searchCriteria.itemPath': path,
'searchCriteria.itemVersion.version': branch,
'searchCriteria.$top': '1',
},
});
const [commit] = value;
return {
author: commit.author.name || commit.author.email,
updatedOn: commit.author.date,
};
} catch (error) {
return { author: '', updatedOn: '' };
}
};
const fileMetadata = await readFileMetadata(sha, fetchFileMetadata, localForage);
return fileMetadata;
}
readFile = (
path: string,
sha?: string | null,
{ parseText = true, branch = this.branch } = {},
) => {
const fetchContent = () => {
return this.request({
url: `${this.endpointUrl}/items/`,
params: { version: branch, path },
cache: 'no-store',
}).then<Blob | string>(parseText ? this.responseToText : this.responseToBlob);
};
return readFile(sha, fetchContent, localForage, parseText);
};
listFiles = async (path: string, recursive: boolean, branch = this.branch) => {
try {
const { value: items } = await this.requestJSON<AzureArray<AzureGitItem>>({
url: `${this.endpointUrl}/items/`,
params: {
version: branch,
scopePath: path,
recursionLevel: recursive ? 'full' : 'oneLevel',
},
});
const files = items
.filter(item => item.gitObjectType === AzureObjectType.BLOB)
.map(file => ({
id: file.objectId,
path: trimStart(file.path, '/'),
name: basename(file.path),
}));
return files;
} catch (err: any) {
if (err && err.status === 404) {
console.info('This 404 was expected and handled appropriately.');
return [];
} else {
throw err;
}
}
};
async getRef(branch: string = this.branch) {
const { value: refs } = await this.requestJSON<AzureArray<AzureRef>>({
url: `${this.endpointUrl}/refs`,
params: {
$top: '1', // There's only one ref, so keep the payload small
filter: 'heads/' + branch,
},
});
return refs.find(b => b.name == this.branchToRef(branch))!;
}
async uploadAndCommit(
items: AzureCommitItem[],
comment: string,
branch: string,
newBranch: boolean,
) {
const ref = await this.getRef(newBranch ? this.branch : branch);
const refUpdate = [
{
name: this.branchToRef(branch),
oldObjectId: ref.objectId,
},
];
const changes = items.map(item => getChangeItem(item));
const commits = [{ comment, changes }];
const push = {
refUpdates: refUpdate,
commits,
};
return this.requestJSON({
url: `${this.endpointUrl}/pushes`,
method: 'POST',
body: JSON.stringify(push),
});
}
async getCommitItems(files: { path: string; newPath?: string }[], branch: string) {
const items = await Promise.all(
files.map(async file => {
const [base64Content, fileExists] = await Promise.all([
result(file, 'toBase64', partial(this.toBase64, (file as DataFile).raw)),
this.isFileExists(file.path, branch),
]);
const path = file.newPath || file.path;
const oldPath = file.path;
const renameOrEdit =
path !== oldPath ? AzureCommitChangeType.RENAME : AzureCommitChangeType.EDIT;
const action = fileExists ? renameOrEdit : AzureCommitChangeType.ADD;
return {
action,
base64Content,
path,
oldPath,
} as AzureCommitItem;
}),
);
// move children
for (const item of items.filter(i => i.oldPath && i.action === AzureCommitChangeType.RENAME)) {
const sourceDir = dirname(item.oldPath as string);
const destDir = dirname(item.path);
const children = await this.listFiles(sourceDir, true, branch);
children
.filter(file => file.path !== item.oldPath)
.forEach(file => {
items.push({
action: AzureCommitChangeType.RENAME,
path: file.path.replace(sourceDir, destDir),
oldPath: file.path,
});
});
}
return items;
}
async persistFiles(dataFiles: DataFile[], mediaFiles: AssetProxy[], options: PersistOptions) {
const files = [...dataFiles, ...mediaFiles];
const items = await this.getCommitItems(files, this.branch);
return this.uploadAndCommit(items, options.commitMessage, this.branch, true);
}
async deleteFiles(paths: string[], comment: string) {
const ref = await this.getRef(this.branch);
const refUpdate = {
name: ref.name,
oldObjectId: ref.objectId,
};
const changes = paths.map(path =>
getChangeItem({ action: AzureCommitChangeType.DELETE, path }),
);
const commits = [{ comment, changes }];
const push = {
refUpdates: [refUpdate],
commits,
};
return this.requestJSON({
url: `${this.endpointUrl}/pushes`,
method: 'POST',
body: JSON.stringify(push),
});
}
async isFileExists(path: string, branch: string) {
try {
await this.requestText({
url: `${this.endpointUrl}/items/`,
params: { version: branch, path },
cache: 'no-store',
});
return true;
} catch (error) {
if (error instanceof APIError && error.status === 404) {
return false;
}
throw error;
}
}
}

View File

@ -1,80 +0,0 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import alert from '../../components/UI/Alert';
import AuthenticationPage from '../../components/UI/AuthenticationPage';
import Icon from '../../components/UI/Icon';
import { ImplicitAuthenticator } from '../../lib/auth';
import type { MouseEvent } from 'react';
import type { AuthenticationPageProps, TranslatedProps } from '../../interface';
const AzureAuthenticationPage = ({
inProgress = false,
config,
clearHash,
onLogin,
t,
}: TranslatedProps<AuthenticationPageProps>) => {
const [loginError, setLoginError] = useState<string | null>(null);
const auth = useMemo(
() =>
new ImplicitAuthenticator({
base_url: `https://login.microsoftonline.com/${config.backend.tenant_id}`,
auth_endpoint: 'oauth2/authorize',
app_id: config.backend.app_id,
clearHash,
}),
[clearHash, config.backend.app_id, config.backend.tenant_id],
);
useEffect(() => {
// Complete implicit authentication if we were redirected back to from the provider.
auth.completeAuth((err, data) => {
if (err) {
alert({
title: 'auth.errors.authTitle',
body: { key: 'auth.errors.authBody', options: { details: err.toString() } },
});
return;
} else if (data) {
onLogin(data);
}
});
}, [auth, onLogin]);
const handleLogin = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
auth.authenticate(
{
scope: 'vso.code_full,user.read',
resource: '499b84ac-1321-427f-aa17-267ca6975798',
prompt: 'select_account',
},
(err, data) => {
if (err) {
setLoginError(err.toString());
} else if (data) {
onLogin(data);
}
},
);
},
[auth, onLogin],
);
return (
<AuthenticationPage
onLogin={handleLogin}
loginDisabled={inProgress}
loginErrorMessage={loginError}
logoUrl={config.logo_url}
icon={<Icon type="azure" />}
buttonContent={inProgress ? t('auth.loggingIn') : t('auth.loginWithAzure')}
t={t}
/>
);
};
export default AzureAuthenticationPage;

View File

@ -1,265 +0,0 @@
import trim from 'lodash/trim';
import trimStart from 'lodash/trimStart';
import semaphore from 'semaphore';
import { BackendClass } from '../../interface';
import {
asyncLock,
basename,
entriesByFiles,
entriesByFolder,
filterByExtension,
getBlobSHA,
getMediaAsBlob,
getMediaDisplayURL,
} from '../../lib/util';
import API, { API_NAME } from './API';
import AuthenticationPage from './AuthenticationPage';
import type { Semaphore } from 'semaphore';
import type {
BackendEntry,
BackendInitializerOptions,
Config,
Credentials,
DisplayURL,
ImplementationEntry,
ImplementationFile,
ImplementationMediaFile,
PersistOptions,
User,
} from '../../interface';
import type { AsyncLock, Cursor } from '../../lib/util';
import type AssetProxy from '../../valueObjects/AssetProxy';
const MAX_CONCURRENT_DOWNLOADS = 10;
function parseAzureRepo(config: Config) {
const { repo } = config.backend;
if (typeof repo !== 'string') {
throw new Error('The Azure backend needs a "repo" in the backend configuration.');
}
const parts = repo.split('/');
if (parts.length !== 3) {
throw new Error('The Azure backend must be in a the format of {org}/{project}/{repo}');
}
const [org, project, repoName] = parts;
return {
org,
project,
repoName,
};
}
export default class Azure extends BackendClass {
lock: AsyncLock;
api?: API;
options: BackendInitializerOptions;
repo: {
org: string;
project: string;
repoName: string;
};
branch: string;
apiRoot: string;
apiVersion: string;
token: string | null;
mediaFolder: string;
_mediaDisplayURLSem?: Semaphore;
constructor(config: Config, options: BackendInitializerOptions) {
super(config, options);
this.options = {
...options,
};
this.repo = parseAzureRepo(config);
this.branch = config.backend.branch || 'main';
this.apiRoot = config.backend.api_root || 'https://dev.azure.com';
this.apiVersion = config.backend.api_version || '6.1-preview';
this.token = '';
this.mediaFolder = trim(config.media_folder, '/');
this.lock = asyncLock();
}
isGitBackend() {
return true;
}
async status(): Promise<{
auth: { status: boolean };
api: { status: boolean; statusPage: string };
}> {
const auth =
(await this.api!.user()
.then(user => !!user)
.catch(e => {
console.warn('Failed getting Azure user', e);
return false;
})) || false;
return { auth: { status: auth }, api: { status: true, statusPage: '' } };
}
authComponent() {
return AuthenticationPage;
}
restoreUser(user: User) {
return this.authenticate(user);
}
async authenticate(state: Credentials) {
this.token = state.token as string;
this.api = new API(
{
apiRoot: this.apiRoot,
apiVersion: this.apiVersion,
repo: this.repo,
branch: this.branch,
},
this.token,
);
const user = await this.api.user();
return { token: state.token as string, ...user };
}
/**
* Log the user out by forgetting their access token.
* TODO: *Actual* logout by redirecting to:
* https://login.microsoftonline.com/{tenantId}/oauth2/logout?client_id={clientId}&post_logout_redirect_uri={baseUrl}
*/
logout() {
this.token = null;
return;
}
getToken() {
return Promise.resolve(this.token);
}
async entriesByFolder(folder: string, extension: string, depth: number) {
const listFiles = async () => {
const files = await this.api!.listFiles(folder, depth > 1);
const filtered = files.filter(file => filterByExtension({ path: file.path }, extension));
return filtered.map(file => ({
id: file.id,
path: file.path,
}));
};
const entries = await entriesByFolder(
listFiles,
this.api!.readFile.bind(this.api!),
this.api!.readFileMetadata.bind(this.api),
API_NAME,
);
return entries;
}
entriesByFiles(files: ImplementationFile[]) {
return entriesByFiles(
files,
this.api!.readFile.bind(this.api!),
this.api!.readFileMetadata.bind(this.api),
API_NAME,
);
}
async getEntry(path: string) {
const data = (await this.api!.readFile(path)) as string;
return {
file: { path },
data,
};
}
async getMedia() {
const files = await this.api!.listFiles(this.mediaFolder, false);
const mediaFiles = await Promise.all(
files.map(async ({ id, path, name }) => {
const blobUrl = await this.getMediaDisplayURL({ id, path });
return { id, name, displayURL: blobUrl, path };
}),
);
return mediaFiles;
}
getMediaDisplayURL(displayURL: DisplayURL) {
this._mediaDisplayURLSem = this._mediaDisplayURLSem || semaphore(MAX_CONCURRENT_DOWNLOADS);
return getMediaDisplayURL(
displayURL,
this.api!.readFile.bind(this.api!),
this._mediaDisplayURLSem,
);
}
async getMediaFile(path: string) {
const name = basename(path);
const blob = await getMediaAsBlob(path, null, this.api!.readFile.bind(this.api!));
const fileObj = new File([blob], name);
const url = URL.createObjectURL(fileObj);
const id = await getBlobSHA(blob);
return {
id,
displayURL: url,
path,
name,
size: fileObj.size,
file: fileObj,
url,
};
}
async persistEntry(entry: BackendEntry, options: PersistOptions): Promise<void> {
const mediaFiles: AssetProxy[] = entry.assets;
await this.api!.persistFiles(entry.dataFiles, mediaFiles, options);
}
async persistMedia(
mediaFile: AssetProxy,
options: PersistOptions,
): Promise<ImplementationMediaFile> {
const fileObj = mediaFile.fileObj as File;
const [id] = await Promise.all([
getBlobSHA(fileObj),
this.api!.persistFiles([], [mediaFile], options),
]);
const { path } = mediaFile;
const url = URL.createObjectURL(fileObj);
return {
displayURL: url,
path: trimStart(path, '/'),
name: fileObj!.name,
size: fileObj!.size,
file: fileObj,
url,
id: id as string,
};
}
async deleteFiles(paths: string[], commitMessage: string) {
await this.api!.deleteFiles(paths, commitMessage);
}
traverseCursor(): Promise<{ entries: ImplementationEntry[]; cursor: Cursor }> {
throw new Error('Not supported');
}
allEntriesByFolder(
_folder: string,
_extension: string,
_depth: number,
): Promise<ImplementationEntry[]> {
throw new Error('Not supported');
}
}

View File

@ -1,3 +0,0 @@
export { default as AzureBackend } from './implementation';
export { default as API } from './API';
export { default as AuthenticationPage } from './AuthenticationPage';

View File

@ -1,4 +1,3 @@
export { AzureBackend } from './azure';
export { BitbucketBackend } from './bitbucket';
export { GitGatewayBackend } from './git-gateway';
export { GitHubBackend } from './github';

View File

@ -16,6 +16,7 @@ import { history } from '../../routing/history';
import CollectionRoute from '../Collection/CollectionRoute';
import EditorRoute from '../Editor/EditorRoute';
import MediaLibrary from '../MediaLibrary/MediaLibrary';
import Page from '../page/Page';
import Snackbars from '../snackbar/Snackbars';
import { Alert } from '../UI/Alert';
import { Confirm } from '../UI/Confirm';
@ -212,6 +213,7 @@ const App = ({
element={<CollectionRoute collections={collections} isSearchResults />}
/>
<Route path="/edit/:name/:entryName" element={<EditEntityRedirect />} />
<Route path="/page/:id" element={<Page />} />
<Route element={<NotFoundPage />} />
</Routes>
{useMediaLibrary ? <MediaLibrary /> : null}

View File

@ -57,9 +57,9 @@ const Sidebar = ({
const iconName = collection.icon;
let icon: ReactNode = <ArticleIcon />;
if (iconName) {
const storedIcon = getIcon(iconName);
if (storedIcon) {
icon = storedIcon();
const StoredIcon = getIcon(iconName);
if (StoredIcon) {
icon = <StoredIcon />;
}
}
@ -99,9 +99,9 @@ const Sidebar = ({
Object.values(additionalLinks).map(({ id, title, data, options: { iconName } = {} }) => {
let icon: ReactNode = <ArticleIcon />;
if (iconName) {
const storedIcon = getIcon(iconName);
if (storedIcon) {
icon = storedIcon();
const StoredIcon = getIcon(iconName);
if (StoredIcon) {
icon = <StoredIcon />;
}
}

View File

@ -7,8 +7,6 @@ import { connect } from 'react-redux';
import {
changeDraftField as changeDraftFieldAction,
changeDraftFieldValidation as changeDraftFieldValidationAction,
clearFieldErrors as clearFieldErrorsAction,
tryLoadEntry,
} from '../../../actions/entries';
import { getAsset as getAssetAction } from '../../../actions/media';
import {
@ -17,7 +15,7 @@ import {
removeInsertedMedia as removeInsertedMediaAction,
removeMediaControl as removeMediaControlAction,
} from '../../../actions/mediaLibrary';
import { clearSearch as clearSearchAction, query as queryAction } from '../../../actions/search';
import { query as queryAction } from '../../../actions/search';
import { borders, colors, lengths, transitions } from '../../../components/UI/styles';
import { transientOptions } from '../../../lib';
import { resolveWidget } from '../../../lib/registry';
@ -37,7 +35,6 @@ import type {
Widget,
} from '../../../interface';
import type { RootState } from '../../../store';
import type { EditorControlPaneProps } from './EditorControlPane';
/**
* This is a necessary bridge as we are still passing classnames to widgets
@ -133,9 +130,7 @@ const ControlHint = styled(
const EditorControl = ({
className,
clearFieldErrors,
clearMediaControl,
clearSearch,
collection,
config: configState,
entry,
@ -144,11 +139,9 @@ const EditorControl = ({
submitted,
getAsset,
isDisabled,
isFetching,
isFieldDuplicate,
isFieldHidden,
isHidden = false,
loadEntry,
locale,
mediaPaths,
changeDraftFieldValidation,
@ -210,8 +203,6 @@ const EditorControl = ({
<>
{React.createElement(widget.control, {
key: `field_${path}`,
clearFieldErrors,
clearSearch,
collection,
config,
entry,
@ -220,11 +211,9 @@ const EditorControl = ({
submitted,
getAsset: handleGetAsset,
isDisabled: isDisabled ?? false,
isFetching,
isFieldDuplicate,
isFieldHidden,
label: getFieldLabel(field, t),
loadEntry,
locale,
mediaPaths,
onChange: handleChangeDraftField,
@ -264,7 +253,6 @@ const EditorControl = ({
interface EditorControlOwnProps {
className?: string;
clearFieldErrors: EditorControlPaneProps['clearFieldErrors'];
field: Field;
fieldsErrors: FieldsErrors;
submitted: boolean;
@ -285,25 +273,13 @@ function mapStateToProps(state: RootState, ownProps: EditorControlOwnProps) {
const collection = entryDraft.entry ? collections[entryDraft.entry.collection] : null;
const isLoadingAsset = selectIsLoadingAsset(state.medias);
async function loadEntry(collectionName: string, slug: string) {
const targetCollection = collections[collectionName];
if (targetCollection) {
const loadedEntry = await tryLoadEntry(state, targetCollection, slug);
return loadedEntry;
} else {
throw new Error(`Can't find collection '${collectionName}'`);
}
}
return {
...ownProps,
mediaPaths: state.mediaLibrary.controlMedia,
isFetching: state.search.isFetching,
config: state.config,
entry,
collection,
isLoadingAsset,
loadEntry,
};
}
@ -315,8 +291,6 @@ const mapDispatchToProps = {
removeMediaControl: removeMediaControlAction,
removeInsertedMedia: removeInsertedMediaAction,
query: queryAction,
clearSearch: clearSearchAction,
clearFieldErrors: clearFieldErrorsAction,
getAsset: getAssetAction,
};

View File

@ -6,10 +6,7 @@ import get from 'lodash/get';
import React, { useCallback, useMemo } from 'react';
import { connect } from 'react-redux';
import {
changeDraftField as changeDraftFieldAction,
clearFieldErrors as clearFieldErrorsAction,
} from '../../../actions/entries';
import { changeDraftField as changeDraftFieldAction } from '../../../actions/entries';
import confirm from '../../../components/UI/Confirm';
import {
getI18nInfo,
@ -115,7 +112,6 @@ const EditorControlPane = ({
changeDraftField,
locale,
onLocaleChange,
clearFieldErrors,
t,
}: TranslatedProps<EditorControlPaneProps>) => {
const i18n = useMemo(() => {
@ -211,7 +207,6 @@ const EditorControlPane = ({
isFieldDuplicate={field => isFieldDuplicate(field, locale, i18n?.defaultLocale)}
isFieldHidden={field => isFieldHidden(field, locale, i18n?.defaultLocale)}
locale={locale}
clearFieldErrors={clearFieldErrors}
parentPath=""
i18n={i18n}
/>
@ -239,7 +234,6 @@ function mapStateToProps(_state: RootState, ownProps: EditorControlPaneOwnProps)
const mapDispatchToProps = {
changeDraftField: changeDraftFieldAction,
clearFieldErrors: clearFieldErrorsAction,
};
const connector = connect(mapStateToProps, mapDispatchToProps);

View File

@ -212,16 +212,11 @@ function getWidget(
config={config}
collection={collection}
value={
value &&
!widget.allowMapValue &&
typeof value === 'object' &&
!isJsxElement(value) &&
!isReactFragment(value)
value && typeof value === 'object' && !isJsxElement(value) && !isReactFragment(value)
? (value as Record<string, unknown>)[field.name]
: value
}
entry={entry}
resolveWidget={resolveWidget}
/>
);
}

View File

@ -1,35 +1,21 @@
import { styled } from '@mui/material/styles';
import React from 'react';
import { colors } from '../../components/UI/styles';
import { transientOptions } from '../../lib';
interface EmptyMessageContainerProps {
$isPrivate: boolean;
}
const EmptyMessageContainer = styled(
'div',
transientOptions,
)<EmptyMessageContainerProps>(
({ $isPrivate }) => `
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
${$isPrivate ? `color: ${colors.textFieldBorder};` : ''}
`,
);
const EmptyMessageContainer = styled('div')`
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
`;
interface EmptyMessageProps {
content: string;
isPrivate?: boolean;
}
const EmptyMessage = ({ content, isPrivate = false }: EmptyMessageProps) => {
const EmptyMessage = ({ content }: EmptyMessageProps) => {
return (
<EmptyMessageContainer $isPrivate={isPrivate}>
<EmptyMessageContainer>
<h1>{content}</h1>
</EmptyMessageContainer>
);

View File

@ -53,7 +53,6 @@ const MediaLibrary = ({
isDeleting,
hasNextPage,
isPaginating,
privateUpload = false,
config,
loadMedia,
dynamicSearchQuery,
@ -69,7 +68,6 @@ const MediaLibrary = ({
const [query, setQuery] = useState<string | undefined>(undefined);
const [prevIsVisible, setPrevIsVisible] = useState(false);
const [prevPrivateUpload, setPrevPrivateUpload] = useState(false);
useEffect(() => {
loadMedia();
@ -85,14 +83,10 @@ const MediaLibrary = ({
}, [isVisible, prevIsVisible]);
useEffect(() => {
setPrevPrivateUpload(privateUpload);
}, [privateUpload]);
useEffect(() => {
if (!prevIsVisible && isVisible && !prevPrivateUpload && privateUpload) {
loadMedia({ privateUpload });
if (!prevIsVisible && isVisible) {
loadMedia();
}
}, [isVisible, loadMedia, prevIsVisible, prevPrivateUpload, privateUpload]);
}, [isVisible, loadMedia, prevIsVisible]);
const loadDisplayURL = useCallback(
(file: MediaFile) => {
@ -208,7 +202,7 @@ const MediaLibrary = ({
},
});
} else {
await persistMedia(file, { privateUpload, field });
await persistMedia(file, { field });
setSelectedFile(files[0] as unknown as MediaFile);
@ -219,7 +213,7 @@ const MediaLibrary = ({
event.target.value = '';
}
},
[config.max_file_size, field, persistMedia, privateUpload],
[config.max_file_size, field, persistMedia],
);
/**
@ -251,11 +245,11 @@ const MediaLibrary = ({
}
const file = files.find(file => selectedFile?.key === file.key);
if (file) {
deleteMedia(file, { privateUpload }).then(() => {
deleteMedia(file).then(() => {
setSelectedFile(null);
});
}
}, [deleteMedia, files, privateUpload, selectedFile?.key]);
}, [deleteMedia, files, selectedFile?.key]);
/**
* Downloads the selected file.
@ -286,8 +280,8 @@ const MediaLibrary = ({
}, [displayURLs, selectedFile]);
const handleLoadMore = useCallback(() => {
loadMedia({ query: dynamicSearchQuery, page: (page ?? 0) + 1, privateUpload });
}, [dynamicSearchQuery, loadMedia, page, privateUpload]);
loadMedia({ query: dynamicSearchQuery, page: (page ?? 0) + 1 });
}, [dynamicSearchQuery, loadMedia, page]);
/**
* Executes media library search for implementations that support dynamic
@ -299,11 +293,11 @@ const MediaLibrary = ({
const handleSearchKeyDown = useCallback(
async (event: KeyboardEvent) => {
if (event.key === 'Enter' && dynamicSearch) {
await loadMedia({ query, privateUpload });
await loadMedia({ query });
scrollToTop();
}
},
[dynamicSearch, loadMedia, privateUpload, query],
[dynamicSearch, loadMedia, query],
);
/**
@ -344,7 +338,6 @@ const MediaLibrary = ({
isDeleting={isDeleting}
hasNextPage={hasNextPage}
isPaginating={isPaginating}
privateUpload={privateUpload}
query={query}
selectedFile={selectedFile}
handleFilter={filterImages}
@ -382,7 +375,6 @@ function mapStateToProps(state: RootState) {
isLoading: mediaLibrary.isLoading,
isPersisting: mediaLibrary.isPersisting,
isDeleting: mediaLibrary.isDeleting,
privateUpload: mediaLibrary.privateUpload,
config: mediaLibrary.config,
page: mediaLibrary.page,
hasNextPage: mediaLibrary.hasNextPage,

View File

@ -1,8 +1,8 @@
import { styled } from '@mui/material/styles';
import React, { useEffect, useMemo } from 'react';
import { transientOptions } from '../../lib';
import { borders, colors, effects, lengths, shadows } from '../../components/UI/styles';
import { transientOptions } from '../../lib';
import type { MediaLibraryDisplayURL } from '../../reducers/mediaLibrary';
@ -13,14 +13,13 @@ interface CardProps {
$height: string;
$margin: string;
$isSelected: boolean;
$isPrivate: boolean;
}
const Card = styled(
'div',
transientOptions,
)<CardProps>(
({ $width, $height, $margin, $isSelected, $isPrivate }) => `
({ $width, $height, $margin, $isSelected }) => `
width: ${$width};
height: ${$height};
margin: ${$margin};
@ -29,7 +28,6 @@ const Card = styled(
border-radius: ${lengths.borderRadius};
cursor: pointer;
overflow: hidden;
${$isPrivate ? `background-color: ${colors.textFieldBorder};` : ''}
&:focus {
outline: none;
@ -86,7 +84,6 @@ interface MediaLibraryCardProps {
width: string;
height: string;
margin: string;
isPrivate?: boolean;
type?: string;
isViewableImage: boolean;
loadDisplayURL: () => void;
@ -102,7 +99,6 @@ const MediaLibraryCard = ({
width,
height,
margin,
isPrivate = false,
type,
isViewableImage,
isDraft,
@ -122,7 +118,6 @@ const MediaLibraryCard = ({
$width={width}
$height={height}
$margin={margin}
$isPrivate={isPrivate}
onClick={onClick}
tabIndex={-1}
>

View File

@ -4,13 +4,11 @@ import AutoSizer from 'react-virtualized-auto-sizer';
import { Waypoint } from 'react-waypoint';
import { FixedSizeGrid as Grid } from 'react-window';
import { transientOptions } from '../../lib';
import { colors } from '../../components/UI/styles';
import MediaLibraryCard from './MediaLibraryCard';
import type { GridChildComponentProps } from 'react-window';
import type { MediaLibraryDisplayURL, MediaLibraryState } from '../../reducers/mediaLibrary';
import type { MediaFile } from '../../interface';
import type { MediaLibraryDisplayURL, MediaLibraryState } from '../../reducers/mediaLibrary';
export interface MediaLibraryCardItem {
displayURL?: MediaLibraryDisplayURL;
@ -37,7 +35,6 @@ export interface MediaLibraryCardGridProps {
cardHeight: string;
cardMargin: string;
loadDisplayURL: (asset: MediaFile) => void;
isPrivate?: boolean;
displayURLs: MediaLibraryState['displayURLs'];
}
@ -57,7 +54,6 @@ const CardWrapper = ({
cardDraftText,
cardWidth,
cardHeight,
isPrivate,
displayURLs,
loadDisplayURL,
columnCount,
@ -90,7 +86,6 @@ const CardWrapper = ({
width={cardWidth}
height={cardHeight}
margin={'0px'}
isPrivate={isPrivate}
displayURL={displayURLs[file.id] ?? (file.url ? { url: file.url } : {})}
loadDisplayURL={() => loadDisplayURL(file)}
type={file.type}
@ -168,19 +163,6 @@ const VirtualizedGrid = (props: MediaLibraryCardGridProps) => {
);
};
interface PaginatingMessageProps {
$isPrivate: boolean;
}
const PaginatingMessage = styled(
'h1',
transientOptions,
)<PaginatingMessageProps>(
({ $isPrivate }) => `
${$isPrivate ? `color: ${colors.textFieldBorder};` : ''}
`,
);
const PaginatedGrid = ({
setScrollContainerRef,
mediaItems,
@ -190,7 +172,6 @@ const PaginatedGrid = ({
cardWidth,
cardHeight,
cardMargin,
isPrivate = false,
displayURLs,
loadDisplayURL,
canLoadMore,
@ -212,7 +193,6 @@ const PaginatedGrid = ({
width={cardWidth}
height={cardHeight}
margin={cardMargin}
isPrivate={isPrivate}
displayURL={displayURLs[file.id] ?? (file.url ? { url: file.url } : {})}
loadDisplayURL={() => loadDisplayURL(file)}
type={file.type}
@ -221,9 +201,7 @@ const PaginatedGrid = ({
))}
{!canLoadMore ? null : <Waypoint onEnter={onLoadMore} />}
</CardGrid>
{!isPaginating ? null : (
<PaginatingMessage $isPrivate={isPrivate}>{paginatingMessage}</PaginatingMessage>
)}
{!isPaginating ? null : <h1>{paginatingMessage}</h1>}
</StyledCardGridContainer>
);
};

View File

@ -1,14 +1,12 @@
import { styled } from '@mui/material/styles';
import CloseIcon from '@mui/icons-material/Close';
import Dialog from '@mui/material/Dialog';
import DialogContent from '@mui/material/DialogContent';
import Fab from '@mui/material/Fab';
import { styled } from '@mui/material/styles';
import isEmpty from 'lodash/isEmpty';
import React from 'react';
import { translate } from 'react-polyglot';
import { transientOptions } from '../../lib';
import { colors, colorsRaw } from '../../components/UI/styles';
import EmptyMessage from './EmptyMessage';
import MediaLibraryCardGrid from './MediaLibraryCardGrid';
import MediaLibraryTop from './MediaLibraryTop';
@ -37,60 +35,41 @@ const cardMargin = `10px`;
*/
const cardOutsideWidth = `300px`;
interface StyledModalProps {
$isPrivate: boolean;
}
const StyledModal = styled(Dialog)`
.MuiDialog-paper {
display: flex;
flex-direction: column;
overflow: visible;
height: 80%;
width: calc(${cardOutsideWidth} + 20px);
max-width: calc(${cardOutsideWidth} + 20px);
const StyledModal = styled(
Dialog,
transientOptions,
)<StyledModalProps>(
({ $isPrivate }) => `
.MuiDialog-paper {
display: flex;
flex-direction: column;
overflow: visible;
height: 80%;
width: calc(${cardOutsideWidth} + 20px);
max-width: calc(${cardOutsideWidth} + 20px);
${$isPrivate ? `background-color: ${colorsRaw.grayDark};` : ''}
@media (min-width: 800px) {
width: calc(${cardOutsideWidth} * 2 + 20px);
max-width: calc(${cardOutsideWidth} * 2 + 20px);
}
@media (min-width: 1120px) {
width: calc(${cardOutsideWidth} * 3 + 20px);
max-width: calc(${cardOutsideWidth} * 3 + 20px);
}
@media (min-width: 1440px) {
width: calc(${cardOutsideWidth} * 4 + 20px);
max-width: calc(${cardOutsideWidth} * 4 + 20px);
}
@media (min-width: 1760px) {
width: calc(${cardOutsideWidth} * 5 + 20px);
max-width: calc(${cardOutsideWidth} * 5 + 20px);
}
@media (min-width: 2080px) {
width: calc(${cardOutsideWidth} * 6 + 20px);
max-width: calc(${cardOutsideWidth} * 6 + 20px);
}
h1 {
${$isPrivate && `color: ${colors.textFieldBorder};`}
}
button:disabled,
label[disabled] {
${$isPrivate ? 'background-color: rgba(217, 217, 217, 0.15);' : ''}
}
@media (min-width: 800px) {
width: calc(${cardOutsideWidth} * 2 + 20px);
max-width: calc(${cardOutsideWidth} * 2 + 20px);
}
`,
);
@media (min-width: 1120px) {
width: calc(${cardOutsideWidth} * 3 + 20px);
max-width: calc(${cardOutsideWidth} * 3 + 20px);
}
@media (min-width: 1440px) {
width: calc(${cardOutsideWidth} * 4 + 20px);
max-width: calc(${cardOutsideWidth} * 4 + 20px);
}
@media (min-width: 1760px) {
width: calc(${cardOutsideWidth} * 5 + 20px);
max-width: calc(${cardOutsideWidth} * 5 + 20px);
}
@media (min-width: 2080px) {
width: calc(${cardOutsideWidth} * 6 + 20px);
max-width: calc(${cardOutsideWidth} * 6 + 20px);
}
}
`;
interface MediaLibraryModalProps {
isVisible?: boolean;
@ -104,7 +83,6 @@ interface MediaLibraryModalProps {
isDeleting?: boolean;
hasNextPage?: boolean;
isPaginating?: boolean;
privateUpload?: boolean;
query?: string;
selectedFile?: MediaFile;
handleFilter: (files: MediaFile[]) => MediaFile[];
@ -136,7 +114,6 @@ const MediaLibraryModal = ({
isDeleting,
hasNextPage,
isPaginating,
privateUpload = false,
query,
selectedFile,
handleFilter,
@ -175,14 +152,13 @@ const MediaLibraryModal = ({
const hasSelection = hasMedia && !isEmpty(selectedFile);
return (
<StyledModal open={isVisible} onClose={handleClose} $isPrivate={privateUpload}>
<StyledModal open={isVisible} onClose={handleClose}>
<StyledFab color="default" aria-label="add" onClick={handleClose} size="small">
<CloseIcon />
</StyledFab>
<MediaLibraryTop
t={t}
onClose={handleClose}
privateUpload={privateUpload}
forImage={forImage}
onDownload={handleDownload}
onUpload={handlePersist}
@ -199,9 +175,7 @@ const MediaLibraryModal = ({
selectedFile={selectedFile}
/>
<DialogContent>
{!shouldShowEmptyMessage ? null : (
<EmptyMessage content={emptyMessage} isPrivate={privateUpload} />
)}
{!shouldShowEmptyMessage ? null : <EmptyMessage content={emptyMessage} />}
<MediaLibraryCardGrid
setScrollContainerRef={setScrollContainerRef}
mediaItems={tableData}
@ -215,7 +189,6 @@ const MediaLibraryModal = ({
cardWidth={cardWidth}
cardHeight={cardHeight}
cardMargin={cardMargin}
isPrivate={privateUpload}
loadDisplayURL={loadDisplayURL}
displayURLs={displayURLs}
/>

View File

@ -29,7 +29,6 @@ const StyledDialogTitle = styled(DialogTitle)`
export interface MediaLibraryTopProps {
onClose: () => void;
privateUpload?: boolean;
forImage?: boolean;
onDownload: () => void;
onUpload: (event: ChangeEvent<HTMLInputElement> | DragEvent) => void;
@ -62,7 +61,6 @@ const MediaLibraryTop = ({
isPersisting,
isDeleting,
selectedFile,
privateUpload,
}: TranslatedProps<MediaLibraryTopProps>) => {
const shouldShowButtonLoader = isPersisting || isDeleting;
const uploadEnabled = !shouldShowButtonLoader;
@ -80,11 +78,9 @@ const MediaLibraryTop = ({
return (
<LibraryTop>
<StyledDialogTitle>
{`${privateUpload ? t('mediaLibrary.mediaLibraryModal.private') : ''}${
forImage
? t('mediaLibrary.mediaLibraryModal.images')
: t('mediaLibrary.mediaLibraryModal.mediaAssets')
}`}
{forImage
? t('mediaLibrary.mediaLibraryModal.images')
: t('mediaLibrary.mediaLibraryModal.mediaAssets')}
<StyledButtonsContainer>
<CopyToClipBoardButton
disabled={!hasSelection}

View File

@ -1,11 +1,9 @@
import azure from './azure.svg';
import bitbucket from './bitbucket.svg';
import github from './github.svg';
import gitlab from './gitlab.svg';
import staticCms from './static-cms-logo.svg';
const images = {
azure,
bitbucket,
github,
gitlab,

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
viewBox="0 0 26 26"
height="26px"
width="26px">
<path
d="M 14.015456,4.2171913 7.0990002,9.9261887 1.5,19.751857 l 5.2698338,0.05491 z m 0.768596,1.2626133 -3.019209,8.0141944 5.599244,6.312735 L 6.6049864,21.727927 24.5,21.782809 Z" id="Shape" fill="#2684FF" fill-rule="nonzero" />
</svg>

Before

Width:  |  Height:  |  Size: 382 B

View File

@ -4,23 +4,19 @@ import { translate } from 'react-polyglot';
import { connect } from 'react-redux';
import { useParams } from 'react-router-dom';
import { lengths } from '../../components/UI/styles';
import { getAdditionalLink } from '../../lib/registry';
import MainView from '../App/MainView';
import Sidebar from '../Collection/Sidebar';
import type { ComponentType } from 'react';
import type { ConnectedProps } from 'react-redux';
import type { RootState } from '../../store';
const StylePage = styled('div')`
margin: ${lengths.pageMargin};
`;
const StyledPageContent = styled('div')`
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
`;
const Page = ({ collections, isSearchEnabled, searchTerm, filterTerm }: PageProps) => {
@ -51,7 +47,7 @@ const Page = ({ collections, isSearchEnabled, searchTerm, filterTerm }: PageProp
}, [Content]);
return (
<StylePage>
<MainView>
<Sidebar
collections={collections}
collection={false}
@ -60,7 +56,7 @@ const Page = ({ collections, isSearchEnabled, searchTerm, filterTerm }: PageProp
filterTerm={filterTerm}
/>
{pageContent}
</StylePage>
</MainView>
);
};

View File

@ -202,8 +202,6 @@ function getConfigSchema() {
label_singular: { type: 'string' },
description: { type: 'string' },
file: { type: 'string' },
preview_path: { type: 'string' },
preview_path_date_field: { type: 'string' },
editor: {
type: 'object',
properties: {
@ -220,8 +218,6 @@ function getConfigSchema() {
summary: { type: 'string' },
slug: { type: 'string' },
path: { type: 'string' },
preview_path: { type: 'string' },
preview_path_date_field: { type: 'string' },
create: { type: 'boolean' },
publish: { type: 'boolean' },
hide: { type: 'boolean' },

View File

@ -1,5 +1,4 @@
import {
AzureBackend,
BitbucketBackend,
GitGatewayBackend,
GitHubBackend,
@ -30,7 +29,6 @@ import {
export function addExtensions() {
// Register all the things
registerBackend('git-gateway', GitGatewayBackend);
registerBackend('azure', AzureBackend);
registerBackend('github', GitHubBackend);
registerBackend('gitlab', GitLabBackend);
registerBackend('bitbucket', BitbucketBackend);

View File

@ -18,6 +18,10 @@ export const CMS = {
if (typeof window !== 'undefined') {
window.CMS = CMS;
window.createClass = window.createClass || createReactClass;
window.useState = window.useState || React.useState;
window.useMemo = window.useMemo || React.useMemo;
window.useEffect = window.useEffect || React.useEffect;
window.useCallback = window.useCallback || React.useCallback;
window.h = window.h || React.createElement;
}

View File

@ -1,28 +1,19 @@
import Algolia from './providers/algolia/implementation';
import AssetStore from './providers/assetStore/implementation';
import type {
AlgoliaConfig,
AssetStoreConfig,
MediaIntegrationProvider,
SearchIntegrationProvider,
} from '../interface';
import type { AlgoliaConfig, SearchIntegrationProvider } from '../interface';
interface IntegrationsConfig {
providers?: {
algolia?: AlgoliaConfig;
assetStore?: AssetStoreConfig;
};
}
interface Integrations {
algolia?: Algolia;
assetStore?: AssetStore;
}
export function resolveIntegrations(
config: IntegrationsConfig | undefined,
getToken: () => Promise<string | null>,
) {
const integrationInstances: Integrations = {};
@ -30,10 +21,6 @@ export function resolveIntegrations(
integrationInstances.algolia = new Algolia(config.providers.algolia);
}
if (config?.providers?.['assetStore']) {
integrationInstances['assetStore'] = new AssetStore(config.providers['assetStore'], getToken);
}
return integrationInstances;
}
@ -42,32 +29,13 @@ export const getSearchIntegrationProvider = (function () {
return (
config: IntegrationsConfig | undefined,
getToken: () => Promise<string | null>,
provider: SearchIntegrationProvider,
) => {
if (provider in (config?.providers ?? {}))
if (integrations) {
return integrations[provider];
} else {
integrations = resolveIntegrations(config, getToken);
return integrations[provider];
}
};
})();
export const getMediaIntegrationProvider = (function () {
let integrations: Integrations = {};
return (
config: IntegrationsConfig | undefined,
getToken: () => Promise<string | null>,
provider: MediaIntegrationProvider,
) => {
if (provider in (config?.providers ?? {}))
if (integrations) {
return integrations[provider];
} else {
integrations = resolveIntegrations(config, getToken);
integrations = resolveIntegrations(config);
return integrations[provider];
}
};

View File

@ -1,168 +0,0 @@
import pickBy from 'lodash/pickBy';
import trimEnd from 'lodash/trimEnd';
import { unsentRequest } from '../../../lib/util';
import { addParams } from '../../../lib/urlHelper';
import type { AssetStoreConfig } from '../../../interface';
const { fetchWithTimeout: fetch } = unsentRequest;
interface AssetStoreResponse {
id: string;
name: string;
size: number;
url: string;
}
export default class AssetStore {
private shouldConfirmUpload: boolean;
private getSignedFormURL: string;
private getToken: () => Promise<string | null>;
constructor(config: AssetStoreConfig, getToken: () => Promise<string | null>) {
if (config.getSignedFormURL == null) {
throw 'The AssetStore integration needs the getSignedFormURL in the integration configuration.';
}
this.getToken = getToken;
this.shouldConfirmUpload = config.shouldConfirmUpload ?? false;
this.getSignedFormURL = trimEnd(config.getSignedFormURL, '/');
}
parseJsonResponse(response: Response) {
return response.json().then(json => {
if (!response.ok) {
return Promise.reject(json);
}
return json;
});
}
urlFor(path: string, optionParams: Record<string, string> = {}) {
const params = [];
for (const key in optionParams) {
params.push(`${key}=${encodeURIComponent(optionParams[key])}`);
}
if (params.length) {
path += `?${params.join('&')}`;
}
return path;
}
requestHeaders(headers = {}) {
return {
...headers,
};
}
confirmRequest(assetID: string) {
this.getToken().then(token =>
this.request(`${this.getSignedFormURL}/${assetID}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ state: 'uploaded' }),
}),
);
}
async request(
path: string,
options: RequestInit & {
params?: Record<string, string>;
},
) {
const headers = this.requestHeaders(options.headers || {});
const url = this.urlFor(path, options.params);
const response = await fetch(url, { ...options, headers });
const contentType = response.headers.get('Content-Type');
const isJson = contentType && contentType.match(/json/);
const content = isJson ? await this.parseJsonResponse(response) : response.text();
return content;
}
async retrieve(query: string, page: number, privateUpload: boolean) {
const params = pickBy(
{ search: query, page: `${page}`, filter: privateUpload ? 'private' : 'public' },
val => !!val,
);
const url = addParams(this.getSignedFormURL, params);
const token = await this.getToken();
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
};
const response: AssetStoreResponse[] = await this.request(url, { headers });
const files = response.map(({ id, name, size, url }) => {
return { id, name, size, displayURL: url, url, path: url };
});
return files;
}
delete(assetID: string) {
const url = `${this.getSignedFormURL}/${assetID}`;
return this.getToken().then(token =>
this.request(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
}),
);
}
async upload(file: File, privateUpload = false) {
const fileData: {
name: string;
size: number;
content_type?: string;
visibility?: 'private';
} = {
name: file.name,
size: file.size,
};
if (file.type) {
fileData.content_type = file.type;
}
if (privateUpload) {
fileData.visibility = 'private';
}
try {
const token = await this.getToken();
const response = await this.request(this.getSignedFormURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(fileData),
});
const formURL = response.form.url;
const formFields = response.form.fields;
const { id, name, size, url } = response.asset;
const formData = new FormData();
Object.keys(formFields).forEach(key => formData.append(key, formFields[key]));
formData.append('file', file, file.name);
await this.request(formURL, { method: 'POST', body: formData });
if (this.shouldConfirmUpload) {
await this.confirmRequest(id);
}
const asset = { id, name, size, displayURL: url, url, path: url };
return { success: true, asset };
} catch (error) {
console.error(error);
throw error;
}
}
}

View File

@ -4,7 +4,7 @@ import type {
} from '@toast-ui/editor/types/editor';
import type { ToolbarItemOptions as MarkdownToolbarItemOptions } from '@toast-ui/editor/types/ui';
import type { PropertiesSchema } from 'ajv/dist/types/json-schema';
import type { ComponentType, ReactNode } from 'react';
import type { FunctionComponent, ComponentType, ReactNode } from 'react';
import type { t, TranslateProps as ReactPolyglotTranslateProps } from 'react-polyglot';
import type { MediaFile as BackendMediaFile } from './backend';
import type { EditorControlProps } from './components/Editor/EditorControlPane/EditorControl';
@ -15,12 +15,6 @@ import type Cursor from './lib/util/Cursor';
import type AssetProxy from './valueObjects/AssetProxy';
import type { MediaHolder } from './widgets/markdown/hooks/useMedia';
export interface SlugConfig {
encoding: string;
clean_accents: boolean;
sanitize_replacement: string;
}
export interface Pages {
[collection: string]: { isFetching?: boolean; page?: number; ids: string[] };
}
@ -68,6 +62,7 @@ export type ValueOrNestedValue =
| number
| boolean
| string[]
| (string | number)[]
| null
| undefined
| ObjectValue
@ -176,8 +171,6 @@ export interface Collection {
isFetching?: boolean;
media_folder?: string;
public_folder?: string;
preview_path?: string;
preview_path_date_field?: string;
summary?: string;
filter?: FilterRule;
type: 'file_based_collection' | 'folder_based_collection';
@ -224,15 +217,11 @@ export interface DisplayURLState {
err?: Error;
}
export type Hook = string | boolean;
export type TranslatedProps<T> = T & ReactPolyglotTranslateProps;
export type GetAssetFunction = (path: string, field?: Field) => Promise<AssetProxy>;
export interface WidgetControlProps<T, F extends Field = Field> {
clearFieldErrors: EditorControlProps['clearFieldErrors'];
clearSearch: EditorControlProps['clearSearch'];
collection: Collection;
config: Config;
entry: Entry;
@ -242,11 +231,9 @@ export interface WidgetControlProps<T, F extends Field = Field> {
forList: boolean;
getAsset: GetAssetFunction;
isDisabled: boolean;
isFetching: boolean;
isFieldDuplicate: EditorControlProps['isFieldDuplicate'];
isFieldHidden: EditorControlProps['isFieldHidden'];
label: string;
loadEntry: EditorControlProps['loadEntry'];
locale: string | undefined;
mediaPaths: Record<string, string | string[]>;
onChange: (value: T | null | undefined) => void;
@ -268,7 +255,6 @@ export interface WidgetPreviewProps<T = unknown, F extends Field = Field> {
entry: Entry;
field: RenderedField<F>;
getAsset: GetAssetFunction;
resolveWidget: <W = unknown, WF extends Field = Field>(name: string) => Widget<W, WF>;
value: T | undefined | null;
}
@ -305,7 +291,6 @@ export interface WidgetOptions<T = unknown, F extends Field = Field> {
validator?: Widget<T, F>['validator'];
getValidValue?: Widget<T, F>['getValidValue'];
schema?: Widget<T, F>['schema'];
allowMapValue?: boolean;
}
export interface Widget<T = unknown, F extends Field = Field> {
@ -314,7 +299,6 @@ export interface Widget<T = unknown, F extends Field = Field> {
validator: FieldValidationMethod<T, F>;
getValidValue: (value: T | undefined | null) => T | undefined | null;
schema?: PropertiesSchema<unknown>;
allowMapValue?: boolean;
}
export interface WidgetParam<T = unknown, F extends Field = Field> {
@ -367,8 +351,6 @@ export interface BackendEntry {
assets: AssetProxy[];
}
export type DeleteOptions = {};
export interface Credentials {
token: string | {};
refresh_token?: string;
@ -446,7 +428,7 @@ export interface LocalePhrasesRoot {
}
export type LocalePhrases = string | { [property: string]: LocalePhrases };
export type CustomIcon = () => JSX.Element;
export type CustomIcon = FunctionComponent;
export type WidgetValueSerializer = {
serialize: (value: ValueOrNestedValue) => ValueOrNestedValue;
@ -473,33 +455,10 @@ export interface MediaLibraryInternalOptions {
export type MediaLibrary = MediaLibraryExternalLibrary | MediaLibraryInternalOptions;
export type BackendType =
| 'azure'
| 'git-gateway'
| 'github'
| 'gitlab'
| 'bitbucket'
| 'test-repo'
| 'proxy';
export type BackendType = 'git-gateway' | 'github' | 'gitlab' | 'bitbucket' | 'test-repo' | 'proxy';
export type MapWidgetType = 'Point' | 'LineString' | 'Polygon';
export type MarkdownWidgetButton =
| 'bold'
| 'italic'
| 'code'
| 'link'
| 'heading-one'
| 'heading-two'
| 'heading-three'
| 'heading-four'
| 'heading-five'
| 'heading-six'
| 'quote'
| 'code-block'
| 'bulleted-list'
| 'numbered-list';
export interface SelectWidgetOptionObject {
label: string;
value: string;
@ -567,12 +526,10 @@ export interface FileOrImageField extends BaseField {
media_library?: MediaLibrary;
media_folder?: string;
public_folder?: string;
private?: boolean;
}
export interface ObjectField extends BaseField {
widget: 'object';
default?: ObjectValue;
collapsed?: boolean;
summary?: string;
@ -626,9 +583,9 @@ export interface NumberField extends BaseField {
export interface SelectField extends BaseField {
widget: 'select';
default?: string | string[];
default?: string | number | (string | number)[];
options: string[] | SelectWidgetOptionObject[];
options: (string | number)[] | SelectWidgetOptionObject[];
multiple?: boolean;
min?: number;
max?: number;
@ -708,12 +665,9 @@ export interface SortableFields {
export interface Backend {
name: BackendType;
auth_scope?: AuthScope;
repo?: string;
branch?: string;
api_root?: string;
api_version?: string;
tenant_id?: string;
site_domain?: string;
base_url?: string;
auth_endpoint?: string;
@ -725,6 +679,7 @@ export interface Backend {
use_large_media_transforms_in_media_library?: boolean;
identity_url?: string;
gateway_url?: string;
auth_scope?: AuthScope;
commit_messages?: {
create?: string;
update?: string;
@ -784,23 +739,27 @@ export interface EventData {
author: { login: string | undefined; name: string };
}
export type EventListenerOptions = Record<string, unknown>;
export type EventListenerHandler = (
data: EventData,
options: EventListenerOptions,
) => Promise<EntryData | undefined | null | void>;
export interface EventListener {
name: AllowedEvent;
handler: (
data: EventData,
options: Record<string, unknown>,
) => Promise<EntryData | undefined | null | void>;
handler: EventListenerHandler;
}
export type EventListenerOptions = Record<string, unknown>;
export interface AdditionalLinkOptions {
iconName?: string;
}
export interface AdditionalLink {
id: string;
title: string;
data: string | (() => JSX.Element);
options?: {
iconName?: string;
};
data: string | FunctionComponent;
options?: AdditionalLinkOptions;
}
export interface AuthenticationPageProps {
@ -816,11 +775,9 @@ export interface AuthenticationPageProps {
export type Integration = {
collections?: '*' | string[];
} & (AlgoliaIntegration | AssetStoreIntegration);
} & AlgoliaIntegration;
export type IntegrationProvider = Integration['provider'];
export type SearchIntegrationProvider = 'algolia';
export type MediaIntegrationProvider = 'assetStore';
export interface AlgoliaIntegration extends AlgoliaConfig {
provider: 'algolia';
@ -833,16 +790,6 @@ export interface AlgoliaConfig {
indexPrefix?: string;
}
export interface AssetStoreIntegration extends AssetStoreConfig {
provider: 'assetStore';
}
export interface AssetStoreConfig {
hooks: ['assetStore'];
shouldConfirmUpload?: boolean;
getSignedFormURL: string;
}
export interface SearchResponse {
entries: Entry[];
pagination: number;

View File

@ -171,7 +171,6 @@ export function registerWidget<T = unknown>(
validator = () => false,
getValidValue = (value: T | undefined | null) => value,
schema,
allowMapValue,
} = {},
} = name;
if (registry.widgets[widgetName]) {
@ -189,7 +188,6 @@ export function registerWidget<T = unknown>(
validator: validator as Widget['validator'],
getValidValue: getValidValue as Widget['getValidValue'],
schema,
allowMapValue,
};
} else {
console.error('`registerWidget` failed, called with incorrect arguments.');
@ -360,6 +358,7 @@ export function getAdditionalLinks(): Record<string, AdditionalLink> {
}
export function getAdditionalLink(id: string): AdditionalLink | undefined {
console.log('additionalLinks', registry.additionalLinks);
return registry.additionalLinks[id];
}

View File

@ -5,7 +5,6 @@ const bg: LocalePhrasesRoot = {
login: 'Вход',
loggingIn: 'Влизане...',
loginWithNetlifyIdentity: 'Вход с Netlify Identity',
loginWithAzure: 'Вход с Azure',
loginWithBitbucket: 'Вход с Bitbucket',
loginWithGitHub: 'Вход с GitHub',
loginWithGitLab: 'Вход с GitLab',
@ -208,7 +207,6 @@ const bg: LocalePhrasesRoot = {
noResults: 'Няма резултати.',
noAssetsFound: 'Няма намерени ресурси.',
noImagesFound: 'Няма намерени изображения.',
private: 'Частен ',
images: 'Изображения',
mediaAssets: 'Медийни ресурси',
search: 'Търсене...',

View File

@ -206,7 +206,6 @@ const ca: LocalePhrasesRoot = {
noResults: 'Sense resultats.',
noAssetsFound: 'Arxius no trobats.',
noImagesFound: 'Imatges no trobades.',
private: 'Privat',
images: 'Imatges',
mediaAssets: 'Arxius multimèdia',
search: 'Buscar...',

View File

@ -5,7 +5,6 @@ const cs: LocalePhrasesRoot = {
login: 'Přihlásit',
loggingIn: 'Přihlašování…',
loginWithNetlifyIdentity: 'Přihlásit pomocí Netlify Identity',
loginWithAzure: 'Přihlásit pomocí Azure',
loginWithBitbucket: 'Přihlásit pomocí Bitbucket',
loginWithGitHub: 'Přihlásit pomocí GitHub',
loginWithGitLab: 'Přihlásit pomocí GitLab',
@ -206,7 +205,6 @@ const cs: LocalePhrasesRoot = {
noResults: 'Nic nenalezeno.',
noAssetsFound: 'Média nenalezena.',
noImagesFound: 'Obrázky nenalezeny.',
private: 'Soukromé ',
images: 'Obrázky',
mediaAssets: 'Média',
search: 'Hledat…',

View File

@ -5,7 +5,6 @@ const da: LocalePhrasesRoot = {
login: 'Log ind',
loggingIn: 'Logger ind...',
loginWithNetlifyIdentity: 'Log ind med Netlify Identity',
loginWithAzure: 'Log ing med Azure',
loginWithBitbucket: 'Log ind med Bitbucket',
loginWithGitHub: 'Log ind med GitHub',
loginWithGitLab: 'Log ind med GitLab',
@ -192,7 +191,6 @@ const da: LocalePhrasesRoot = {
noResults: 'Ingen resultater.',
noAssetsFound: 'Ingen elementer fundet.',
noImagesFound: 'Ingen billeder fundet.',
private: 'Privat ',
images: 'Billeder',
mediaAssets: 'Medie elementer',
search: 'Søg...',

View File

@ -5,7 +5,6 @@ const de: LocalePhrasesRoot = {
login: 'Login',
loggingIn: 'Sie werden eingeloggt...',
loginWithNetlifyIdentity: 'Mit Netlify Identity einloggen',
loginWithAzure: 'Mit Azure einloggen',
loginWithBitbucket: 'Mit Bitbucket einloggen',
loginWithGitHub: 'Mit GitHub einloggen',
loginWithGitLab: 'Mit GitLab einloggen',
@ -220,7 +219,6 @@ const de: LocalePhrasesRoot = {
noResults: 'Keine Egebnisse.',
noAssetsFound: 'Keine Medien gefunden.',
noImagesFound: 'Keine Bilder gefunden.',
private: 'Privat ',
images: 'Bilder',
mediaAssets: 'Medien',
search: 'Suchen...',

View File

@ -5,7 +5,6 @@ const en: LocalePhrasesRoot = {
login: 'Login',
loggingIn: 'Logging in...',
loginWithNetlifyIdentity: 'Login with Netlify Identity',
loginWithAzure: 'Login with Azure',
loginWithBitbucket: 'Login with Bitbucket',
loginWithGitHub: 'Login with GitHub',
loginWithGitLab: 'Login with GitLab',
@ -240,7 +239,6 @@ const en: LocalePhrasesRoot = {
noResults: 'No results.',
noAssetsFound: 'No assets found.',
noImagesFound: 'No images found.',
private: 'Private ',
images: 'Images',
mediaAssets: 'Media assets',
search: 'Search...',

View File

@ -170,7 +170,6 @@ const es: LocalePhrasesRoot = {
noResults: 'Sin resultados.',
noAssetsFound: 'Archivos no encontrados.',
noImagesFound: 'Imágenes no encontradas.',
private: 'Privado ',
images: 'Imágenes',
mediaAssets: 'Archivos multimedia',
search: 'Buscar...',

View File

@ -5,7 +5,6 @@ const fr: LocalePhrasesRoot = {
login: 'Se connecter',
loggingIn: 'Connexion en cours...',
loginWithNetlifyIdentity: 'Se connecter avec Netlify Identity',
loginWithAzure: 'Se connecter avec Azure',
loginWithBitbucket: 'Se connecter avec Bitbucket',
loginWithGitHub: 'Se connecter avec GitHub',
loginWithGitLab: 'Se connecter avec GitLab',
@ -216,7 +215,6 @@ const fr: LocalePhrasesRoot = {
noResults: 'Aucun résultat.',
noAssetsFound: 'Aucune ressource trouvée.',
noImagesFound: 'Aucune image trouvée.',
private: 'Privé ',
images: 'Images',
mediaAssets: 'Ressources',
search: 'Recherche...',

View File

@ -149,7 +149,6 @@ const gr: LocalePhrasesRoot = {
noResults: 'Χωρίς αποτελέσματα.',
noAssetsFound: 'Δεν βρέθηκαν αρχεία.',
noImagesFound: 'Δεν βρέθηκαν εικόνες.',
private: 'Ιδιωτικό',
images: 'Εικόνες',
mediaAssets: 'Αρχεία πολυμέσων',
search: 'Αναζήτηση...',

View File

@ -5,7 +5,6 @@ const he: LocalePhrasesRoot = {
login: 'התחברות',
loggingIn: 'התחברות...',
loginWithNetlifyIdentity: 'התחברות עם Netlify Identity',
loginWithAzure: 'התחברות עם Azure',
loginWithBitbucket: 'התחברות עם Bitbucket',
loginWithGitHub: 'התחברות עם GitHub',
loginWithGitLab: 'התחברות עם GitLab',
@ -216,7 +215,6 @@ const he: LocalePhrasesRoot = {
noResults: 'לא נמצאו תוצאות.',
noAssetsFound: 'לא נמצאו קבצים.',
noImagesFound: 'לא נמצאו תמונות.',
private: 'פרטי ',
images: 'תמונות',
mediaAssets: 'קבצי מדיה',
search: 'חיפוש...',

View File

@ -5,7 +5,6 @@ const hr: LocalePhrasesRoot = {
login: 'Prijava',
loggingIn: 'Prijava u tijeku...',
loginWithNetlifyIdentity: 'Prijava sa Netlify računom',
loginWithAzure: 'Prijava za Azure računom',
loginWithBitbucket: 'Prijava sa Bitbucket računom',
loginWithGitHub: 'Prijava sa GitHub računom',
loginWithGitLab: 'Prijava sa GitLab računom',
@ -195,7 +194,6 @@ const hr: LocalePhrasesRoot = {
noResults: 'Nema rezultata.',
noAssetsFound: 'Sredstva nisu pronađena.',
noImagesFound: 'Slike nisu pronađene.',
private: 'Privatno ',
images: 'Slike',
mediaAssets: 'Medijska sredstva',
search: 'Pretraži...',

View File

@ -135,7 +135,6 @@ const hu: LocalePhrasesRoot = {
noResults: 'Nincs találat.',
noAssetsFound: 'Nem található tartalom.',
noImagesFound: 'Nem található kép.',
private: 'Privát ',
images: 'Képek',
mediaAssets: 'Média tartalmak',
search: 'Keresés...',

View File

@ -146,7 +146,6 @@ const it: LocalePhrasesRoot = {
noResults: 'Nessun risultato.',
noAssetsFound: 'Nessun assets trovato.',
noImagesFound: 'Nessuna immagine trovata.',
private: 'Privato ',
images: 'Immagini',
mediaAssets: 'Media assets',
search: 'Cerca...',

View File

@ -5,7 +5,6 @@ const ja: LocalePhrasesRoot = {
login: 'ログイン',
loggingIn: 'ログインしています...',
loginWithNetlifyIdentity: 'Netlify Identity でログインする',
loginWithAzure: 'Azure でログインする',
loginWithBitbucket: 'Bitbucket でログインする',
loginWithGitHub: 'GitHub でログインする',
loginWithGitLab: 'GitLab でログインする',
@ -213,7 +212,6 @@ const ja: LocalePhrasesRoot = {
noResults: 'データがありません。',
noAssetsFound: 'データがありません。',
noImagesFound: 'データがありません。',
private: 'プライベート',
images: '画像',
mediaAssets: 'メディア',
search: '検索',

View File

@ -177,7 +177,6 @@ const ko: LocalePhrasesRoot = {
noResults: '일치 항목 없음.',
noAssetsFound: '발견된 에셋 없음.',
noImagesFound: '발견된 이미지 없음.',
private: '개인 ',
images: '이미지',
mediaAssets: '미디어 에셋',
search: '검색...',

View File

@ -5,7 +5,6 @@ const lt: LocalePhrasesRoot = {
login: 'Prisijungti',
loggingIn: 'Prisijungiama...',
loginWithNetlifyIdentity: 'Prisijungti su Netlify Identity',
loginWithAzure: 'Prisijungti su Azure',
loginWithBitbucket: 'Prisijungti su Bitbucket',
loginWithGitHub: 'Prisijungti su GitHub',
loginWithGitLab: 'Prisijungti su GitLab',
@ -197,7 +196,6 @@ const lt: LocalePhrasesRoot = {
noResults: 'Nėra rezultatų.',
noAssetsFound: 'Turinio nerasta.',
noImagesFound: 'Vaizdų nerasta.',
private: 'Privatu ',
images: 'Vaizdai',
mediaAssets: 'Medijos turinys',
search: 'Paieška...',

View File

@ -166,7 +166,6 @@ const nb_no: LocalePhrasesRoot = {
noResults: 'Ingen resultater.',
noAssetsFound: 'Ingen elementer funnet.',
noImagesFound: 'Ingen bilder funnet.',
private: 'Privat ',
images: 'Bilder',
mediaAssets: 'Mediebibliotek',
search: 'Søk...',

View File

@ -5,7 +5,6 @@ const nl: LocalePhrasesRoot = {
login: 'Inloggen',
loggingIn: 'Inloggen...',
loginWithNetlifyIdentity: 'Inloggen met Netlify Identity',
loginWithAzure: 'Inloggen met Azure',
loginWithBitbucket: 'Inloggen met Bitbucket',
loginWithGitHub: 'Inloggen met GitHub',
loginWithGitLab: 'Inloggen met GitLab',
@ -212,7 +211,6 @@ const nl: LocalePhrasesRoot = {
noResults: 'Geen resultaten.',
noAssetsFound: 'Geen media gevonden.',
noImagesFound: 'Geen afbeeldingen gevonden.',
private: 'Privé',
images: 'Afbeeldingen',
mediaAssets: 'Media',
search: 'Zoeken...',

View File

@ -167,7 +167,6 @@ const nn_no: LocalePhrasesRoot = {
noResults: 'Ingen resultat.',
noAssetsFound: 'Ingen elementer funne.',
noImagesFound: 'Ingen bilete funne.',
private: 'Privat ',
images: 'Bileter',
mediaAssets: 'Mediebibliotek',
search: 'Søk...',

View File

@ -5,7 +5,6 @@ const pl: LocalePhrasesRoot = {
login: 'Zaloguj się',
loggingIn: 'Logowanie...',
loginWithNetlifyIdentity: 'Zaloguj przez konto Netlify',
loginWithAzure: 'Zaloguj przez konto Azure',
loginWithBitbucket: 'Zaloguj przez Bitbucket',
loginWithGitHub: 'Zaloguj przez GitHub',
loginWithGitLab: 'Zaloguj przez GitLab',
@ -217,7 +216,6 @@ const pl: LocalePhrasesRoot = {
noResults: 'Brak wyników.',
noAssetsFound: 'Nie znaleziono żadnych zasobów.',
noImagesFound: 'Nie znaleziono żadnych obrazów.',
private: 'Prywatne ',
images: 'Obrazy',
mediaAssets: 'Zasoby multimedialne',
search: 'Szukaj...',

View File

@ -5,7 +5,6 @@ const pt: LocalePhrasesRoot = {
login: 'Entrar',
loggingIn: 'Entrando...',
loginWithNetlifyIdentity: 'Entrar com o Netlify Identity',
loginWithAzure: 'Entrar com o Azure',
loginWithBitbucket: 'Entrar com o Bitbucket',
loginWithGitHub: 'Entrar com o GitHub',
loginWithGitLab: 'Entrar com o GitLab',
@ -219,7 +218,6 @@ const pt: LocalePhrasesRoot = {
noResults: 'Nenhum resultado.',
noAssetsFound: 'Nenhum recurso encontrado.',
noImagesFound: 'Nenhuma imagem encontrada.',
private: 'Privado ',
images: 'Imagens',
mediaAssets: 'Recursos de mídia',
search: 'Pesquisar...',

View File

@ -5,7 +5,6 @@ const ro: LocalePhrasesRoot = {
login: 'Autentifică-te',
loggingIn: 'Te autentificăm...',
loginWithNetlifyIdentity: 'Autentifică-te cu Netlify Identity',
loginWithAzure: 'Autentifică-te cu Azure',
loginWithBitbucket: 'Autentifică-te cu Bitbucket',
loginWithGitHub: 'Autentifică-te cu GitHub',
loginWithGitLab: 'Autentifică-te cu GitLab',
@ -211,7 +210,6 @@ const ro: LocalePhrasesRoot = {
noResults: 'Nu sunt rezultate.',
noAssetsFound: 'Nu s-au găsit fișiere.',
noImagesFound: 'Nu s-au găsit imagini.',
private: 'Privat ',
images: 'Imagini',
mediaAssets: 'Fișiere media',
search: 'Caută...',

View File

@ -5,7 +5,6 @@ const ru: LocalePhrasesRoot = {
login: 'Войти',
loggingIn: 'Вхожу...',
loginWithNetlifyIdentity: 'Войти через Netlify Identity',
loginWithAzure: 'Войти через Azure',
loginWithBitbucket: 'Войти через Bitbucket',
loginWithGitHub: 'Войти через GitHub',
loginWithGitLab: 'Войти через GitLab',
@ -207,7 +206,6 @@ const ru: LocalePhrasesRoot = {
noResults: 'Нет результатов.',
noAssetsFound: 'Ресурсы не найдены.',
noImagesFound: 'Изображения не найдены.',
private: 'Приватные ',
images: 'Изображения',
mediaAssets: 'Медиаресурсы',
search: 'Идёт поиск…',

View File

@ -5,7 +5,6 @@ const sv: LocalePhrasesRoot = {
login: 'Logga in',
loggingIn: 'Loggar in...',
loginWithNetlifyIdentity: 'Logga in med Netlify Identity',
loginWithAzure: 'Logga in med Azure',
loginWithBitbucket: 'Logga in med Bitbucket',
loginWithGitHub: 'Logga in med GitHub',
loginWithGitLab: 'Logga in med GitLab',
@ -211,7 +210,6 @@ const sv: LocalePhrasesRoot = {
noResults: 'Inga resultat.',
noAssetsFound: 'Hittade inga mediaobjekt.',
noImagesFound: 'Hittade inga bilder.',
private: 'Privat ',
images: 'Bilder',
mediaAssets: 'Mediaobjekt',
search: 'Sök...',

View File

@ -178,7 +178,6 @@ const th: LocalePhrasesRoot = {
noResults: 'ไม่มีผลลัพธ์',
noAssetsFound: 'ไม่พบข้อมูล',
noImagesFound: 'ไม่พบรูปภาพ',
private: 'ส่วนตัว ',
images: 'รูปภาพ',
mediaAssets: 'ข้อมูลมีเดีย',
search: 'ค้นหา...',

View File

@ -5,7 +5,6 @@ const tr: LocalePhrasesRoot = {
login: 'Giriş',
loggingIn: 'Giriş yapılıyor..',
loginWithNetlifyIdentity: 'Netlify Identity ile Giriş',
loginWithAzure: 'Azure ile Giriş',
loginWithBitbucket: 'Bitbucket ile Giriş',
loginWithGitHub: 'GitHub ile Giriş',
loginWithGitLab: 'GitLab ile Giriş',
@ -223,7 +222,6 @@ const tr: LocalePhrasesRoot = {
noResults: 'Sonuç yok.',
noAssetsFound: 'Hiçbir dosya bulunamadı.',
noImagesFound: 'Resim bulunamadı.',
private: 'Özel ',
images: 'Görseller',
mediaAssets: 'Medya dosyaları',
search: 'Ara...',

View File

@ -125,7 +125,6 @@ const uk: LocalePhrasesRoot = {
noResults: 'Результати відсутні.',
noAssetsFound: 'Матеріали відсутні.',
noImagesFound: 'Зображення відсутні.',
private: 'Private ',
images: 'Зображення',
mediaAssets: 'Медіа матеріали',
search: 'Пошук...',

View File

@ -175,7 +175,6 @@ const vi: LocalePhrasesRoot = {
noResults: 'Không có kết quả.',
noAssetsFound: 'Không tìm thấy tập tin nào.',
noImagesFound: 'Không tìm thấy hình nào.',
private: 'Riêng tư ',
images: 'Hình ảnh',
mediaAssets: 'Tập tin',
search: 'Tìm kiếm...',

View File

@ -5,7 +5,6 @@ const zh_Hans: LocalePhrasesRoot = {
login: '登录',
loggingIn: '正在登录...',
loginWithNetlifyIdentity: '使用 Netlify Identity 登录',
loginWithAzure: '使用 Azure 登录',
loginWithBitbucket: '使用 Bitbucket 登录',
loginWithGitHub: '使用 GitHub 登录',
loginWithGitLab: '使用 GitLab 登录',
@ -207,7 +206,6 @@ const zh_Hans: LocalePhrasesRoot = {
noResults: '暂无结果',
noAssetsFound: '未找到资源',
noImagesFound: '未找到图片',
private: '私有',
images: '图片',
mediaAssets: '媒体资源',
search: '搜索...',

View File

@ -185,7 +185,6 @@ const zh_Hant: LocalePhrasesRoot = {
noResults: '沒有結果',
noAssetsFound: '沒有發現媒體資產。',
noImagesFound: '沒有發現影像。',
private: '私人',
images: '影像',
mediaAssets: '媒體資產',
search: '搜尋中...',

View File

@ -5,7 +5,6 @@ import { v4 as uuid } from 'uuid';
import {
ADD_DRAFT_ENTRY_MEDIA_FILE,
DRAFT_CHANGE_FIELD,
DRAFT_CLEAR_ERRORS,
DRAFT_CREATE_DUPLICATE_FROM_ENTRY,
DRAFT_CREATE_EMPTY,
DRAFT_CREATE_FROM_ENTRY,
@ -176,13 +175,6 @@ function entryDraftReducer(
};
}
case DRAFT_CLEAR_ERRORS: {
return {
...state,
fieldsErrors: {},
};
}
case ENTRY_PERSIST_REQUEST: {
if (!state.entry) {
return state;

View File

@ -18,18 +18,18 @@ import type { IntegrationHooks } from './integrations';
const reducers = {
auth,
config,
collections,
search,
integrations,
entries,
config,
cursors,
entries,
entryDraft,
medias,
mediaLibrary,
globalUI,
status,
integrations,
mediaLibrary,
medias,
scroll,
search,
status,
};
export default reducers;

View File

@ -5,22 +5,18 @@ import { CONFIG_SUCCESS } from '../actions/config';
import type { ConfigAction } from '../actions/config';
import type {
AlgoliaConfig,
AssetStoreConfig,
Config,
MediaIntegrationProvider,
SearchIntegrationProvider,
} from '../interface';
export interface IntegrationHooks {
search?: SearchIntegrationProvider;
listEntries?: SearchIntegrationProvider;
assetStore?: MediaIntegrationProvider;
}
export interface IntegrationsState {
providers: {
algolia?: AlgoliaConfig;
assetStore?: AssetStoreConfig;
};
hooks: IntegrationHooks;
collectionHooks: Record<string, IntegrationHooks>;
@ -47,8 +43,6 @@ export function getIntegrations(config: Config): IntegrationsState {
hook => (acc.collectionHooks[collection][hook] = providerData.provider),
);
});
} else if (providerData.provider === 'assetStore') {
acc.providers[providerData.provider] = providerData;
}
return acc;
},

View File

@ -21,12 +21,11 @@ import {
MEDIA_PERSIST_SUCCESS,
MEDIA_REMOVE_INSERTED,
} from '../actions/mediaLibrary';
import { selectIntegration } from './';
import { selectEditingDraft } from './entries';
import { selectMediaFolder } from '../lib/util/media.util';
import { selectEditingDraft } from './entries';
import type { MediaLibraryAction } from '../actions/mediaLibrary';
import type { Field, DisplayURLState, MediaFile, MediaLibraryInstance } from '../interface';
import type { DisplayURLState, Field, MediaFile, MediaLibraryInstance } from '../interface';
import type { RootState } from '../store';
export interface MediaLibraryDisplayURL {
@ -49,7 +48,6 @@ export type MediaLibraryState = {
value?: string | string[];
replaceIndex?: number;
canInsert?: boolean;
privateUpload?: boolean;
isLoading?: boolean;
dynamicSearch?: boolean;
dynamicSearchActive?: boolean;
@ -82,26 +80,8 @@ function mediaLibrary(
};
case MEDIA_LIBRARY_OPEN: {
const { controlID, forImage, privateUpload, config, field, value, replaceIndex } =
action.payload;
const { controlID, forImage, config, field, value, replaceIndex } = action.payload;
const libConfig = config || {};
const privateUploadChanged = state.privateUpload !== privateUpload;
if (privateUploadChanged) {
return {
...state,
isVisible: true,
forImage,
controlID,
canInsert: Boolean(controlID),
privateUpload,
config: libConfig,
controlMedia: {},
displayURLs: {},
field,
value,
replaceIndex,
};
}
return {
...state,
@ -109,7 +89,6 @@ function mediaLibrary(
forImage: Boolean(forImage),
controlID,
canInsert: !!controlID,
privateUpload: Boolean(privateUpload),
config: libConfig,
field,
value,
@ -180,19 +159,7 @@ function mediaLibrary(
};
case MEDIA_LOAD_SUCCESS: {
const {
files = [],
page,
canPaginate,
dynamicSearch,
dynamicSearchQuery,
privateUpload,
} = action.payload;
const privateUploadChanged = state.privateUpload !== privateUpload;
if (privateUploadChanged) {
return state;
}
const { files = [], page, canPaginate, dynamicSearch, dynamicSearchQuery } = action.payload;
const filesWithKeys = files.map(file => ({ ...file, key: uuid() }));
return {
@ -210,11 +177,6 @@ function mediaLibrary(
}
case MEDIA_LOAD_FAILURE: {
const privateUploadChanged = state.privateUpload !== action.payload.privateUpload;
if (privateUploadChanged) {
return state;
}
return {
...state,
isLoading: false,
@ -228,12 +190,7 @@ function mediaLibrary(
};
case MEDIA_PERSIST_SUCCESS: {
const { file, privateUpload } = action.payload;
const privateUploadChanged = state.privateUpload !== privateUpload;
if (privateUploadChanged) {
return state;
}
const { file } = action.payload;
const fileWithKey = { ...file, key: uuid() };
const files = state.files as MediaFile[];
const updatedFiles = [fileWithKey, ...files];
@ -245,11 +202,6 @@ function mediaLibrary(
}
case MEDIA_PERSIST_FAILURE: {
const privateUploadChanged = state.privateUpload !== action.payload.privateUpload;
if (privateUploadChanged) {
return state;
}
return {
...state,
isPersisting: false,
@ -263,12 +215,8 @@ function mediaLibrary(
};
case MEDIA_DELETE_SUCCESS: {
const { file, privateUpload } = action.payload;
const { file } = action.payload;
const { key, id } = file;
const privateUploadChanged = state.privateUpload !== privateUpload;
if (privateUploadChanged) {
return state;
}
const files = state.files as MediaFile[];
const updatedFiles = files.filter(file => (key ? file.key !== key : file.id !== id));
@ -288,11 +236,6 @@ function mediaLibrary(
}
case MEDIA_DELETE_FAILURE: {
const privateUploadChanged = state.privateUpload !== action.payload.privateUpload;
if (privateUploadChanged) {
return state;
}
return {
...state,
isDeleting: false,
@ -347,10 +290,9 @@ function mediaLibrary(
export function selectMediaFiles(state: RootState, field?: Field): MediaFile[] {
const { mediaLibrary, entryDraft } = state;
const editingDraft = selectEditingDraft(entryDraft);
const integration = selectIntegration(state, null, 'assetStore');
let files: MediaFile[] = [];
if (editingDraft && !integration) {
if (editingDraft) {
const entryFiles = entryDraft?.entry?.mediaFiles ?? [];
const entry = entryDraft['entry'];
const collection = entry?.collection ? state.collections[entry.collection] : null;

View File

@ -3,14 +3,21 @@ export {};
import type { Config } from '../interface';
import type CmsAPI from '../index';
import type createReactClass from 'create-react-class';
import type { createElement } from 'react';
import type { createElement, useEffect, useState, useMemo, useCallback } from 'react';
declare global {
interface Window {
CMS?: CmsAPI;
CMS_CONFIG?: Config;
CMS_ENV?: string;
/**
* @deprecated Should use react functional components instead
*/
createClass: createReactClass;
h: createElement;
useState: useState;
useMemo: useMemo;
useEffect: useEffect;
useCallback: useCallback;
}
}

View File

@ -11,7 +11,6 @@ const CodeWidget = (): WidgetParam<string | { [key: string]: string }, CodeField
previewComponent,
options: {
schema,
allowMapValue: true,
},
};
};

View File

@ -11,7 +11,9 @@ import formatDate from 'date-fns/format';
import formatISO from 'date-fns/formatISO';
import parse from 'date-fns/parse';
import parseISO from 'date-fns/parseISO';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { isNotEmpty } from '../../lib/util/string.util';
import type { MouseEvent } from 'react';
import type { DateTimeField, TranslatedProps, WidgetControlProps } from '../../interface';
@ -58,8 +60,6 @@ const DateTimeControl = ({
onChange,
hasErrors,
}: WidgetControlProps<string, DateTimeField>) => {
const [internalValue, setInternalValue] = useState(value ?? '');
const { format, dateFormat, timeFormat } = useMemo(() => {
const format = field.format;
@ -75,18 +75,7 @@ const DateTimeControl = ({
};
}, [field.date_format, field.format, field.time_format]);
const dateValue = useMemo(
() => (format ? parse(internalValue, format, new Date()) : parseISO(internalValue)),
[format, internalValue],
);
const timezoneOffset = useMemo(() => dateValue.getTimezoneOffset() * 60000, [dateValue]);
const utcDate = useMemo(() => {
const dateTime = new Date(dateValue);
const utcFromLocal = new Date(dateTime.getTime() + timezoneOffset);
return utcFromLocal;
}, [dateValue, timezoneOffset]);
const timezoneOffset = useMemo(() => new Date().getTimezoneOffset() * 60000, []);
const localToUTC = useCallback(
(dateTime: Date) => {
@ -105,6 +94,20 @@ const DateTimeControl = ({
: field.default;
}, [field.default, field.picker_utc, format, localToUTC]);
const [internalValue, setInternalValue] = useState(value ?? defaultValue);
const dateValue = useMemo(
() =>
format ? parse(internalValue, format, new Date()) ?? defaultValue : parseISO(internalValue),
[defaultValue, format, internalValue],
);
const utcDate = useMemo(() => {
const dateTime = new Date(dateValue);
const utcFromLocal = new Date(dateTime.getTime() + timezoneOffset) ?? defaultValue;
return utcFromLocal;
}, [dateValue, defaultValue, timezoneOffset]);
const handleChange = useCallback(
(datetime: Date | null) => {
if (datetime === null) {
@ -127,27 +130,16 @@ const DateTimeControl = ({
[defaultValue, field.picker_utc, format, localToUTC, onChange],
);
useEffect(() => {
/**
* Set the current date as default value if no value is provided and default is absent. An
* empty default string means the value is intentionally blank.
*/
if (internalValue === undefined) {
setTimeout(() => {
setInternalValue(defaultValue);
onChange(defaultValue);
}, 0);
}
}, [defaultValue, handleChange, internalValue, onChange]);
const dateTimePicker = useMemo(() => {
if (dateFormat && !timeFormat) {
const inputDateFormat = typeof dateFormat === 'string' ? dateFormat : 'MMM d, yyyy';
return (
<MobileDatePicker
key="mobile-date-picker"
inputFormat={typeof dateFormat === 'string' ? dateFormat : 'MMM d, yyyy'}
inputFormat={inputDateFormat}
label={label}
value={field.picker_utc ? utcDate : dateValue}
value={formatDate(field.picker_utc ? utcDate : dateValue, inputDateFormat)}
onChange={handleChange}
renderInput={params => (
<TextField
@ -172,12 +164,14 @@ const DateTimeControl = ({
}
if (!dateFormat && timeFormat) {
const inputTimeFormat = typeof timeFormat === 'string' ? timeFormat : 'H:mm';
return (
<TimePicker
key="time-picker"
label={label}
inputFormat={typeof timeFormat === 'string' ? timeFormat : 'H:mm'}
value={field.picker_utc ? utcDate : dateValue}
inputFormat={inputTimeFormat}
value={formatDate(field.picker_utc ? utcDate : dateValue, inputTimeFormat)}
onChange={handleChange}
renderInput={params => (
<TextField
@ -202,17 +196,19 @@ const DateTimeControl = ({
}
let inputFormat = 'MMM d, yyyy H:mm';
if (dateFormat || timeFormat) {
if (typeof dateFormat === 'string' || typeof timeFormat === 'string') {
const formatParts: string[] = [];
if (typeof dateFormat === 'string') {
if (typeof dateFormat === 'string' && isNotEmpty(dateFormat)) {
formatParts.push(dateFormat);
}
if (typeof timeFormat === 'string') {
if (typeof timeFormat === 'string' && isNotEmpty(timeFormat)) {
formatParts.push(timeFormat);
}
inputFormat = formatParts.join(' ');
if (formatParts.length > 0) {
inputFormat = formatParts.join(' ');
}
}
return (
@ -220,7 +216,7 @@ const DateTimeControl = ({
key="mobile-date-time-picker"
inputFormat={inputFormat}
label={label}
value={field.picker_utc ? utcDate : dateValue}
value={formatDate(field.picker_utc ? utcDate : dateValue, inputFormat)}
onChange={handleChange}
renderInput={params => (
<TextField

View File

@ -306,7 +306,6 @@ export default function withFileControl({ forImage = false }: WithImageOptions =
return openMediaLibrary({
controlID,
forImage,
privateUpload: field.private,
value: internalValue,
allowMultiple:
'allow_multiple' in mediaLibraryFieldOptions
@ -355,7 +354,6 @@ export default function withFileControl({ forImage = false }: WithImageOptions =
return openMediaLibrary({
controlID,
forImage,
privateUpload: field.private,
value: internalValue,
replaceIndex: index,
allowMultiple: false,

View File

@ -18,7 +18,7 @@ import type {
ListField,
ObjectValue,
ValueOrNestedValue,
WidgetControlProps,
WidgetControlProps
} from '../../interface';
const StyledListWrapper = styled('div')`
@ -96,7 +96,6 @@ function getFieldsDefault(fields: Field[], initialValue: ObjectValue = {}): Obje
}
const ListControl = ({
clearFieldErrors,
entry,
field,
fieldsErrors,
@ -223,7 +222,6 @@ const ListControl = ({
key={key}
valueType={valueType}
handleRemove={handleRemove}
clearFieldErrors={clearFieldErrors}
data-testid={`object-control-${index}`}
entry={entry}
field={field}
@ -242,7 +240,6 @@ const ListControl = ({
keys,
valueType,
handleRemove,
clearFieldErrors,
entry,
field,
fieldsErrors,

View File

@ -79,7 +79,6 @@ function validateItem(field: ListField, item: ObjectValue) {
interface ListItemProps
extends Pick<
WidgetControlProps<ObjectValue, ListField>,
| 'clearFieldErrors'
| 'entry'
| 'field'
| 'fieldsErrors'
@ -98,7 +97,6 @@ interface ListItemProps
const ListItem = ({
index,
clearFieldErrors,
entry,
field,
fieldsErrors,
@ -201,7 +199,6 @@ const ListItem = ({
key={index}
field={objectField}
value={value}
clearFieldErrors={clearFieldErrors}
fieldsErrors={fieldsErrors}
submitted={submitted}
parentPath={path}

View File

@ -86,7 +86,6 @@ const MarkdownControl = ({
openMediaLibrary({
controlID,
forImage,
privateUpload: false,
allowMultiple: false,
field,
config: 'config' in mediaLibraryFieldOptions ? mediaLibraryFieldOptions.config : undefined,

View File

@ -47,7 +47,6 @@ const StyledNoFieldsMessage = styled('div')`
`;
const ObjectControl = ({
clearFieldErrors,
field,
fieldsErrors,
submitted,
@ -89,7 +88,6 @@ const ObjectControl = ({
key={index}
field={field}
value={fieldValue}
clearFieldErrors={clearFieldErrors}
fieldsErrors={fieldsErrors}
submitted={submitted}
parentPath={path}
@ -104,7 +102,6 @@ const ObjectControl = ({
}) ?? null
);
}, [
clearFieldErrors,
fieldsErrors,
i18n,
isFieldDuplicate,

View File

@ -34,7 +34,7 @@ const SelectControl = ({
}: WidgetControlProps<string | number | (string | number)[], SelectField>) => {
const [internalValue, setInternalValue] = useState(value);
const fieldOptions: (string | Option)[] = useMemo(() => field.options, [field.options]);
const fieldOptions: (string | number | Option)[] = useMemo(() => field.options, [field.options]);
const isMultiple = useMemo(() => field.multiple ?? false, [field.multiple]);
const options = useMemo(