diff --git a/src/lib/components/contents/details/editor/field-editor.svelte b/src/lib/components/contents/details/editor/field-editor.svelte index ea060555..4b6f5316 100644 --- a/src/lib/components/contents/details/editor/field-editor.svelte +++ b/src/lib/components/contents/details/editor/field-editor.svelte @@ -9,6 +9,7 @@ import { _ } from 'svelte-i18n'; import { writable } from 'svelte/store'; import { defaultI18nConfig } from '$lib/services/contents/i18n'; + import { isFieldRequired } from '$lib/services/contents/entry/fields'; import { revertChanges } from '$lib/services/contents/draft/update'; import { entryDraft } from '$lib/services/contents/draft'; import { editors } from '$lib/components/contents/details/widgets'; @@ -55,10 +56,10 @@ comment = '', hint = '', widget: widgetName = 'string', - required = true, i18n = false, pattern = /** @type {string[]} */ ([]), } = $derived(fieldConfig); + const required = $derived(isFieldRequired({ fieldConfig, locale })); const { field: subField, fields: subFields, diff --git a/src/lib/services/contents/draft/create.js b/src/lib/services/contents/draft/create.js index 43f8e1a1..0dbc8db8 100644 --- a/src/lib/services/contents/draft/create.js +++ b/src/lib/services/contents/draft/create.js @@ -4,7 +4,7 @@ import { getCollection } from '$lib/services/contents/collection'; import { entryDraft, i18nAutoDupEnabled } from '$lib/services/contents/draft'; import { restoreBackupIfNeeded } from '$lib/services/contents/draft/backup'; import { showDuplicateToast } from '$lib/services/contents/draft/editor'; -import { getFieldConfig } from '$lib/services/contents/entry/fields'; +import { getFieldConfig, isFieldRequired } from '$lib/services/contents/entry/fields'; import { getDefaultValue as getDefaultDateTimeValue } from '$lib/services/contents/widgets/date-time/helper'; import { getDefaultValue as getDefaultHiddenValue } from '$lib/services/contents/widgets/hidden/helper'; import { getDefaultValue as getDefaultUuidValue } from '$lib/services/contents/widgets/uuid/helper'; @@ -113,7 +113,8 @@ export const getDefaultValues = (fields, locale, dynamicValues = {}) => { return; } - const { widget: widgetName = 'string', default: defaultValue, required = true } = fieldConfig; + const { widget: widgetName = 'string', default: defaultValue } = fieldConfig; + const required = isFieldRequired({ fieldConfig, locale }); const isArray = Array.isArray(defaultValue) && !!defaultValue.length; if (widgetName === 'list') { @@ -264,7 +265,7 @@ export const createProxy = ({ // Update validity in real time if validation has already been performed if (validity) { // @todo Perform all the field validations, not just `valueMissing` for string fields - if (typeof value === 'string' && fieldConfig.required !== false) { + if (typeof value === 'string' && isFieldRequired({ fieldConfig, locale: sourceLocale })) { validity.valueMissing = !value; } } diff --git a/src/lib/services/contents/draft/validate.js b/src/lib/services/contents/draft/validate.js index bf821162..e728fd43 100644 --- a/src/lib/services/contents/draft/validate.js +++ b/src/lib/services/contents/draft/validate.js @@ -1,7 +1,7 @@ import { escapeRegExp } from '@sveltia/utils/string'; import { get } from 'svelte/store'; import { entryDraft } from '$lib/services/contents/draft'; -import { getFieldConfig } from '$lib/services/contents/entry/fields'; +import { getFieldConfig, isFieldRequired } from '$lib/services/contents/entry/fields'; import { validateStringField } from '$lib/services/contents/widgets/string/helper'; // cspell:disable-next-line @@ -53,12 +53,8 @@ export const validateEntry = () => { return; } - const { - widget: widgetName = 'string', - required = true, - i18n = false, - pattern: validation, - } = fieldConfig; + const { widget: widgetName = 'string', i18n = false, pattern: validation } = fieldConfig; + const required = isFieldRequired({ fieldConfig, locale }); // Skip validation on non-editable fields if ( diff --git a/src/lib/services/contents/entry/fields.js b/src/lib/services/contents/entry/fields.js index 7f8980e7..7469f3c0 100644 --- a/src/lib/services/contents/entry/fields.js +++ b/src/lib/services/contents/entry/fields.js @@ -93,6 +93,16 @@ export const getFieldConfig = ({ return field; }; +/** + * Check if the field requires user input. + * @param {object} args - Arguments. + * @param {Field} args.fieldConfig - Field configuration. + * @param {LocaleCode} args.locale - Current pane’s locale. + * @returns {boolean} Result. + */ +export const isFieldRequired = ({ fieldConfig: { required = true }, locale }) => + Array.isArray(required) ? required.includes(locale) : !!required; + /** * Get a field’s display value that matches the given field name (key path). * @param {object} args - Arguments. diff --git a/src/lib/services/contents/entry/fields.spec.js b/src/lib/services/contents/entry/fields.spec.js new file mode 100644 index 00000000..4d2e52ed --- /dev/null +++ b/src/lib/services/contents/entry/fields.spec.js @@ -0,0 +1,23 @@ +import { describe, expect, test } from 'vitest'; +import { isFieldRequired } from '$lib/services/contents/entry/fields'; + +describe('Test isFieldRequired()', () => { + const name = 'title'; + const locale = 'en'; + + test('required: undefined', () => { + expect(isFieldRequired({ fieldConfig: { name }, locale })).toBe(true); + }); + + test('required: boolean', () => { + expect(isFieldRequired({ fieldConfig: { name, required: true }, locale })).toBe(true); + expect(isFieldRequired({ fieldConfig: { name, required: false }, locale })).toBe(false); + }); + + test('required: array', () => { + expect(isFieldRequired({ fieldConfig: { name, required: ['en'] }, locale })).toBe(true); + expect(isFieldRequired({ fieldConfig: { name, required: ['ja'] }, locale })).toBe(false); + expect(isFieldRequired({ fieldConfig: { name, required: ['en', 'ja'] }, locale })).toBe(true); + expect(isFieldRequired({ fieldConfig: { name, required: [] }, locale })).toBe(false); + }); +}); diff --git a/src/lib/typedefs.js b/src/lib/typedefs.js index a52708d1..43e66c1b 100644 --- a/src/lib/typedefs.js +++ b/src/lib/typedefs.js @@ -490,7 +490,9 @@ * @property {string} [label] - Field label. * @property {string} [comment] - Field description. * @property {string} [widget] - Widget name. - * @property {boolean} [required] - Whether to require input. + * @property {boolean | LocaleCode[]} [required] - Whether to require user input for the field. If + * i18n is enabled and the field doesn’t require input for every locale, a subset of locales can be + * passed as an array. * @property {string[]} [pattern] - Validation format. * @property {string} [hint] - Value hint to be displayed below the input. * @property {boolean} [preview] - Whether to show the preview of the field. Default: `true`.