feat: custom collection card template (#433)
This commit is contained in:
committed by
GitHub
parent
c6994ea45b
commit
1641630cfd
@ -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;
|
||||
}
|
Reference in New Issue
Block a user