Merge branch 'next' of https://github.com/StaticJsCMS/static-cms into next
This commit is contained in:
commit
da4efbbc44
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
);
|
||||
}),
|
||||
|
@ -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,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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={
|
||||
{
|
||||
|
@ -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';
|
||||
|
@ -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 = {
|
||||
|
@ -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> {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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;
|
@ -1,2 +0,0 @@
|
||||
export { default as MediaLibraryCloudinary } from './cloudinary';
|
||||
export { default as MediaLibraryUploadcare } from './uploadcare';
|
@ -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;
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
@ -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,
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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?.();
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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.
|
@ -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>
|
||||
|
@ -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 |
|
||||
|
@ -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.
|
@ -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>
|
@ -21,10 +21,6 @@
|
||||
"name": "Widgets",
|
||||
"title": "Widgets"
|
||||
},
|
||||
{
|
||||
"name": "Media",
|
||||
"title": "Media"
|
||||
},
|
||||
{
|
||||
"name": "Customization",
|
||||
"title": "Customizing Static CMS"
|
||||
|
Loading…
x
Reference in New Issue
Block a user