Skip to content

Commit

Permalink
Support summary string transformations for object and list widgets
Browse files Browse the repository at this point in the history
Fix #255
  • Loading branch information
kyoshino committed Nov 21, 2024
1 parent ff9eea7 commit abc1d2d
Show file tree
Hide file tree
Showing 9 changed files with 555 additions and 115 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,19 +87,19 @@
return String(getIndex() ?? '');
}
if (tagName.startsWith('fields.')) {
const value = getFieldDisplayValue({
collectionName,
fileName,
valueMap,
keyPath: tagName.replace(/^fields\./, ''),
locale,
});
return Array.isArray(value) ? listFormatter.format(value) : String(value);
if (!tagName.startsWith('fields.')) {
return '';
}
return '';
const value = getFieldDisplayValue({
collectionName,
fileName,
valueMap,
keyPath: tagName.replace(/^fields\./, ''),
locale,
});
return Array.isArray(value) ? listFormatter.format(value) : String(value);
});
})();
Expand Down
71 changes: 71 additions & 0 deletions src/lib/components/contents/details/widgets/list/helper.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { escapeRegExp } from '@sveltia/utils/string';
import { getFieldConfig, getFieldDisplayValue } from '$lib/services/contents/entry';

/**
* Check if the given fields contain a single List widget with the `root` option enabled.
* @param {Field[]} fields - Field list.
Expand All @@ -7,3 +10,71 @@ export const hasRootListField = (fields) =>
fields.length === 1 &&
fields[0].widget === 'list' &&
/** @type {ListField} */ (fields[0]).root === true;

/**
* Format the summary template of a List field.
* @param {object} args - Arguments.
* @param {string} args.collectionName - Collection name.
* @param {string} [args.fileName] - File name.
* @param {FieldKeyPath} args.keyPath - Field key path.
* @param {FlattenedEntryContent} args.valueMap - Entry content.
* @param {LocaleCode} args.locale - Locale code.
* @param {string} [args.summaryTemplate] - Summary template, e.g. `{{fields.slug}}`.
* @param {boolean} args.hasSingleSubField - Whether the field has a single `field` instead of
* multiple `fields`.
* @param {number} args.index - List index.
* @returns {string} Formatted summary.
*/
export const formatSummary = ({
collectionName,
fileName,
keyPath,
valueMap,
locale,
summaryTemplate,
hasSingleSubField,
index,
}) => {
if (!summaryTemplate) {
if (hasSingleSubField) {
return valueMap[`${keyPath}.${index}`];
}

const prefixRegex = new RegExp(`^${escapeRegExp(keyPath)}\\.${index}[\\b\\.]`);

const item = Object.fromEntries(
Object.entries(valueMap)
.filter(([key]) => key.match(prefixRegex))
.map(([key, value]) => [key.replace(prefixRegex, ''), value]),
);

return (
item.title ||
item.name ||
// Use the first string-type field value, if available
Object.values(item).find((value) => typeof value === 'string' && !!value) ||
''
);
}

return summaryTemplate.replaceAll(/{{(.+?)}}/g, (_match, /** @type {string} */ placeholder) => {
const [tag, ...transformations] = placeholder.split(/\s*\|\s*/);
const _keyPath = `${keyPath}.${index}.${tag.replace(/^fields\./, '')}`;

if (hasSingleSubField) {
// Check if the key path is valid
if (!getFieldConfig({ collectionName, fileName, valueMap, keyPath: _keyPath })) {
return '';
}
}

return getFieldDisplayValue({
collectionName,
fileName,
valueMap,
keyPath: hasSingleSubField ? `${keyPath}.${index}` : _keyPath,
locale,
transformations,
});
});
};
195 changes: 195 additions & 0 deletions src/lib/components/contents/details/widgets/list/helper.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { writable } from 'svelte/store';
import { describe, expect, test, vi } from 'vitest';
import { formatSummary } from '$lib/components/contents/details/widgets/list/helper';

describe('Test formatSummary() — multiple fields', () => {
const valueMap = {
'images.0.src': 'hello.jpg',
'images.0.alt': 'hello',
'images.0.featured': true,
'images.0.date': '2024-01-01',
};

const baseArgs = {
collectionName: 'posts',
keyPath: 'images',
locale: 'en',
hasSingleSubField: false,
index: 0,
};

vi.mock('$lib/services/config', () => ({
siteConfig: writable({
backend: { name: 'github' },
media_folder: 'static/uploads',
collections: [
{
name: 'posts',
folder: 'content/posts',
fields: [
{
name: 'images',
widget: 'list',
fields: [
{ name: 'title', widget: 'string' },
{ name: 'src', widget: 'image' },
{ name: 'alt', widget: 'string' },
{ name: 'featured', widget: 'boolean' },
{ name: 'date', widget: 'date', picker_utc: true, time_format: false },
],
},
],
},
],
}),
}));

test('without template', () => {
expect(
formatSummary({
...baseArgs,
valueMap: { 'images.0.title': '', ...valueMap },
}),
).toEqual('hello.jpg');
expect(
formatSummary({
...baseArgs,
valueMap: { 'images.0.title': 'Hello World', ...valueMap },
}),
).toEqual('Hello World');
});

test('with template', () => {
expect(
formatSummary({
...baseArgs,
valueMap: { 'images.0.title': '', ...valueMap },
summaryTemplate: '{{fields.alt}}',
}),
).toEqual('hello');
expect(
formatSummary({
...baseArgs,
valueMap: { 'images.0.title': 'Hello World', ...valueMap },
summaryTemplate: '{{fields.alt}}',
}),
).toEqual('hello');
expect(
formatSummary({
...baseArgs,
valueMap: { 'images.0.title': 'Hello World', ...valueMap },
summaryTemplate: '{{fields.src}}',
}),
).toEqual('hello.jpg');
expect(
formatSummary({
...baseArgs,
valueMap: { 'images.0.title': 'Hello World', ...valueMap },
summaryTemplate: '{{fields.name}}',
}),
).toEqual('');
});

test('with template + transformations', () => {
expect(
formatSummary({
...baseArgs,
valueMap,
summaryTemplate: '{{fields.alt | upper}}',
}),
).toEqual('HELLO');
expect(
formatSummary({
...baseArgs,
valueMap,
summaryTemplate: '{{fields.alt | upper | truncate(2)}}',
}),
).toEqual('HE');
expect(
formatSummary({
...baseArgs,
valueMap,
summaryTemplate: "{{fields.featured | ternary('featured','')}}",
}),
).toEqual('featured');
expect(
formatSummary({
...baseArgs,
valueMap,
summaryTemplate: "{{fields.date | date('MMM YYYY')}}",
}),
).toEqual('Jan 2024');
});
});

describe('Test formatSummary() — single field', () => {
const baseArgs = {
collectionName: 'posts',
keyPath: 'images',
locale: 'en',
hasSingleSubField: true,
index: 0,
};

vi.mock('$lib/services/config', () => ({
siteConfig: writable({
backend: { name: 'github' },
media_folder: 'static/uploads',
collections: [
{
name: 'posts',
folder: 'content/posts',
fields: [
{
name: 'images',
widget: 'list',
field: { name: 'src', widget: 'image' },
},
],
},
],
}),
}));

test('without template', () => {
expect(
formatSummary({
...baseArgs,
valueMap: { 'images.0': 'hello.jpg' },
}),
).toEqual('hello.jpg');
expect(
formatSummary({
...baseArgs,
valueMap: { 'images.0': '' },
}),
).toEqual('');
});

test('with template', () => {
expect(
formatSummary({
...baseArgs,
valueMap: { 'images.0': 'hello.jpg' },
summaryTemplate: '{{fields.src}}',
}),
).toEqual('hello.jpg');
expect(
formatSummary({
...baseArgs,
valueMap: { 'images.0': 'hello.jpg' },
summaryTemplate: '{{fields.alt}}',
}),
).toEqual('');
});

test('with template + transformations', () => {
expect(
formatSummary({
...baseArgs,
valueMap: { 'images.0': 'hello.jpg' },
summaryTemplate: '{{fields.src | upper | truncate(5)}}',
}),
).toEqual('HELLO');
});
});
44 changes: 13 additions & 31 deletions src/lib/components/contents/details/widgets/list/list-editor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import FieldEditor from '$lib/components/contents/details/editor/field-editor.svelte';
import { formatSummary } from '$lib/components/contents/details/widgets/list/helper';
import AddItemButton from '$lib/components/contents/details/widgets/object/add-item-button.svelte';
import ObjectHeader from '$lib/components/contents/details/widgets/object/object-header.svelte';
import { entryDraft } from '$lib/services/contents/draft';
import { getDefaultValues } from '$lib/services/contents/draft/create';
import { syncExpanderStates } from '$lib/services/contents/draft/editor';
import { updateListField } from '$lib/services/contents/draft/update';
import { getFieldDisplayValue } from '$lib/services/contents/entry';
import { defaultI18nConfig, getListFormatter } from '$lib/services/contents/i18n';
import { defaultI18nConfig } from '$lib/services/contents/i18n';
/**
* @type {LocaleCode}
Expand Down Expand Up @@ -87,7 +87,6 @@
$: ({ defaultLocale } = (collectionFile ?? collection)?._i18n ?? defaultI18nConfig);
$: isDuplicateField = locale !== defaultLocale && i18n === 'duplicate';
$: valueMap = currentValues[locale];
$: listFormatter = getListFormatter(locale);
$: parentExpandedKeyPath = `${keyPath}#`;
$: parentExpanded = expanderStates?._[parentExpandedKeyPath] ?? true;
Expand Down Expand Up @@ -261,38 +260,21 @@
/**
* Format the summary template.
* @param {Record<string, any>} item - List item.
* @param {number} index - List index.
* @param {string} [summaryTemplate] - Summary template, e.g. `{{fields.slug}}`.
* @returns {string} Formatted summary.
*/
const formatSummary = (item, index, summaryTemplate) => {
if (!summaryTemplate) {
if (typeof item === 'string') {
return item;
}
return (
item.title ||
item.name ||
// Use the first string-type field value, if available
Object.values(item).find((value) => typeof value === 'string' && !!value) ||
''
);
}
return summaryTemplate.replaceAll(/{{fields\.(.+?)}}/g, (_match, _fieldName) => {
const value = getFieldDisplayValue({
collectionName,
fileName,
valueMap,
keyPath: hasSingleSubField ? `${keyPath}.${index}` : `${keyPath}.${index}.${_fieldName}`,
locale,
});
return Array.isArray(value) ? listFormatter.format(value) : String(value);
const _formatSummary = (index, summaryTemplate) =>
formatSummary({
collectionName,
fileName,
keyPath,
valueMap,
locale,
summaryTemplate,
hasSingleSubField,
index,
});
};
</script>
<Group aria-labelledby="list-{widgetId}-summary">
Expand Down Expand Up @@ -419,7 +401,7 @@
{/each}
{:else}
<div role="none" class="summary">
{formatSummary(item, index, summaryTemplate)}
{_formatSummary(index, summaryTemplate)}
</div>
{/if}
</div>
Expand Down
Loading

0 comments on commit abc1d2d

Please sign in to comment.