feat: custom collection card template (#433)
This commit is contained in:
parent
c6994ea45b
commit
1641630cfd
@ -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",
|
||||
|
@ -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: {
|
||||
|
@ -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.
|
||||
|
@ -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}>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
|
147
packages/core/src/components/common/widget/useWidgetsFor.tsx
Normal file
147
packages/core/src/components/common/widget/useWidgetsFor.tsx
Normal 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,
|
||||
};
|
||||
}
|
277
packages/core/src/components/common/widget/widgetFor.tsx
Normal file
277
packages/core/src/components/common/widget/widgetFor.tsx
Normal 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;
|
||||
}
|
@ -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',
|
||||
|
495
packages/core/src/formats/util/j-toml.d.ts
vendored
495
packages/core/src/formats/util/j-toml.d.ts
vendored
@ -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;
|
||||
|
@ -2,14 +2,20 @@
|
||||
* 模块名称:j-toml
|
||||
* 模块功能:龙腾道为汤小明语写的实现。从属于“简计划”。
|
||||
An implementation of TOML written by LongTengDao. Belong to "Plan J".
|
||||
* 模块版本:1.37.0
|
||||
* 模块版本: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;
|
||||
|
||||
@ -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, '');
|
||||
@ -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);
|
||||
@ -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); }
|
||||
}
|
||||
}
|
||||
@ -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 */
|
||||
}));
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
BIN
packages/docs/public/img/preview_card_grid.png
Normal file
BIN
packages/docs/public/img/preview_card_grid.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 241 KiB |
BIN
packages/docs/public/img/preview_card_list.png
Normal file
BIN
packages/docs/public/img/preview_card_list.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
Loading…
x
Reference in New Issue
Block a user