Feature/docs (#67)
This commit is contained in:
committed by
GitHub
parent
7a1ec55a5c
commit
81ca566b5e
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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)),
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -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');
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export { default as AzureBackend } from './implementation';
|
||||
export { default as API } from './API';
|
||||
export { default as AuthenticationPage } from './AuthenticationPage';
|
@ -1,4 +1,3 @@
|
||||
export { AzureBackend } from './azure';
|
||||
export { BitbucketBackend } from './bitbucket';
|
||||
export { GitGatewayBackend } from './git-gateway';
|
||||
export { GitHubBackend } from './github';
|
||||
|
@ -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}
|
||||
|
@ -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 />;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
|
@ -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 |
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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' },
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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];
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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];
|
||||
}
|
||||
|
||||
|
@ -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: 'Търсене...',
|
||||
|
@ -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...',
|
||||
|
@ -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…',
|
||||
|
@ -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...',
|
||||
|
@ -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...',
|
||||
|
@ -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...',
|
||||
|
@ -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...',
|
||||
|
@ -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...',
|
||||
|
@ -149,7 +149,6 @@ const gr: LocalePhrasesRoot = {
|
||||
noResults: 'Χωρίς αποτελέσματα.',
|
||||
noAssetsFound: 'Δεν βρέθηκαν αρχεία.',
|
||||
noImagesFound: 'Δεν βρέθηκαν εικόνες.',
|
||||
private: 'Ιδιωτικό',
|
||||
images: 'Εικόνες',
|
||||
mediaAssets: 'Αρχεία πολυμέσων',
|
||||
search: 'Αναζήτηση...',
|
||||
|
@ -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: 'חיפוש...',
|
||||
|
@ -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...',
|
||||
|
@ -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...',
|
||||
|
@ -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...',
|
||||
|
@ -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: '検索',
|
||||
|
@ -177,7 +177,6 @@ const ko: LocalePhrasesRoot = {
|
||||
noResults: '일치 항목 없음.',
|
||||
noAssetsFound: '발견된 에셋 없음.',
|
||||
noImagesFound: '발견된 이미지 없음.',
|
||||
private: '개인 ',
|
||||
images: '이미지',
|
||||
mediaAssets: '미디어 에셋',
|
||||
search: '검색...',
|
||||
|
@ -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...',
|
||||
|
@ -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...',
|
||||
|
@ -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...',
|
||||
|
@ -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...',
|
||||
|
@ -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...',
|
||||
|
@ -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...',
|
||||
|
@ -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ă...',
|
||||
|
@ -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: 'Идёт поиск…',
|
||||
|
@ -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...',
|
||||
|
@ -178,7 +178,6 @@ const th: LocalePhrasesRoot = {
|
||||
noResults: 'ไม่มีผลลัพธ์',
|
||||
noAssetsFound: 'ไม่พบข้อมูล',
|
||||
noImagesFound: 'ไม่พบรูปภาพ',
|
||||
private: 'ส่วนตัว ',
|
||||
images: 'รูปภาพ',
|
||||
mediaAssets: 'ข้อมูลมีเดีย',
|
||||
search: 'ค้นหา...',
|
||||
|
@ -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...',
|
||||
|
@ -125,7 +125,6 @@ const uk: LocalePhrasesRoot = {
|
||||
noResults: 'Результати відсутні.',
|
||||
noAssetsFound: 'Матеріали відсутні.',
|
||||
noImagesFound: 'Зображення відсутні.',
|
||||
private: 'Private ',
|
||||
images: 'Зображення',
|
||||
mediaAssets: 'Медіа матеріали',
|
||||
search: 'Пошук...',
|
||||
|
@ -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...',
|
||||
|
@ -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: '搜索...',
|
||||
|
@ -185,7 +185,6 @@ const zh_Hant: LocalePhrasesRoot = {
|
||||
noResults: '沒有結果',
|
||||
noAssetsFound: '沒有發現媒體資產。',
|
||||
noImagesFound: '沒有發現影像。',
|
||||
private: '私人',
|
||||
images: '影像',
|
||||
mediaAssets: '媒體資產',
|
||||
search: '搜尋中...',
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -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;
|
||||
|
9
core/src/types/global.d.ts
vendored
9
core/src/types/global.d.ts
vendored
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ const CodeWidget = (): WidgetParam<string | { [key: string]: string }, CodeField
|
||||
previewComponent,
|
||||
options: {
|
||||
schema,
|
||||
allowMapValue: true,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -86,7 +86,6 @@ const MarkdownControl = ({
|
||||
openMediaLibrary({
|
||||
controlID,
|
||||
forImage,
|
||||
privateUpload: false,
|
||||
allowMultiple: false,
|
||||
field,
|
||||
config: 'config' in mediaLibraryFieldOptions ? mediaLibraryFieldOptions.config : undefined,
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
Reference in New Issue
Block a user