mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-03-28 17:42:40 +01:00
563 lines
18 KiB
TypeScript
563 lines
18 KiB
TypeScript
import { messageHandler } from '@estruyf/vscode/dist/client';
|
|
import * as React from 'react';
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
import { DateHelper } from '../../../helpers/DateHelper';
|
|
import { BlockFieldData, ContentType, CustomPanelViewResult, Field, PanelSettings } from '../../../models';
|
|
import { CommandToCode } from '../../CommandToCode';
|
|
import { TagType } from '../../TagType';
|
|
import { DataBlockField } from '../DataBlock';
|
|
import FieldBoundary from '../ErrorBoundary/FieldBoundary';
|
|
import { ListUnorderedIcon } from '../Icons/ListUnorderedIcon';
|
|
import { TagIcon } from '../Icons/TagIcon';
|
|
import { JsonField } from '../JsonField';
|
|
import { IMetadata } from '../Metadata';
|
|
import {
|
|
ChoiceField,
|
|
DataFileField,
|
|
DateTimeField,
|
|
DraftField,
|
|
FieldTitle,
|
|
FileField,
|
|
ListField,
|
|
Toggle,
|
|
TextField,
|
|
SlugField,
|
|
PreviewImageField,
|
|
PreviewImageValue,
|
|
NumberField,
|
|
CustomField,
|
|
FieldCollection,
|
|
TagPicker
|
|
} from '.';
|
|
import { fieldWhenClause } from '../../../utils/fieldWhenClause';
|
|
import { ContentTypeRelationshipField } from './ContentTypeRelationshipField';
|
|
import * as l10n from '@vscode/l10n';
|
|
import { LocalizationKey } from '../../../localization';
|
|
|
|
export interface IWrapperFieldProps {
|
|
field: Field;
|
|
allFields: Field[];
|
|
parent: IMetadata;
|
|
parentFields: string[];
|
|
metadata: IMetadata;
|
|
settings: PanelSettings;
|
|
contentType: ContentType | null;
|
|
blockData: BlockFieldData | undefined;
|
|
focusElm: TagType | null;
|
|
parentBlock: string | null | undefined;
|
|
onSendUpdate: (field: string | undefined, value: any, parents: string[]) => void;
|
|
unsetFocus: () => void;
|
|
renderFields: (
|
|
ctFields: Field[],
|
|
parent: IMetadata,
|
|
parentFields: string[],
|
|
blockData?: BlockFieldData,
|
|
onFieldUpdate?: (field: string | undefined, value: any, parents: string[]) => void,
|
|
parentBlock?: string | null
|
|
) => (JSX.Element | null)[] | undefined;
|
|
}
|
|
|
|
export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
|
field,
|
|
allFields,
|
|
parent,
|
|
parentFields,
|
|
metadata,
|
|
settings,
|
|
contentType,
|
|
blockData,
|
|
focusElm,
|
|
parentBlock,
|
|
onSendUpdate,
|
|
unsetFocus,
|
|
renderFields
|
|
}: React.PropsWithChildren<IWrapperFieldProps>) => {
|
|
const [fieldValue, setFieldValue] = useState<any | undefined>(undefined);
|
|
const [customFields, setCustomFields] = useState<{
|
|
name: string;
|
|
html: (data: any, onChange: (value: any) => void) => Promise<CustomPanelViewResult | undefined>;
|
|
}[]>([]);
|
|
|
|
const getDate = (date: string | Date): Date | null => {
|
|
const parsedDate = DateHelper.tryParse(date, settings?.date?.format);
|
|
return parsedDate || (date as Date | null);
|
|
};
|
|
|
|
const onFieldChange = useCallback((value: any) => onSendUpdate(field.name, value, parentFields), [field.name, parentFields, onSendUpdate]);
|
|
|
|
useEffect(() => {
|
|
let value: any = parent[field.name];
|
|
|
|
if (field.type === 'tags' || field.type === 'categories' || field.type === 'taxonomy') {
|
|
setFieldValue(value || []);
|
|
return;
|
|
}
|
|
|
|
if (field.type === 'datetime') {
|
|
value = value ? getDate(value) : undefined;
|
|
}
|
|
|
|
if (value === undefined && field.default) {
|
|
value = field.default;
|
|
|
|
if (field.type === 'datetime') {
|
|
if (value === '{{now}}') {
|
|
value = new Date();
|
|
}
|
|
|
|
value = getDate(value) || null;
|
|
}
|
|
|
|
//onSendUpdate(field.name, value, parentFields);
|
|
}
|
|
|
|
// Check if the field value contains a placeholder
|
|
if (value && typeof value === 'string' && value.includes(`{{`) && value.includes(`}}`)) {
|
|
messageHandler.request<{ field: string; value: any; }>(CommandToCode.updatePlaceholder, {
|
|
field: field.name,
|
|
value,
|
|
data: metadata,
|
|
contentType
|
|
}).then((data) => {
|
|
if (data.field === field.name) {
|
|
setFieldValue(data.value);
|
|
onSendUpdate(field.name, data.value, parentFields);
|
|
}
|
|
}).catch((err) => {
|
|
console.error(err);
|
|
});
|
|
} else {
|
|
// Did not contain a placeholder, so value can be set
|
|
if (fieldValue === undefined || value !== fieldValue) {
|
|
if (typeof value === 'number') {
|
|
setFieldValue(value);
|
|
} else if (field.type === "slug") {
|
|
setFieldValue(value);
|
|
} else {
|
|
setFieldValue(value || null);
|
|
}
|
|
}
|
|
}
|
|
}, [field, parent]);
|
|
|
|
useEffect(() => {
|
|
if (window.fmExternal && window.fmExternal.getCustomFields) {
|
|
setCustomFields(window.fmExternal.getCustomFields || []);
|
|
} else {
|
|
setCustomFields([]);
|
|
}
|
|
}, []);
|
|
|
|
if (field.hidden || fieldValue === undefined) {
|
|
if (field.hidden && fieldValue === undefined && field.default && parentFields.length > 0) {
|
|
onSendUpdate(field.name, field.default, parentFields);
|
|
}
|
|
|
|
if (field.hidden) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Conditional fields
|
|
if (typeof field.when !== 'undefined') {
|
|
const shouldRender = fieldWhenClause(field, parent, allFields);
|
|
|
|
if (!shouldRender) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (field.type === 'divider') {
|
|
return <div key={field.name} className="metadata_field__divider" />;
|
|
} else if (field.type === 'heading') {
|
|
return (
|
|
<div key={field.name} className="metadata_field__heading">
|
|
<h3>{field.title}</h3>
|
|
{field.description && <p className="metadata_field__description">{field.description}</p>}
|
|
</div>
|
|
);
|
|
} else if (field.type === 'datetime') {
|
|
return (
|
|
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
|
<DateTimeField
|
|
label={field.title || field.name}
|
|
description={field.description}
|
|
value={fieldValue}
|
|
required={!!field.required}
|
|
format={field.dateFormat || settings?.date?.format}
|
|
onChange={onFieldChange}
|
|
/>
|
|
</FieldBoundary>
|
|
);
|
|
} else if (field.type === 'boolean') {
|
|
return (
|
|
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
|
<Toggle
|
|
key={field.name}
|
|
label={field.title || field.name}
|
|
description={field.description}
|
|
value={fieldValue}
|
|
required={!!field.required}
|
|
onChanged={onFieldChange}
|
|
/>
|
|
</FieldBoundary>
|
|
);
|
|
} else if (field.type === 'string') {
|
|
let limit = -1;
|
|
if (field.name === settings.seo.titleField) {
|
|
limit = settings?.seo.title;
|
|
} else if (field.name === settings.seo.descriptionField) {
|
|
limit = settings?.seo.description;
|
|
}
|
|
|
|
return (
|
|
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
|
<TextField
|
|
name={field.name}
|
|
label={field.title || field.name}
|
|
description={field.description}
|
|
singleLine={field.single}
|
|
limit={limit}
|
|
wysiwyg={field.wysiwyg}
|
|
rows={4}
|
|
onChange={onFieldChange}
|
|
value={(fieldValue as string) || null}
|
|
required={!!field.required}
|
|
settings={settings}
|
|
actions={field.actions}
|
|
/>
|
|
</FieldBoundary>
|
|
);
|
|
} else if (field.type === 'number') {
|
|
let nrValue: number | null = field.numberOptions?.isDecimal ? parseFloat(fieldValue as string) : parseInt(fieldValue as string);
|
|
if (isNaN(nrValue)) {
|
|
nrValue = null;
|
|
}
|
|
|
|
return (
|
|
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
|
<NumberField
|
|
label={field.title || field.name}
|
|
description={field.description}
|
|
options={field.numberOptions}
|
|
onChange={onFieldChange}
|
|
value={nrValue}
|
|
required={!!field.required}
|
|
/>
|
|
</FieldBoundary>
|
|
);
|
|
} else if (field.type === 'image') {
|
|
return (
|
|
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
|
<PreviewImageField
|
|
label={field.title || field.name}
|
|
description={field.description}
|
|
fieldName={field.name}
|
|
filePath={metadata.filePath as string}
|
|
parents={parentFields}
|
|
value={fieldValue as PreviewImageValue | PreviewImageValue[] | null}
|
|
required={!!field.required}
|
|
multiple={field.multiple}
|
|
blockData={blockData}
|
|
onChange={onFieldChange}
|
|
actions={field.actions}
|
|
/>
|
|
</FieldBoundary>
|
|
);
|
|
} else if (field.type === 'file') {
|
|
return (
|
|
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
|
<FileField
|
|
label={field.title || field.name}
|
|
description={field.description}
|
|
fieldName={field.name}
|
|
multiple={field.multiple}
|
|
fileExtensions={field.fileExtensions}
|
|
filePath={metadata.filePath as string}
|
|
value={fieldValue as string | string[] | null}
|
|
required={!!field.required}
|
|
parents={parentFields}
|
|
blockData={blockData}
|
|
onChange={onFieldChange}
|
|
/>
|
|
</FieldBoundary>
|
|
);
|
|
} else if (field.type === 'choice') {
|
|
const choices = field.choices || [];
|
|
|
|
return (
|
|
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
|
<ChoiceField
|
|
label={field.title || field.name}
|
|
description={field.description}
|
|
value={fieldValue as string}
|
|
required={!!field.required}
|
|
choices={choices}
|
|
multiSelect={field.multiple}
|
|
onChange={onFieldChange}
|
|
/>
|
|
</FieldBoundary>
|
|
);
|
|
} else if (field.type === 'tags') {
|
|
return (
|
|
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
|
<TagPicker
|
|
type={TagType.tags}
|
|
label={field.title || field.name}
|
|
description={field.description}
|
|
fieldName={field.name}
|
|
icon={<TagIcon />}
|
|
crntSelected={(fieldValue as string[]) || []}
|
|
options={settings?.tags || []}
|
|
freeform={settings.freeform || false}
|
|
focussed={focusElm === TagType.tags}
|
|
unsetFocus={unsetFocus}
|
|
parents={parentFields}
|
|
blockData={blockData}
|
|
limit={field.taxonomyLimit}
|
|
renderAsString={field.singleValueAsString}
|
|
required={!!field.required}
|
|
actions={field.actions}
|
|
/>
|
|
</FieldBoundary>
|
|
);
|
|
} else if (field.type === 'taxonomy') {
|
|
const taxonomyData = settings.customTaxonomy.find((ct) => ct.id === field.taxonomyId);
|
|
|
|
return (
|
|
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
|
<TagPicker
|
|
type={TagType.custom}
|
|
label={field.title || field.name}
|
|
description={field.description}
|
|
icon={<ListUnorderedIcon />}
|
|
crntSelected={(fieldValue as string[]) || []}
|
|
options={taxonomyData?.options || []}
|
|
freeform={settings.freeform || false}
|
|
focussed={focusElm === TagType.custom}
|
|
unsetFocus={unsetFocus}
|
|
fieldName={field.name}
|
|
taxonomyId={field.taxonomyId}
|
|
parents={parentFields}
|
|
blockData={blockData}
|
|
limit={field.taxonomyLimit}
|
|
renderAsString={field.singleValueAsString}
|
|
required={!!field.required}
|
|
/>
|
|
</FieldBoundary>
|
|
);
|
|
} else if (field.type === 'categories') {
|
|
return (
|
|
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
|
<TagPicker
|
|
type={TagType.categories}
|
|
label={field.title || field.name}
|
|
description={field.description}
|
|
fieldName={field.name}
|
|
icon={<ListUnorderedIcon />}
|
|
crntSelected={(fieldValue as string[]) || []}
|
|
options={settings.categories}
|
|
freeform={settings.freeform || false}
|
|
focussed={focusElm === TagType.categories}
|
|
unsetFocus={unsetFocus}
|
|
parents={parentFields}
|
|
blockData={blockData}
|
|
limit={field.taxonomyLimit}
|
|
renderAsString={field.singleValueAsString}
|
|
required={!!field.required}
|
|
/>
|
|
</FieldBoundary>
|
|
);
|
|
} else if (field.type === 'draft') {
|
|
const draftField = settings?.draftField;
|
|
let draftValue = parent[field.name];
|
|
|
|
if (!draftValue && typeof parent[field.name] === 'undefined' && field.default) {
|
|
draftValue = field.default as string;
|
|
onSendUpdate(field.name, draftValue, parentFields);
|
|
}
|
|
|
|
return (
|
|
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
|
<DraftField
|
|
label={field.title || field.name}
|
|
description={field.description}
|
|
type={draftField?.type || 'boolean'}
|
|
choices={draftField?.choices || []}
|
|
value={draftValue as boolean | string | null | undefined}
|
|
required={!!field.required}
|
|
onChanged={onFieldChange}
|
|
/>
|
|
</FieldBoundary>
|
|
);
|
|
} else if (field.type === 'fields') {
|
|
if (field.fields && parent) {
|
|
if (!parent[field.name]) {
|
|
parent[field.name] = {};
|
|
}
|
|
|
|
const subMetadata = parent[field.name] as IMetadata;
|
|
return (
|
|
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
|
<div className={`metadata_field__box`}>
|
|
<FieldTitle
|
|
className={`metadata_field__label_parent`}
|
|
label={field.title || field.name}
|
|
icon={undefined}
|
|
required={field.required}
|
|
/>
|
|
|
|
{field.description && (
|
|
<p className={`metadata_field__description`}>{field.description}</p>
|
|
)}
|
|
|
|
{renderFields(
|
|
field.fields,
|
|
subMetadata,
|
|
[...parentFields, field.name],
|
|
blockData,
|
|
onSendUpdate
|
|
)}
|
|
</div>
|
|
</FieldBoundary>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
} else if (field.type === 'json') {
|
|
const collectionData = parent[field.name];
|
|
|
|
return (
|
|
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
|
<JsonField
|
|
label={field.title || field.name}
|
|
settings={settings}
|
|
field={field}
|
|
value={collectionData}
|
|
onSubmit={onFieldChange}
|
|
/>
|
|
</FieldBoundary>
|
|
);
|
|
} else if (field.type === 'block') {
|
|
const fieldData = Object.assign([], parent[field.name]);
|
|
|
|
return (
|
|
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
|
<DataBlockField
|
|
label={field.title || field.name}
|
|
description={field.description}
|
|
settings={settings}
|
|
field={field}
|
|
value={fieldData}
|
|
blockData={blockData}
|
|
fieldsRenderer={renderFields}
|
|
parentFields={parentFields}
|
|
filePath={metadata.filePath as string}
|
|
parentBlock={parentBlock}
|
|
required={!!field.required}
|
|
onSubmit={onFieldChange}
|
|
/>
|
|
</FieldBoundary>
|
|
);
|
|
} else if (field.type === 'dataFile') {
|
|
return (
|
|
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
|
<DataFileField
|
|
label={field.title || field.name}
|
|
description={field.description}
|
|
dataFileId={field.dataFileId}
|
|
dataFileKey={field.dataFileKey}
|
|
dataFileValue={field.dataFileValue}
|
|
selected={fieldValue as string}
|
|
required={!!field.required}
|
|
multiSelect={field.multiple}
|
|
onChange={onFieldChange}
|
|
/>
|
|
</FieldBoundary>
|
|
);
|
|
} else if (field.type === 'list') {
|
|
return (
|
|
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
|
<ListField
|
|
label={field.title || field.name}
|
|
description={field.description}
|
|
value={fieldValue}
|
|
required={!!field.required}
|
|
onChange={onFieldChange}
|
|
/>
|
|
</FieldBoundary>
|
|
);
|
|
} else if (field.type === 'contentRelationship') {
|
|
return (
|
|
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
|
<ContentTypeRelationshipField
|
|
label={field.title || field.name}
|
|
description={field.description}
|
|
value={fieldValue as string}
|
|
required={!!field.required}
|
|
contentTypeName={field.contentTypeName}
|
|
contentTypeValue={field.contentTypeValue}
|
|
sameContentLocale={field.sameContentLocale}
|
|
multiSelect={field.multiple}
|
|
onChange={onFieldChange}
|
|
/>
|
|
</FieldBoundary>
|
|
);
|
|
} else if (field.type === 'slug') {
|
|
return (
|
|
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
|
<SlugField
|
|
label={field.title || field.name}
|
|
description={field.description}
|
|
titleValue={metadata.title as string}
|
|
value={fieldValue}
|
|
slugTemplate={contentType?.slugTemplate}
|
|
required={!!field.required}
|
|
editable={field.editable}
|
|
onChange={onFieldChange}
|
|
/>
|
|
</FieldBoundary>
|
|
);
|
|
} else if (field.type === 'customField') {
|
|
const fieldData = customFields.find(f => f.name === field.customType);
|
|
if (fieldData) {
|
|
return (
|
|
<CustomField
|
|
key={field.name}
|
|
label={field.title || field.name}
|
|
description={field.description}
|
|
value={fieldValue}
|
|
required={!!field.required}
|
|
onChange={onFieldChange}
|
|
fieldData={fieldData} />
|
|
);
|
|
} else {
|
|
return null;
|
|
}
|
|
} else if (field.type === "fieldCollection") {
|
|
if (!parent[field.name]) {
|
|
parent[field.name] = {};
|
|
}
|
|
|
|
const subMetadata = parent[field.name] as IMetadata;
|
|
|
|
return (
|
|
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
|
<FieldCollection
|
|
key={field.name}
|
|
field={field}
|
|
parent={subMetadata}
|
|
parentFields={parentFields}
|
|
renderFields={renderFields}
|
|
settings={settings}
|
|
blockData={blockData}
|
|
onChange={onSendUpdate}
|
|
/>
|
|
</FieldBoundary>
|
|
);
|
|
} else {
|
|
console.warn(l10n.t(LocalizationKey.panelFieldsWrapperFieldUnknown, field.type));
|
|
return null;
|
|
}
|
|
};
|