diff --git a/package.json b/package.json index 5c0fda37..24c7a8a5 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "test:ci": "lerna run test:ci", "test:integration:ci": "lerna run test:integration:ci", "test:integration": "lerna run test:integration", - "test": "lerna run test" + "test": "lerna run test", + "type-check": "lerna run type-check --scope @staticcms/core" }, "devDependencies": { "husky": "8.0.3", diff --git a/packages/core/dev-test/index.html b/packages/core/dev-test/index.html index 6274da6d..99323002 100644 --- a/packages/core/dev-test/index.html +++ b/packages/core/dev-test/index.html @@ -9,11 +9,11 @@ _posts: { '2015-02-14-this-is-a-post.md': { content: - '---\ntitle: This is a YAML front matter post\nimage: /assets/uploads/moby-dick.jpg\ndate: 2015-02-14T00:00:00.000Z\n---\n\n# I Am a Title in Markdown\n\nHello, world\n\n* One Thing\n* Another Thing\n* A Third Thing\n', + '---\ntitle: This is a YAML front matter post\ndraft: true\nimage: /assets/uploads/moby-dick.jpg\ndate: 2015-02-14T00:00:00.000Z\n---\n\n# I Am a Title in Markdown\n\nHello, world\n\n* One Thing\n* Another Thing\n* A Third Thing\n', }, '2015-02-15-this-is-a-json-frontmatter-post.md': { content: - '{\n"title": "This is a JSON front matter post",\n"image": "/assets/uploads/moby-dick.jpg",\n"date": "2015-02-15T00:00:00.000Z"\n}\n\n# I Am a Title in Markdown\n\nHello, world\n\n* One Thing\n* Another Thing\n* A Third Thing\n', + '{\n"title": "This is a JSON front matter post",\n"draft": false,\n"image": "/assets/uploads/moby-dick.jpg",\n"date": "2015-02-15T00:00:00.000Z"\n}\n\n# I Am a Title in Markdown\n\nHello, world\n\n* One Thing\n* Another Thing\n* A Third Thing\n', }, '2015-02-16-this-is-a-toml-frontmatter-post.md': { content: @@ -21,7 +21,7 @@ }, '2015-02-14-this-is-a-post-with-a-different-extension.other': { content: - '---\ntitle: This post should not appear because the extension is different\nimage: /assets/uploads/moby-dick.jpg\ndate: 2015-02-14T00:00:00.000Z\n---\n\n# I Am a Title in Markdown\n\nHello, world\n\n* One Thing\n* Another Thing\n* A Third Thing\n', + '---\ntitle: This post should not appear because the extension is different\ndraft: false\nimage: /assets/uploads/moby-dick.jpg\ndate: 2015-02-14T00:00:00.000Z\n---\n\n# I Am a Title in Markdown\n\nHello, world\n\n* One Thing\n* Another Thing\n* A Third Thing\n', }, }, _faqs: { diff --git a/packages/core/dev-test/index.js b/packages/core/dev-test/index.js index 14d2d8aa..0c5b302c 100644 --- a/packages/core/dev-test/index.js +++ b/packages/core/dev-test/index.js @@ -11,6 +11,59 @@ const PostPreview = ({ entry, widgetFor }) => { ); }; +const PostPreviewCard = ({ entry, widgetFor, viewStyle }) => { + return h( + 'div', + { style: { width: '100%' } }, + viewStyle === 'grid' ? widgetFor('image') : null, + h( + 'div', + { style: { padding: '16px', width: '100%' } }, + h( + 'div', + { + style: { + display: 'flex', + width: '100%', + justifyContent: 'space-between', + alignItems: 'start', + }, + }, + h( + 'div', + { + style: { + display: 'flex', + flexDirection: viewStyle === 'grid' ? 'column' : 'row', + alignItems: 'baseline', + gap: '8px', + }, + }, + h('strong', { style: { fontSize: '24px' } }, entry.data.title), + h('span', { style: { fontSize: '16px' } }, entry.data.date), + ), + h( + 'div', + { + style: { + backgroundColor: entry.data.draft === true ? 'blue' : 'green', + color: 'white', + border: 'none', + padding: '4px 8px', + textAlign: 'center', + textDecoration: 'none', + display: 'inline-block', + cursor: 'pointer', + borderRadius: '4px', + }, + }, + entry.data.draft === true ? 'Draft' : 'Published', + ), + ), + ), + ); +}; + const GeneralPreview = ({ widgetsFor, entry, collection }) => { const title = entry.data.site_title; const posts = entry.data.posts; @@ -80,6 +133,7 @@ const CustomPage = () => { }; CMS.registerPreviewTemplate('posts', PostPreview); +CMS.registerPreviewCard('posts', PostPreviewCard); CMS.registerPreviewTemplate('general', GeneralPreview); CMS.registerPreviewTemplate('authors', AuthorsPreview); // Pass the name of a registered control to reuse with a new widget preview. diff --git a/packages/core/src/components/Collection/Entries/EntryCard.tsx b/packages/core/src/components/Collection/Entries/EntryCard.tsx index b0c3642d..e102ed7a 100644 --- a/packages/core/src/components/Collection/Entries/EntryCard.tsx +++ b/packages/core/src/components/Collection/Entries/EntryCard.tsx @@ -10,8 +10,16 @@ import { Link } from 'react-router-dom'; import { getAsset as getAssetAction } from '@staticcms/core/actions/media'; import { VIEW_STYLE_GRID, VIEW_STYLE_LIST } from '@staticcms/core/constants/collectionViews'; import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset'; -import { selectEntryCollectionTitle } from '@staticcms/core/lib/util/collection.util'; +import { getPreviewCard } from '@staticcms/core/lib/registry'; +import { + selectEntryCollectionTitle, + selectFields, + selectTemplateName, +} from '@staticcms/core/lib/util/collection.util'; +import { selectConfig } from '@staticcms/core/reducers/selectors/config'; import { selectIsLoadingAsset } from '@staticcms/core/reducers/selectors/medias'; +import { useAppSelector } from '@staticcms/core/store/hooks'; +import useWidgetsFor from '../../common/widget/useWidgetsFor'; import type { CollectionViewStyle } from '@staticcms/core/constants/collectionViews'; import type { Collection, Entry, Field } from '@staticcms/core/interface'; @@ -29,8 +37,45 @@ const EntryCard = ({ }: NestedCollectionProps) => { const summary = useMemo(() => selectEntryCollectionTitle(collection, entry), [collection, entry]); + const fields = selectFields(collection, entry.slug); const imageUrl = useMediaAsset(image, collection, imageField, entry); + const config = useAppSelector(selectConfig); + + const { widgetFor, widgetsFor } = useWidgetsFor(config, collection, fields, entry); + + const PreviewCardComponent = useMemo( + () => getPreviewCard(selectTemplateName(collection, entry.slug)) ?? null, + [collection, entry.slug], + ); + + if (PreviewCardComponent) { + return ( + + + + + + ); + } + return ( diff --git a/packages/core/src/components/Editor/EditorPreviewPane/EditorPreview.tsx b/packages/core/src/components/Editor/EditorPreviewPane/EditorPreview.tsx index 86306f03..df1460a6 100644 --- a/packages/core/src/components/Editor/EditorPreviewPane/EditorPreview.tsx +++ b/packages/core/src/components/Editor/EditorPreviewPane/EditorPreview.tsx @@ -1,7 +1,7 @@ import { styled } from '@mui/material/styles'; import React from 'react'; -import type { TemplatePreviewProps } from '@staticcms/core/interface'; +import type { ObjectValue, TemplatePreviewProps } from '@staticcms/core/interface'; const PreviewContainer = styled('div')` overflow-y: auto; @@ -10,7 +10,7 @@ const PreviewContainer = styled('div')` font-family: Roboto, 'Helvetica Neue', HelveticaNeue, Helvetica, Arial, sans-serif; `; -const Preview = ({ collection, fields, widgetFor }: TemplatePreviewProps) => { +const Preview = ({ collection, fields, widgetFor }: TemplatePreviewProps) => { if (!collection || !fields) { return null; } diff --git a/packages/core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.tsx b/packages/core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.tsx index f0770f55..14262069 100644 --- a/packages/core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.tsx +++ b/packages/core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.tsx @@ -1,5 +1,5 @@ import { styled } from '@mui/material/styles'; -import React, { Fragment, isValidElement, useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import ReactDOM from 'react-dom'; import Frame from 'react-frame-component'; import { translate } from 'react-polyglot'; @@ -9,34 +9,23 @@ import { ScrollSyncPane } from 'react-scroll-sync'; import { getAsset as getAssetAction } from '@staticcms/core/actions/media'; import { ErrorBoundary } from '@staticcms/core/components/UI'; import { lengths } from '@staticcms/core/components/UI/styles'; -import { getPreviewStyles, getPreviewTemplate, resolveWidget } from '@staticcms/core/lib/registry'; -import { selectTemplateName, useInferredFields } from '@staticcms/core/lib/util/collection.util'; -import { selectField } from '@staticcms/core/lib/util/field.util'; +import { getPreviewStyles, getPreviewTemplate } from '@staticcms/core/lib/registry'; +import { selectTemplateName } from '@staticcms/core/lib/util/collection.util'; import { selectIsLoadingAsset } from '@staticcms/core/reducers/selectors/medias'; -import { getTypedFieldForValue } from '@staticcms/list/typedListHelpers'; +import useWidgetsFor from '../../common/widget/useWidgetsFor'; import EditorPreview from './EditorPreview'; import EditorPreviewContent from './EditorPreviewContent'; import PreviewFrameContent from './PreviewFrameContent'; -import PreviewHOC from './PreviewHOC'; -import type { InferredField } from '@staticcms/core/constants/fieldInference'; import type { Collection, - Config, Entry, - EntryData, Field, - GetAssetFunction, - ListField, - ObjectValue, - RenderedField, TemplatePreviewProps, TranslatedProps, - ValueOrNestedValue, - WidgetPreviewComponent, } from '@staticcms/core/interface'; import type { RootState } from '@staticcms/core/store'; -import type { ComponentType, ReactFragment, ReactNode } from 'react'; +import type { ComponentType } from 'react'; import type { ConnectedProps } from 'react-redux'; const PreviewPaneFrame = styled(Frame)` @@ -81,265 +70,10 @@ const StyledPreviewContent = styled('div')` overflow: hidden; `; -/** - * Returns the widget component for a named field, and makes recursive calls - * to retrieve components for nested and deeply nested fields, which occur in - * object and list type fields. Used internally to retrieve widgets, and also - * exposed for use in custom preview templates. - */ -function getWidgetFor( - config: Config, - collection: Collection, - name: string, - fields: Field[], - entry: Entry, - inferredFields: Record, - getAsset: GetAssetFunction, - widgetFields: Field[] = fields, - values: EntryData = entry.data, - idx: number | null = null, -): ReactNode { - // We retrieve the field by name so that this function can also be used in - // custom preview templates, where the field object can't be passed in. - const field = widgetFields && widgetFields.find(f => f.name === name); - if (!field) { - return null; - } - - const value = values?.[field.name]; - let fieldWithWidgets = field as RenderedField; - - if ('fields' in field && field.fields) { - fieldWithWidgets = { - ...fieldWithWidgets, - renderedFields: getNestedWidgets( - config, - collection, - fields, - entry, - inferredFields, - getAsset, - field.fields, - value as EntryData | EntryData[], - ), - }; - } else if ('types' in field && field.types) { - fieldWithWidgets = { - ...fieldWithWidgets, - renderedFields: getTypedNestedWidgets( - config, - collection, - field, - entry, - inferredFields, - getAsset, - value as EntryData[], - ), - }; - } - - const labelledWidgets = ['string', 'text', 'number']; - const inferredField = Object.entries(inferredFields) - .filter(([key]) => { - const fieldToMatch = selectField(collection, key); - return fieldToMatch === fieldWithWidgets; - }) - .map(([, value]) => value)[0]; - - let renderedValue: ValueOrNestedValue | ReactNode = value; - if (inferredField) { - renderedValue = inferredField.defaultPreview(String(value)); - } else if ( - value && - fieldWithWidgets.widget && - labelledWidgets.indexOf(fieldWithWidgets.widget) !== -1 && - value.toString().length < 50 - ) { - renderedValue = ( -
- <> - {field.label ?? field.name}: {value} - -
- ); - } - return renderedValue - ? getWidget(config, fieldWithWidgets, collection, renderedValue, entry, getAsset, idx) - : null; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function isJsxElement(value: any): value is JSX.Element { - return isValidElement(value); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function isReactFragment(value: any): value is ReactFragment { - if (value.type) { - return value.type === Fragment; - } - - return value === Fragment; -} - -function getWidget( - config: Config, - field: RenderedField, - collection: Collection, - value: ValueOrNestedValue | ReactNode, - entry: Entry, - getAsset: GetAssetFunction, - idx: number | null = null, -) { - if (!field.widget) { - return null; - } - - const widget = resolveWidget(field.widget); - const key = idx ? field.name + '_' + idx : field.name; - - if (field.widget === 'hidden' || !widget.preview) { - return null; - } - - /** - * Use an HOC to provide conditional updates for all previews. - */ - return !widget.preview ? null : ( - )[field.name] - : value - } - entry={entry} - /> - ); -} - -/** - * Use getWidgetFor as a mapping function for recursive widget retrieval - */ -function widgetsForNestedFields( - config: Config, - collection: Collection, - fields: Field[], - entry: Entry, - inferredFields: Record, - getAsset: GetAssetFunction, - widgetFields: Field[], - values: EntryData, - idx: number | null = null, -) { - return widgetFields - .map(field => - getWidgetFor( - config, - collection, - field.name, - fields, - entry, - inferredFields, - getAsset, - widgetFields, - values, - idx, - ), - ) - .filter(widget => Boolean(widget)) as JSX.Element[]; -} - -/** - * Retrieves widgets for nested fields (children of object/list fields) - */ -function getTypedNestedWidgets( - config: Config, - collection: Collection, - field: ListField, - entry: Entry, - inferredFields: Record, - getAsset: GetAssetFunction, - values: EntryData[], -) { - return values - ?.flatMap((value, index) => { - const itemType = getTypedFieldForValue(field, value ?? {}, index); - if (!itemType) { - return null; - } - - return widgetsForNestedFields( - config, - collection, - itemType.fields, - entry, - inferredFields, - getAsset, - itemType.fields, - value, - index, - ); - }) - .filter(Boolean); -} - -/** - * Retrieves widgets for nested fields (children of object/list fields) - */ -function getNestedWidgets( - config: Config, - collection: Collection, - fields: Field[], - entry: Entry, - inferredFields: Record, - getAsset: GetAssetFunction, - widgetFields: Field[], - values: EntryData | EntryData[], -) { - // Fields nested within a list field will be paired with a List of value Maps. - if (Array.isArray(values)) { - return values.flatMap(value => - widgetsForNestedFields( - config, - collection, - fields, - entry, - inferredFields, - getAsset, - widgetFields, - value, - ), - ); - } - - // Fields nested within an object field will be paired with a single Record of values. - return widgetsForNestedFields( - config, - collection, - fields, - entry, - inferredFields, - getAsset, - widgetFields, - values, - ); -} - const PreviewPane = (props: TranslatedProps) => { const { entry, collection, config, fields, previewInFrame, getAsset, t } = props; - const inferredFields = useInferredFields(collection); + const { widgetFor, widgetsFor } = useWidgetsFor(config.config, collection, fields, entry); const handleGetAsset = useCallback( (path: string, field?: Field) => { @@ -349,118 +83,6 @@ const PreviewPane = (props: TranslatedProps) => { [collection], ); - const widgetFor = useCallback( - (name: string) => { - if (!config.config) { - return null; - } - return getWidgetFor( - config.config, - collection, - name, - fields, - entry, - inferredFields, - handleGetAsset, - ); - }, - [collection, config, entry, fields, handleGetAsset, inferredFields], - ); - - /** - * This function exists entirely to expose nested widgets for object and list - * fields to custom preview templates. - */ - const widgetsFor = useCallback( - (name: string) => { - const cmsConfig = config.config; - if (!cmsConfig) { - return { - data: null, - widgets: {}, - }; - } - - const field = fields.find(f => f.name === name); - if (!field || !('fields' in field)) { - return { - data: null, - widgets: {}, - }; - } - - const value = entry.data?.[field.name]; - const nestedFields = field && 'fields' in field ? field.fields ?? [] : []; - - if (field.widget === 'list' || Array.isArray(value)) { - let finalValue: ObjectValue[]; - if (!value || typeof value !== 'object') { - finalValue = []; - } else if (!Array.isArray(value)) { - finalValue = [value]; - } else { - finalValue = value as ObjectValue[]; - } - - return finalValue - .filter((val: unknown) => typeof val === 'object') - .map((val: ObjectValue) => { - const widgets = nestedFields.reduce((acc, field, index) => { - acc[field.name] = ( -
- {getWidgetFor( - cmsConfig, - collection, - field.name, - fields, - entry, - inferredFields, - handleGetAsset, - nestedFields, - val, - index, - )} -
- ); - return acc; - }, {} as Record); - return { data: val, widgets }; - }); - } - - if (typeof value !== 'object') { - return { - data: {}, - widgets: {}, - }; - } - - return { - data: value, - widgets: nestedFields.reduce((acc, field, index) => { - acc[field.name] = ( -
- {getWidgetFor( - cmsConfig, - collection, - field.name, - fields, - entry, - inferredFields, - handleGetAsset, - nestedFields, - value, - index, - )} -
- ); - return acc; - }, {} as Record), - }; - }, - [collection, config.config, entry, fields, handleGetAsset, inferredFields], - ); - const previewStyles = useMemo( () => [ ...getPreviewStyles().map((style, i) => { diff --git a/packages/core/src/components/UI/WidgetPreviewContainer.tsx b/packages/core/src/components/UI/WidgetPreviewContainer.tsx index 9e3485a8..a10c3d40 100644 --- a/packages/core/src/components/UI/WidgetPreviewContainer.tsx +++ b/packages/core/src/components/UI/WidgetPreviewContainer.tsx @@ -1,18 +1,13 @@ import React from 'react'; -import { styled } from '@mui/material/styles'; import type { ReactNode } from 'react'; -const StyledWidgetPreviewContainer = styled('div')` - margin: 15px 2px; -`; - interface WidgetPreviewContainerProps { children?: ReactNode; } const WidgetPreviewContainer = ({ children }: WidgetPreviewContainerProps) => { - return {children}; + return
{children}
; }; export default WidgetPreviewContainer; diff --git a/packages/core/src/components/Editor/EditorPreviewPane/PreviewHOC.tsx b/packages/core/src/components/common/widget/PreviewHOC.tsx similarity index 100% rename from packages/core/src/components/Editor/EditorPreviewPane/PreviewHOC.tsx rename to packages/core/src/components/common/widget/PreviewHOC.tsx diff --git a/packages/core/src/components/common/widget/useWidgetsFor.tsx b/packages/core/src/components/common/widget/useWidgetsFor.tsx new file mode 100644 index 00000000..b4e6c15d --- /dev/null +++ b/packages/core/src/components/common/widget/useWidgetsFor.tsx @@ -0,0 +1,147 @@ +import React, { useCallback } from 'react'; + +import { getAsset } from '@staticcms/core/actions/media'; +import { useInferredFields } from '@staticcms/core/lib/util/collection.util'; +import { useAppDispatch } from '@staticcms/core/store/hooks'; +import getWidgetFor from './widgetFor'; + +import type { + Collection, + Config, + Entry, + EntryData, + Field, + ObjectValue, + WidgetFor, + WidgetsFor, +} from '@staticcms/core/interface'; +import type { ReactNode } from 'react'; + +export default function useWidgetsFor( + config: Config | undefined, + collection: Collection, + fields: Field[], + entry: Entry, +): { + widgetFor: WidgetFor; + widgetsFor: WidgetsFor; +} { + const inferredFields = useInferredFields(collection); + const dispatch = useAppDispatch(); + + const handleGetAsset = useCallback( + (path: string, field?: Field) => { + return dispatch(getAsset(collection, entry, path, field)); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [collection], + ); + + const widgetFor = useCallback( + (name: string): ReturnType> => { + if (!config) { + return null; + } + return getWidgetFor(config, collection, name, fields, entry, inferredFields, handleGetAsset); + }, + [collection, config, entry, fields, handleGetAsset, inferredFields], + ); + + /** + * This function exists entirely to expose nested widgets for object and list + * fields to custom preview templates. + */ + const widgetsFor = useCallback( + (name: string): ReturnType> => { + if (!config) { + return { + data: null, + widgets: {}, + }; + } + + const field = fields.find(f => f.name === name); + if (!field || !('fields' in field)) { + return { + data: null, + widgets: {}, + }; + } + + const value = entry.data?.[field.name]; + const nestedFields = field && 'fields' in field ? field.fields ?? [] : []; + + if (field.widget === 'list' || Array.isArray(value)) { + let finalValue: ObjectValue[]; + if (!value || typeof value !== 'object') { + finalValue = []; + } else if (!Array.isArray(value)) { + finalValue = [value]; + } else { + finalValue = value as ObjectValue[]; + } + + return finalValue + .filter((val: unknown) => typeof val === 'object') + .map((val: ObjectValue) => { + const widgets = nestedFields.reduce((acc, field, index) => { + acc[field.name] = ( +
+ {getWidgetFor( + config, + collection, + field.name, + fields, + entry, + inferredFields, + handleGetAsset, + nestedFields, + val, + index, + )} +
+ ); + return acc; + }, {} as Record); + return { data: val, widgets }; + }); + } + + if (typeof value !== 'object') { + return { + data: {}, + widgets: {}, + }; + } + + return { + data: value, + widgets: nestedFields.reduce((acc, field, index) => { + acc[field.name] = ( +
+ {getWidgetFor( + config, + collection, + field.name, + fields, + entry, + inferredFields, + handleGetAsset, + nestedFields, + value, + index, + )} +
+ ); + return acc; + }, {} as Record), + }; + }, + [collection, config, entry, fields, handleGetAsset, inferredFields], + ); + + return { + widgetFor, + widgetsFor, + }; +} diff --git a/packages/core/src/components/common/widget/widgetFor.tsx b/packages/core/src/components/common/widget/widgetFor.tsx new file mode 100644 index 00000000..cbd3a7dc --- /dev/null +++ b/packages/core/src/components/common/widget/widgetFor.tsx @@ -0,0 +1,277 @@ +import React, { Fragment, isValidElement } from 'react'; + +import { resolveWidget } from '@staticcms/core/lib/registry'; +import { selectField } from '@staticcms/core/lib/util/field.util'; +import { getTypedFieldForValue } from '@staticcms/list/typedListHelpers'; +import PreviewHOC from './PreviewHOC'; + +import type { + Collection, + Config, + Entry, + EntryData, + Field, + GetAssetFunction, + InferredField, + ListField, + RenderedField, + ValueOrNestedValue, + WidgetPreviewComponent, +} from '@staticcms/core/interface'; +import type { ReactFragment, ReactNode } from 'react'; + +/** + * Returns the widget component for a named field, and makes recursive calls + * to retrieve components for nested and deeply nested fields, which occur in + * object and list type fields. Used internally to retrieve widgets, and also + * exposed for use in custom preview templates. + */ +export default function getWidgetFor( + config: Config, + collection: Collection, + name: string, + fields: Field[], + entry: Entry, + inferredFields: Record, + getAsset: GetAssetFunction, + widgetFields: Field[] = fields, + values: EntryData = entry.data, + idx: number | null = null, +): ReactNode { + // We retrieve the field by name so that this function can also be used in + // custom preview templates, where the field object can't be passed in. + const field = widgetFields && widgetFields.find(f => f.name === name); + if (!field) { + return null; + } + + const value = values?.[field.name]; + let fieldWithWidgets = field as RenderedField; + + if ('fields' in field && field.fields) { + fieldWithWidgets = { + ...fieldWithWidgets, + renderedFields: getNestedWidgets( + config, + collection, + fields, + entry, + inferredFields, + getAsset, + field.fields, + value as EntryData | EntryData[], + ), + }; + } else if ('types' in field && field.types) { + fieldWithWidgets = { + ...fieldWithWidgets, + renderedFields: getTypedNestedWidgets( + config, + collection, + field, + entry, + inferredFields, + getAsset, + value as EntryData[], + ), + }; + } + + const labelledWidgets = ['string', 'text', 'number']; + const inferredField = Object.entries(inferredFields) + .filter(([key]) => { + const fieldToMatch = selectField(collection, key); + return fieldToMatch === fieldWithWidgets; + }) + .map(([, value]) => value)[0]; + + let renderedValue: ValueOrNestedValue | ReactNode = value; + if (inferredField) { + renderedValue = inferredField.defaultPreview(String(value)); + } else if ( + value && + fieldWithWidgets.widget && + labelledWidgets.indexOf(fieldWithWidgets.widget) !== -1 && + value.toString().length < 50 + ) { + renderedValue = ( +
+ <> + {field.label ?? field.name}: {value} + +
+ ); + } + + return renderedValue + ? getWidget(config, fieldWithWidgets, collection, renderedValue, entry, getAsset, idx) + : null; +} + +/** + * Retrieves widgets for nested fields (children of object/list fields) + */ +function getNestedWidgets( + config: Config, + collection: Collection, + fields: Field[], + entry: Entry, + inferredFields: Record, + getAsset: GetAssetFunction, + widgetFields: Field[], + values: EntryData | EntryData[], +) { + // Fields nested within a list field will be paired with a List of value Maps. + if (Array.isArray(values)) { + return values.flatMap(value => + widgetsForNestedFields( + config, + collection, + fields, + entry, + inferredFields, + getAsset, + widgetFields, + value, + ), + ); + } + + // Fields nested within an object field will be paired with a single Record of values. + return widgetsForNestedFields( + config, + collection, + fields, + entry, + inferredFields, + getAsset, + widgetFields, + values, + ); +} + +/** + * Retrieves widgets for nested fields (children of object/list fields) + */ +function getTypedNestedWidgets( + config: Config, + collection: Collection, + field: ListField, + entry: Entry, + inferredFields: Record, + getAsset: GetAssetFunction, + values: EntryData[], +) { + return values + ?.flatMap((value, index) => { + const itemType = getTypedFieldForValue(field, value ?? {}, index); + if (!itemType) { + return null; + } + + return widgetsForNestedFields( + config, + collection, + itemType.fields, + entry, + inferredFields, + getAsset, + itemType.fields, + value, + index, + ); + }) + .filter(Boolean); +} + +/** + * Use getWidgetFor as a mapping function for recursive widget retrieval + */ +function widgetsForNestedFields( + config: Config, + collection: Collection, + fields: Field[], + entry: Entry, + inferredFields: Record, + getAsset: GetAssetFunction, + widgetFields: Field[], + values: EntryData, + idx: number | null = null, +) { + return widgetFields + .map(field => + getWidgetFor( + config, + collection, + field.name, + fields, + entry, + inferredFields, + getAsset, + widgetFields, + values, + idx, + ), + ) + .filter(widget => Boolean(widget)) as JSX.Element[]; +} + +function getWidget( + config: Config, + field: RenderedField, + collection: Collection, + value: ValueOrNestedValue | ReactNode, + entry: Entry, + getAsset: GetAssetFunction, + idx: number | null = null, +) { + if (!field.widget) { + return null; + } + + const widget = resolveWidget(field.widget); + const key = idx ? field.name + '_' + idx : field.name; + + if (field.widget === 'hidden' || !widget.preview) { + return null; + } + + /** + * Use an HOC to provide conditional updates for all previews. + */ + return !widget.preview ? null : ( + )[field.name] + : value + } + entry={entry} + /> + ); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function isJsxElement(value: any): value is JSX.Element { + return isValidElement(value); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function isReactFragment(value: any): value is ReactFragment { + if (value.type) { + return value.type === Fragment; + } + + return value === Fragment; +} diff --git a/packages/core/src/constants/fieldInference.tsx b/packages/core/src/constants/fieldInference.tsx index 728e0ce8..a55d4177 100644 --- a/packages/core/src/constants/fieldInference.tsx +++ b/packages/core/src/constants/fieldInference.tsx @@ -1,20 +1,11 @@ import React from 'react'; -import type { ReactNode } from 'react'; +import type { InferredField } from '../interface'; export const IDENTIFIER_FIELDS = ['title', 'path'] as const; export const SORTABLE_FIELDS = ['title', 'date', 'author', 'description'] as const; -export interface InferredField { - type: string; - secondaryTypes: string[]; - synonyms: string[]; - defaultPreview: (value: string | boolean | number) => JSX.Element | ReactNode; - fallbackToFirstField: boolean; - showError: boolean; -} - export const INFERABLE_FIELDS: Record = { title: { type: 'string', diff --git a/packages/core/src/formats/util/j-toml.d.ts b/packages/core/src/formats/util/j-toml.d.ts index 7e3a6799..d36270a0 100644 --- a/packages/core/src/formats/util/j-toml.d.ts +++ b/packages/core/src/formats/util/j-toml.d.ts @@ -1,6 +1,493 @@ -interface StringifyOptions { - newline?: string; +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-use-before-define */ +export as namespace TOML; +export = exports; + +declare namespace exports { + export const version: '1.38.0'; + + export const parse: { + ( + this: void, + source: Source, + specificationVersion: 1.0 | 0.5 | 0.4 | 0.3 | 0.2 | 0.1, + multilineStringJoiner?: string, + useBigInt?: boolean | number, + xOptions?: XOptions, + ): Table; + ( + this: void, + source: Source, + multilineStringJoiner?: string, + useBigInt?: boolean | number, + xOptions?: XOptions, + ): Table; + ( + this: void, + source: Source, + options?: { + readonly joiner?: string; + readonly bigint?: boolean | number; + readonly x?: XOptions; + }, + ): Table; + } & { + readonly [SpecificationVersion in 1.0 | 0.5 | 0.4 | 0.3 | 0.2 | 0.1]: { + ( + this: void, + source: Source, + multilineStringJoiner?: string, + useBigInt?: boolean | number, + xOptions?: XOptions, + ): Table; + ( + this: void, + source: Source, + options?: { + readonly joiner?: string; + readonly bigint?: boolean | number; + readonly x?: XOptions; + }, + ): Table; + }; + }; + + export function stringify( + this: void, + rootTable: ReadonlyTable, + options?: { + readonly integer?: number; + readonly newline?: '\n' | '\r\n'; + readonly newlineAround?: 'document' | 'section' | 'header' | 'pairs' | 'pair'; + readonly indent?: string | number; + readonly T?: 'T' | 't' | ' '; + readonly Z?: 'Z' | 'z'; + readonly xNull?: boolean; + readonly xBeforeNewlineInMultilineTable?: ',' | ''; + readonly forceInlineArraySpacing?: 0 | 1 | 2 | 3; + }, + ): string; + + export function isSection(this: void, table: ReadonlyTable): boolean; + export function isInline(this: void, value: ReadonlyTable | ReadonlyArray): boolean; + + export function Section(this: void, table: T): T; + export function inline( + this: void, + value: T, + inlineMode?: 0 | 1 | 2 | 3, + ): T; + export function inline(this: void, value: T): T; + export const multiline: { + readonly array: { + (this: void, array: T): T; + }; + (this: void, table: T): T; + (this: void, value: string): { + [_literal]: [`"""`, ...string[], `${string}"""`] | [`'''`, ...string[], `${string}'''`]; + } & object & + String; + (this: void, lines: readonly string[]): { + [_literal]: [`"""`, ...string[], `${string}"""`] | [`'''`, ...string[], `${string}'''`]; + } & object; + (this: void, lines: readonly string[], value: string): { + [_literal]: [`"""`, ...string[], `${string}"""`] | [`'''`, ...string[], `${string}'''`]; + } & object & + String; + readonly basic: { + (this: void, value: string): { [_literal]: [`"""`, ...string[], `${string}"""`] } & object & + String; + (this: void, lines: readonly string[]): { + [_literal]: [`"""`, ...string[], `${string}"""`]; + } & object; + (this: void, lines: readonly string[], value: string): { + [_literal]: [`"""`, ...string[], `${string}"""`]; + } & object & + String; + }; + }; + export function basic(this: void, value: string): { [_literal]: `"${string}"` } & object & String; + export function literal( + this: void, + literal: string, + ): { [_literal]: string | [string, ...string[]] } & object; + export function literal( + this: void, + literal: string, + value: string, + ): { [_literal]: string | [string, ...string[]] } & object & String; + export function literal( + this: void, + literal: string, + value: number, + ): { [_literal]: string | [string, ...string[]] } & object & Number; + export function literal( + this: void, + literal: string, + value: bigint, + ): { [_literal]: string | [string, ...string[]] } & object & BigInt; + export function literal( + this: void, + literal: TemplateStringsArray, + ...chars: string[] + ): { [_literal]: string | [string, ...string[]] } & object; + + export function commentFor(this: void, key: string): symbol; + export const commentForThis: unique symbol; + + export { OffsetDateTime, LocalDateTime, LocalDate, LocalTime, Keys }; + + export { exports as default }; } -export const parse = (_content: string) => object; -export const stringify = (_content: object, _options?: StringifyOptions) => string; +declare class OffsetDateTime { + readonly toJSON: Date['toJSON']; + + readonly [Symbol.toStringTag]: 'OffsetDateTime'; + + readonly toISOString: ( + this: Readonly, + ) => `${number}-${number}-${number}T${number}:${number}:${number}${'' | `.${number}`}${ + | 'Z' + | `${'+' | '-'}${number}:${number}`}`; + readonly valueOf: (this: Readonly) => `${number}${'' | `.${number}`}`; + + private [OffsetDateTime_ISOString]: string; + private [OffsetDateTime_value]: string; + + constructor( + literal: `${number}-${number}-${number}${'T' | 't' | ' '}${number}:${number}:${number}${ + | '' + | `.${number}`}${'Z' | 'z' | `${'+' | '-'}${number}:${number}`}`, + ); + + readonly getUTCFullYear: (this: Readonly) => _1_10000; + readonly getUTCMonth: (this: Readonly) => _0_11; + readonly getUTCDate: (this: Readonly) => _1_31; + + readonly getUTCHours: (this: Readonly) => _0_23; + readonly getUTCMinutes: (this: Readonly) => _0_59; + readonly getUTCSeconds: (this: Readonly) => _0_59; + readonly getUTCMilliseconds: (this: Readonly) => _0_999; + + readonly getUTCDay: (this: Readonly) => _0_6; + readonly getTimezoneOffset: (this: Readonly) => _1439_1439; + readonly getTime: (this: Readonly) => number; +} +declare class LocalDateTime { + readonly toJSON: Date['toJSON']; + + readonly [Symbol.toStringTag]: 'LocalDateTime'; + + readonly toISOString: ( + this: Readonly, + ) => `${number}-${number}-${number}T${number}:${number}:${number}${'' | `.${number}`}`; + readonly valueOf: (this: Readonly) => `${number}`; + + private [LocalDateTime_ISOString]: string; + private [LocalDateTime_value]: string; + + constructor( + literal: `${number}-${number}-${number}${'T' | 't' | ' '}${number}:${number}:${number}${ + | '' + | `.${number}`}`, + ); + + readonly getFullYear: (this: Readonly) => _0_9999; + readonly setFullYear: (this: LocalDateTime, year: _0_9999) => void; + readonly getMonth: (this: Readonly) => _0_11; + readonly setMonth: (this: LocalDateTime, month: _0_11) => void; + readonly getDate: (this: Readonly) => _1_31; + readonly setDate: (this: LocalDateTime, date: _1_31) => void; + + readonly getHours: (this: Readonly) => _0_23; + readonly setHours: (this: LocalDateTime, hours: _0_23) => void; + readonly getMinutes: (this: Readonly) => _0_59; + readonly setMinutes: (this: LocalDateTime, min: _0_59) => void; + readonly getSeconds: (this: Readonly) => _0_59; + readonly setSeconds: (this: LocalDateTime, sec: _0_59) => void; + readonly getMilliseconds: (this: Readonly) => _0_999; + readonly setMilliseconds: (this: LocalDateTime, ms: _0_999) => void; +} +declare class LocalDate { + readonly toJSON: Date['toJSON']; + + readonly [Symbol.toStringTag]: 'LocalDate'; + + readonly toISOString: (this: Readonly) => `${number}-${number}-${number}`; + readonly valueOf: (this: Readonly) => `${number}`; + + private [LocalDate_ISOString]: string; + private [LocalDate_value]: string; + + constructor(literal: `${number}-${number}-${number}`); + + readonly getFullYear: (this: Readonly) => _0_9999; + readonly setFullYear: (this: LocalDate, year: _0_9999) => void; + readonly getMonth: (this: Readonly) => _0_11; + readonly setMonth: (this: LocalDate, month: _0_11) => void; + readonly getDate: (this: Readonly) => _1_31; + readonly setDate: (this: LocalDate, date: _1_31) => void; +} +declare class LocalTime { + readonly toJSON: Date['toJSON']; + + readonly [Symbol.toStringTag]: 'LocalTime'; + + readonly toISOString: ( + this: Readonly, + ) => `${number}:${number}:${number}${'' | `.${number}`}`; + readonly valueOf: (this: Readonly) => `${number}`; + + private [LocalTime_ISOString]: string; + private [LocalTime_value]: string; + + constructor(literal: `${number}:${number}:${number}${'' | `.${number}`}`); + + readonly getHours: (this: Readonly) => _0_23; + readonly setHours: (this: LocalTime, hours: _0_23) => void; + readonly getMinutes: (this: Readonly) => _0_59; + readonly setMinutes: (this: LocalTime, min: _0_59) => void; + readonly getSeconds: (this: Readonly) => _0_59; + readonly setSeconds: (this: LocalTime, sec: _0_59) => void; + readonly getMilliseconds: (this: Readonly) => _0_999; + readonly setMilliseconds: (this: LocalTime, ms: _0_999) => void; +} + +declare class Keys extends RegExp { + readonly lastIndex: number; + constructor(keys: ArrayLike); + readonly test: (this: Keys, key: string) => boolean; +} + +declare const _literal: unique symbol; + +type Source = + | string + | ArrayBufferLike + | { + readonly path: string; + readonly data?: undefined; + readonly require: + | { + readonly resolve?: { readonly paths?: undefined }; + (this: void, id: 'fs'): { + readonly readFileSync: (this: void, path: string) => ArrayBufferLike; + }; + } + | { + readonly resolve: { readonly paths: (this: void, request: string) => null | string[] }; + (this: void, id: 'path'): { + readonly resolve: (this: void, dirname: string, filename: string) => string; + }; + (this: void, id: 'fs'): { + readonly readFileSync: (this: void, path: string) => ArrayBufferLike; + }; + }; + } + | { + readonly path: string; + readonly data: string | ArrayBufferLike; + readonly require?: + | { + readonly resolve?: { readonly paths?: undefined }; + } + | { + readonly resolve: { readonly paths: (this: void, request: string) => null | string[] }; + (this: void, id: 'path'): { + readonly resolve: (this: void, dirname: string, filename: string) => string; + }; + }; + }; + +type XOptions = null | { + readonly keys?: null | Keys; + readonly order?: boolean; + readonly exact?: boolean; + readonly multi?: boolean; + readonly longer?: boolean; + readonly string?: boolean; + readonly comment?: boolean; + readonly literal?: boolean; + readonly null?: boolean; + readonly tag?: + | null + | (< + Table extends object & { [key: string | symbol]: any }, + Key extends string | symbol, + Array extends any[], + Index extends number, + Tag extends string, + >( + this: void, + each: + | { table: Table; key: Key; tag: Tag } + | { array: Array; index: Index; tag: Tag } + | { table: Table; key: Key; array: Array; index: Index; tag: Tag }, + ) => void); +}; + +type ReadonlyTable = object & { readonly [key: string]: ReadonlyValue }; +type ReadonlyArray = readonly ReadonlyValue[]; +type ReadonlyValue = + | ({ readonly [_literal]: string | readonly [string, ...string[]] } & object) + | null + | boolean + | bigint + | number + | string + | ReadonlyDatetime + | ReadonlyArray + | ReadonlyTable; +interface ReadonlyDatetime { + readonly toISOString: (this: this) => string; +} + +type Table = object & { [key: string]: Value }; +type Array = Value[]; +type Value = + | (object & BigInt & { [_literal]: string }) + | (object & Number & { [_literal]: string }) + | (object & String & { [_literal]: string | [string, ...string[]] }) + | null + | boolean + | bigint + | number + | string + | Datetime + | Array + | Table; +type Datetime = OffsetDateTime | LocalDateTime | LocalDate | LocalTime; +declare const OffsetDateTime_ISOString: unique symbol; +declare const OffsetDateTime_value: unique symbol; +declare const LocalDateTime_ISOString: unique symbol; +declare const LocalDateTime_value: unique symbol; +declare const LocalDate_ISOString: unique symbol; +declare const LocalDate_value: unique symbol; +declare const LocalTime_ISOString: unique symbol; +declare const LocalTime_value: unique symbol; + +type _1439_1439 = -1439 | ({} & number) | 1439; +type _1_10000 = -1 | ({} & number) | 10000; +type _0_9999 = 0 | ({} & number) | 9999; +type _0_999 = 0 | ({} & number) | 999; +type _0_6 = 0 | 1 | 2 | 3 | 4 | 5 | 6; +type _0_11 = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11; +type _0_23 = + | 0 + | 1 + | 2 + | 3 + | 4 + | 5 + | 6 + | 7 + | 8 + | 9 + | 10 + | 11 + | 12 + | 13 + | 14 + | 15 + | 16 + | 17 + | 18 + | 19 + | 20 + | 21 + | 22 + | 23; +type _1_31 = + | 1 + | 2 + | 3 + | 4 + | 5 + | 6 + | 7 + | 8 + | 9 + | 10 + | 11 + | 12 + | 13 + | 14 + | 15 + | 16 + | 17 + | 18 + | 19 + | 20 + | 21 + | 22 + | 23 + | 24 + | 25 + | 26 + | 27 + | 28 + | 29 + | 30 + | 31; +type _0_59 = + | 0 + | 1 + | 2 + | 3 + | 4 + | 5 + | 6 + | 7 + | 8 + | 9 + | 10 + | 11 + | 12 + | 13 + | 14 + | 15 + | 16 + | 17 + | 18 + | 19 + | 20 + | 21 + | 22 + | 23 + | 24 + | 25 + | 26 + | 27 + | 28 + | 29 + | 30 + | 31 + | 32 + | 33 + | 34 + | 35 + | 36 + | 37 + | 38 + | 39 + | 40 + | 41 + | 42 + | 43 + | 44 + | 45 + | 46 + | 47 + | 48 + | 49 + | 50 + | 51 + | 52 + | 53 + | 54 + | 55 + | 56 + | 57 + | 58 + | 59; diff --git a/packages/core/src/formats/util/j-toml.js b/packages/core/src/formats/util/j-toml.js index 8f8c7d8a..6d2286d6 100644 --- a/packages/core/src/formats/util/j-toml.js +++ b/packages/core/src/formats/util/j-toml.js @@ -1,15 +1,21 @@ /*!@preserve@license -* 模块名称:j-toml -* 模块功能:龙腾道为汤小明语写的实现。从属于“简计划”。 -      An implementation of TOML written by LongTengDao. Belong to "Plan J". -* 模块版本:1.37.0 -* 许可条款:LGPL-3.0 -* 所属作者:龙腾道 (www.LongTengDao.com) -* 问题反馈:https://GitHub.com/LongTengDao/j-toml/issues -* 项目主页:https://GitHub.com/LongTengDao/j-toml/ -*/ + * 模块名称:j-toml + * 模块功能:龙腾道为汤小明语写的实现。从属于“简计划”。 +      An implementation of TOML written by LongTengDao. Belong to "Plan J". + * 模块版本:1.38.0 + * 许可条款:LGPL-3.0 + * 所属作者:龙腾道 (www.LongTengDao.com) + * 问题反馈:https://GitHub.com/LongTengDao/j-toml/issues + * 项目主页:https://GitHub.com/LongTengDao/j-toml/ + */ -const version = '1.37.0'; +(function (global, factory) { +typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : +typeof define === 'function' && define.amd ? define(factory) : +(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.TOML = factory()); +})(this, (function () { 'use strict'; + +const version = '1.38.0'; const SyntaxError$1 = SyntaxError; @@ -128,15 +134,15 @@ const Default = ( ); /*!@preserve@license -* 模块名称:j-regexp -* 模块功能:可读性更好的正则表达式创建方式。从属于“简计划”。 -      More readable way for creating RegExp. Belong to "Plan J". -* 模块版本:8.2.0 -* 许可条款:LGPL-3.0 -* 所属作者:龙腾道 (www.LongTengDao.com) -* 问题反馈:https://GitHub.com/LongTengDao/j-regexp/issues -* 项目主页:https://GitHub.com/LongTengDao/j-regexp/ -*/ + * 模块名称:j-regexp + * 模块功能:可读性更好的正则表达式创建方式。从属于“简计划”。 +      More readable way for creating RegExp. Belong to "Plan J". + * 模块版本:8.2.0 + * 许可条款:LGPL-3.0 + * 所属作者:龙腾道 (www.LongTengDao.com) + * 问题反馈:https://GitHub.com/LongTengDao/j-regexp/issues + * 项目主页:https://GitHub.com/LongTengDao/j-regexp/ + */ var Test = bind ? /*#__PURE__*/bind.bind(test ) @@ -401,15 +407,15 @@ const Reflect_deleteProperty = Reflect.deleteProperty; const ownKeys = Reflect.ownKeys; /*!@preserve@license -* 模块名称:j-orderify -* 模块功能:返回一个能保证给定对象的属性按此后添加顺序排列的 proxy,即使键名是 symbol,或整数 string。从属于“简计划”。 -      Return a proxy for given object, which can guarantee own keys are in setting order, even if the key name is symbol or int string. Belong to "Plan J". -* 模块版本:7.0.1 -* 许可条款:LGPL-3.0 -* 所属作者:龙腾道 (www.LongTengDao.com) -* 问题反馈:https://GitHub.com/LongTengDao/j-orderify/issues -* 项目主页:https://GitHub.com/LongTengDao/j-orderify/ -*/ + * 模块名称:j-orderify + * 模块功能:返回一个能保证给定对象的属性按此后添加顺序排列的 proxy,即使键名是 symbol,或整数 string。从属于“简计划”。 +      Return a proxy for given object, which can guarantee own keys are in setting order, even if the key name is symbol or int string. Belong to "Plan J". + * 模块版本:7.0.1 + * 许可条款:LGPL-3.0 + * 所属作者:龙腾道 (www.LongTengDao.com) + * 问题反馈:https://GitHub.com/LongTengDao/j-orderify/issues + * 项目主页:https://GitHub.com/LongTengDao/j-orderify/ + */ const Keeper = () => []; @@ -423,16 +429,16 @@ const newWeakMap = () => { const target2keeper = /*#__PURE__*/newWeakMap() -; + ; const proxy2target = /*#__PURE__*/newWeakMap() -; + ; const target2proxy = /*#__PURE__*/newWeakMap() -; + ; const handlers = /*#__PURE__*/assign$1(create$1(NULL), { defineProperty: (target , key , descriptor ) => { @@ -518,11 +524,11 @@ const ofInline = /*#__PURE__*/get.bind(INLINES) -; + ; const beInline = /*#__PURE__*/set.bind(INLINES) -; + ; const inline = (value , mode , looping ) => { if ( isArray$1(value) ) { if ( looping ) { mode = 3; } @@ -636,9 +642,9 @@ const next = () => sourceLines[++lineIndex] ; const rest = () => lineIndex!==lastLineIndex; class mark { - lineIndex = lineIndex; - type ; - restColumn ; + lineIndex = lineIndex; + type ; + restColumn ; constructor (type , restColumn ) { this.type = type; this.restColumn = restColumn; @@ -667,7 +673,7 @@ const done = () => { const Whitespace = /[ \t]/; const PRE_WHITESPACE = /*#__PURE__*/newRegExp` - ^${Whitespace}+`.valueOf(); +^${Whitespace}+`.valueOf(); const { exec: VALUE_REST_exec } = /*#__PURE__*/newRegExp.s ` ^ @@ -713,8 +719,8 @@ ${Whitespace}* = ${Whitespace}* (?: -<(${Tag})> -${Whitespace}* + <(${Tag})> + ${Whitespace}* )? (.*) $`.valueOf(); @@ -917,7 +923,7 @@ const Keys = class KeysRegExp extends RegExp$1 { this.lastIndex = maxLength+1; return this; } - test ( key ) { + test ( key ) { return key.length { if ( literal==='nan' || literal==='+nan' ) { return NaN$1; } if ( literal==='-nan' ) { return _NaN; } } + else if ( !sError ) { + if ( literal==='inf' || literal==='+inf' ) { return Infinity; } + if ( literal==='-inf' ) { return _Infinity$1; } + } throw throws(SyntaxError$1(`Invalid Float ${literal}` + where(' at '))); } const withoutUnderscores = literal.replace(UNDERSCORES, ''); @@ -1899,7 +1909,7 @@ const assignLiteralString = ( (table , finalKey , literal ) } ) -; + ; const assignBasicString = ( (table , finalKey , literal ) => { if ( !literal.startsWith('"""') ) { @@ -1952,7 +1962,7 @@ const assignBasicString = ( (table , finalKey , literal ) } ) -; + ; const KEYS = /*#__PURE__*/Null$1 (null); const commentFor = (key ) => KEYS[key] || ( KEYS[key] = Symbol$1(key) ); @@ -2097,7 +2107,7 @@ const equalStaticArray = function * ( table , finalKey , } else { if ( lineRest[0]===']' ) { break; } - throw throws(SyntaxError$1(`Unexpected character in static array item value` + where(', which is found at '))); + throw throws(SyntaxError$1(`Unexpect character in static array item value` + where(', which is found at '))); } } inline===null || beInline(staticArray, inline); @@ -2105,7 +2115,7 @@ const equalStaticArray = function * ( table , finalKey , } -; + ; const equalInlineTable = function * ( table , finalKey , lineRest ) { const inlineTable = table[finalKey] = new Table(DIRECTLY, INLINE); @@ -2158,7 +2168,7 @@ const equalInlineTable = function * ( table , finalKey , } -; + ; const ForComment = (lastInlineTable , lineRest ) => { @@ -2231,7 +2241,7 @@ const Root = () => { const { leadingKeys, finalKey, asArrayItem, tag, lineRest } = TABLE_DEFINITION_exec_groups(line, parseKeys); const table = prepareTable(rootTable, leadingKeys); if ( lineRest ) { - lineRest[0]==='#' || throws(SyntaxError$1(`Unexpected charachtor after table header` + where(' at '))); + lineRest[0]==='#' || throws(SyntaxError$1(`Unexpect charachtor after table header` + where(' at '))); } lastSectionTable = appendTable(table, finalKey, asArrayItem, tag); preserveComment && lineRest && ( lastSectionTable[commentForThis] = asArrayItem ? lineRest.slice(1) : table[commentFor(finalKey)] = lineRest.slice(1) ); @@ -2244,7 +2254,7 @@ const Root = () => { let rest = assign(forComment); typeof rest==='string' || ( rest = x (rest) ); if ( rest ) { - rest[0]==='#' || throws(SyntaxError$1(`Unexpected charachtor after key/value pair` + where(' at '))); + rest[0]==='#' || throws(SyntaxError$1(`Unexpect charachtor after key/value pair` + where(' at '))); if ( preserveComment ) { forComment.table[commentFor(forComment.finalKey)] = rest.slice(1); } } } @@ -2455,7 +2465,7 @@ const $Keys = (keys ) => isAmazing(keys) ? keys.replace(FIRST, li class TOMLSection extends Array$1 { - document ; + document ; constructor (document ) { super(); @@ -2706,26 +2716,26 @@ const return_false = () => false; class TOMLDocument extends Array$1 { - get ['constructor'] () { return Array$1; } + get ['constructor'] () { return Array$1; } 0 = new TOMLSection(this); - asInteger = return_false; - newline = ''; - newlineUnderSection = true; - newlineUnderSectionButPair = true; - newlineUnderHeader = true; - newlineUnderPair = false; - newlineUnderPairButDotted = false; - newlineUnderDotted = false; - indent = '\t'; - T = 'T'; - Z = 'Z'; - nullDisabled = true; - multilineTableDisabled = true; - multilineTableComma ; - preferCommentForThis = false; - $singlelineArray ; + asInteger = return_false; + newline = ''; + newlineUnderSection = true; + newlineUnderSectionButPair = true; + newlineUnderHeader = true; + newlineUnderPair = false; + newlineUnderPairButDotted = false; + newlineUnderDotted = false; + indent = '\t'; + T = 'T'; + Z = 'Z'; + nullDisabled = true; + multilineTableDisabled = true; + multilineTableComma ; + preferCommentForThis = false; + $singlelineArray ; constructor (options ) { @@ -2887,7 +2897,16 @@ const assertFulScalar = (string ) => { let holding = false; const parse = (source , specificationVersion , multilineStringJoiner , bigint , x , argsMode ) => { - let sourcePath = '';if ( typeof source==='string' ) { assertFulScalar(source); } + let sourcePath = ''; + if ( typeof source==='object' && source ) { + if ( isArray$1(source) ) { throw TypeError$1(isLinesFromStringify(source) ? `TOML.parse(array from TOML.stringify(,{newline?}))` : `TOML.parse(array)`); } + else if ( isBinaryLike(source) ) { source = binary2string(source); } + else { + sourcePath = source.path; + if ( typeof sourcePath!=='string' ) { throw TypeError$1(`TOML.parse(source.path)`); } + } + } + else if ( typeof source==='string' ) { assertFulScalar(source); } else { throw TypeError$1(`TOML.parse(source)`); } let joiner ; let keys ; @@ -2948,6 +2967,6 @@ const _export = /*#__PURE__*/Default({ Keys, }); -export { Keys, LocalDate, LocalDateTime, LocalTime, OffsetDateTime, Section, basic, commentFor, commentForThis, _export as default, inline, isInline, isSection, literal, multiline, parse$1 as parse, stringify, version }; +return _export; -/*¡ j-toml */ +})); diff --git a/packages/core/src/interface.ts b/packages/core/src/interface.ts index bc833b9e..6819e97c 100644 --- a/packages/core/src/interface.ts +++ b/packages/core/src/interface.ts @@ -290,6 +290,8 @@ export type WidgetPreviewComponent> | ComponentType>; +export type WidgetFor

= (name: K) => ReactNode; + export type WidgetsFor

= ( name: K, ) => P[K] extends Array @@ -312,7 +314,7 @@ export interface TemplatePreviewProps>; - widgetFor: (name: T extends EntryData ? string : keyof T) => ReactNode; + widgetFor: WidgetFor; widgetsFor: WidgetsFor; } @@ -321,6 +323,20 @@ export type TemplatePreviewComponent< EF extends BaseField = UnknownField, > = ComponentType>; +export interface TemplatePreviewCardProps { + collection: Collection; + fields: Field[]; + entry: Entry; + viewStyle: 'list' | 'grid'; + widgetFor: WidgetFor; + widgetsFor: WidgetsFor; +} + +export type TemplatePreviewCardComponent< + T = EntryData, + EF extends BaseField = UnknownField, +> = ComponentType>; + export interface WidgetOptions { validator?: Widget['validator']; getValidValue?: Widget['getValidValue']; @@ -912,3 +928,12 @@ export type DeepPartial = T extends object [P in keyof T]?: DeepPartial; } : T; + +export interface InferredField { + type: string; + secondaryTypes: string[]; + synonyms: string[]; + defaultPreview: (value: string | boolean | number) => JSX.Element | ReactNode; + fallbackToFirstField: boolean; + showError: boolean; +} diff --git a/packages/core/src/lib/registry.ts b/packages/core/src/lib/registry.ts index 6ea19037..1fdc4959 100644 --- a/packages/core/src/lib/registry.ts +++ b/packages/core/src/lib/registry.ts @@ -19,6 +19,7 @@ import type { PreviewStyle, PreviewStyleOptions, ShortcodeConfig, + TemplatePreviewCardComponent, TemplatePreviewComponent, UnknownField, Widget, @@ -38,6 +39,7 @@ const eventHandlers = allowedEvents.reduce((acc, e) => { interface Registry { backends: Record; templates: Record>; + cards: Record>; widgets: Record; icons: Record; additionalLinks: Record; @@ -57,6 +59,7 @@ interface Registry { const registry: Registry = { backends: {}, templates: {}, + cards: {}, widgets: {}, icons: {}, additionalLinks: {}, @@ -71,6 +74,8 @@ const registry: Registry = { export default { registerPreviewTemplate, getPreviewTemplate, + registerPreviewCard, + getPreviewCard, registerWidget, getWidget, getWidgets, @@ -123,6 +128,17 @@ export function getPreviewTemplate(name: string): TemplatePreviewComponent(name: string, component: TemplatePreviewCardComponent) { + registry.cards[name] = component as TemplatePreviewCardComponent; +} + +export function getPreviewCard(name: string): TemplatePreviewCardComponent { + return registry.cards[name]; +} + /** * Editor Widgets */ diff --git a/packages/core/src/lib/util/collection.util.ts b/packages/core/src/lib/util/collection.util.ts index a761929e..ff4b0b48 100644 --- a/packages/core/src/lib/util/collection.util.ts +++ b/packages/core/src/lib/util/collection.util.ts @@ -15,13 +15,13 @@ import { selectField } from './field.util'; import { selectMediaFolder } from './media.util'; import type { Backend } from '@staticcms/core/backend'; -import type { InferredField } from '@staticcms/core/constants/fieldInference'; import type { Collection, Config, Entry, Field, FilesCollection, + InferredField, ObjectField, SortableField, } from '@staticcms/core/interface'; diff --git a/packages/core/src/reducers/selectors/config.ts b/packages/core/src/reducers/selectors/config.ts index ac9519e1..2776e11b 100644 --- a/packages/core/src/reducers/selectors/config.ts +++ b/packages/core/src/reducers/selectors/config.ts @@ -1,7 +1,12 @@ /* eslint-disable import/prefer-default-export */ import type { Config } from '@staticcms/core/interface'; +import type { RootState } from '@staticcms/core/store'; export function selectLocale(config?: Config) { return config?.locale || 'en'; } + +export function selectConfig(state: RootState) { + return state.config.config; +} diff --git a/packages/demo/public/index.html b/packages/demo/public/index.html index 5bb2b928..813dea5f 100644 --- a/packages/demo/public/index.html +++ b/packages/demo/public/index.html @@ -20,7 +20,7 @@ }, '2015-02-16-this-is-a-toml-frontmatter-post.md': { content: - '+++\ntitle = "This is a TOML front matter post"\nimage = "/assets/uploads/moby-dick.jpg"\n"date" = "2015-02-16T00:00:00.000Z"\n+++\n\n# I Am a Title in Markdown\n\nHello, world\n\n* One Thing\n* Another Thing\n* A Third Thing\n', + '+++\ntitle = This is a TOML front matter post\nimage = "/assets/uploads/moby-dick.jpg"\n"date" = "2015-02-16T00:00:00.000Z"\n+++\n\n# I Am a Title in Markdown\n\nHello, world\n\n* One Thing\n* Another Thing\n* A Third Thing\n', }, '2015-02-14-this-is-a-post-with-a-different-extension.other': { content: diff --git a/packages/demo/src/cms.jsx b/packages/demo/src/cms.jsx index aa879200..ccfaa37a 100644 --- a/packages/demo/src/cms.jsx +++ b/packages/demo/src/cms.jsx @@ -18,6 +18,51 @@ const PostPreview = ({ entry, widgetFor }) => { ); }; +const PostPreviewCard = ({ entry, widgetFor, viewStyle }) => { + return ( +

+ {viewStyle === "grid" ? widgetFor("image") : null} +
+
+
+ {entry.data.title} + {entry.data.date} +
+
+ {entry.data.draft === true ? "Draft" : "Published"} +
+
+
+
+ ); +}; + const GeneralPreview = ({ widgetsFor, entry }) => { const title = entry.data.site_title; const posts = entry.data.posts; @@ -88,6 +133,7 @@ const CustomPage = () => { cms.registerPreviewStyle(".toastui-editor-contents h1 { color: blue }", { raw: true }); cms.registerPreviewTemplate("posts", PostPreview); +CMS.registerPreviewCard("posts", PostPreviewCard); cms.registerPreviewTemplate("general", GeneralPreview); cms.registerPreviewTemplate("authors", AuthorsPreview); // Pass the name of a registered control to reuse with a new widget preview. diff --git a/packages/docs/content/docs/custom-previews.mdx b/packages/docs/content/docs/custom-previews.mdx index f307589b..6e8cb5ce 100644 --- a/packages/docs/content/docs/custom-previews.mdx +++ b/packages/docs/content/docs/custom-previews.mdx @@ -4,17 +4,21 @@ title: Creating Custom Previews weight: 50 --- -The Static CMS exposes a `window.CMS` global object that you can use to register custom previews for an entire collection (or file within a file collection) via `registerPreviewTemplate`. +The Static CMS exposes a `window.CMS` global object that you can use to register custom previews for an entire collection (or file within a file collection) via `registerPreviewTemplate` (editor view) and `registerPreviewCard` (collection view). ### React Components Inline -The `registerPreviewTemplate` requires you to provide a React component. If you have a build process in place for your project, it is possible to integrate with this build process. +The `registerPreviewTemplate` and `registerPreviewCard` require you to provide a React component. If you have a build process in place for your project, it is possible to integrate with this build process. However, although possible, it may be cumbersome or even impractical to add a React build phase. For this reason, Static CMS exposes some constructs globally to allow you to create components inline: `h` (alias for React.createElement) as well some basic hooks (`useState`, `useMemo`, `useEffect`, `useCallback`). **NOTE**: `createClass` is still provided, allowing for the creation of react class components. However it has now been deprecated and will be removed in `v2.0.0`. -## Params +## Editor Preview + +`registerPreviewTemplate` allows you to create a template that overrides the entire editor preview for a given collection. + +### `registerPreviewTemplate` Params | Param | Type | Description | | --------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | @@ -26,13 +30,15 @@ The following parameters will be passed to your `react_component` during render: | Param | Type | Description | | ---------- | -------------- | ---------------------------------------------------------------------------------------------------- | | entry | object | Object with a `data` field that contains the current value of all widgets in the editor | +| collection | object | Collection configuration | +| fields | object | The fields for the given collection | | document | Document | The document object the preview is within. If rendered with a frame, it will be the frame's document | | window | Window | The window object the preview is within. If rendered with a frame, it will be the frame's window | | getAsset | Async function | Function that given a url returns (as a promise) a loaded asset | | widgetFor | Function | Given a field name, returns the rendered preview of that field's widget and value | | widgetsFor | Function | Given a field name, returns the rendered previews of that field's nested child widgets and values | -### Example +#### `registerPreviewTemplate` Example @@ -62,7 +68,7 @@ const PostPreview = ({ widgetFor, getAsset, entry, collection, field }) => {

{entry.data.title}

-
{widgetFor('body')}
+
{widgetFor('body')}
); }; @@ -84,14 +90,20 @@ interface Post { body: string; } -const PostPreview = ({ widgetFor, getAsset, entry, collection, field }: TemplatePreviewProps) => { +const PostPreview = ({ + widgetFor, + getAsset, + entry, + collection, + field, +}: TemplatePreviewProps) => { const imageUrl = useMediaAsset(entry.data.image, collection, field, entry); return (

{entry.data.title}

-
{widgetFor('body')}
+
{widgetFor('body')}
); }; @@ -101,11 +113,11 @@ CMS.registerPreviewTemplate('posts', PostPreview);
-### Lists and Objects +#### Lists and Objects The API for accessing the individual fields of list- and object-type entries is similar to the API for accessing fields in standard entries, but there are a few key differences. Access to these nested fields is facilitated through the `widgetsFor` function, which is passed to the preview template component during render. -#### List Template Example +##### List Template Example For list fields, the widgetFor function returns an array of objects that you can map over in your template. If your field is a list of authors containing two entries, with fields `name` and `description`, the return value of `widgetsFor` would look like this: @@ -210,7 +222,7 @@ CMS.registerPreviewTemplate('authors', AuthorsPreview); -#### Object Example +##### Object Example Object fields are simpler than lists - instead of `widgetsFor` returning an array of objects, it returns a single object. Accessing the shape of that object is the same as the shape of objects returned for list fields: @@ -309,3 +321,205 @@ CMS.registerPreviewTemplate('general', GeneralPreview); ``` + +## Collection Card Preview + +`registerPreviewCard` allows you to create a card template that overrides the cards displayed in the collection view. + +### `registerPreviewCard` Params + +| Param | Type | Description | +| --------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| name | string | The name of the collection (or file for file collections) which this preview component will be used for
  • Folder collections: Use the name of the collection
  • File collections: Use the name of the file
| +| react_component | [React Function Component](https://reactjs.org/docs/components-and-props.html) | A React functional component that renders a preview card for a given entry in your collection | + +The following parameters will be passed to your `react_component` during render: + +| Param | Type | Description | +| ---------- | --------------------- | ------------------------------------------------------------------------------------------------- | +| viewStyle | 'list'
\| 'grid' | The current view style being displayed | +| entry | object | Object with a `data` field that contains the current value of all widgets in the editor | +| widgetFor | Function | Given a field name, returns the rendered preview of that field's widget and value | +| widgetsFor | Function | Given a field name, returns the rendered previews of that field's nested child widgets and values | + +#### `registerPreviewTemplate` Example + + + +```js +const PostPreviewCard = ({ entry, widgetFor, viewStyle }) => { + return h( + 'div', + { style: { width: '100%' } }, + viewStyle === 'grid' ? widgetFor('image') : null, + h( + 'div', + { style: { padding: '16px', width: '100%' } }, + h( + 'div', + { + style: { + display: 'flex', + width: '100%', + justifyContent: 'space-between', + alignItems: 'start', + }, + }, + h( + 'div', + { + style: { + display: 'flex', + flexDirection: viewStyle === 'grid' ? 'column' : 'row', + alignItems: 'baseline', + gap: '8px', + }, + }, + h('strong', { style: { fontSize: '24px' } }, entry.data.title), + h('span', { style: { fontSize: '16px' } }, entry.data.date), + ), + h( + 'div', + { + style: { + backgroundColor: entry.data.draft === true ? 'blue' : 'green', + color: 'white', + border: 'none', + padding: '4px 8px', + textAlign: 'center', + textDecoration: 'none', + display: 'inline-block', + cursor: 'pointer', + borderRadius: '4px', + }, + }, + entry.data.draft === true ? 'Draft' : 'Published', + ), + ), + ), + ); +}; + +CMS.registerPreviewCard('posts', PostPreviewCard); +``` + +```jsx +import CMS from '@staticcms/core'; + +const PostPreviewCard = ({ entry, widgetFor, viewStyle }) => { + return ( +
+ {viewStyle === 'grid' ? widgetFor('image') : null} +
+
+
+ {entry.data.title} + {entry.data.date} +
+
+ {entry.data.draft === true ? 'Draft' : 'Published'} +
+
+
+
+ ); +}; +``` + +```tsx +import CMS from '@staticcms/core'; + +import type { TemplatePreviewCardProps } from '@staticcms/core'; + +/** + * The type for 'entry.data' + */ +interface Post { + image: string; + title: string; + body: string; +} + +const PostPreviewCard = ({ entry, widgetFor, viewStyle }: TemplatePreviewCardProps) => { + return ( +
+ {viewStyle === 'grid' ? widgetFor('image') : null} +
+
+
+ {entry.data.title} + {entry.data.date} +
+
+ {entry.data.draft === true ? 'Draft' : 'Published'} +
+
+
+
+ ); +}; + +CMS.registerPreviewTemplate('posts', PostPreview); +``` + +
+ +##### List View + +![Post Preview Card List View](/img/preview_card_list.png) + +##### Grid View + +![Post Preview Card List View](/img/preview_card_grid.png) diff --git a/packages/docs/public/img/preview_card_grid.png b/packages/docs/public/img/preview_card_grid.png new file mode 100644 index 00000000..f7729470 Binary files /dev/null and b/packages/docs/public/img/preview_card_grid.png differ diff --git a/packages/docs/public/img/preview_card_list.png b/packages/docs/public/img/preview_card_list.png new file mode 100644 index 00000000..5dedd07a Binary files /dev/null and b/packages/docs/public/img/preview_card_list.png differ