chore: remove external media library integrations

This commit is contained in:
Daniel Lautzenheiser
2023-04-12 22:27:15 -04:00
parent 1849729bab
commit 56dc4a1d64
30 changed files with 139 additions and 1057 deletions

View File

@ -376,9 +376,7 @@ const testConfig: Config<RelationKitchenSinkPostField> = {
label: 'Choose URL',
widget: 'file',
required: false,
media_library: {
choose_url: true,
},
choose_url: true,
},
],
},
@ -411,9 +409,7 @@ const testConfig: Config<RelationKitchenSinkPostField> = {
label: 'Choose URL',
widget: 'image',
required: false,
media_library: {
choose_url: true,
},
choose_url: true,
},
],
},

View File

@ -9,7 +9,6 @@ import {
MEDIA_DISPLAY_URL_SUCCESS,
MEDIA_INSERT,
MEDIA_LIBRARY_CLOSE,
MEDIA_LIBRARY_CREATE,
MEDIA_LIBRARY_OPEN,
MEDIA_LOAD_FAILURE,
MEDIA_LOAD_REQUEST,
@ -40,43 +39,12 @@ import type {
ImplementationMediaFile,
MediaFile,
MediaLibrarInsertOptions,
MediaLibraryInstance,
MediaLibraryConfig,
UnknownField,
} from '../interface';
import type { RootState } from '../store';
import type AssetProxy from '../valueObjects/AssetProxy';
export function createMediaLibrary(instance: MediaLibraryInstance) {
const api = {
show: instance.show || (() => undefined),
hide: instance.hide || (() => undefined),
onClearControl: instance.onClearControl || (() => undefined),
onRemoveControl: instance.onRemoveControl || (() => undefined),
enableStandalone: instance.enableStandalone || (() => undefined),
};
return { type: MEDIA_LIBRARY_CREATE, payload: api } as const;
}
export function clearMediaControl(id: string) {
return (_dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => {
const state = getState();
const mediaLibrary = state.mediaLibrary.externalLibrary;
if (mediaLibrary) {
mediaLibrary.onClearControl?.({ id });
}
};
}
export function removeMediaControl(id: string) {
return (_dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => {
const state = getState();
const mediaLibrary = state.mediaLibrary.externalLibrary;
if (mediaLibrary) {
mediaLibrary.onRemoveControl?.({ id });
}
};
}
export function openMediaLibrary<EF extends BaseField = UnknownField>(
payload: {
controlID?: string;
@ -85,15 +53,13 @@ export function openMediaLibrary<EF extends BaseField = UnknownField>(
alt?: string;
allowMultiple?: boolean;
replaceIndex?: number;
config?: Record<string, unknown>;
config?: MediaLibraryConfig;
collection?: Collection<EF>;
field?: EF;
insertOptions?: MediaLibrarInsertOptions;
} = {},
) {
return (dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => {
const state = getState();
const mediaLibrary = state.mediaLibrary.externalLibrary;
return (dispatch: ThunkDispatch<RootState, {}, AnyAction>) => {
const {
controlID,
value,
@ -107,10 +73,6 @@ export function openMediaLibrary<EF extends BaseField = UnknownField>(
insertOptions,
} = payload;
if (mediaLibrary) {
mediaLibrary.show({ id: controlID, value, config, allowMultiple, imagesOnly: forImage });
}
dispatch(
mediaLibraryOpened({
controlID,
@ -129,12 +91,7 @@ export function openMediaLibrary<EF extends BaseField = UnknownField>(
}
export function closeMediaLibrary() {
return (dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => {
const state = getState();
const mediaLibrary = state.mediaLibrary.externalLibrary;
if (mediaLibrary) {
mediaLibrary.hide?.();
}
return (dispatch: ThunkDispatch<RootState, {}, AnyAction>) => {
dispatch(mediaLibraryClosed());
};
}
@ -178,7 +135,12 @@ export function removeInsertedMedia(controlID: string) {
}
export function loadMedia(
opts: { delay?: number; query?: string; page?: number; currentFolder?: string } = {},
opts: {
delay?: number;
query?: string;
page?: number;
currentFolder?: string;
} = {},
) {
const { delay = 0, page = 1, currentFolder } = opts;
return async (dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => {
@ -193,7 +155,7 @@ export function loadMedia(
function loadFunction() {
return backend
.getMedia(currentFolder, config?.media_library_folder_support ?? false)
.getMedia(currentFolder, config?.media_library?.folder_support ?? false)
.then(files => dispatch(mediaLoaded(files)))
.catch((error: { status?: number }) => {
console.error(error);
@ -446,7 +408,7 @@ function mediaLibraryOpened(payload: {
alt?: string;
replaceIndex?: number;
allowMultiple?: boolean;
config?: Record<string, unknown>;
config?: MediaLibraryConfig;
collection?: Collection;
field?: Field;
insertOptions?: MediaLibrarInsertOptions;
@ -540,7 +502,7 @@ export async function waitForMediaLibraryToLoad(
dispatch: ThunkDispatch<RootState, {}, AnyAction>,
state: RootState,
) {
if (state.mediaLibrary.isLoading !== false && !state.mediaLibrary.externalLibrary) {
if (state.mediaLibrary.isLoading !== false) {
await waitUntilWithTimeout(dispatch, resolve => ({
predicate: ({ type }) => type === MEDIA_LOAD_SUCCESS || type === MEDIA_LOAD_FAILURE,
run: () => resolve(),
@ -583,7 +545,6 @@ export async function getMediaDisplayURL(
}
export type MediaLibraryAction = ReturnType<
| typeof createMediaLibrary
| typeof mediaLibraryOpened
| typeof mediaLibraryClosed
| typeof mediaInserted

View File

@ -807,7 +807,7 @@ export class Backend<EF extends BaseField = UnknownField, BC extends BackendClas
);
return this.implementation.getMedia(
folder,
configState.config?.media_library_folder_support ?? false,
configState.config?.media_library?.folder_support ?? false,
mediaPath,
);
}),

View File

@ -117,7 +117,6 @@ export default class GitGateway implements BackendClass {
api?: GitHubAPI | GitLabAPI | BitBucketAPI;
branch: string;
mediaFolder?: string;
transformImages: boolean;
gatewayUrl: string;
netlifyLargeMediaURL: string;
backendType: string | null;
@ -141,8 +140,6 @@ export default class GitGateway implements BackendClass {
this.config = config;
this.branch = config.backend.branch?.trim() || 'main';
this.mediaFolder = config.media_folder;
const { use_large_media_transforms_in_media_library: transformImages = true } = config.backend;
this.transformImages = transformImages;
const netlifySiteURL = localStorage.getItem('netlifySiteURL');
this.apiUrl = getEndpoint(config.backend.identity_url || defaults.identity, netlifySiteURL);
@ -440,7 +437,7 @@ export default class GitGateway implements BackendClass {
rootURL: this.netlifyLargeMediaURL,
makeAuthorizedRequest: this.requestFunction,
patterns,
transformImages: this.transformImages ? { nf_resize: 'fit', w: 560, h: 320 } : false,
transformImages: false,
});
},
);

View File

@ -14,7 +14,6 @@ import './components/entry-editor/widgets';
import ErrorBoundary from './components/ErrorBoundary';
import addExtensions from './extensions';
import { getPhrases } from './lib/phrases';
import './mediaLibrary';
import { selectLocale } from './reducers/selectors/config';
import { store } from './store';

View File

@ -9,10 +9,8 @@ import {
changeDraftFieldValidation,
} from '@staticcms/core/actions/entries';
import {
clearMediaControl as clearMediaControlAction,
openMediaLibrary as openMediaLibraryAction,
removeInsertedMedia as removeInsertedMediaAction,
removeMediaControl as removeMediaControlAction,
} from '@staticcms/core/actions/mediaLibrary';
import { query as queryAction } from '@staticcms/core/actions/search';
import useMemoCompare from '@staticcms/core/lib/hooks/useMemoCompare';
@ -41,7 +39,6 @@ import type { ComponentType } from 'react';
import type { ConnectedProps } from 'react-redux';
const EditorControl = ({
clearMediaControl,
collection,
config: configState,
entry,
@ -57,7 +54,6 @@ const EditorControl = ({
parentPath,
query,
removeInsertedMedia,
removeMediaControl,
t,
value,
forList = false,
@ -169,10 +165,8 @@ const EditorControl = ({
locale,
mediaPaths,
onChange: handleChangeDraftField,
clearMediaControl,
openMediaLibrary,
removeInsertedMedia,
removeMediaControl,
path,
query,
t,
@ -203,10 +197,8 @@ const EditorControl = ({
locale,
mediaPaths,
handleChangeDraftField,
clearMediaControl,
openMediaLibrary,
removeInsertedMedia,
removeMediaControl,
path,
query,
finalValue,
@ -254,8 +246,6 @@ function mapStateToProps(state: RootState, ownProps: EditorControlOwnProps) {
const mapDispatchToProps = {
changeDraftField: changeDraftFieldAction,
openMediaLibrary: openMediaLibraryAction,
clearMediaControl: clearMediaControlAction,
removeMediaControl: removeMediaControlAction,
removeInsertedMedia: removeInsertedMediaAction,
query: queryAction,
};

View File

@ -484,7 +484,7 @@ const MediaLibrary: FC<TranslatedProps<MediaLibraryProps>> = ({
px-5
pt-4
`,
config?.media_library_folder_support &&
config?.media_library?.folder_support &&
`
pb-4
border-b
@ -517,7 +517,7 @@ const MediaLibrary: FC<TranslatedProps<MediaLibraryProps>> = ({
placeholder={t('mediaLibrary.mediaLibraryModal.search')}
disabled={!dynamicSearchActive && !hasFilteredFiles}
/>
{config?.media_library_folder_support ? (
{config?.media_library?.folder_support ? (
<div className="flex gap-1.5 items-center">
<IconButton
onClick={handleHome}
@ -565,7 +565,7 @@ const MediaLibrary: FC<TranslatedProps<MediaLibraryProps>> = ({
) : null}
</div>
</div>
{config?.media_library_folder_support ? (
{config?.media_library?.folder_support ? (
<div
className="
flex

View File

@ -164,7 +164,7 @@ const MediaLibraryCardGrid: FC<MediaLibraryCardGridProps> = props => {
overflow-hidden
`,
isDialog && 'rounded-b-lg',
!config?.media_library_folder_support && 'pt-[20px]',
!config?.media_library?.folder_support && 'pt-[20px]',
)}
style={{
width,
@ -180,7 +180,7 @@ const MediaLibraryCardGrid: FC<MediaLibraryCardGridProps> = props => {
rowHeight={() => rowHeightWithGutter}
width={width}
height={
height - (!config?.media_library_folder_support ? MEDIA_LIBRARY_PADDING : 0)
height - (!config?.media_library?.folder_support ? MEDIA_LIBRARY_PADDING : 0)
}
itemData={
{

View File

@ -79,7 +79,6 @@ export const LOAD_ASSET_FAILURE = 'LOAD_ASSET_FAILURE';
// Media Library
export const MEDIA_LIBRARY_OPEN = 'MEDIA_LIBRARY_OPEN';
export const MEDIA_LIBRARY_CLOSE = 'MEDIA_LIBRARY_CLOSE';
export const MEDIA_LIBRARY_CREATE = 'MEDIA_LIBRARY_CREATE';
export const MEDIA_INSERT = 'MEDIA_INSERT';
export const MEDIA_REMOVE_INSERTED = 'MEDIA_REMOVE_INSERTED';
export const MEDIA_LOAD_REQUEST = 'MEDIA_LOAD_REQUEST';

View File

@ -11,7 +11,6 @@ export * from './backends';
export * from './interface';
export * from './lib';
export { default as locales } from './locales';
export * from './media-libraries';
export * from './widgets';
const CMS = {

View File

@ -234,20 +234,6 @@ export type Collection<EF extends BaseField = UnknownField> =
export type Collections<EF extends BaseField = UnknownField> = Record<string, Collection<EF>>;
export interface MediaLibraryInstance {
show: (args: {
id?: string;
value?: string | string[];
config: Record<string, unknown>;
allowMultiple?: boolean;
imagesOnly?: boolean;
}) => void;
hide?: () => void;
onClearControl?: (args: { id: string }) => void;
onRemoveControl?: (args: { id: string }) => void;
enableStandalone: () => boolean;
}
export type MediaFile = BackendMediaFile & { key?: string };
export interface DisplayURLState {
@ -281,13 +267,9 @@ export interface WidgetControlProps<T, F extends BaseField = UnknownField, EV =
mediaPaths: Record<string, MediaPath>;
onChange: (value: T | null | undefined) => void;
// @deprecated Use useMediaInsert instead
clearMediaControl: EditorControlProps['clearMediaControl'];
// @deprecated Use useMediaInsert instead
openMediaLibrary: EditorControlProps['openMediaLibrary'];
// @deprecated Use useMediaInsert instead
removeInsertedMedia: EditorControlProps['removeInsertedMedia'];
// @deprecated Use useMediaInsert instead
removeMediaControl: EditorControlProps['removeMediaControl'];
i18n: I18nSettings | undefined;
hasErrors: boolean;
errors: FieldError[];
@ -530,26 +512,16 @@ export type WidgetValueSerializer = {
deserialize: (value: ValueOrNestedValue) => ValueOrNestedValue;
};
export type MediaLibraryOptions = Record<string, unknown>;
export interface MediaLibraryInitOptions {
options: Record<string, unknown> | undefined;
handleInsert: (url: string | string[]) => void;
}
export interface MediaLibraryExternalLibrary {
name: string;
config?: MediaLibraryOptions;
init: ({ options, handleInsert }: MediaLibraryInitOptions) => Promise<MediaLibraryInstance>;
export interface MediaLibraryConfig {
max_file_size?: number;
folder_support?: boolean;
}
export interface MediaLibraryInternalOptions {
allow_multiple?: boolean;
choose_url?: boolean;
}
export type MediaLibrary = MediaLibraryExternalLibrary | MediaLibraryInternalOptions;
export type BackendType = 'git-gateway' | 'github' | 'gitlab' | 'bitbucket' | 'test-repo' | 'proxy';
export type MapWidgetType = 'Point' | 'LineString' | 'Polygon';
@ -579,9 +551,10 @@ export interface BaseField {
}
export interface MediaField extends BaseField {
media_library?: MediaLibrary;
media_folder?: string;
public_folder?: string;
choose_url?: boolean;
multiple?: boolean;
}
export interface BooleanField extends BaseField {
@ -774,7 +747,6 @@ export interface Backend {
proxy_url?: string;
large_media_url?: string;
login?: boolean;
use_large_media_transforms_in_media_library?: boolean;
identity_url?: string;
gateway_url?: string;
auth_scope?: AuthScope;
@ -810,14 +782,13 @@ export interface Config<EF extends BaseField = UnknownField> {
media_folder?: string;
public_folder?: string;
media_folder_relative?: boolean;
media_library?: MediaLibrary;
media_library?: MediaLibraryConfig;
load_config_file?: boolean;
slug?: Slug;
i18n?: I18nInfo;
local_backend?: boolean | LocalBackend;
editor?: EditorConfig;
search?: boolean;
media_library_folder_support?: boolean;
}
export interface InitOptions<EF extends BaseField = UnknownField> {

View File

@ -36,7 +36,7 @@ export default function useMediaFiles(field?: MediaField, currentFolder?: string
const backend = currentBackend(config);
const files = await backend.getMedia(
currentFolder,
config.media_library_folder_support ?? false,
config.media_library?.folder_support ?? false,
config.public_folder
? trim(currentFolder, '/').replace(trim(config.media_folder!), config.public_folder)
: currentFolder,

View File

@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { v4 as uuid } from 'uuid';
import { openMediaLibrary, removeInsertedMedia } from '@staticcms/core/actions/mediaLibrary';
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
import { selectMediaPath } from '@staticcms/core/reducers/selectors/mediaLibrary';
import { useAppDispatch, useAppSelector } from '@staticcms/core/store/hooks';
@ -28,20 +29,13 @@ export default function useMediaInsert<T extends string | string[], F extends Me
const { controlID, collection, field, forImage = false, insertOptions } = options;
const config = useAppSelector(selectConfig);
const finalControlID = useMemo(() => controlID ?? uuid(), [controlID]);
const mediaPathSelector = useMemo(() => selectMediaPath(finalControlID), [finalControlID]);
const mediaPath = useAppSelector(mediaPathSelector);
const [selected, setSelected] = useState(false);
const mediaLibraryFieldOptions = useMemo(() => {
return field?.media_library ?? {};
}, [field?.media_library]);
const config = useMemo(
() => ('config' in mediaLibraryFieldOptions ? mediaLibraryFieldOptions.config : undefined),
[mediaLibraryFieldOptions],
);
useEffect(() => {
if (!selected && mediaPath && (mediaPath.path !== value.path || mediaPath.alt !== value.alt)) {
setSelected(true);
@ -63,7 +57,7 @@ export default function useMediaInsert<T extends string | string[], F extends Me
alt: value.alt,
replaceIndex,
allowMultiple: false,
config,
config: config?.media_library,
collection,
field,
insertOptions,
@ -77,7 +71,7 @@ export default function useMediaInsert<T extends string | string[], F extends Me
forImage,
value.path,
value.alt,
config,
config?.media_library,
collection,
field,
insertOptions,

View File

@ -13,8 +13,6 @@ import type {
EventListener,
FieldPreviewComponent,
LocalePhrasesRoot,
MediaLibraryExternalLibrary,
MediaLibraryOptions,
ObjectValue,
PreviewStyle,
PreviewStyleOptions,
@ -25,7 +23,7 @@ import type {
Widget,
WidgetOptions,
WidgetParam,
WidgetValueSerializer,
WidgetValueSerializer
} from '../interface';
export const allowedEvents = ['prePublish', 'postPublish', 'preSave', 'postSave'] as const;
@ -45,7 +43,6 @@ interface Registry {
icons: Record<string, CustomIcon>;
additionalLinks: Record<string, AdditionalLink>;
widgetValueSerializers: Record<string, WidgetValueSerializer>;
mediaLibraries: (MediaLibraryExternalLibrary & { options: MediaLibraryOptions })[];
locales: Record<string, LocalePhrasesRoot>;
eventHandlers: typeof eventHandlers;
previewStyles: PreviewStyle[];
@ -66,7 +63,6 @@ const registry: Registry = {
icons: {},
additionalLinks: {},
widgetValueSerializers: {},
mediaLibraries: [],
locales: {},
eventHandlers,
previewStyles: [],
@ -88,8 +84,6 @@ export default {
getWidgetValueSerializer,
registerBackend,
getBackend,
registerMediaLibrary,
getMediaLibrary,
registerLocale,
getLocale,
registerEventListener,
@ -306,25 +300,6 @@ export function getBackend<EF extends BaseField = UnknownField>(
return registry.backends[name] as unknown as BackendInitializer<EF>;
}
/**
* Media Libraries
*/
export function registerMediaLibrary(
mediaLibrary: MediaLibraryExternalLibrary,
options: MediaLibraryOptions = {},
) {
if (registry.mediaLibraries.find(ml => mediaLibrary.name === ml.name)) {
throw new Error(`A media library named ${mediaLibrary.name} has already been registered.`);
}
registry.mediaLibraries.push({ ...mediaLibrary, options });
}
export function getMediaLibrary(
name: string,
): (MediaLibraryExternalLibrary & { options: MediaLibraryOptions }) | undefined {
return registry.mediaLibraries.find(ml => ml.name === name);
}
/**
* Event Handlers
*/

View File

@ -1,130 +0,0 @@
import pick from 'lodash/pick';
import { loadScript } from '@staticcms/core/lib/util';
import type { MediaLibraryInitOptions, MediaLibraryInstance } from '@staticcms/core/interface';
interface GetAssetOptions {
use_secure_url: boolean;
use_transformations: boolean;
output_filename_only: boolean;
}
const defaultOptions = {
use_secure_url: true,
use_transformations: true,
output_filename_only: false,
};
/**
* This configuration hash cannot be overridden, as the values here are required
* for the integration to work properly.
*/
const enforcedConfig = {
button_class: undefined,
inline_container: undefined,
insert_transformation: false,
z_index: '1003',
};
const defaultConfig = {
multiple: false,
};
interface CloudinaryAsset {
public_id: string;
format: string;
secure_url: string;
url: string;
derived?: [
{
secure_url: string;
url: string;
},
];
}
declare global {
interface Window {
cloudinary: {
createMediaLibrary: (
config: Record<string, unknown>,
handlers: { insertHandler: (data: { assets: CloudinaryAsset[] }) => void },
) => {
show: (config: Record<string, unknown>) => void;
hide: () => void;
};
};
}
}
function getAssetUrl(
asset: CloudinaryAsset,
{ use_secure_url, use_transformations, output_filename_only }: GetAssetOptions,
): string {
/**
* Allow output of the file name only, in which case the rest of the url (including)
* transformations) can be handled by the static site generator.
*/
if (output_filename_only) {
return `${asset.public_id}.${asset.format}`;
}
/**
* Get url from `derived` property if it exists. This property contains the
* transformed version of image if transformations have been applied.
*/
const urlObject = asset.derived && use_transformations ? asset.derived[0] : asset;
/**
* Retrieve the `https` variant of the image url if the `useSecureUrl` option
* is set to `true` (this is the default setting).
*/
const urlKey = use_secure_url ? 'secure_url' : 'url';
return urlObject[urlKey];
}
async function init({
options,
handleInsert,
}: MediaLibraryInitOptions): Promise<MediaLibraryInstance> {
/**
* Configuration is specific to Cloudinary, while options are specific to this
* integration.
*/
const { config = {}, ...integrationOptions } = options ?? {};
const providedConfig = config as { multiple?: boolean };
const resolvedOptions = { ...defaultOptions, ...integrationOptions };
const cloudinaryConfig = { ...defaultConfig, ...providedConfig, ...enforcedConfig };
const cloudinaryBehaviorConfigKeys = ['default_transformations', 'max_files', 'multiple'];
const cloudinaryBehaviorConfig = pick(cloudinaryConfig, cloudinaryBehaviorConfigKeys);
await loadScript('https://media-library.cloudinary.com/global/all.js');
function insertHandler(data: { assets: CloudinaryAsset[] }) {
const assets = data.assets.map(asset => getAssetUrl(asset, resolvedOptions));
handleInsert(providedConfig.multiple || assets.length > 1 ? assets : assets[0]);
}
const mediaLibrary = window.cloudinary.createMediaLibrary(cloudinaryConfig, { insertHandler });
return {
show: ({ config: instanceConfig = {}, allowMultiple }) => {
/**
* Ensure multiple selection is not available if the field is configured
* to disallow it.
*/
if (allowMultiple === false) {
instanceConfig.multiple = false;
}
return mediaLibrary.show({ ...cloudinaryBehaviorConfig, ...instanceConfig });
},
hide: () => mediaLibrary.hide(),
enableStandalone: () => true,
};
}
const cloudinaryMediaLibrary = { name: 'cloudinary', init };
export const StaticMediaLibraryCloudinary = cloudinaryMediaLibrary;
export default cloudinaryMediaLibrary;

View File

@ -1,2 +0,0 @@
export { default as MediaLibraryCloudinary } from './cloudinary';
export { default as MediaLibraryUploadcare } from './uploadcare';

View File

@ -1,232 +0,0 @@
import uploadcare from 'uploadcare-widget';
import uploadcareTabEffects from 'uploadcare-widget-tab-effects';
import type { MediaLibraryInitOptions, MediaLibraryInstance } from '@staticcms/core/interface';
declare global {
interface Window {
UPLOADCARE_PUBLIC_KEY: string;
UPLOADCARE_LIVE: boolean;
UPLOADCARE_MANUAL_START: boolean;
}
}
window.UPLOADCARE_LIVE = false;
window.UPLOADCARE_MANUAL_START = true;
const USER_AGENT = 'StaticCms-Uploadcare-MediaLibrary';
const CDN_BASE_URL = 'https://ucarecdn.com';
/**
* Default Uploadcare widget configuration, can be overridden via config.yml.
*/
const defaultConfig = {
previewStep: true,
integration: USER_AGENT,
};
/**
* Determine whether an array of urls represents an unaltered set of Uploadcare
* group urls. If they've been changed or any are missing, a new group will need
* to be created to represent the current values.
*/
function isFileGroup(files: string[]) {
const basePatternString = `~${files.length}/nth/`;
function mapExpression(_val: string, idx: number) {
return new RegExp(`${basePatternString}${idx}/$`);
}
const expressions = Array.from({ length: files.length }, mapExpression);
return expressions.every(exp => files.some(url => exp.test(url)));
}
export interface UploadcareFileGroupInfo {
cdnUrl: string;
name: string;
isImage: boolean;
}
/**
* Returns a fileGroupInfo object wrapped in a promise-like object.
*/
function getFileGroup(files: string[]): Promise<UploadcareFileGroupInfo> {
/**
* Capture the group id from the first file in the files array.
*/
const groupId = new RegExp(`^.+/([^/]+~${files.length})/nth/`).exec(files[0])?.[1];
/**
* The `openDialog` method handles the jQuery promise object returned by
* `fileFrom`, but requires the promise returned by `loadFileGroup` to provide
* the result of it's `done` method.
*/
return new Promise(resolve => uploadcare.loadFileGroup(groupId).done(group => resolve(group)));
}
/**
* Convert a url or array/List of urls to Uploadcare file objects wrapped in
* promises, or Uploadcare groups when possible. Output is wrapped in a promise
* because the value we're returning may be a promise that we created.
*/
function getFiles(
value: string[] | string | undefined,
): Promise<UploadcareFileGroupInfo | UploadcareFileGroupInfo[]> | null {
if (Array.isArray(value)) {
return isFileGroup(value) ? getFileGroup(value) : Promise.all(value.map(val => getFile(val)));
}
return value && typeof value === 'string' ? getFile(value) : null;
}
/**
* Convert a single url to an Uploadcare file object wrapped in a promise-like
* object. Group urls that get passed here were not a part of a complete and
* untouched group, so they'll be uploaded as new images (only way to do it).
*/
function getFile(url: string): Promise<UploadcareFileGroupInfo> {
const groupPattern = /~\d+\/nth\/\d+\//;
const uploaded = url.startsWith(CDN_BASE_URL) && !groupPattern.test(url);
return uploadcare.fileFrom(uploaded ? 'uploaded' : 'url', url);
}
interface OpenDialogOptions {
files:
| UploadcareFileGroupInfo
| UploadcareFileGroupInfo[]
| Promise<UploadcareFileGroupInfo | UploadcareFileGroupInfo[]>
| null;
config: {
multiple: boolean;
imagesOnly: boolean;
previewStep: boolean;
integration: string;
};
handleInsert: (url: string | string[]) => void;
settings: {
defaultOperations?: string;
autoFilename?: boolean;
};
}
/**
* Open the standalone dialog. A single instance is created and destroyed for
* each use.
*/
function openDialog({ files, config, handleInsert, settings = {} }: OpenDialogOptions) {
if (!files) {
return;
}
if (settings.defaultOperations && !settings.defaultOperations.startsWith('/')) {
console.warn(
'Uploadcare default operations should start with `/`. Example: `/preview/-/resize/100x100/image.png`',
);
}
function buildUrl(fileInfo: UploadcareFileGroupInfo) {
const { cdnUrl, name, isImage } = fileInfo;
let url =
isImage && settings.defaultOperations ? `${cdnUrl}-${settings.defaultOperations}` : cdnUrl;
const filenameDefined = !url.endsWith('/');
if (!filenameDefined && settings.autoFilename) {
url = url + name;
}
return url;
}
uploadcare.openDialog(files, config).done(({ promise, files }) => {
const isGroup = Boolean(files);
return promise().then(info => {
if (isGroup) {
return Promise.all(
files().map(promise => promise.then(fileInfo => buildUrl(fileInfo))),
).then(urls => handleInsert(urls));
} else {
handleInsert(buildUrl(info));
}
});
});
}
/**
* Initialization function will only run once, returns an API object for Simple
* CMS to call methods on.
*/
async function init({
options = { config: {}, settings: {} },
handleInsert,
}: MediaLibraryInitOptions): Promise<MediaLibraryInstance> {
const { publicKey, ...globalConfig } = options.config as {
publicKey: string;
} & Record<string, unknown>;
const baseConfig = { ...defaultConfig, ...globalConfig };
window.UPLOADCARE_PUBLIC_KEY = publicKey;
/**
* Register the effects tab by default because the effects tab is awesome. Can
* be disabled via config.
*/
uploadcare.registerTab('preview', uploadcareTabEffects);
return {
/**
* On show, create a new widget, cache it in the widgets object, and open.
* No hide method is provided because the widget doesn't provide it.
*/
show: ({ value, config: instanceConfig = {}, allowMultiple, imagesOnly = false }) => {
const config = { ...baseConfig, imagesOnly, ...instanceConfig } as {
imagesOnly: boolean;
previewStep: boolean;
integration: string;
multiple?: boolean;
};
const multiple = allowMultiple === false ? false : Boolean(config.multiple);
const resolvedConfig = { ...config, multiple };
const files = getFiles(value);
/**
* Resolve the promise only if it's ours. Only the jQuery promise objects
* from the Uploadcare library will have a `state` method.
*/
if (files && !('state' in files)) {
return files.then(result =>
openDialog({
files: result,
config: resolvedConfig,
settings: options.settings as Record<string, unknown>,
handleInsert,
}),
);
} else {
return openDialog({
files,
config: resolvedConfig,
settings: options.settings as Record<string, unknown>,
handleInsert,
});
}
},
/**
* Uploadcare doesn't provide a "media library" widget for viewing and
* selecting existing files, so we return `false` here so Static CMS only
* opens the Uploadcare widget when called from an editor control. This
* results in the "Media" button in the global nav being hidden.
*/
enableStandalone: () => false,
};
}
/**
* The object that will be registered only needs a (default) name and `init`
* method. The `init` method returns the API object.
*/
const uploadcareMediaLibrary = { name: 'uploadcare', init };
export const StaticMediaLibraryUploadcare = uploadcareMediaLibrary;
export default uploadcareMediaLibrary;

View File

@ -1,52 +0,0 @@
/**
* This module is currently concerned only with external media libraries
* registered via `registerMediaLibrary`.
*/
import once from 'lodash/once';
import { configFailed } from './actions/config';
import { createMediaLibrary, insertMedia } from './actions/mediaLibrary';
import { getMediaLibrary } from './lib/registry';
import { store } from './store';
import type { MediaLibrary, MediaLibraryExternalLibrary } from './interface';
import type { RootState } from './store';
function handleInsert(url: string | string[]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return store.dispatch(insertMedia(url, undefined));
}
const initializeMediaLibrary = once(async function initializeMediaLibrary(
name: string,
{ config }: MediaLibraryExternalLibrary,
) {
const lib = getMediaLibrary(name);
if (!lib) {
const err = new Error(
`Missing external media library '${name}'. Please use 'registerMediaLibrary' to register it.`,
);
store.dispatch(configFailed(err));
} else {
const instance = await lib.init({ options: config, handleInsert });
store.dispatch(createMediaLibrary(instance));
}
});
function isExternalMediaLibraryConfig(
config: MediaLibrary | undefined,
): config is MediaLibraryExternalLibrary {
return Boolean(config && 'name' in config);
}
store.subscribe(() => {
const state = store.getState() as unknown as RootState;
if (state.config.config && isExternalMediaLibraryConfig(state.config.config.media_library)) {
const mediaLibraryName = state.config.config.media_library?.name;
if (mediaLibraryName && !state.mediaLibrary.externalLibrary) {
const mediaLibraryConfig = state.config.config.media_library;
initializeMediaLibrary(mediaLibraryName, mediaLibraryConfig);
}
}
});

View File

@ -10,7 +10,6 @@ import {
MEDIA_DISPLAY_URL_SUCCESS,
MEDIA_INSERT,
MEDIA_LIBRARY_CLOSE,
MEDIA_LIBRARY_CREATE,
MEDIA_LIBRARY_OPEN,
MEDIA_LOAD_FAILURE,
MEDIA_LOAD_REQUEST,
@ -27,8 +26,8 @@ import type {
Field,
MediaFile,
MediaLibrarInsertOptions,
MediaLibraryConfig,
MediaLibraryDisplayURL,
MediaLibraryInstance,
MediaPath,
} from '../interface';
@ -37,11 +36,10 @@ export type MediaLibraryState = {
showMediaButton: boolean;
controlMedia: Record<string, MediaPath>;
displayURLs: Record<string, MediaLibraryDisplayURL>;
externalLibrary?: MediaLibraryInstance;
controlID?: string;
page?: number;
files?: MediaFile[];
config: Record<string, unknown>;
config: MediaLibraryConfig;
collection?: Collection;
field?: Field;
value?: string | string[];
@ -72,13 +70,6 @@ function mediaLibrary(
action: MediaLibraryAction,
): MediaLibraryState {
switch (action.type) {
case MEDIA_LIBRARY_CREATE:
return {
...state,
externalLibrary: action.payload,
showMediaButton: action.payload.enableStandalone(),
};
case MEDIA_LIBRARY_OPEN: {
const {
controlID,

View File

@ -121,7 +121,7 @@ describe('File Control', () => {
it('should show only the choose upload and choose url buttons by default when choose url is true', () => {
const { getByTestId, queryByTestId } = renderControl({
label: 'I am a label',
field: { ...mockFileField, media_library: { choose_url: true } },
field: { ...mockFileField, choose_url: true },
});
expect(getByTestId('choose-upload')).toBeInTheDocument();
@ -147,7 +147,7 @@ describe('File Control', () => {
it('should show the add/replace upload, replace url and remove buttons by there is a value and choose url is true', () => {
const { getByTestId, queryByTestId } = renderControl({
label: 'I am a label',
field: { ...mockFileField, media_library: { choose_url: true } },
field: { ...mockFileField, choose_url: true },
value: 'https://example.com/file.pdf',
});
@ -241,7 +241,7 @@ describe('File Control', () => {
it('should show only the choose upload and choose url buttons by default when choose url is true', () => {
const { getByTestId } = renderControl({
label: 'I am a label',
field: { ...mockFileField, media_library: { choose_url: true } },
field: { ...mockFileField, choose_url: true },
disabled: true,
});
@ -263,7 +263,7 @@ describe('File Control', () => {
it('should show the add/replace upload, replace url and remove buttons by there is a value and choose url is true', () => {
const { getByTestId } = renderControl({
label: 'I am a label',
field: { ...mockFileField, media_library: { choose_url: true } },
field: { ...mockFileField, choose_url: true },
value: 'https://example.com/file.pdf',
disabled: true,
});

View File

@ -1,4 +1,4 @@
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
import Button from '@staticcms/core/components/common/button/Button';
import Field from '@staticcms/core/components/common/field/Field';
@ -8,6 +8,8 @@ import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
import useUUID from '@staticcms/core/lib/hooks/useUUID';
import { basename } from '@staticcms/core/lib/util';
import { isEmpty } from '@staticcms/core/lib/util/string.util';
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
import { useAppSelector } from '@staticcms/core/store/hooks';
import SortableImage from './components/SortableImage';
import type {
@ -49,8 +51,6 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
duplicate,
onChange,
openMediaLibrary,
clearMediaControl,
removeMediaControl,
hasErrors,
disabled,
t,
@ -82,31 +82,13 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
handleOnChange,
);
useEffect(() => {
return () => {
removeMediaControl(controlID);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const mediaLibraryFieldOptions = useMemo(() => {
return field.media_library ?? {};
}, [field.media_library]);
const config = useMemo(
() => ('config' in mediaLibraryFieldOptions ? mediaLibraryFieldOptions.config : undefined),
[mediaLibraryFieldOptions],
);
const config = useAppSelector(selectConfig);
const allowsMultiple = useMemo(() => {
return config?.multiple ?? false;
}, [config?.multiple]);
return field.multiple ?? false;
}, [field.multiple]);
const chooseUrl = useMemo(
() =>
'choose_url' in mediaLibraryFieldOptions && (mediaLibraryFieldOptions.choose_url ?? true),
[mediaLibraryFieldOptions],
);
const chooseUrl = useMemo(() => field.choose_url ?? false, [field.choose_url]);
const handleUrl = useCallback(
(subject: 'image' | 'file') => (e: MouseEvent) => {
@ -123,10 +105,9 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
(e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
clearMediaControl(controlID);
handleOnChange({ path: '' });
},
[clearMediaControl, controlID, handleOnChange],
[handleOnChange],
);
const onRemoveOne = useCallback(
@ -148,7 +129,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
value: internalValue,
replaceIndex: index,
allowMultiple: false,
config,
config: config?.media_library,
collection: collection as Collection<BaseField>,
field,
});

View File

@ -45,14 +45,7 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
onMediaToggle?.(false);
});
const mediaLibraryFieldOptions = useMemo(() => {
return field.media_library ?? {};
}, [field.media_library]);
const chooseUrl = useMemo(
() => 'choose_url' in mediaLibraryFieldOptions && (mediaLibraryFieldOptions.choose_url ?? true),
[mediaLibraryFieldOptions],
);
const chooseUrl = useMemo(() => field.choose_url ?? false, [field.choose_url]);
const handleFocus = useCallback(() => {
onFocus?.();

View File

@ -20,10 +20,8 @@ export const createMockWidgetControlProps = <
| 'data'
| 'hasErrors'
| 'onChange'
| 'clearMediaControl'
| 'openMediaLibrary'
| 'removeInsertedMedia'
| 'removeMediaControl'
| 'query'
| 't'
> &
@ -73,10 +71,8 @@ export const createMockWidgetControlProps = <
hidden: false,
theme: 'light',
onChange: jest.fn(),
clearMediaControl: jest.fn(),
openMediaLibrary: jest.fn(),
removeInsertedMedia: jest.fn(),
removeMediaControl: jest.fn(),
query: jest.fn(),
t: jest.fn(),
...extra,