feat: add support for additional fields in data file configuration and update related components #409

This commit is contained in:
Elio Struyf
2026-06-24 11:45:36 +02:00
parent de1bcbbe53
commit 64d0bb49f7
6 changed files with 77 additions and 15 deletions
+1
View File
@@ -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
+8
View File
@@ -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,
+1
View File
@@ -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",
+1
View File
@@ -129,6 +129,7 @@ export interface Field {
dataFileId?: string;
dataFileKey?: string;
dataFileValue?: string;
dataFileAdditionalFields?: string[];
// Number field options
numberOptions?: NumberOptions;
@@ -17,10 +17,11 @@ export interface IDataFileFieldProps {
dataFileId?: string;
dataFileKey?: string;
dataFileValue?: string;
selected: string | string[];
dataFileAdditionalFields?: string[];
selected: string | string[] | Record<string, unknown> | Record<string, unknown>[];
multiSelect?: boolean;
required?: boolean;
onChange: (value: string | string[]) => void;
onChange: (value: string | string[] | Record<string, unknown> | Record<string, unknown>[]) => void;
}
export const DataFileField: React.FunctionComponent<IDataFileFieldProps> = ({
@@ -29,6 +30,7 @@ export const DataFileField: React.FunctionComponent<IDataFileFieldProps> = ({
dataFileId,
dataFileKey,
dataFileValue,
dataFileAdditionalFields,
selected,
multiSelect,
onChange,
@@ -40,37 +42,83 @@ export const DataFileField: React.FunctionComponent<IDataFileFieldProps> = ({
const inputRef = React.useRef<HTMLInputElement | null>(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<string, unknown>)[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<string, unknown> => {
if (hasAdditionalFields && dataEntries && dataFileKey && dataFileAdditionalFields) {
const entry = (dataEntries as any[]).find((r: any) => r[dataFileKey] === keyValue);
if (entry) {
const obj: Record<string, unknown> = { [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<string, unknown>[]);
} else {
onChange(newKeys);
}
} else {
setCrntSelected(txtValue);
onChange(txtValue);
if (hasAdditionalFields) {
onChange(buildEmitValue(txtValue) as Record<string, unknown>);
} 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<string, unknown>[]);
} 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<IDataFileFieldProps> = ({
}, [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;
}
}
@@ -511,6 +511,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
dataFileId={field.dataFileId}
dataFileKey={field.dataFileKey}
dataFileValue={field.dataFileValue}
dataFileAdditionalFields={field.dataFileAdditionalFields}
selected={fieldValue as string}
required={!!field.required}
multiSelect={field.multiple}