From 64d0bb49f70486f29b52cbcee6b7d67b6e8aee4f Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Wed, 24 Jun 2026 11:45:36 +0200 Subject: [PATCH] feat: add support for additional fields in data file configuration and update related components #409 --- CHANGELOG.md | 1 + package.json | 8 ++ package.nls.json | 1 + src/models/PanelSettings.ts | 1 + .../components/Fields/DataFileField.tsx | 80 +++++++++++++++---- .../components/Fields/WrapperField.tsx | 1 + 6 files changed, 77 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcabf279..e99a94d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Support `fieldGroup` as a single value on `fields` fields - Added the new content health feature to the Front Matter panel with readability scoring, link checks, and freshness warnings (`frontMatter.contentHealth.enabled`, `frontMatter.contentHealth.checkExternalLinks`, `frontMatter.contentHealth.freshnessThreshold`, `frontMatter.contentHealth.minReadability`) +- [#409](https://github.com/estruyf/vscode-front-matter/issues/409): Added the ability to output multiple properties from a data file in the `dataFile` field type - [#1030](https://github.com/estruyf/vscode-front-matter/pull/1030): Add `frontMatter.file.slugSeparator` setting - [#1033](https://github.com/estruyf/vscode-front-matter/issues/1033): Support freeform tags and categories in the front matter validation - [#1036](https://github.com/estruyf/vscode-front-matter/issues/1036): Default filter, sorting, and grouping configuration for the `contents` dashboard diff --git a/package.json b/package.json index 5e573cc4..22e689af 100644 --- a/package.json +++ b/package.json @@ -1582,6 +1582,14 @@ "default": "", "description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.dataFileValue.description%" }, + "dataFileAdditionalFields": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.dataFileAdditionalFields.description%" + }, "editable": { "type": "boolean", "default": true, diff --git a/package.nls.json b/package.nls.json index 353add82..da09e680 100644 --- a/package.nls.json +++ b/package.nls.json @@ -232,6 +232,7 @@ "setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.dataFileId.description": "Specify the ID of the data file to use for this field", "setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.dataFileKey.description": "Specify the key of the data file to use for this field", "setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.dataFileValue.description": "Specify the property name that will be used to show the value for the field", + "setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.dataFileAdditionalFields.description": "Specify additional field names from the data record to include when storing the value. When set, the frontmatter value will be an object containing the dataFileKey field plus these additional fields.", "setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.editable.description": "Specify if the field is editable", "setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.encodeEmoji.description": "Specify if the field should encode emoji", "setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.dateFormat.description": "Specify the date format to use", diff --git a/src/models/PanelSettings.ts b/src/models/PanelSettings.ts index d11d885e..1a610d51 100644 --- a/src/models/PanelSettings.ts +++ b/src/models/PanelSettings.ts @@ -129,6 +129,7 @@ export interface Field { dataFileId?: string; dataFileKey?: string; dataFileValue?: string; + dataFileAdditionalFields?: string[]; // Number field options numberOptions?: NumberOptions; diff --git a/src/panelWebView/components/Fields/DataFileField.tsx b/src/panelWebView/components/Fields/DataFileField.tsx index 6d6c390b..dedcb40f 100644 --- a/src/panelWebView/components/Fields/DataFileField.tsx +++ b/src/panelWebView/components/Fields/DataFileField.tsx @@ -17,10 +17,11 @@ export interface IDataFileFieldProps { dataFileId?: string; dataFileKey?: string; dataFileValue?: string; - selected: string | string[]; + dataFileAdditionalFields?: string[]; + selected: string | string[] | Record | Record[]; multiSelect?: boolean; required?: boolean; - onChange: (value: string | string[]) => void; + onChange: (value: string | string[] | Record | Record[]) => void; } export const DataFileField: React.FunctionComponent = ({ @@ -29,6 +30,7 @@ export const DataFileField: React.FunctionComponent = ({ dataFileId, dataFileKey, dataFileValue, + dataFileAdditionalFields, selected, multiSelect, onChange, @@ -40,37 +42,83 @@ export const DataFileField: React.FunctionComponent = ({ const inputRef = React.useRef(null); const { getDropdownStyle } = useDropdownStyle(inputRef as any); + const hasAdditionalFields = useMemo( + () => !!dataFileAdditionalFields && dataFileAdditionalFields.length > 0, + [dataFileAdditionalFields] + ); + + // Extract the key string from a selected value (which may be a plain object when additionalFields is configured) + const extractKey = useCallback( + (value: unknown): string => { + if (value && typeof value === 'object' && !Array.isArray(value) && dataFileKey) { + return ((value as Record)[dataFileKey] as string) || ''; + } + return (value as string) || ''; + }, + [dataFileKey] + ); + + // Build the value to emit: an object when additionalFields is configured, otherwise just the key string + const buildEmitValue = useCallback( + (keyValue: string): string | Record => { + if (hasAdditionalFields && dataEntries && dataFileKey && dataFileAdditionalFields) { + const entry = (dataEntries as any[]).find((r: any) => r[dataFileKey] === keyValue); + if (entry) { + const obj: Record = { [dataFileKey]: entry[dataFileKey] }; + for (const field of dataFileAdditionalFields) { + obj[field] = entry[field]; + } + return obj; + } + } + return keyValue; + }, + [hasAdditionalFields, dataEntries, dataFileKey, dataFileAdditionalFields] + ); + const onValueChange = useCallback( (txtValue: string) => { if (multiSelect) { - const newValue = [...((crntSelected || []) as string[]), txtValue]; - setCrntSelected(newValue); - onChange(newValue); + const newKeys = [...((crntSelected || []) as string[]), txtValue]; + setCrntSelected(newKeys); + if (hasAdditionalFields) { + onChange(newKeys.map(buildEmitValue) as Record[]); + } else { + onChange(newKeys); + } } else { setCrntSelected(txtValue); - onChange(txtValue); + if (hasAdditionalFields) { + onChange(buildEmitValue(txtValue) as Record); + } else { + onChange(txtValue); + } } }, - [crntSelected, multiSelect, onChange] + [crntSelected, multiSelect, onChange, hasAdditionalFields, buildEmitValue] ); const removeSelected = useCallback( (txtValue: string) => { if (multiSelect) { - const newValue = [...(crntSelected || [])].filter((v) => v !== txtValue); - setCrntSelected(newValue); - onChange(newValue); + const newKeys = [...(crntSelected || [])].filter((v) => v !== txtValue) as string[]; + setCrntSelected(newKeys); + if (hasAdditionalFields) { + onChange(newKeys.map(buildEmitValue) as Record[]); + } else { + onChange(newKeys); + } } else { setCrntSelected(''); onChange(''); } }, - [crntSelected, multiSelect, onChange] + [crntSelected, multiSelect, onChange, hasAdditionalFields, buildEmitValue] ); const allChoices = useMemo(() => { if (dataEntries && dataFileKey) { - return dataEntries + return (dataEntries as any[]) .map((r: any) => ({ id: r[dataFileKey], title: r[dataFileValue || dataFileKey] || r[dataFileKey] @@ -117,12 +165,14 @@ export const DataFileField: React.FunctionComponent = ({ }, [required, crntSelected]); useEffect(() => { - if (selected) { + if (selected !== undefined && selected !== null && selected !== '') { if (multiSelect) { - setCrntSelected(typeof selected === 'string' ? [selected] : selected); + const keys = (Array.isArray(selected) ? selected : [selected]).map(extractKey); + setCrntSelected(keys); return; } else { - setCrntSelected(selected instanceof Array ? selected[0] : selected); + const key = extractKey(Array.isArray(selected) ? selected[0] : selected); + setCrntSelected(key); return; } } diff --git a/src/panelWebView/components/Fields/WrapperField.tsx b/src/panelWebView/components/Fields/WrapperField.tsx index fa877f2c..b14fe895 100644 --- a/src/panelWebView/components/Fields/WrapperField.tsx +++ b/src/panelWebView/components/Fields/WrapperField.tsx @@ -511,6 +511,7 @@ export const WrapperField: React.FunctionComponent = ({ dataFileId={field.dataFileId} dataFileKey={field.dataFileKey} dataFileValue={field.dataFileValue} + dataFileAdditionalFields={field.dataFileAdditionalFields} selected={fieldValue as string} required={!!field.required} multiSelect={field.multiple}