feat: custom collection card template (#433)

This commit is contained in:
Daniel Lautzenheiser 2023-01-25 15:11:59 -05:00 committed by GitHub
parent c6994ea45b
commit 1641630cfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1440 additions and 496 deletions

View File

@ -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",

View File

@ -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: {

View File

@ -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.

View File

@ -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 (
<Card>
<CardActionArea
component={Link}
to={path}
sx={{
height: '100%',
position: 'relative',
display: 'flex',
width: '100%',
justifyContent: 'start',
}}
>
<PreviewCardComponent
collection={collection}
fields={fields}
entry={entry}
viewStyle={viewStyle === VIEW_STYLE_LIST ? 'list' : 'grid'}
widgetFor={widgetFor}
widgetsFor={widgetsFor}
/>
</CardActionArea>
</Card>
);
}
return (
<Card>
<CardActionArea component={Link} to={path}>

View File

@ -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<ObjectValue>) => {
if (!collection || !fields) {
return null;
}

View File

@ -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<string, InferredField>,
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 = (
<div key={field.name}>
<>
<strong>{field.label ?? field.name}:</strong> {value}
</>
</div>
);
}
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<Field>,
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 : (
<PreviewHOC
previewComponent={widget.preview as WidgetPreviewComponent}
key={key}
field={field as RenderedField}
getAsset={getAsset}
config={config}
collection={collection}
value={
value &&
typeof value === 'object' &&
!Array.isArray(value) &&
field.name in value &&
!isJsxElement(value) &&
!isReactFragment(value)
? (value as Record<string, unknown>)[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<string, InferredField>,
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<string, InferredField>,
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<string, InferredField>,
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<EditorPreviewPaneProps>) => {
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<EditorPreviewPaneProps>) => {
[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] = (
<div key={index}>
{getWidgetFor(
cmsConfig,
collection,
field.name,
fields,
entry,
inferredFields,
handleGetAsset,
nestedFields,
val,
index,
)}
</div>
);
return acc;
}, {} as Record<string, ReactNode>);
return { data: val, widgets };
});
}
if (typeof value !== 'object') {
return {
data: {},
widgets: {},
};
}
return {
data: value,
widgets: nestedFields.reduce((acc, field, index) => {
acc[field.name] = (
<div key={index}>
{getWidgetFor(
cmsConfig,
collection,
field.name,
fields,
entry,
inferredFields,
handleGetAsset,
nestedFields,
value,
index,
)}
</div>
);
return acc;
}, {} as Record<string, ReactNode>),
};
},
[collection, config.config, entry, fields, handleGetAsset, inferredFields],
);
const previewStyles = useMemo(
() => [
...getPreviewStyles().map((style, i) => {

View File

@ -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 <StyledWidgetPreviewContainer>{children}</StyledWidgetPreviewContainer>;
return <div>{children}</div>;
};
export default WidgetPreviewContainer;

View File

@ -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<EntryData>;
widgetsFor: WidgetsFor<EntryData>;
} {
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<WidgetFor<EntryData>> => {
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<WidgetsFor<EntryData>> => {
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] = (
<div key={index}>
{getWidgetFor(
config,
collection,
field.name,
fields,
entry,
inferredFields,
handleGetAsset,
nestedFields,
val,
index,
)}
</div>
);
return acc;
}, {} as Record<string, ReactNode>);
return { data: val, widgets };
});
}
if (typeof value !== 'object') {
return {
data: {},
widgets: {},
};
}
return {
data: value,
widgets: nestedFields.reduce((acc, field, index) => {
acc[field.name] = (
<div key={index}>
{getWidgetFor(
config,
collection,
field.name,
fields,
entry,
inferredFields,
handleGetAsset,
nestedFields,
value,
index,
)}
</div>
);
return acc;
}, {} as Record<string, ReactNode>),
};
},
[collection, config, entry, fields, handleGetAsset, inferredFields],
);
return {
widgetFor,
widgetsFor,
};
}

View File

@ -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<string, InferredField>,
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 = (
<div key={field.name}>
<>
<strong>{field.label ?? field.name}:</strong> {value}
</>
</div>
);
}
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<string, InferredField>,
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<string, InferredField>,
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<string, InferredField>,
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<Field>,
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 : (
<PreviewHOC
previewComponent={widget.preview as WidgetPreviewComponent}
key={key}
field={field as RenderedField}
getAsset={getAsset}
config={config}
collection={collection}
value={
value &&
typeof value === 'object' &&
!Array.isArray(value) &&
field.name in value &&
!isJsxElement(value) &&
!isReactFragment(value)
? (value as Record<string, unknown>)[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;
}

View File

@ -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<string, InferredField> = {
title: {
type: 'string',

View File

@ -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<T extends ReadonlyTable>(this: void, table: T): T;
export function inline<T extends ReadonlyArray>(
this: void,
value: T,
inlineMode?: 0 | 1 | 2 | 3,
): T;
export function inline<T extends ReadonlyTable>(this: void, value: T): T;
export const multiline: {
readonly array: {
<T extends ReadonlyArray>(this: void, array: T): T;
};
<T extends ReadonlyTable>(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<OffsetDateTime>,
) => `${number}-${number}-${number}T${number}:${number}:${number}${'' | `.${number}`}${
| 'Z'
| `${'+' | '-'}${number}:${number}`}`;
readonly valueOf: (this: Readonly<OffsetDateTime>) => `${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<OffsetDateTime>) => _1_10000;
readonly getUTCMonth: (this: Readonly<OffsetDateTime>) => _0_11;
readonly getUTCDate: (this: Readonly<OffsetDateTime>) => _1_31;
readonly getUTCHours: (this: Readonly<OffsetDateTime>) => _0_23;
readonly getUTCMinutes: (this: Readonly<OffsetDateTime>) => _0_59;
readonly getUTCSeconds: (this: Readonly<OffsetDateTime>) => _0_59;
readonly getUTCMilliseconds: (this: Readonly<OffsetDateTime>) => _0_999;
readonly getUTCDay: (this: Readonly<OffsetDateTime>) => _0_6;
readonly getTimezoneOffset: (this: Readonly<OffsetDateTime>) => _1439_1439;
readonly getTime: (this: Readonly<OffsetDateTime>) => number;
}
declare class LocalDateTime {
readonly toJSON: Date['toJSON'];
readonly [Symbol.toStringTag]: 'LocalDateTime';
readonly toISOString: (
this: Readonly<LocalDateTime>,
) => `${number}-${number}-${number}T${number}:${number}:${number}${'' | `.${number}`}`;
readonly valueOf: (this: Readonly<LocalDateTime>) => `${number}`;
private [LocalDateTime_ISOString]: string;
private [LocalDateTime_value]: string;
constructor(
literal: `${number}-${number}-${number}${'T' | 't' | ' '}${number}:${number}:${number}${
| ''
| `.${number}`}`,
);
readonly getFullYear: (this: Readonly<LocalDateTime>) => _0_9999;
readonly setFullYear: (this: LocalDateTime, year: _0_9999) => void;
readonly getMonth: (this: Readonly<LocalDateTime>) => _0_11;
readonly setMonth: (this: LocalDateTime, month: _0_11) => void;
readonly getDate: (this: Readonly<LocalDateTime>) => _1_31;
readonly setDate: (this: LocalDateTime, date: _1_31) => void;
readonly getHours: (this: Readonly<LocalDateTime>) => _0_23;
readonly setHours: (this: LocalDateTime, hours: _0_23) => void;
readonly getMinutes: (this: Readonly<LocalDateTime>) => _0_59;
readonly setMinutes: (this: LocalDateTime, min: _0_59) => void;
readonly getSeconds: (this: Readonly<LocalDateTime>) => _0_59;
readonly setSeconds: (this: LocalDateTime, sec: _0_59) => void;
readonly getMilliseconds: (this: Readonly<LocalDateTime>) => _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<LocalDate>) => `${number}-${number}-${number}`;
readonly valueOf: (this: Readonly<LocalDate>) => `${number}`;
private [LocalDate_ISOString]: string;
private [LocalDate_value]: string;
constructor(literal: `${number}-${number}-${number}`);
readonly getFullYear: (this: Readonly<LocalDate>) => _0_9999;
readonly setFullYear: (this: LocalDate, year: _0_9999) => void;
readonly getMonth: (this: Readonly<LocalDate>) => _0_11;
readonly setMonth: (this: LocalDate, month: _0_11) => void;
readonly getDate: (this: Readonly<LocalDate>) => _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<LocalTime>,
) => `${number}:${number}:${number}${'' | `.${number}`}`;
readonly valueOf: (this: Readonly<LocalTime>) => `${number}`;
private [LocalTime_ISOString]: string;
private [LocalTime_value]: string;
constructor(literal: `${number}:${number}:${number}${'' | `.${number}`}`);
readonly getHours: (this: Readonly<LocalTime>) => _0_23;
readonly setHours: (this: LocalTime, hours: _0_23) => void;
readonly getMinutes: (this: Readonly<LocalTime>) => _0_59;
readonly setMinutes: (this: LocalTime, min: _0_59) => void;
readonly getSeconds: (this: Readonly<LocalTime>) => _0_59;
readonly setSeconds: (this: LocalTime, sec: _0_59) => void;
readonly getMilliseconds: (this: Readonly<LocalTime>) => _0_999;
readonly setMilliseconds: (this: LocalTime, ms: _0_999) => void;
}
declare class Keys extends RegExp {
readonly lastIndex: number;
constructor(keys: ArrayLike<string>);
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;

View File

@ -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
* 所属作者龙腾道 <LongTengDao@LongTengDao.com> (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
* 所属作者龙腾道 <LongTengDao@LongTengDao.com> (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
* 所属作者龙腾道 <LongTengDao@LongTengDao.com> (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
* 所属作者龙腾道 <LongTengDao@LongTengDao.com> (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
* 所属作者龙腾道 <LongTengDao@LongTengDao.com> (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
* 所属作者龙腾道 <LongTengDao@LongTengDao.com> (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<this.lastIndex && super.test(key);
}
};
@ -1398,7 +1404,7 @@ const OffsetDateTime = /*#__PURE__*/fpc(class OffsetDateTime extends Datetime {
get [Symbol$1.toStringTag] () { return 'OffsetDateTime' ; }
valueOf ( ) { return this[OffsetDateTime_value]; }
valueOf ( ) { return this[OffsetDateTime_value]; }
toISOString ( ) { return this[OffsetDateTime_ISOString]; }
constructor (literal ) {
@ -1488,7 +1494,7 @@ const LocalDateTime = /*#__PURE__*/fpc(class LocalDateTime extends Datetime {
get [Symbol$1.toStringTag] () { return 'LocalDateTime' ; }
valueOf ( ) { return this[LocalDateTime_value]; }
valueOf ( ) { return this[LocalDateTime_value]; }
toISOString ( ) { return this[LocalDateTime_ISOString]; }
constructor (literal ) {
@ -1540,7 +1546,7 @@ const LocalDate = /*#__PURE__*/fpc(class LocalDate extends Datetime {
get [Symbol$1.toStringTag] () { return 'LocalDate' ; }
valueOf ( ) { return this[LocalDate_value]; }
valueOf ( ) { return this[LocalDate_value]; }
toISOString ( ) { return this[LocalDate_ISOString]; }
constructor (literal ) {
@ -1579,7 +1585,7 @@ const LocalTime = /*#__PURE__*/fpc(class LocalTime extends Datetime {
get [Symbol$1.toStringTag] () { return 'LocalTime' ; }
valueOf ( ) { return this[LocalTime_value]; }
valueOf ( ) { return this[LocalTime_value]; }
toISOString ( ) { return this[LocalTime_ISOString]; }
constructor (literal ) {
@ -1766,6 +1772,10 @@ const Float = (literal ) => {
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 */
}));

View File

@ -290,6 +290,8 @@ export type WidgetPreviewComponent<T = unknown, F extends BaseField = UnknownFie
| ReactElement<unknown, string | JSXElementConstructor<any>>
| ComponentType<WidgetPreviewProps<T, F>>;
export type WidgetFor<P = EntryData> = <K extends keyof P>(name: K) => ReactNode;
export type WidgetsFor<P = EntryData> = <K extends keyof P>(
name: K,
) => P[K] extends Array<infer U>
@ -312,7 +314,7 @@ export interface TemplatePreviewProps<T = EntryData, EF extends BaseField = Unkn
* @deprecated Should use `useMediaAsset` React hook instead
*/
getAsset: GetAssetFunction<Field<EF>>;
widgetFor: (name: T extends EntryData ? string : keyof T) => ReactNode;
widgetFor: WidgetFor<T>;
widgetsFor: WidgetsFor<T>;
}
@ -321,6 +323,20 @@ export type TemplatePreviewComponent<
EF extends BaseField = UnknownField,
> = ComponentType<TemplatePreviewProps<T, EF>>;
export interface TemplatePreviewCardProps<T = EntryData, EF extends BaseField = UnknownField> {
collection: Collection<EF>;
fields: Field<EF>[];
entry: Entry<T>;
viewStyle: 'list' | 'grid';
widgetFor: WidgetFor<T>;
widgetsFor: WidgetsFor<T>;
}
export type TemplatePreviewCardComponent<
T = EntryData,
EF extends BaseField = UnknownField,
> = ComponentType<TemplatePreviewCardProps<T, EF>>;
export interface WidgetOptions<T = unknown, F extends BaseField = UnknownField> {
validator?: Widget<T, F>['validator'];
getValidValue?: Widget<T, F>['getValidValue'];
@ -912,3 +928,12 @@ export type DeepPartial<T> = T extends object
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
export interface InferredField {
type: string;
secondaryTypes: string[];
synonyms: string[];
defaultPreview: (value: string | boolean | number) => JSX.Element | ReactNode;
fallbackToFirstField: boolean;
showError: boolean;
}

View File

@ -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<string, BackendInitializer>;
templates: Record<string, TemplatePreviewComponent<EntryData>>;
cards: Record<string, TemplatePreviewCardComponent<EntryData>>;
widgets: Record<string, Widget>;
icons: Record<string, CustomIcon>;
additionalLinks: Record<string, AdditionalLink>;
@ -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<Entry
return registry.templates[name];
}
/**
* Preview Cards
*/
export function registerPreviewCard<T>(name: string, component: TemplatePreviewCardComponent<T>) {
registry.cards[name] = component as TemplatePreviewCardComponent<EntryData>;
}
export function getPreviewCard(name: string): TemplatePreviewCardComponent<EntryData> {
return registry.cards[name];
}
/**
* Editor Widgets
*/

View File

@ -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';

View File

@ -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;
}

View File

@ -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:

View File

@ -18,6 +18,51 @@ const PostPreview = ({ entry, widgetFor }) => {
);
};
const PostPreviewCard = ({ entry, widgetFor, viewStyle }) => {
return (
<div style={{ width: "100%" }}>
{viewStyle === "grid" ? widgetFor("image") : null}
<div style={{ padding: "16px", width: "100%" }}>
<div
style={{
display: "flex",
width: "100%",
justifyContent: "space-between",
alignItems: "start",
}}
>
<div
style={{
display: "flex",
flexDirection: viewStyle === "grid" ? "column" : "row",
alignItems: "baseline",
gap: "8px",
}}
>
<strong style={{ fontSize: "24px" }}>{entry.data.title}</strong>
<span style={{ fontSize: "16px" }}>{entry.data.date}</span>
</div>
<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"}
</div>
</div>
</div>
</div>
);
};
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.

View File

@ -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
<CodeTabs>
@ -62,7 +68,7 @@ const PostPreview = ({ widgetFor, getAsset, entry, collection, field }) => {
<div>
<h1>{entry.data.title}</h1>
<img src={imageUrl} />
<div className='text'>{widgetFor('body')}</div>
<div className="text">{widgetFor('body')}</div>
</div>
);
};
@ -84,14 +90,20 @@ interface Post {
body: string;
}
const PostPreview = ({ widgetFor, getAsset, entry, collection, field }: TemplatePreviewProps<Post>) => {
const PostPreview = ({
widgetFor,
getAsset,
entry,
collection,
field,
}: TemplatePreviewProps<Post>) => {
const imageUrl = useMediaAsset(entry.data.image, collection, field, entry);
return (
<div>
<h1>{entry.data.title}</h1>
<img src={imageUrl} />
<div className='text'>{widgetFor('body')}</div>
<div className="text">{widgetFor('body')}</div>
</div>
);
};
@ -101,11 +113,11 @@ CMS.registerPreviewTemplate('posts', PostPreview);
</CodeTabs>
### 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);
</CodeTabs>
#### 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);
```
</CodeTabs>
## 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<br /><ul><li>Folder collections: Use the name of the collection</li><li>File collections: Use the name of the file</li></ul> |
| 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'<br />\| '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
<CodeTabs>
```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 (
<div style={{ width: '100%' }}>
{viewStyle === 'grid' ? widgetFor('image') : null}
<div style={{ padding: '16px', width: '100%' }}>
<div
style={{
display: 'flex',
width: '100%',
justifyContent: 'space-between',
alignItems: 'start',
}}
>
<div
style={{
display: 'flex',
flexDirection: viewStyle === 'grid' ? 'column' : 'row',
alignItems: 'baseline',
gap: '8px',
}}
>
<strong style={{ fontSize: '24px' }}>{entry.data.title}</strong>
<span style={{ fontSize: '16px' }}>{entry.data.date}</span>
</div>
<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'}
</div>
</div>
</div>
</div>
);
};
```
```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<Post>) => {
return (
<div style={{ width: '100%' }}>
{viewStyle === 'grid' ? widgetFor('image') : null}
<div style={{ padding: '16px', width: '100%' }}>
<div
style={{
display: 'flex',
width: '100%',
justifyContent: 'space-between',
alignItems: 'start',
}}
>
<div
style={{
display: 'flex',
flexDirection: viewStyle === 'grid' ? 'column' : 'row',
alignItems: 'baseline',
gap: '8px',
}}
>
<strong style={{ fontSize: '24px' }}>{entry.data.title}</strong>
<span style={{ fontSize: '16px' }}>{entry.data.date}</span>
</div>
<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'}
</div>
</div>
</div>
</div>
);
};
CMS.registerPreviewTemplate('posts', PostPreview);
```
</CodeTabs>
##### List View
![Post Preview Card List View](/img/preview_card_list.png)
##### Grid View
![Post Preview Card List View](/img/preview_card_grid.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB