/** * This Source Code is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright (c) Infonomic Company Limited */ import type { ArrayField as ArrayFieldType, BlocksField as BlocksFieldType, Field, FieldAdminConfig, FieldComponentSlots, GroupField as GroupFieldType, RichTextEditorComponent, } from '@byline/core' import { getAdminConfig } from '@byline/core' import cx from 'clsx' import { useFormContext } from '../forms/form-context' import { useScopedDomId } from '../forms/form-dom-scope' import { ArrayField } from './array/array-field' import { BlocksField } from './blocks/blocks-field' import { CheckboxField } from './checkbox/checkbox-field' import { CodeField } from './code/code-field' import { DateTimeField } from './datetime/datetime-field' import styles from './field-renderer.module.css' import { FileField } from './file/file-field' import { GroupField } from './group/group-field' import { ImageField } from './image/image-field' import { LocaleBadge } from './locale-badge' import { NumericalField } from './numerical/numerical-field' import { RelationField } from './relation/relation-field' import { RelationManyField } from './relation/relation-many-field' import { SelectField } from './select/select-field' import { TextField } from './text/text-field' import { TextAreaField } from './text-area/text-area-field' import { useFieldChangeHandler } from './use-field-change-handler' import { useFieldCondition } from './use-field-condition' // --------------------------------------------------------------------------- // FieldRenderer — the main field type switch. Delegates to the appropriate // field widget based on `field.type`. // --------------------------------------------------------------------------- interface FieldRendererProps { field: Field defaultValue?: any basePath?: string disableSorting?: boolean hideLabel?: boolean /** * The active content locale (e.g. `'en'`, `'fr'`). When provided and * `field.localized === true`, a small locale badge is shown so the editor * knows they are working on a localised field in the current language. */ contentLocale?: string /** * Optional UI component slot overrides from the admin config. * Forwarded to value-field widgets that support custom slots. */ components?: FieldComponentSlots /** * Per-field rich-text editor component override from the admin config. * Takes precedence over the globally registered * `AdminConfig.fields.richText.editor` for this single field. * Ignored when `field.type !== 'richText'`. */ editor?: RichTextEditorComponent /** * Admin overrides for this field's *descendants*, keyed by dotted, * index-free schema paths relative to this field ('answer', * 'filesGroup.publicationFile'). Only meaningful when `field` is a * structural `group` / `array` — the widget slices the map per child * (see `sliceFieldAdmin`). `components` / `editor` above stay the * overrides for this field itself. */ fieldAdmin?: Record } export const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableSorting, hideLabel, contentLocale, components, editor, fieldAdmin, }: FieldRendererProps) => { const path = basePath ? `${basePath}.${field.name}` : field.name const htmlId = useScopedDomId(path.replace(/[[\].]/g, '-')) const handleChange = useFieldChangeHandler(field, path) const { getFieldValue } = useFormContext() // Conditional visibility (BaseField.condition) — re-evaluated on every form // edit; the field unmounts while its condition is false. const visible = useFieldCondition(field, basePath) // Conditional fields unmount while hidden, so on re-show the uncontrolled // widget must be re-seeded from the live form store (which survives the // unmount) rather than the initial document data — otherwise an edit made // before hiding would be visually reverted while the store (and the patch // stream) still carried it. `undefined` means the path was never written, // in which case the initial default stands. const storedValue = field.condition ? getFieldValue(path) : undefined const defaultValue = storedValue !== undefined ? storedValue : initialDefault // When a locale is active and the field is localised, inject a badge into // the field label so the editor knows they are editing locale-specific content. const isLocalised = (field as any).localized === true const badge = isLocalised && contentLocale && !hideLabel ? : null // All hooks have run by this point, so a conditional bail-out is safe. // Hiding retains the field's stored value — no clearing patch is emitted. if (!visible) return null /** * Render the underlying field widget. If the field is localised, we wrap it * so we can append the locale badge after the label. */ const renderField = () => { switch (field.type) { case 'text': return ( ) case 'textArea': return ( ) case 'code': return ( ) case 'checkbox': return ( ) case 'select': return ( ) case 'richText': { // Admin-side per-field override takes precedence over the globally // registered editor (`AdminConfig.fields.richText.editor`). The // override travels via `FieldAdminConfig.fields..editor` — // see admin-types — so React component references stay out of the // schema graph that's loaded by the server bootstrap. const RichTextEditor = editor ?? getAdminConfig().fields?.richText?.editor if (!RichTextEditor) { throw new Error( 'No richText editor registered. Install @byline/richtext-lexical and set ' + '`fields.richText.editor` in your admin config.' ) } return ( ) } case 'datetime': return ( ) case 'integer': case 'float': case 'decimal': return ( ) case 'counter': // Counter values are allocator-assigned; force readOnly at the // renderer level so the widget is always non-editable regardless // of whether the developer set `field.readOnly` explicitly. return ( ) case 'file': return ( ) case 'image': return ( ) case 'relation': if (field.hasMany) { return ( ) } return ( ) case 'group': // Render a group field as a fixed-order inline field group. return ( ) case 'blocks': if (!field.blocks) return null return ( ) case 'array': if (!field.fields) return null return ( ) default: return null } } // text, textArea, and code render the badge inside their own Label row; // the outer wrapper is only needed for other field types. const selfBadge = field.type === 'text' || field.type === 'textArea' || field.type === 'code' || field.type === 'richText' if (badge && !selfBadge) { return (
{renderField()} {badge}
) } return renderField() }