feat: media library cleanup (#699)

This commit is contained in:
Daniel Lautzenheiser 2023-04-13 13:27:13 -04:00 committed by GitHub
parent da4efbbc44
commit d599895679
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 332 additions and 275 deletions

View File

@ -2,7 +2,8 @@ backend:
name: test-repo
site_url: 'https://example.com'
media_folder: assets/uploads
media_library_folder_support: true
media_library:
folder_support: true
locale: en
i18n:
# Required and can be one of multiple_folders, multiple_files or single_file
@ -142,6 +143,7 @@ collections:
label: Pattern Validation
widget: code
pattern: ['.{12,}', 'Must have at least 12 characters']
allow_input: true
required: false
- name: language
label: Language Selection
@ -279,12 +281,13 @@ collections:
label: Choose URL
widget: file
required: false
media_library:
choose_url: true
- name: image
label: Image
file: _widgets/image.json
description: Image widget
media_library:
folder_support: false
fields:
- name: required
label: Required Validation
@ -302,8 +305,12 @@ collections:
label: Choose URL
widget: image
required: false
media_library:
choose_url: true
- name: folder_support
label: Folder Support
widget: image
media_library:
folder_support: true
- name: list
label: List
file: _widgets/list.yml

View File

@ -19,39 +19,26 @@ import type { AnyAction } from 'redux';
import type { ThunkDispatch } from 'redux-thunk';
import type {
BaseField,
Collection,
CollectionFile,
Config,
Field,
I18nInfo,
ListField,
LocalBackend,
ObjectField,
UnknownField,
} from '../interface';
import type { RootState } from '../store';
function isObjectField<F extends BaseField = UnknownField>(
field: Field<F>,
): field is ObjectField<F> {
return 'fields' in (field as ObjectField);
}
function isFieldList<F extends BaseField = UnknownField>(field: Field<F>): field is ListField<F> {
return 'types' in (field as ListField) || 'field' in (field as ListField);
}
function traverseFieldsJS<F extends BaseField = UnknownField>(
fields: F[],
updater: <T extends BaseField = UnknownField>(field: T) => T,
): F[] {
function traverseFields(fields: Field[], updater: (field: Field) => Field): Field[] {
return fields.map(field => {
const newField = updater(field);
if (isObjectField(newField)) {
return { ...newField, fields: traverseFieldsJS(newField.fields, updater) };
} else if (isFieldList(newField) && newField.types) {
return { ...newField, types: traverseFieldsJS(newField.types, updater) };
if ('fields' in newField && newField.fields) {
return { ...newField, fields: traverseFields(newField.fields, updater) } as Field;
} else if (newField.widget === 'list' && newField.types) {
return { ...newField, types: traverseFields(newField.types, updater) } as Field;
}
return newField;
return newField as Field;
});
}
@ -68,13 +55,22 @@ function getConfigUrl() {
return 'config.yml';
}
function setDefaultPublicFolderForField<T extends BaseField = UnknownField>(field: T) {
const setFieldDefaults =
(collection: Collection, collectionFile?: CollectionFile) => (field: Field) => {
if ('media_folder' in field && !('public_folder' in field)) {
return { ...field, public_folder: field.media_folder };
}
return field;
if (field.widget === 'image' || field.widget === 'file') {
field.media_library = {
...((collectionFile ?? collection).media_library ?? {}),
...(field.media_library ?? {}),
};
}
return field;
};
function setI18nField<T extends BaseField = UnknownField>(field: T) {
if (field[I18N] === true) {
return { ...field, [I18N]: I18N_FIELD_TRANSLATE };
@ -100,9 +96,9 @@ function getI18nDefaults(collectionOrFileI18n: boolean | I18nInfo, defaultI18n:
function setI18nDefaultsForFields(collectionOrFileFields: Field[], hasI18n: boolean) {
if (hasI18n) {
return traverseFieldsJS(collectionOrFileFields, setI18nField);
return traverseFields(collectionOrFileFields, setI18nField);
} else {
return traverseFieldsJS(collectionOrFileFields, field => {
return traverseFields(collectionOrFileFields, field => {
const newField = { ...field };
delete newField[I18N];
return newField;
@ -174,6 +170,11 @@ export function applyDefaults(originalConfig: Config) {
collection.editor = { preview: config.editor.preview, frame: config.editor.frame };
}
collection.media_library = {
...(config.media_library ?? {}),
...(collection.media_library ?? {}),
};
if (i18n && collectionI18n) {
collectionI18n = getI18nDefaults(collectionI18n, i18n);
collection[I18N] = collectionI18n;
@ -199,7 +200,7 @@ export function applyDefaults(originalConfig: Config) {
}
if ('fields' in collection && collection.fields) {
collection.fields = traverseFieldsJS(collection.fields, setDefaultPublicFolderForField);
collection.fields = traverseFields(collection.fields, setFieldDefaults(collection));
}
collection.folder = trim(collection.folder, '/');
@ -215,8 +216,13 @@ export function applyDefaults(originalConfig: Config) {
file.public_folder = file.media_folder;
}
file.media_library = {
...(collection.media_library ?? {}),
...(file.media_library ?? {}),
};
if (file.fields) {
file.fields = traverseFieldsJS(file.fields, setDefaultPublicFolderForField);
file.fields = traverseFields(file.fields, setFieldDefaults(collection, file));
}
let fileI18n = file[I18N];

View File

@ -34,9 +34,11 @@ import type { ThunkDispatch } from 'redux-thunk';
import type {
BaseField,
Collection,
CollectionFile,
DisplayURLState,
Field,
ImplementationMediaFile,
MediaField,
MediaFile,
MediaLibrarInsertOptions,
MediaLibraryConfig,
@ -55,11 +57,11 @@ export function openMediaLibrary<EF extends BaseField = UnknownField>(
replaceIndex?: number;
config?: MediaLibraryConfig;
collection?: Collection<EF>;
collectionFile?: CollectionFile<EF>;
field?: EF;
insertOptions?: MediaLibrarInsertOptions;
} = {},
) {
return (dispatch: ThunkDispatch<RootState, {}, AnyAction>) => {
const {
controlID,
value,
@ -69,12 +71,14 @@ export function openMediaLibrary<EF extends BaseField = UnknownField>(
forImage,
replaceIndex,
collection,
collectionFile,
field,
insertOptions,
} = payload;
dispatch(
mediaLibraryOpened({
return {
type: MEDIA_LIBRARY_OPEN,
payload: {
controlID,
forImage,
value,
@ -83,22 +87,20 @@ export function openMediaLibrary<EF extends BaseField = UnknownField>(
replaceIndex,
config,
collection: collection as Collection,
collectionFile: collectionFile as CollectionFile,
field: field as Field,
insertOptions,
}),
);
};
},
} as const;
}
export function closeMediaLibrary() {
return (dispatch: ThunkDispatch<RootState, {}, AnyAction>) => {
dispatch(mediaLibraryClosed());
};
return { type: MEDIA_LIBRARY_CLOSE } as const;
}
export function insertMedia(
mediaPath: string | string[],
field: Field | undefined,
field: MediaField | undefined,
alt?: string,
currentFolder?: string,
) {
@ -401,25 +403,6 @@ export function loadMediaDisplayURL(file: MediaFile) {
};
}
function mediaLibraryOpened(payload: {
controlID?: string;
forImage?: boolean;
value?: string | string[];
alt?: string;
replaceIndex?: number;
allowMultiple?: boolean;
config?: MediaLibraryConfig;
collection?: Collection;
field?: Field;
insertOptions?: MediaLibrarInsertOptions;
}) {
return { type: MEDIA_LIBRARY_OPEN, payload } as const;
}
function mediaLibraryClosed() {
return { type: MEDIA_LIBRARY_CLOSE } as const;
}
export function mediaInserted(mediaPath: string | string[], alt?: string) {
return { type: MEDIA_INSERT, payload: { mediaPath, alt } } as const;
}
@ -432,7 +415,7 @@ export function mediaLoading(page: number) {
}
export interface MediaOptions {
field?: Field;
field?: MediaField;
page?: number;
canPaginate?: boolean;
dynamicSearch?: boolean;
@ -545,8 +528,8 @@ export async function getMediaDisplayURL(
}
export type MediaLibraryAction = ReturnType<
| typeof mediaLibraryOpened
| typeof mediaLibraryClosed
| typeof openMediaLibrary
| typeof closeMediaLibrary
| typeof mediaInserted
| typeof removeInsertedMedia
| typeof mediaLoading

View File

@ -57,9 +57,9 @@ import type {
Entry,
EntryData,
EntryDraft,
Field,
FilterRule,
ImplementationEntry,
MediaField,
PersistArgs,
SearchQueryResponse,
SearchResponse,
@ -247,7 +247,7 @@ export interface MediaFile {
draft?: boolean;
url?: string;
file?: File;
field?: Field;
field?: MediaField;
queryOrder?: unknown;
isViewableImage?: boolean;
type?: string;
@ -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,
collection.media_library?.folder_support ?? false,
mediaPath,
);
}),

View File

@ -17,6 +17,7 @@ import useMemoCompare from '@staticcms/core/lib/hooks/useMemoCompare';
import useUUID from '@staticcms/core/lib/hooks/useUUID';
import { isFieldDuplicate, isFieldHidden } from '@staticcms/core/lib/i18n';
import { resolveWidget } from '@staticcms/core/lib/registry';
import { fileForEntry } from '@staticcms/core/lib/util/collection.util';
import { getFieldLabel } from '@staticcms/core/lib/util/field.util';
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
import { validate } from '@staticcms/core/lib/util/validation.util';
@ -40,6 +41,7 @@ import type { ConnectedProps } from 'react-redux';
const EditorControl = ({
collection,
collectionFile,
config: configState,
entry,
field,
@ -153,6 +155,7 @@ const EditorControl = ({
{createElement(widget.control, {
key: `${id}-${version}`,
collection,
collectionFile,
config,
entry,
field: field as UnknownField,
@ -231,6 +234,7 @@ function mapStateToProps(state: RootState, ownProps: EditorControlOwnProps) {
const { collections, entryDraft } = state;
const entry = entryDraft.entry;
const collection = entryDraft.entry ? collections[entryDraft.entry.collection] : null;
const collectionFile = fileForEntry(collection, entryDraft.entry?.slug);
const isLoadingAsset = selectIsLoadingAsset(state);
return {
@ -239,6 +243,7 @@ function mapStateToProps(state: RootState, ownProps: EditorControlOwnProps) {
config: state.config,
entry,
collection,
collectionFile,
isLoadingAsset,
};
}

View File

@ -17,6 +17,7 @@ import {
loadMediaDisplayURL,
persistMedia,
} from '@staticcms/core/actions/mediaLibrary';
import useFolderSupport from '@staticcms/core/lib/hooks/useFolderSupport';
import useMediaFiles from '@staticcms/core/lib/hooks/useMediaFiles';
import { fileExtension } from '@staticcms/core/lib/util';
import classNames from '@staticcms/core/lib/util/classNames.util';
@ -85,6 +86,7 @@ const MediaLibrary: FC<TranslatedProps<MediaLibraryProps>> = ({
dynamicSearchQuery,
page,
collection,
collectionFile,
field,
value: initialValue,
alt: initialAlt,
@ -463,6 +465,8 @@ const MediaLibrary: FC<TranslatedProps<MediaLibraryProps>> = ({
const hasSelection = hasMedia && !isEmpty(selectedFile);
const folderSupport = useFolderSupport({ config, collection, collectionFile, field });
return (
<>
<div className="flex flex-col w-full h-full">
@ -484,7 +488,7 @@ const MediaLibrary: FC<TranslatedProps<MediaLibraryProps>> = ({
px-5
pt-4
`,
config?.media_library?.folder_support &&
folderSupport &&
`
pb-4
border-b
@ -517,7 +521,7 @@ const MediaLibrary: FC<TranslatedProps<MediaLibraryProps>> = ({
placeholder={t('mediaLibrary.mediaLibraryModal.search')}
disabled={!dynamicSearchActive && !hasFilteredFiles}
/>
{config?.media_library?.folder_support ? (
{folderSupport ? (
<div className="flex gap-1.5 items-center">
<IconButton
onClick={handleHome}
@ -565,7 +569,7 @@ const MediaLibrary: FC<TranslatedProps<MediaLibraryProps>> = ({
) : null}
</div>
</div>
{config?.media_library?.folder_support ? (
{folderSupport ? (
<div
className="
flex

View File

@ -9,6 +9,7 @@ import {
MEDIA_CARD_WIDTH,
MEDIA_LIBRARY_PADDING,
} from '@staticcms/core/constants/mediaLibrary';
import useFolderSupport from '@staticcms/core/lib/hooks/useFolderSupport';
import classNames from '@staticcms/core/lib/util/classNames.util';
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
import { useAppSelector } from '@staticcms/core/store/hooks';
@ -16,7 +17,8 @@ import MediaLibraryCard from './MediaLibraryCard';
import type {
Collection,
Field,
CollectionFile,
MediaField,
MediaFile,
MediaLibraryDisplayURL,
} from '@staticcms/core/interface';
@ -51,7 +53,8 @@ export interface MediaLibraryCardGridProps {
loadDisplayURL: (asset: MediaFile) => void;
displayURLs: MediaLibraryState['displayURLs'];
collection?: Collection;
field?: Field;
collectionFile?: CollectionFile;
field?: MediaField;
isDialog: boolean;
onDelete: (file: MediaFile) => void;
}
@ -136,9 +139,21 @@ const CardWrapper = ({
};
const MediaLibraryCardGrid: FC<MediaLibraryCardGridProps> = props => {
const { mediaItems, scrollContainerRef, canLoadMore, isDialog, onLoadMore } = props;
const {
mediaItems,
scrollContainerRef,
canLoadMore,
isDialog,
onLoadMore,
field,
collection,
collectionFile,
} = props;
const config = useAppSelector(selectConfig);
const folderSupport = useFolderSupport({ config, collection, collectionFile, field });
const [version, setVersion] = useState(0);
const handleResize = useCallback(() => {
@ -164,7 +179,7 @@ const MediaLibraryCardGrid: FC<MediaLibraryCardGridProps> = props => {
overflow-hidden
`,
isDialog && 'rounded-b-lg',
!config?.media_library?.folder_support && 'pt-[20px]',
!folderSupport && 'pt-[20px]',
)}
style={{
width,
@ -179,9 +194,7 @@ const MediaLibraryCardGrid: FC<MediaLibraryCardGridProps> = props => {
rowCount={rowCount}
rowHeight={() => rowHeightWithGutter}
width={width}
height={
height - (!config?.media_library?.folder_support ? MEDIA_LIBRARY_PADDING : 0)
}
height={height - (!folderSupport ? MEDIA_LIBRARY_PADDING : 0)}
itemData={
{
...props,

View File

@ -7,7 +7,7 @@ import type {
ReactElement,
ReactNode,
} from 'react';
import type { t, TranslateProps as ReactPolyglotTranslateProps } from 'react-polyglot';
import type { TranslateProps as ReactPolyglotTranslateProps, t } from 'react-polyglot';
import type { MediaFile as BackendMediaFile } from './backend';
import type { EditorControlProps } from './components/entry-editor/editor-control-pane/EditorControl';
import type {
@ -164,6 +164,7 @@ export interface CollectionFile<EF extends BaseField = UnknownField> {
description?: string;
media_folder?: string;
public_folder?: string;
media_library?: MediaLibraryConfig;
i18n?: boolean | I18nInfo;
editor?: EditorConfig;
}
@ -214,6 +215,7 @@ export interface BaseCollection {
slug?: string;
media_folder?: string;
public_folder?: string;
media_library?: MediaLibraryConfig;
}
export interface FilesCollection<EF extends BaseField = UnknownField> extends BaseCollection {
@ -251,6 +253,7 @@ export interface MediaPath<T = string | string[]> {
export interface WidgetControlProps<T, F extends BaseField = UnknownField, EV = ObjectValue> {
collection: Collection<F>;
collectionFile: CollectionFile<F> | undefined;
config: Config<F>;
entry: Entry<EV>;
field: F;
@ -409,7 +412,7 @@ export interface ImplementationMediaFile {
draft?: boolean;
url?: string;
file?: File;
field?: Field;
field?: MediaField;
}
export interface DataFile {
@ -555,6 +558,7 @@ export interface MediaField extends BaseField {
public_folder?: string;
choose_url?: boolean;
multiple?: boolean;
media_library?: MediaLibraryConfig;
}
export interface BooleanField extends BaseField {

View File

@ -0,0 +1,23 @@
import { useMemo } from 'react';
import type { Collection, CollectionFile, Config, MediaField } from '@staticcms/core/interface';
interface UseFolderSupportProps {
config?: Config;
collection?: Collection;
collectionFile?: CollectionFile;
field?: MediaField;
}
export function getFolderSupport({
config,
collection,
collectionFile,
field,
}: UseFolderSupportProps) {
return (field ?? collectionFile ?? collection ?? config)?.media_library?.folder_support ?? false;
}
export default function useFolderSupport(props: UseFolderSupportProps) {
return useMemo(() => getFolderSupport(props), [props]);
}

View File

@ -9,6 +9,8 @@ import { selectEditingDraft } from '@staticcms/core/reducers/selectors/entryDraf
import { selectMediaLibraryFiles } from '@staticcms/core/reducers/selectors/mediaLibrary';
import { useAppSelector } from '@staticcms/core/store/hooks';
import { selectMediaFolder } from '../util/media.util';
import useFolderSupport from './useFolderSupport';
import { fileForEntry } from '../util/collection.util';
import type { MediaField, MediaFile } from '@staticcms/core/interface';
@ -23,6 +25,12 @@ export default function useMediaFiles(field?: MediaField, currentFolder?: string
[entry?.collection],
);
const collection = useAppSelector(collectionSelector);
const collectionFile = useMemo(
() => fileForEntry(collection, entry?.slug),
[collection, entry?.slug],
);
const folderSupport = useFolderSupport({ config, collection, collectionFile, field });
useEffect(() => {
if (!currentFolder || !config || !entry) {
@ -36,7 +44,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,
folderSupport,
config.public_folder
? trim(currentFolder, '/').replace(trim(config.media_folder!), config.public_folder)
: currentFolder,
@ -52,7 +60,7 @@ export default function useMediaFiles(field?: MediaField, currentFolder?: string
return () => {
alive = false;
};
}, [currentFolder, config, entry]);
}, [currentFolder, config, entry, field, collection, folderSupport]);
const files = useMemo(() => {
if (entry) {

View File

@ -2,7 +2,6 @@ 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';
@ -29,8 +28,6 @@ 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);
@ -57,7 +54,7 @@ export default function useMediaInsert<T extends string | string[], F extends Me
alt: value.alt,
replaceIndex,
allowMultiple: false,
config: config?.media_library,
config: field.media_library,
collection,
field,
insertOptions,
@ -65,17 +62,7 @@ export default function useMediaInsert<T extends string | string[], F extends Me
);
setSelected(false);
},
[
dispatch,
finalControlID,
forImage,
value.path,
value.alt,
config?.media_library,
collection,
field,
insertOptions,
],
[dispatch, finalControlID, forImage, value.path, value.alt, collection, field, insertOptions],
);
return handleOpenMediaLibrary;

View File

@ -23,7 +23,7 @@ import type {
Widget,
WidgetOptions,
WidgetParam,
WidgetValueSerializer
WidgetValueSerializer,
} from '../interface';
export const allowedEvents = ['prePublish', 'postPublish', 'preSave', 'postSave'] as const;

View File

@ -18,6 +18,7 @@ import type { Backend } from '@staticcms/core/backend';
import type {
BaseField,
Collection,
CollectionFile,
Collections,
Config,
Entry,
@ -28,7 +29,14 @@ import type {
SortableField,
} from '@staticcms/core/interface';
function fileForEntry<EF extends BaseField>(collection: FilesCollection<EF>, slug?: string) {
export function fileForEntry<EF extends BaseField>(
collection: Collection<EF> | undefined | null,
slug?: string,
): CollectionFile<EF> | undefined {
if (!collection || !('files' in collection)) {
return undefined;
}
const files = collection.files;
if (!slug) {
return files?.[0];
@ -36,13 +44,16 @@ function fileForEntry<EF extends BaseField>(collection: FilesCollection<EF>, slu
return files && files.filter(f => f?.name === slug)?.[0];
}
export function selectFields<EF extends BaseField>(collection: Collection<EF>, slug?: string) {
export function selectFields<EF extends BaseField>(
collection: Collection<EF>,
slug?: string,
): Field<EF>[] {
if ('fields' in collection) {
return collection.fields;
}
const file = fileForEntry(collection, slug);
return file && file.fields;
return file ? file.fields : [];
}
export function selectFolderEntryExtension<EF extends BaseField>(collection: Collection<EF>) {

View File

@ -260,7 +260,7 @@ export function selectMediaFilePublicPath<EF extends BaseField>(
collection: Collection<EF> | null,
mediaPath: string,
entryMap: Entry | undefined,
field: Field<EF> | undefined,
field: MediaField | undefined,
currentFolder?: string,
) {
if (isAbsolutePath(mediaPath)) {
@ -297,7 +297,7 @@ export function selectMediaFilePath(
collection: Collection | null | undefined,
entryMap: Entry | null | undefined,
mediaPath: string,
field: Field | undefined,
field: MediaField | undefined,
currentFolder?: string,
) {
if (isAbsolutePath(mediaPath)) {

View File

@ -23,7 +23,8 @@ import {
import type { MediaLibraryAction } from '../actions/mediaLibrary';
import type {
Collection,
Field,
CollectionFile,
MediaField,
MediaFile,
MediaLibrarInsertOptions,
MediaLibraryConfig,
@ -41,7 +42,8 @@ export type MediaLibraryState = {
files?: MediaFile[];
config: MediaLibraryConfig;
collection?: Collection;
field?: Field;
collectionFile?: CollectionFile;
field?: MediaField;
value?: string | string[];
alt?: string;
replaceIndex?: number;
@ -76,6 +78,7 @@ function mediaLibrary(
forImage,
config,
collection,
collectionFile,
field,
value,
alt,
@ -91,6 +94,7 @@ function mediaLibrary(
controlID,
config: libConfig,
collection,
collectionFile,
field,
value,
alt,

View File

@ -1,17 +1,17 @@
import type { Field } from '../interface';
import type { MediaField } from '../interface';
interface AssetProxyArgs {
path: string;
url?: string;
file?: File;
field?: Field;
field?: MediaField;
}
export default class AssetProxy {
url: string;
fileObj?: File;
path: string;
field?: Field;
field?: MediaField;
constructor({ url, file, path, field }: AssetProxyArgs) {
this.url = url ? url : file ? window.URL.createObjectURL(file as Blob) : '';

View File

@ -8,8 +8,6 @@ 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 {
@ -82,8 +80,6 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
handleOnChange,
);
const config = useAppSelector(selectConfig);
const allowsMultiple = useMemo(() => {
return field.multiple ?? false;
}, [field.multiple]);
@ -129,12 +125,12 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
value: internalValue,
replaceIndex: index,
allowMultiple: false,
config: config?.media_library,
config: field.media_library,
collection: collection as Collection<BaseField>,
field,
});
},
[openMediaLibrary, controlID, internalValue, config, collection, field],
[openMediaLibrary, controlID, internalValue, collection, field],
);
// TODO Readd when multiple uploads is supported

View File

@ -54,6 +54,7 @@ export const createMockWidgetControlProps = <
label: 'Mock Widget',
config,
collection,
collectionFile: undefined,
entry,
value,
path,

View File

@ -8,7 +8,7 @@
},
"dependencies": {
"@babel/eslint-parser": "7.21.3",
"@staticcms/core": "^2.0.0-beta.5",
"@staticcms/core": "^2.0.0-beta.4",
"babel-loader": "9.1.2",
"react": "18.2.0",
"react-dom": "18.2.0"

View File

@ -2,7 +2,8 @@ backend:
name: test-repo
site_url: 'https://staticcms.org/'
media_folder: assets/uploads
media_library_folder_support: true
media_library:
folder_support: true
locale: en
i18n:
# Required and can be one of multiple_folders, multiple_files or single_file
@ -280,12 +281,13 @@ collections:
label: Choose URL
widget: file
required: false
media_library:
choose_url: true
- name: image
label: Image
file: _widgets/image.json
description: Image widget
media_library:
folder_support: false
fields:
- name: required
label: Required Validation
@ -303,8 +305,12 @@ collections:
label: Choose URL
widget: image
required: false
media_library:
choose_url: true
- name: folder_support
label: Folder Support
widget: image
media_library:
folder_support: true
- name: list
label: List
file: _widgets/list.yml

View File

@ -83,31 +83,13 @@ Template tags produce the following output:
You can set a limit to as what the maximum file size of a file is that users can upload directly into a image field.
Example config:
See [Media Library](/docs/configuration-options#media-library) for more information.
<CodeTabs>
```yaml
- label: 'Featured Image'
name: 'thumbnail'
widget: 'image'
default: '/uploads/chocolate-dogecoin.jpg'
media_library:
max_file_size: 512000 # in bytes, only for default media library
```
## Media Library Folders
```js
{
label: 'Featured Image',
name: 'thumbnail',
widget: 'image',
default: '/uploads/chocolate-dogecoin.jpg',
media_library: {
max_file_size: 512000 // in bytes, only for default media library
},
},
```
Adds support for viewing subfolders and creating new subfolders in the media library, under your configured `media_folder`.
</CodeTabs>
See [Media Library](/docs/configuration-options#media-library) for more information.
## Summary string template transformations
@ -188,9 +170,3 @@ Static CMS can provide a side by side interface for authoring content in multipl
For repositories stored on Gitea, the gitea backend allows CMS users to log in directly with their Gitea account. Note that all users must have push access to your content repository for this to work.
See [Gitea Backend](/docs/gitea-backend) for more information.
## Media Library Folders
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.

View File

@ -31,7 +31,7 @@ weight: 9
| view_filters | ViewFilter | | _Optional_. See [view_filters](#view_filters) below |
| view_groups | ViewGroup | | _Optional_. See [view_groups](#view_groups) below |
## `identifier_field`
## Identifier Field
Static CMS expects every entry to provide a field named `"title"` that serves as an identifier for the entry. The identifier field serves as an entry's title when viewing a list of entries, and is used in [slug](#slug) creation. If you would like to use a field other than `"title"` as the identifier, you can set `identifier_field` to the name of the other field.
@ -53,7 +53,7 @@ collections: [
</CodeTabs>
## `extension` and `format`
## Extension and Format
These settings determine how collection files are parsed and saved. Both are optional—Static CMS will attempt to infer your settings based on existing items in the collection. If your collection is empty, or you'd like more control, you can set these fields explicitly.
@ -69,11 +69,11 @@ You may also specify a custom `extension` not included in the list above, as lon
- `yaml-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved only as YAML, followed by unparsed body text. The default delimiter for this option is `---`.
- `json-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved as JSON, followed by unparsed body text. The default delimiter for this option is `{` `}`.
## `frontmatter_delimiter`
## Frontmatter Delimiter
If you have an explicit frontmatter format declared, this option allows you to specify a custom delimiter like `~~~`. If you need different beginning and ending delimiters, you can use an array like `["(", ")"]`.
If you have an explicit frontmatter format declared, the `frontmatter_delimiter` option allows you to specify a custom delimiter like `~~~`. If you need different beginning and ending delimiters, you can use an array like `["(", ")"]`.
## `slug`
## Slug
For folder collections where users can create new items, the `slug` option specifies a template for generating new filenames based on a file's creation date and `title` field. (This means that all collections with `create: true` must have a `title` field (a different field can be used via [`identifier_field`](#identifier_field)).
@ -131,7 +131,7 @@ slug: '{{year}}-{{month}}-{{day}}_{{fields.slug}}',
</CodeTabs>
## `fields`
## Fields
_Ignored if [Files Collection](/docs/collection-types#file-collections)_
@ -164,9 +164,9 @@ fields: [
</CodeTabs>
## `editor`
## Editor
This setting changes options for the editor view of a collection or a file inside a files collection. It has the following options:
The `editor` setting changes options for the editor view of a collection or a file inside a files collection. It has the following options:
| Name | Type | Default | Description |
| ------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------ |
@ -191,9 +191,9 @@ editor: {
**Note**: Setting this as a top level configuration will set the default for all collections
## `summary`
## Summary
This setting allows the customization of the collection table view. Similar to the `slug` field, a string with templates can be used to include values of different fields, e.g. `{{title}}`. This option over-rides the default of `title` field and `identifier_field`.
The `summary` setting allows the customization of the collection table view. Similar to the `slug` field, a string with templates can be used to include values of different fields, e.g. `{{title}}`. This option over-rides the default of `title` field and `identifier_field`.
**Available Template Tags**
@ -216,9 +216,9 @@ summary: 'Version: {{version}} - {{title}}',
</CodeTabs>
## `sortable_fields`
## Sortable Fields
An optional object with the following options:
The `sortable_fields` setting is an optional object with the following options:
| Name | Type | Default | Description |
| ------- | --------------------- | ------- | ----------------------------------------------------------------------------------------------------------------- |
@ -275,9 +275,9 @@ sortable_fields: {
</CodeTabs>
## `view_filters`
## View Filters
An optional list of predefined view filters to show in the UI.
The `view_filters` setting is an optional list of predefined view filters to show in the UI.
Defaults to an empty list.
@ -317,9 +317,9 @@ view_filters: [
</CodeTabs>
## `view_groups`
## View Groups
An optional list of predefined view groups to show in the UI.
The `view_groups` setting is an optional list of predefined view groups to show in the UI.
Defaults to an empty list.
@ -349,3 +349,7 @@ view_groups: [
```
</CodeTabs>
## Media Library
The `media_library` settings allows customization of the media library at the collection level. See [Media Library](/docs/configuration-options#media-library) for more details.

View File

@ -392,6 +392,8 @@ collections:
- name: about
label: About Page
file: site/content/about.yml
media_library:
folder_support: true
fields:
- name: title
label: Title
@ -444,6 +446,9 @@ collections: [
name: 'about',
label: 'About Page',
file: 'site/content/about.yml',
media_library: {
folder_support: true
},
fields: [
{ name: 'title', label: 'Title', widget: 'string' },
{ name: 'intro', label: 'Intro', widget: 'markdown' },

View File

@ -67,16 +67,22 @@ 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
## Media Library <BetaImage />
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.
The `media_library` settings allows customization of the media library.
**Example:**
### Options
| Name | Type | Default | Description |
| -------------- | ------- | -------- | -------------------------------------------------------------------------------------- |
| max_file_size | number | `512000` | _Optional_. <BetaImage /> The max size, in bytes, of files that can be uploaded to the media library |
| folder_support | boolean | `false` | _Optional_. <BetaImage /> Enables directory navigation and folder creation in your media library |
### Example
<CodeTabs>
```yaml
media_library:
choose_url: true,
max_file_size: 512000
folder_support: true
```
@ -84,7 +90,6 @@ media_library:
```js
{
media_library: {
choose_url: "true,",
max_file_size: 512000,
folder_support: true
}
@ -93,26 +98,6 @@ media_library:
</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>
## Site URL
The `site_url` setting should provide a URL to your published site. May be used by Static CMS for various functionality. Used together with a collection's `preview_path` to create links to live content.

View File

@ -16,7 +16,7 @@ However, although possible, it may be cumbersome or even impractical to add a Re
`registerPreviewTemplate` allows you to create a template that overrides the entire editor preview for a given collection.
### `registerPreviewTemplate` Params
### Params
| Param | Type | Description |
| --------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@ -36,7 +36,7 @@ The following parameters will be passed to your `react_component` during render:
| widgetsFor | Function | Given a field name, returns the rendered previews of that field's nested child widgets and values |
| theme | 'light'<br />\| 'dark' | The current theme being used by the app |
#### `registerPreviewTemplate` Example
#### Example
<CodeTabs>
@ -346,7 +346,7 @@ CMS.registerPreviewStyle('.main { color: blue; border: 1px solid gree; }', { raw
`registerPreviewCard` allows you to create a card template that overrides the cards displayed in the collection view.
### `registerPreviewCard` Params
### Params
| Param | Type | Description |
| --------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@ -362,7 +362,7 @@ The following parameters will be passed to your `react_component` during render:
| widgetsFor | Function | Given a field name, returns the rendered previews of that field's nested child widgets and values |
| theme | 'light'<br />\| 'dark' | The current theme being used by the app |
#### `registerPreviewTemplate` Example
#### Example
<CodeTabs>
@ -544,7 +544,7 @@ CMS.registerPreviewTemplate('posts', PostPreview);
`registerFieldPreview` allows you to create a custom preview for a specific field in the table view for collections.
### `registerFieldPreview` Params
### Params
| Param | Type | Description |
| -------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@ -561,7 +561,7 @@ The following parameters will be passed to your `component` during render:
| value | Function | The current value of the field for the given entry |
| theme | 'light'<br />\| 'dark' | The current theme being used by the app |
#### `registerFieldPreview` Example
#### Example
<CodeTabs>

View File

@ -46,6 +46,7 @@ The react component that renders the control. It receives the following props:
| onChange | function | Function to be called when the value changes. Accepts a valid widget value |
| field | object | The field configuration for the current widget. See [Widget Options](/docs/widgets#common-widget-options) |
| collection | object | The collection configuration for the current widget. See [Collections](/docs/collection-overview) |
| collectionFile | object | The collection file configuration for the current widget if entry is part of a [File Collection](/docs/collection-types#file-collections) |
| config | object | The current Static CMS config. See [configuration options](/docs/configuration-options) |
| entry | object | Object with a `data` field that contains the current value of all widgets in the editor |
| path | string | `.` separated string donating the path to the current widget within the entry |

View File

@ -198,10 +198,14 @@ The external media integrations have been broken for sometime, and would require
This brings with it some breaking changes for the `media_library` config property.
**Old Config**
**Old Config (for Image or File Field)**
<CodeTabs>
```yaml
name: thumbnail
label: Featured Image
widget: image
default: /uploads/chocolate-dogecoin.jpg
media_library:
choose_url: true
config:
@ -210,6 +214,10 @@ media_library:
```js
{
name: "thumbnail",
label: "Featured Image",
widget: "image",
default: "/uploads/chocolate-dogecoin.jpg",
media_library: {
choose_url: true,
config: {
@ -225,15 +233,23 @@ media_library:
<CodeTabs>
```yaml
media_library:
name: thumbnail
label: Featured Image
widget: image
default: /uploads/chocolate-dogecoin.jpg
choose_url: true
media_library:
max_file_size: 512000
```
```js
{
media_library: {
name: "thumbnail",
label: "Featured Image",
widget: "image",
default: "/uploads/chocolate-dogecoin.jpg",
choose_url: true,
media_library: {
max_file_size: 512000
}
}
@ -243,7 +259,6 @@ media_library:
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.

View File

@ -10,7 +10,7 @@ weight: 10
The boolean widget translates a toggle switch input to a `true` or `false` value.
## Widget options
## Widget Options
For common options, see [Common widget options](/docs/widgets#common-widget-options).

View File

@ -10,7 +10,7 @@ weight: 11
The code widget provides a code editor (powered by [Codemirror](https://codemirror.net)) with optional syntax awareness. Can output the raw code value or an object with the selected language and the raw code value.
## Widget options
## Widget Options
For common options, see [Common widget options](/docs/widgets#common-widget-options).

View File

@ -10,7 +10,7 @@ weight: 12
The color widget translates a color picker to a color string.
## Widget options
## Widget Options
For common options, see [Common widget options](/docs/widgets#common-widget-options).

View File

@ -10,7 +10,7 @@ weight: 13
The datetime widget translates a datetime picker to a datetime string.
## Widget options
## Widget Options
For common options, see [Common widget options](/docs/widgets#common-widget-options).

View File

@ -10,15 +10,17 @@ weight: 14
The file widget allows editors to upload a file or select an existing one from the media library. The path to the file will be saved to the field as a string.
## Widget options
## Widget Options
For common options, see [Common widget options](/docs/widgets#common-widget-options).
| Name | Type | Default | Description |
| ------------- | ------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| ------------- | --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| default | string | `null` | _Optional_. The default value for the field. Accepts a string. |
| media_folder | string | | _Optional_. Specifies the folder path where uploaded files should be saved, relative to the base of the repo |
| public_folder | string | | _Optional_. Specifies the folder path where the files uploaded by the media library will be accessed, relative to the base of the built site |
| media_library | Media Library Options | `{}` | _Optional_. Media library settings to apply when the media library is opened by the current widget. See [Media Library](/docs/configuration-options#media-library) |
| choose_url | boolean | `false` | _Optional_. When set to `false`, the "Insert from URL" button will be hidden |
## Example

View File

@ -10,7 +10,7 @@ weight: 15
Hidden widgets do not display in the UI. In folder collections that allow users to create new items, you will often want to set a default for hidden fields, so they will be set without requiring an input.
## Widget options
## Widget Options
For common options, see [Common widget options](/docs/widgets#common-widget-options).

View File

@ -10,15 +10,17 @@ weight: 16
The file widget allows editors to upload a file or select an existing one from the media library. The path to the file will be saved to the field as a string.
## Widget options
## Widget Options
For common options, see [Common widget options](/docs/widgets#common-widget-options).
| Name | Type | Default | Description |
| ------------- | ------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| ------------- | --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| default | string | `null` | _Optional_. The default value for the field. Accepts a string. |
| media_folder | string | | _Optional_. Specifies the folder path where uploaded files should be saved, relative to the base of the repo |
| public_folder | string | | _Optional_. Specifies the folder path where the files uploaded by the media library will be accessed, relative to the base of the built site |
| media_library | Media Library Options | `{}` | _Optional_. Media library settings to apply when the media library is opened by the current widget. See [Media Library](/docs/configuration-options#media-library) |
| choose_url | boolean | `false` | _Optional_. When set to `false`, the "Insert from URL" button will be hidden |
## Example

View File

@ -10,7 +10,7 @@ weight: 17
The list widget allows you to create a repeatable item in the UI which saves as a list of widget values. You can choose any widget as a child of a list widget—even other lists.
## Widget options
## Widget Options
For common options, see [Common widget options](/docs/widgets#common-widget-options).

View File

@ -10,7 +10,7 @@ weight: 18
The map widget allows you to edit spatial data using an interactive map. Spatial data for a single piece of geometry saves as a GeoJSON string in WGS84 projection.
## Widget options
## Widget Options
For common options, see [Common widget options](/docs/widgets#common-widget-options).

View File

@ -17,19 +17,12 @@ _Please note:_ If you want to use your markdown editor to fill a markdown file c
For common options, see [Common widget options](/docs/widgets#common-widget-options).
| Name | Type | Default | Description |
| ------------- | --------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| ------------- | --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| default | string | `''` | _Optional_. The default value for the field. Accepts markdown content |
| media_library | Media Library Options | `{}` | _Optional_. Media library settings to apply when a media library is opened by the current widget. See [Media Library Options](#media-library-options) |
| media_folder | string | | _Optional_. Specifies the folder path where uploaded files should be saved, relative to the base of the repo |
| public_folder | string | | _Optional_. Specifies the folder path where the files uploaded by the media library will be accessed, relative to the base of the built site |
### Media Library Options
| Name | Type | Default | Description |
| -------------- | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| allow_multiple | boolean | `true` | _Optional_. When set to `false`, prevents multiple selection for any media library extension, but must be supported by the extension in use |
| config | string | `{}` | _Optional_. A configuration object that will be passed directly to the media library being used - available options are determined by the library |
| choose_url | string<br />\| boolean | `true` | _Optional_. When set to `false`, the "Insert from URL" button will be hidden |
| media_library | Media Library Options | `{}` | _Optional_. Media library settings to apply when the media library is opened by the current widget. See [Media Library](/docs/configuration-options#media-library) |
| choose_url | boolean | `false` | _Optional_. When set to `false`, the "Insert from URL" button will be hidden |
## Example
@ -161,11 +154,7 @@ CMS.registerShortcode('youtube', {
preview: ({ src }) => {
return (
<span>
<iframe
width="420"
height="315"
src={`https://www.youtube.com/embed/${src}`}
/>
<iframe width="420" height="315" src={`https://www.youtube.com/embed/${src}`} />
</span>
);
},
@ -216,11 +205,7 @@ CMS.registerShortcode<YouTubeShortcodeProps>('youtube', {
preview: ({ src }) => {
return (
<span>
<iframe
width="420"
height="315"
src={`https://www.youtube.com/embed/${src}`}
/>
<iframe width="420" height="315" src={`https://www.youtube.com/embed/${src}`} />
</span>
);
},

View File

@ -10,7 +10,7 @@ weight: 20
The number widget uses an HTML number input, saving the value as a string, integer, or floating point number.
## Widget options
## Widget Options
For common options, see [Common widget options](/docs/widgets#common-widget-options).

View File

@ -10,7 +10,7 @@ weight: 21
The object widget allows you to group multiple widgets together, nested under a single field. You can choose any widget as a child of an object widget, even other object widgets.
## Widget options
## Widget Options
For common options, see [Common widget options](/docs/widgets#common-widget-options).

View File

@ -10,7 +10,7 @@ weight: 22
The relation widget allows you to reference items from another collection. It provides a search input with a list of entries from the collection you're referencing, and the list automatically updates with matched entries based on what you've typed.
## Widget options
## Widget Options
For common options, see [Common widget options](/docs/widgets#common-widget-options).

View File

@ -10,7 +10,7 @@ weight: 23
The select widget allows you to pick a string value from a dropdown menu.
## Widget options
## Widget Options
For common options, see [Common widget options](/docs/widgets#common-widget-options).

View File

@ -10,7 +10,7 @@ weight: 24
The string widget translates a basic text input to a string value. For larger textarea inputs, use the text widget.
## Widget options
## Widget Options
For common options, see [Common widget options](/docs/widgets#common-widget-options).

View File

@ -10,7 +10,7 @@ weight: 25
The text widget takes a multiline text field and saves it as a string. For shorter text inputs, use the string widget.
## Widget options
## Widget Options
For common options, see [Common widget options](/docs/widgets#common-widget-options).

View File

@ -46,7 +46,7 @@ The following options are available on all fields:
| i18n | boolean<br />\|'translate'<br />\|'duplicate'<br />\|'none' | | _Optional_. <BetaImage /><ul><li>`translate` - Allows translation of the field</li><li>`duplicate` - Duplicates the value from the default locale</li><li>`true` - Accept parent values as default</li><li>`none` or `false` - Exclude field from translations</li></ul> |
| comment | string | | _Optional_. Adds comment before the field (only supported for yaml) |
### Example
## Example
<CodeTabs>
```yaml

View File

@ -41,7 +41,6 @@ const Header3 = ({ variant, children = '' }: Header3Props) => {
<Typography
variant={variant}
component={variant}
id={anchor}
sx={{
display: 'flex',
alignItems: 'center',

View File

@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from 'react';
import { isNotEmpty } from '../../../util/string.util';
import DocsHeadings from './DocsHeadings';
import { getAnchor } from '../components/headers/hooks/useAnchor';
export interface Heading {
id: string;
@ -14,21 +15,43 @@ export interface NestedHeading extends Heading {
items: Heading[];
}
const getNestedHeadings = (headingElements: HTMLHeadingElement[]) => {
export const getId = (headingsSoFar: string[], potentialHeading: string): string => {
let heading = potentialHeading;
let index = 1;
while (headingsSoFar.includes(heading)) {
heading = `${potentialHeading}-${index}`;
index++;
}
return heading;
};
const getNestedHeadings = (
headingElements: HTMLHeadingElement[],
headingsSoFar: string[],
): NestedHeading[] => {
const nestedHeadings: NestedHeading[] = [];
headingElements.forEach(heading => {
const { innerText, id } = heading;
const { innerText } = heading;
const title = innerText
.replace(/\n/g, '')
.replace(/Beta Feature$/g, '')
.trim();
const id = getAnchor(title);
const finalId = getId(headingsSoFar, id);
headingsSoFar.push(finalId);
heading.id = finalId;
if (heading.nodeName === 'H1' || heading.nodeName === 'H2') {
nestedHeadings.push({ id, title, items: [] });
nestedHeadings.push({ id: finalId, title, items: [] });
} else if (heading.nodeName === 'H3' && nestedHeadings.length > 0) {
nestedHeadings[nestedHeadings.length - 1].items.push({
id,
id: finalId,
title,
});
}
@ -46,8 +69,10 @@ const useHeadingsData = () => {
document.querySelectorAll<HTMLHeadingElement>('main h1, main h2, main h3'),
);
const headingsSoFar: string[] = [];
// Created a list of headings, with H3s nested
const newNestedHeadings = getNestedHeadings(headingElements);
const newNestedHeadings = getNestedHeadings(headingElements, headingsSoFar);
setNestedHeadings(newNestedHeadings);
}, [asPath]);