This commit is contained in:
Daniel Lautzenheiser 2023-04-13 11:15:56 -04:00
commit da4efbbc44
32 changed files with 161 additions and 1059 deletions

View File

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

@ -2,6 +2,7 @@ import React, { useCallback, useMemo } from 'react';
import { Waypoint } from 'react-waypoint';
import { selectFields, selectInferredField } from '@staticcms/core/lib/util/collection.util';
import { toTitleCaseFromKey } from '@staticcms/core/lib/util/string.util';
import Table from '../../common/table/Table';
import EntryCard from './EntryCard';
@ -120,11 +121,30 @@ const EntryListing = ({
});
}, [entries, inferFields, isSingleCollectionInList, otherProps, summaryFields, viewStyle]);
const summaryFieldHeaders = useMemo(() => {
if ('collection' in otherProps) {
const collectionFields = selectFields(otherProps.collection).reduce((acc, f) => {
acc[f.name] = f;
return acc;
}, {} as Record<string, Field>);
return summaryFields.map(summaryField => {
const field = collectionFields[summaryField];
return !field
? toTitleCaseFromKey(summaryField)
: field.label ?? toTitleCaseFromKey(field.name);
});
}
return [];
}, [otherProps, summaryFields]);
if (viewStyle === 'VIEW_STYLE_LIST') {
return (
<>
<Table
columns={!isSingleCollectionInList ? ['Collection', ...summaryFields] : summaryFields}
columns={
!isSingleCollectionInList ? ['Collection', ...summaryFieldHeaders] : summaryFieldHeaders
}
>
{renderedCards}
</Table>

View File

@ -13,7 +13,7 @@ const TableCell = ({ columns, children }: TableCellProps) => {
return (
<div className="relative overflow-x-auto shadow-md sm:rounded-lg border border-slate-200 dark:border-gray-700">
<table className="w-full text-sm text-left text-gray-500 dark:text-gray-300 ">
<thead className="text-xs text-gray-700 uppercase bg-slate-50 dark:bg-slate-700 dark:text-gray-300">
<thead className="text-xs text-gray-700 bg-slate-50 dark:bg-slate-700 dark:text-gray-300">
<tr>
{columns.map((column, index) => (
<TableHeaderCell key={index}>{column}</TableHeaderCell>

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,

View File

@ -92,7 +92,6 @@ Example config:
widget: 'image'
default: '/uploads/chocolate-dogecoin.jpg'
media_library:
config:
max_file_size: 512000 # in bytes, only for default media library
```
@ -103,10 +102,8 @@ Example config:
widget: 'image',
default: '/uploads/chocolate-dogecoin.jpg',
media_library: {
config: {
max_file_size: 512000 // in bytes, only for default media library
},
},
},
```
@ -197,11 +194,3 @@ See [Gitea Backend](/docs/gitea-backend) for more information.
Adds support for viewing subfolders and creating new subfolders in the media library, under your configured `media_folder`.
See [Media Library Folders](/docs/configuration-options#media-library-folders) for more information.
## External Media Library
Using an external media library allows you to store your media files outside of your git backend. This is helpful if you are trying to store large media files. There are three external media libraries available in beta:
- [Cloudinary](/docs/cloudinary)
- [Netlify Large Media](/docs/netlify-large-media)
- [Uploadcare](/docs/uploadcare)

View File

@ -1,235 +0,0 @@
---
group: Media
title: Cloudinary
weight: 10
beta: true
---
Cloudinary is a digital asset management platform with a broad feature set, including support for responsive image generation and url based image transformation. They also provide a powerful media library UI for managing assets, and tools for organizing your assets into a hierarchy.
The Cloudinary media library integration for Static CMS uses Cloudinary's own media library interface within Static CMS. To get started, you'll need a Cloudinary account and Static CMS 2.3.0 or greater.
## Creating a Cloudinary Account
You can [sign up for Cloudinary](https://cloudinary.com/users/register/free) for free. Once you're logged in, you'll need to retrieve your Cloud name and API key from the upper left corner of the Cloudinary console.
![Cloudinary console screenshot](/img/cloudinary-console-details.webp)
## Connecting Cloudinary
To use the Cloudinary media library within Static CMS, you'll need to update your Static CMS configuration file with the information from your Cloudinary account:
<CodeTabs>
```yaml
media_library:
name: cloudinary
config:
cloud_name: your_cloud_name
api_key: your_api_key
```
```js
media_library: {
name: 'cloudinary',
config: {
cloud_name: 'your_cloud_name',
api_key: 'your_api_key'
},
},
```
</CodeTabs>
**Note:** The user must be logged in to the Cloudinary account connected to the `api_key` used in your Static CMS configuration.
### Security Considerations
Although this setup exposes the `cloud_name` and `api_key` publicly via the `config` endpoint, this information is not sensitive. Any integration of the Cloudinary media library requires this information to be exposed publicly. To use this library or use the restricted Cloudinary API endpoints, the user must have access to the Cloudinary account login details or the `api_secret` associated with the `cloud_name` and `api_key`.
## Static CMS configuration options
The following options are specific to the Static CMS integration for Cloudinary:
- **`output_filename_only`**: _(default: `false`)_\
By default, the value provided for a selected image is a complete URL for the asset on Cloudinary's CDN. Setting `output_filename_only` to `true` will instead produce just the filename (e.g. `image.jpg`). This should be `true` if you will be directly embedding cloudinary transformation urls in page templates. Refer to [Inserting Cloudinary URL in page templates](#inserting-cloudinary-url-in-page-templates).
- **`use_transformations`**: _(default: `true`)_\
If `true`, uses derived url when available (the url will have image transformation segments included). Has no effect if `output_filename_only` is set to `true`.
- **`use_secure_url`**: _(default: `true`)_\
Controls whether an `http` or `https` URL is provided. Has no effect if `output_filename_only` is set to `true`.
## Cloudinary configuration options
The following options are used to configure the media library. All options are listed in Cloudinary's [media library documentation](https://cloudinary.com/documentation/media_library_widget#3_set_the_configuration_options), but only options listed below are available or recommended for the Static CMS integration:
### Authentication
- `cloud_name`
- `api_key`
### Media library behavior
- `default_transformations` _\- only the first [image transformation](#image-transformations) is used, be sure to use the `SDK Parameter` column transformation names from the_ [_transformation reference_](https://cloudinary.com/documentation/image_transformation_reference)
- `max_files` _\- has no impact on images inside the [markdown widget](/docs/widgets/#markdown)_. Refer to [media library documentation](https://cloudinary.com/documentation/media_library_widget#3_set_the_configuration_options) for details on this property
- `multiple` _\- has no impact on images inside the [markdown widget](/docs/widgets/#markdown)_. Refer to [media library documentation](https://cloudinary.com/documentation/media_library_widget#3_set_the_configuration_options) for details on this property
## Image transformations
The Cloudinary integration allows images to be transformed in two ways: directly within Static CMS via [Cloudinary's Media Library](#transforming-images-via-media-library), and separately from Static CMS via Cloudinary's [dynamic URL's](https://cloudinary.com/documentation/image_transformations#delivering_media_assets_using_dynamic_urls) by [inserting cloudinary urls](#inserting-cloudinary-url-in-page-templates).
### Transforming Images
If you transform and insert images from within the Cloudinary media library, the transformed image URL will be output by default. This gives the editor complete freedom to make changes to the image output.
There are two ways to configure image transformation via media library - [globally](#global-configuration) and per [field](#field-configuration). Global options will be overridden by field options.
#### Global configuration
Global configuration, which is meant to affect the Cloudinary widget at all times, can be provided
as seen below, under the primary `media_library` property. Settings applied here will affect every
instance of the Cloudinary widget.
<CodeTabs>
```yaml
# global
media_library:
name: cloudinary
output_filename_only: false
config:
default_transformations:
- - fetch_format: auto
width: 160
quality: auto
crop: scale
```
```js
// global
media_library: {
name: 'cloudinary',
output_filename_only: false,
config: {
default_transformations: [
[
{
fetch_format: 'auto',
width: 160,
quality: 'auto',
crop: 'scale'
}
],
],
},
},
```
</CodeTabs>
#### Field configuration
Configuration can also be provided for individual fields that use the media library. The structure
is very similar to the global configuration, except the settings are added to an individual `field`.
For example:
<CodeTabs>
```yaml
# field
fields: # The fields each document in this collection have
- label: 'Cover Image'
name: 'image'
widget: 'image'
required: false
tagtitle: ''
media_library:
config:
default_transformations:
- fetch_format: auto
width: 300
quality: auto
crop: fill
effect: grayscale
```
```js
// field
fields: [
{
label: 'Cover Image',
name: 'image',
widget: 'image',
required: false,
tagtitle: '',
media_library: {
config: {
default_transformations: [
{
fetch_format: 'auto',
width: 300,
quality: 'auto',
crop: 'fill',
effect: 'grayscale',
},
],
},
},
},
],
```
</CodeTabs>
## Inserting Cloudinary URL in page templates
If you prefer to provide direction so that images are transformed in a specific way, or dynamically retrieve images based on viewport size, you can do so by providing your own base Cloudinary URL and only storing the asset filenames in your content:
- Either globally or for specific fields, configure the Cloudinary extension to only output the asset filename
**Global**
<CodeTabs>
```yaml
# global
media_library:
name: cloudinary
output_filename_only: true
```
```js
// global
media_library: {
name: 'cloudinary',
output_filename_only: true,
},
```
</CodeTabs>
**Field**
<CodeTabs>
```yaml
# field
media_library:
name: cloudinary
output_filename_only: true
```
```js
// field
media_library: {
name: 'cloudinary',
output_filename_only: true,
},
```
</CodeTabs>
- Provide a dynamic URL in the site template
```handlebars
{{! handlebars example }}
<img
src='https://res.cloudinary.com/<cloud_name>/<resource_type>/<type>/<transformations>/{{image}}'
/>
```
Your dynamic URL can be formed conditionally to provide any desired transformations - please see Cloudinary's [image transformation reference](https://cloudinary.com/documentation/image_transformation_reference) for available transformations.

View File

@ -67,22 +67,7 @@ Based on the settings above, if a user used an image widget field called `avatar
This setting can be set to an absolute URL e.g. `https://netlify.com/media` should you wish, however in general this is not advisable as content should have relative paths to other content.
## Media Library Folders <BetaImage />
The `media_library_folder_support` flag enables support for viewing subfolders and creating new subfolders in the media library, under your configured `media_folder`.
<CodeTabs>
```yaml
media_library_folder_support: true
```
```js
media_library_folder_support: true,
```
</CodeTabs>
## Media Library Integrations <BetaImage />
## Media Library
Media library integrations are configured via the `media_library` property, and its value should be an object with at least a `name` property. A `config` property can also be used for options that should be passed to the library in use.
@ -91,18 +76,39 @@ Media library integrations are configured via the `media_library` property, and
<CodeTabs>
```yaml
media_library:
name: uploadcare
config:
publicKey: demopublickey
choose_url: true,
max_file_size: 512000
folder_support: true
```
```js
media_library: {
name: 'uploadcare',
config: {
publicKey: 'demopublickey'
{
media_library: {
choose_url: "true,",
max_file_size: 512000,
folder_support: true
}
},
}
```
</CodeTabs>
### Media Library Folders <BetaImage />
The `folder_support` flag enables support for viewing subfolders and creating new subfolders in the media library, under your configured `media_folder`.
<CodeTabs>
```yaml
media_library:
folder_support: true
```
```js
{
media_library: {
folder_support: true;
}
}
```
</CodeTabs>

View File

@ -190,6 +190,60 @@ collections:
</CodeTabs>
## External Media Libraries
External media integrations (Cloudinary, Netlify Large Media and Uploadcare) have been removed as part of an ongoing to effect to narrow the focus of Static CMS. With [Decap](https://decapcms.org/) (previously Netlify CMS) being supported again its not as critical for Static CMS to support every possible option.
The external media integrations have been broken for sometime, and would require a lot of effort to get them back to the level already available in Decap. So, in order to focus our efforts on other features, it has been decided to remove all external media integrations.
This brings with it some breaking changes for the `media_library` config property.
**Old Config**
<CodeTabs>
```yaml
media_library:
choose_url: true
config:
max_file_size: 512000
```
```js
{
media_library: {
choose_url: true,
config: {
max_file_size: 512000
}
}
}
```
</CodeTabs>
**New Config**
<CodeTabs>
```yaml
media_library:
choose_url: true
max_file_size: 512000
```
```js
{
media_library: {
choose_url: true,
max_file_size: 512000
}
}
```
</CodeTabs>
Also the `clearMediaControl` and `removeMediaControl` widget control props have been removed as they were only used for the external media library integrations.
## Other Breaking Changes
- [Card previews](/docs/custom-previews#collection-card-preview) now are only used for the card view. The `viewStyle` property has been removed. [Field previews](/docs/custom-previews#field-preview) can be used to change the table view.
@ -210,9 +264,7 @@ collections:
In the Widget Control component property, the following properties have been deprecated. They will be removed in `v3.0.0`.
| Param | Type | Description |
| ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| mediaPaths | object | <Deprecated>Use [useMediaInsert](/docs/custom-widgets#interacting-with-the-media-library) instead.</Deprecated> Key/value object of control IDs (passed to the media library) mapping to media paths |
| clearMediaControl | function | <Deprecated>Use [useMediaInsert](/docs/custom-widgets#interacting-with-the-media-library) instead.</Deprecated> Clears a control ID's value from the internal store |
| openMediaLibrary | function | <Deprecated>Use [useMediaInsert](/docs/custom-widgets#interacting-with-the-media-library) instead.</Deprecated> Opens the media library popup. See [Open Media Library](#open-media-library) |
| removeInsertedMedia | function | <Deprecated>Use [useMediaInsert](/docs/custom-widgets#interacting-with-the-media-library) instead.</Deprecated> Removes draft media for a give control ID |
| removeMediaControl | function | <Deprecated>Use [useMediaInsert](/docs/custom-widgets#interacting-with-the-media-library) instead.</Deprecated> Clears a control ID completely from the internal store |

View File

@ -1,31 +0,0 @@
---
group: Media
title: Netlify Large Media
weight: 20
beta: true
---
[Netlify Large Media](https://www.netlify.com/features/large-media/) is a [Git LFS](https://git-lfs.github.com/) implementation for repositories connected to Netlify sites. This means that you can use Git to work with large asset files like images, audio, and video, without bloating your repository. It does this by replacing the asset files in your repository with text pointer files, then uploading the assets to the Netlify Large Media storage service.
If you have a Netlify site with Large Media enabled, Static CMS will handle Large Media asset files seamlessly, in the same way as files stored directly in the repository.
## Requirements
To use Netlify Large Media with Static CMS, you will need to do the following:
- Configure Static CMS to use the [Git Gateway backend with Netlify Identity](/docs/git-gateway-backend).
- Configure the Netlify site and connected repository to use Large Media, following the [Large Media docs on Netlify](https://www.netlify.com/docs/large-media/).
When these are complete, you can use Static CMS as normal, and the configured asset files will automatically be handled by Netlify Large Media.
## Image transformations
All JPEG, PNG, and GIF files that are handled with Netlify Large Media also have access to Netlify's on-demand image transformation service. This service allows you to request an image to match the dimensions you specify in a query parameter added to the image URL.
You can learn more about this feature in [Netlify's image transformation docs](https://www.netlify.com/docs/image-transformation/).
### Media Gallery Thumbnails
In repositories enabled with Netlify Large Media, Static CMS will use the image transformation query parameters to load thumbnail-sized images for the media gallery view. This makes images in the media gallery load significantly faster.
**Note:** When using this option all tracked file types have to be imported into Large Media. For example if you track `*.jpg` but still have jpg-files that are not imported into Large Media the backend will throw an error. Check the [netlify docs](https://docs.netlify.com/large-media/setup/#migrate-files-from-git-history) on how to add previously committed files to Large Media.

View File

@ -1,121 +0,0 @@
---
group: Media
title: Uploadcare
weight: 30
beta: true
---
Uploadcare is a sleek service that allows you to upload files without worrying about maintaining a growing collection — more of an asset store than a library. Just upload when you need to, and the files are hosted on their CDN. They provide image processing controls from simple cropping and rotation to filters and face detection, and a lot more. You can check out Uploadcare's full feature set on their [website](https://uploadcare.com/).
The Uploadcare media library integration for Static CMS allows you to use Uploadcare as your media handler within Static CMS itself. It's available by default as of our 2.1.0 release, and works in tandem with the existing file and image widgets, so using it only requires creating an Uploadcare account and updating your Static CMS configuration.
**Please make sure that Static CMS is updated to 2.1.0 or greater before proceeding.**
## Creating an Uploadcare Account
You can [sign up](https://uploadcare.com/accounts/signup/) for a free Uploadcare account to get started. Once you've signed up, go to your dashboard, select a project, and then select "API keys" from the menu on the left. The public key on the API keys page will be needed in your Static CMS configuration. For more info on getting your API key, visit their [walkthrough](https://uploadcare.com/docs/keys/).
## Updating Static CMS Configuration
The next and final step is updating your Static CMS configuration file:
1. Add a `media_library` property at the same level as `media_folder`, with an object as it's value.
2. In the `media_library` object, add the name of the media player under `name`.
3. Add a `config` object under name with a `publicKey` property with your Uploadcare public key as it's value.
Your `config` should now include something like this (except with a real API key):
<CodeTabs>
```yaml
media_library:
name: uploadcare
config:
publicKey: YOUR_UPLOADCARE_PUBLIC_KEY
```
```js
media_library: {
name: 'uploadcare',
config: {
publicKey: 'YOUR_UPLOADCARE_PUBLIC_KEY',
},
},
```
</CodeTabs>
Once you've finished updating your Static CMS configuration, the Uploadcare widget will appear when using the image or file widgets.
**Note:** You'll need to [register the media libraries yourself](/blog/2019/07/netlify-cms-gatsby-plugin-4-0-0#using-media-libraries-with-netlify-cms-app).
## Configuring the Uploadcare Widget
The Uploadcare widget can be configured with settings that are outlined [in their docs](https://uploadcare.com/docs/file_uploads/widget/options/). The widget itself accepts configuration through global variables and data properties on HTML elements, but with Static CMS you can pass configuration options directly through your `config`.
**Note:** all default values described in Uploadcare's documentation also apply in the Static CMS integration, except for `previewStep`, which is set to `true`. This was done because the preview step provides helpful information like upload status, and provides access to image editing controls. This option can be disabled through the configuration options below.
### Global configuration
Global configuration, which is meant to affect the Uploadcare widget at all times, can be provided as seen above, under the primary `media_library` property. Settings applied here will affect every instance of the Uploadcare widget.
## Field configuration
Configuration can also be provided for individual fields that use the media library. The structure is very similar to the global configuration, except the settings are added to an individual `field`. For example:
<CodeTabs>
```yaml
fields:
name: cover
label: Cover Image
widget: image
media_library:
config:
multiple: true
previewStep: false
```
```js
fields: {
name: 'cover',
label: 'Cover Image',
widget: 'image',
media_library: {
config: {
multiple: true,
previewStep: false,
},
},
},
```
</CodeTabs>
## Integration settings
There are several settings that control the behavior of integration with the widget.
* `autoFilename` (`boolean`) - specify whether to add a filename to the end of the url. Example: `http://ucarecdn.com/:uuid/filename.png`
* `defaultOperations` (`string`) - specify a string added at the end of the url. This could be useful to apply a set of CDN operations to each image, for example resizing or compression. All the possible operations are listed [here](https://uploadcare.com/docs/api_reference/cdn/).
<CodeTabs>
```yaml
media_library:
name: uploadcare
config:
publicKey: YOUR_UPLOADCARE_PUBLIC_KEY
settings:
autoFiletitle: true
defaultOperations: '/resize/800x600/'
```
```js
media_library: {
name: 'uploadcare',
config: {
publicKey: 'YOUR_UPLOADCARE_PUBLIC_KEY',
},
settings: {
autoFiletitle: true,
defaultOperations: '/resize/800x600/',
},
},
```
</CodeTabs>

View File

@ -21,10 +21,6 @@
"name": "Widgets",
"title": "Widgets"
},
{
"name": "Media",
"title": "Media"
},
{
"name": "Customization",
"title": "Customizing Static CMS"