From 0621328684dac02a4b93051bcb2088276350e4f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 23:25:52 +0000 Subject: [PATCH] fix(fields,plugin-form): the served help leg retires, and fieldHelpText moves to @object-ui/fields as the one read (objectui#12213) The record form and a field-backed action param read a field's help line through one function, fieldHelpText, now exported by @object-ui/fields: inlineHelpText, else description, an empty string reading as absent. The served help key is no longer read, per the ruling on the card: its one producer was an objectstack server on spec 17.6.0 or earlier, and 17.7.0 serves the translated text on description. Claude-Session: https://claude.ai/code/session_015YrgVw7e65cs1ndbebt46G Co-authored-by: Claude --- .changeset/12213-fields-action-param-help.md | 10 ++- .../12213-fields-field-help-text-export.md | 9 +++ .../12213-plugin-form-inline-help-text.md | 12 ++-- ...onParamDialog.fieldHelpLine-12213.test.tsx | 22 ++++-- packages/fields/README.md | 20 ++++++ .../__tests__/fieldHelpText-12213.test.tsx | 70 +++++++++++++++++++ packages/fields/src/fieldHelpText.ts | 54 ++++++++++++++ packages/fields/src/index.tsx | 8 +++ packages/fields/src/resolveActionParams.ts | 24 +++---- packages/plugin-form/src/ObjectForm.tsx | 3 +- .../objectFormInlineHelpText-12213.test.tsx | 20 ++++-- packages/plugin-form/src/sectionFields.ts | 68 +----------------- packages/types/src/field-types.ts | 2 +- 13 files changed, 221 insertions(+), 101 deletions(-) create mode 100644 .changeset/12213-fields-field-help-text-export.md create mode 100644 packages/fields/src/__tests__/fieldHelpText-12213.test.tsx create mode 100644 packages/fields/src/fieldHelpText.ts diff --git a/.changeset/12213-fields-action-param-help.md b/.changeset/12213-fields-action-param-help.md index 0af511a498..0d98d43fce 100644 --- a/.changeset/12213-fields-action-param-help.md +++ b/.changeset/12213-fields-action-param-help.md @@ -1,11 +1,15 @@ --- -'@object-ui/fields': patch +'@object-ui/fields': minor --- -A field-backed action param's help line reads the field's `inlineHelpText`, in the order the record form reads it (objectui#12213). +A field-backed action param's help line reads the field's `inlineHelpText`, through the one read the record form uses, and no longer reads a field's `help` (objectui#12213). When an action param backed by a field (`{ field: 'x' }`) declares no `helpText` of its own, `resolveActionParam` gives it the field's help text. It read `help`, then `description`, and never `@objectstack/spec`'s `FieldSchema.inlineHelpText`, the key Studio writes. -- FROM `param.helpText ?? field.help ?? field.description` → TO `param.helpText ?? (field.inlineHelpText || field.help || field.description)` +- FROM `param.helpText ?? field.help ?? field.description` → TO `param.helpText ?? fieldHelpText(field)`, which reads `field.inlineHelpText || field.description` The param's own `helpText` still wins. An empty string on the field reads as absent, as it does on the record form, so the next key is used. + +## ⚠️ BREAKING, priced as minor under the fixed group's version policy + +Translated field help on a field-backed param needs an objectstack server on `@objectstack/spec` 17.7.0 or later (objectstack-ai/objectstack `bab76850c`, objectstack-ai/objectstack#21956). `FieldSchema` refuses `help` as an authoring key, and its one producer was a server on spec 17.6.0 or earlier, whose translator put a field's translated description on the served field's `help` key. 17.7.0 serves that text on `description`, which is read. Against an older server, the param's help line shows the field's `description` as the server stores it, untranslated, and a field whose only help is a served `help` key lends none. Fix: run the server on spec 17.7.0 or later, within the same major. (The bump is `minor` by this repo's release model: objectui's major follows the `@objectstack` family major, and its own breaking changes ship as `minor` with the breaking semantics stated here.) diff --git a/.changeset/12213-fields-field-help-text-export.md b/.changeset/12213-fields-field-help-text-export.md new file mode 100644 index 0000000000..372e49bcb4 --- /dev/null +++ b/.changeset/12213-fields-field-help-text-export.md @@ -0,0 +1,9 @@ +--- +'@object-ui/fields': minor +--- + +`@object-ui/fields` exports `fieldHelpText(field)`, the one read of the help line a field draws under its control (objectui#12213). + +It reads `@objectstack/spec`'s `inlineHelpText`, then `description`, an empty string reading as absent, and never `help`. The record form (`@object-ui/plugin-form`, for `ObjectForm`'s field builder and `fromObjectSchema`) and `resolveActionParam` both call it, so the order has one home. It used to be a package-internal function of `@object-ui/plugin-form`, which `resolveActionParam` cannot import, so the resolver restated the order by hand. Its parameter is typed as `BaseFieldMetadata`'s `inlineHelpText` and `description` members (`@object-ui/types`); a typed literal that also carries `help` does not compile. + +**Clause-②: yes**: one export joins this package's public surface, `fieldHelpText`, and the read it carries accepts a field's `inlineHelpText` and `description` only. diff --git a/.changeset/12213-plugin-form-inline-help-text.md b/.changeset/12213-plugin-form-inline-help-text.md index 15dc76517a..426e8d20b2 100644 --- a/.changeset/12213-plugin-form-inline-help-text.md +++ b/.changeset/12213-plugin-form-inline-help-text.md @@ -1,13 +1,15 @@ --- -'@object-ui/plugin-form': patch +'@object-ui/plugin-form': minor --- -The record form draws a field's `inlineHelpText` under its control (objectui#12213). +The record form draws a field's `inlineHelpText` under its control, and no longer reads a field's `help` (objectui#12213). `@objectstack/spec`'s `FieldSchema` declares `inlineHelpText` ("Help text displayed below the field in forms"), and Studio's object field inspector writes it. `ObjectForm`'s field builder and `fromObjectSchema` both built a field's help line from `help`, then `description`, and never read `inlineHelpText`, so help text typed in Studio never appeared on the record form. Flow screens already drew it. -- FROM `field.help || field.description` → TO `field.inlineHelpText || field.help || field.description` +- FROM `field.help || field.description` → TO `field.inlineHelpText || field.description` -The two builders now share one read (`fieldHelpText`), so the flat, sectioned and tabbed forms cannot drift apart. The line is the control's accessible description, drawn under it, where flow screens draw it. An empty `inlineHelpText` reads as absent, so the field's description still shows. A `file` or `image` field's size hint follows the help text as before. +The two builders now share one read (`fieldHelpText`, exported by `@object-ui/fields`), so the flat, sectioned and tabbed forms cannot drift apart. The line is the control's accessible description, drawn under it, where flow screens draw it. An empty `inlineHelpText` reads as absent, so the field's description still shows. A `file` or `image` field's size hint follows the help text as before. -`help` is still read, behind `inlineHelpText`. The spec refuses it as an authoring key, but an objectstack server older than `@objectstack/spec` 17.7.0 serves a field's translated description there. +## ⚠️ BREAKING, priced as minor under the fixed group's version policy + +Translated field help on the record form needs an objectstack server on `@objectstack/spec` 17.7.0 or later (objectstack-ai/objectstack `bab76850c`, objectstack-ai/objectstack#21956). `FieldSchema` refuses `help` as an authoring key, and its one producer was a server on spec 17.6.0 or earlier, whose translator put a field's translated description on the served field's `help` key. 17.7.0 serves that text on `description`, which the form reads. Against an older server, a field's help line shows its `description` as the server stores it, untranslated, and a field whose only help is a served `help` key shows no help line. Fix: run the server on spec 17.7.0 or later, within the same major. (The bump is `minor` by this repo's release model: objectui's major follows the `@objectstack` family major, and its own breaking changes ship as `minor` with the breaking semantics stated here.) diff --git a/packages/app-shell/src/views/ActionParamDialog.fieldHelpLine-12213.test.tsx b/packages/app-shell/src/views/ActionParamDialog.fieldHelpLine-12213.test.tsx index 5780037f2c..00fa331fee 100644 --- a/packages/app-shell/src/views/ActionParamDialog.fieldHelpLine-12213.test.tsx +++ b/packages/app-shell/src/views/ActionParamDialog.fieldHelpLine-12213.test.tsx @@ -14,9 +14,11 @@ * (`{ field: 'x' }`) the field's help text when the param declares none of its * own. It read `field.help ?? field.description`, so the key the spec declares * for a field's help (`FieldSchema.inlineHelpText`, which Studio's object field - * inspector writes) never reached the dialog. It now reads the record form's - * order: `inlineHelpText`, then the served `help` leg, then `description`, an - * empty string reading as absent. + * inspector writes) never reached the dialog. It now reads through + * `fieldHelpText`, the one read the record form shares: `inlineHelpText`, then + * `description`, an empty string reading as absent. A served `help` key is + * ignored (ruled on objectui#12213: only an objectstack server older than spec + * 17.7.0 serves one, and 17.7.0 serves the translated text on `description`). * * Each case resolves a REAL param against an object definition and renders the * real `ActionParamDialog`, so what is asserted is the line a user reads under @@ -35,6 +37,7 @@ const TICKET = { summary: { type: 'text', label: 'Summary', description: 'The summary description' }, blank: { type: 'text', label: 'Blank', inlineHelpText: '', description: 'The blank description' }, served: { type: 'text', label: 'Served', help: 'Served translated help', description: 'Served source description' }, + helpOnly: { type: 'text', label: 'Help only', help: 'Served help alone' }, plain: { type: 'text', label: 'Plain' }, }, }; @@ -76,10 +79,17 @@ describe('objectui#12213 — a field-backed param’s help line reads `inlineHel expect(screen.getByText('The blank description')).toBeTruthy(); }); - it('the served `help` leg is still read, as on the record form', async () => { + it('a served `help` key is ignored, as on the record form: the field lends its `description`', async () => { const { param } = await openFor({ field: 'served' }); - expect(param.helpText).toBe('Served translated help'); - expect(screen.getByText('Served translated help')).toBeTruthy(); + expect(param.helpText).toBe('Served source description'); + expect(screen.getByText('Served source description')).toBeTruthy(); + expect(screen.queryByText('Served translated help')).toBeNull(); + }); + + it('a field whose only help is a served `help` key lends none', async () => { + const { param } = await openFor({ field: 'helpOnly' }); + expect(param.helpText).toBeUndefined(); + expect(screen.queryByText('Served help alone')).toBeNull(); }); it('the param’s own `helpText` still wins over the field’s `inlineHelpText`', async () => { diff --git a/packages/fields/README.md b/packages/fields/README.md index 37223781c2..5af8b09c84 100644 --- a/packages/fields/README.md +++ b/packages/fields/README.md @@ -164,6 +164,26 @@ const resolved = resolveActionParams(params, { resolver's dev warning prints. - Types: `RawActionParam`, `RawActionParamOption`, `ResolveActionParamsContext`. +### A field's help line + +`fieldHelpText(field)` is the one read of the help line a field draws under its +control. The record form (`@object-ui/plugin-form`) and a field-backed action +param both use it, so the two cannot disagree: + +```ts +import { fieldHelpText } from '@object-ui/fields'; + +fieldHelpText({ inlineHelpText: 'Shown under the control', description: 'About the field' }); +// → 'Shown under the control' +fieldHelpText({ inlineHelpText: '', description: 'About the field' }); +// → 'About the field' (an empty string reads as absent) +``` + +It reads `@objectstack/spec`'s `inlineHelpText`, then `description`. It does +not read `help`: the spec refuses that key on a field, and an objectstack +server on spec 17.7.0 or later serves a field's translated help text on +`description`. + ### Enumerating the CELL renderer registry `listCellRendererTypes()` is the read-side twin of `FORM_FIELD_TYPES`: every diff --git a/packages/fields/src/__tests__/fieldHelpText-12213.test.tsx b/packages/fields/src/__tests__/fieldHelpText-12213.test.tsx new file mode 100644 index 0000000000..2b8ceb41f9 --- /dev/null +++ b/packages/fields/src/__tests__/fieldHelpText-12213.test.tsx @@ -0,0 +1,70 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#12213 — `fieldHelpText`, the one read of a field definition's help + * line, exported from this package's entry. + * + * The record form (`@object-ui/plugin-form`) and `resolveActionParam` both call + * it, so the order has one home: `inlineHelpText`, else `description`, an empty + * string reading as absent. A served `help` key is not read: only an + * objectstack server older than `@objectstack/spec` 17.7.0 serves one, and the + * ruling on objectui#12213 retired that leg. + * + * The record form's and the action dialog's own pins + * (`objectFormInlineHelpText-12213.test.tsx` in plugin-form, + * `ActionParamDialog.fieldHelpLine-12213.test.tsx` in app-shell) assert the + * line a user reads. This file pins the export itself, and its source type. + */ +import { describe, it, expect } from 'vitest'; +// This package's own entry is imported RELATIVELY, for the reason +// `humanizeLabel.reexport.test.tsx` gives: a self-import resolves through the +// published `exports` map to `dist/`, which a cold CI cache does not have. +import { fieldHelpText } from '../index'; + +type FieldHelpTextSource = Parameters[0]; + +/** + * A served field document arrives untyped (the form's `objectSchema` is not + * typed), so a case that models one hands it over through a cast, as the data + * itself does. + */ +const served = (doc: object) => doc as FieldHelpTextSource; + +describe('fieldHelpText (objectui#12213)', () => { + it('reads `inlineHelpText` first, over `description`', () => { + expect(fieldHelpText({ inlineHelpText: 'Declared help', description: 'The description' })).toBe('Declared help'); + }); + + it('falls back to `description` when `inlineHelpText` is absent or empty', () => { + expect(fieldHelpText({ description: 'The description' })).toBe('The description'); + expect(fieldHelpText({ inlineHelpText: '', description: 'The description' })).toBe('The description'); + }); + + it('CONTROL: answers undefined when the field declares neither, or both are empty', () => { + expect(fieldHelpText({})).toBeUndefined(); + expect(fieldHelpText({ inlineHelpText: '', description: '' })).toBeUndefined(); + }); + + it('ignores a served `help` key: `description` is read, and `help` alone answers undefined', () => { + expect(fieldHelpText(served({ help: 'Served translated help', description: 'Served source description' }))).toBe( + 'Served source description', + ); + expect(fieldHelpText(served({ help: 'Served help alone' }))).toBeUndefined(); + expect(fieldHelpText(served({ inlineHelpText: 'Declared help wins', help: 'Served help loses' }))).toBe( + 'Declared help wins', + ); + }); + + it('its source type has no `help` member: a typed literal carrying one does not compile', () => { + // @ts-expect-error -- `help` is not a member of the read's source type; if + // it were ever declared again, this directive would be unused and the + // package's test type-check would fail. + expect(fieldHelpText({ help: 'Served help', description: 'The description' })).toBe('The description'); + }); +}); diff --git a/packages/fields/src/fieldHelpText.ts b/packages/fields/src/fieldHelpText.ts new file mode 100644 index 0000000000..a1b6da85b5 --- /dev/null +++ b/packages/fields/src/fieldHelpText.ts @@ -0,0 +1,54 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type { BaseFieldMetadata } from '@object-ui/types'; + +/** + * The help line a field draws under its control: the ONE read of a field + * definition's help text (objectui#12213). The order lives here and nowhere + * else, and both of its readers call this function: + * + * - `@object-ui/plugin-form`, for the record form's help line. `ObjectForm`'s + * field builder and `fromObjectSchema` both call it, so the flat, sectioned + * and tabbed forms cannot drift apart. + * - `resolveActionParam` in this package, for a field-backed action param + * that declares no `helpText` of its own. + * + * First non-empty value wins: + * + * 1. `inlineHelpText`, `@objectstack/spec`'s `FieldSchema.inlineHelpText` + * ("Help text displayed below the field in forms"). Studio's object field + * inspector writes it, and flow screens draw it in the same place. + * 2. `description`, the fallback the form always had. An objectstack server + * on `@objectstack/spec` 17.7.0 or later serves a field's translated help + * text here. + * + * ## `||`, not `??` + * + * An empty string reads as absent, so `inlineHelpText: ''` falls back to the + * description instead of hiding it. The spec declares no setting that hides + * the help line, and Studio's inspector stores a cleared box as no key at all + * (`inlineHelpText: v || undefined`). With `??`, an empty string would become + * a second, undeclared way to suppress the description. + * + * ## `help` is not read + * + * `FieldSchema` refuses `help` by name and suggests `inlineHelpText`, so no + * document that passes the write door carries it. Its one producer was an + * objectstack server older than `@objectstack/spec` 17.7.0, whose translator + * put a field's translated description on the served field's `help` key; + * 17.7.0 serves that text on `description`. The read of that served leg is + * retired (the ruling on objectui#12213): translated field help needs an + * objectstack server on spec 17.7.0 or later. A definition that still carries + * `help` draws its `inlineHelpText`, else its `description`, else nothing. + */ +export function fieldHelpText( + field: Pick, +): string | undefined { + return field.inlineHelpText || field.description || undefined; +} diff --git a/packages/fields/src/index.tsx b/packages/fields/src/index.tsx index 7249b019a4..b6006f4a41 100644 --- a/packages/fields/src/index.tsx +++ b/packages/fields/src/index.tsx @@ -4418,6 +4418,14 @@ export type { ResolveActionParamsContext, } from './resolveActionParams.js'; +/** + * The ONE read of a field definition's help line (objectui#12213): + * `inlineHelpText`, else `description`. Exported from here because both of its + * readers can reach this package: `@object-ui/plugin-form`'s record form and + * this package's own `resolveActionParam`. See the module. + */ +export { fieldHelpText } from './fieldHelpText.js'; + /** Cache so each widget type creates one lazy component (and one chunk request). */ const lazyFieldWidgets = new Map>(); diff --git a/packages/fields/src/resolveActionParams.ts b/packages/fields/src/resolveActionParams.ts index 1812b64591..69e460dbab 100644 --- a/packages/fields/src/resolveActionParams.ts +++ b/packages/fields/src/resolveActionParams.ts @@ -53,6 +53,8 @@ import type { ActionParamDef, ActionParamOption } from '@object-ui/core'; // by — a cycle in the SOURCE graph only, and a call-time one: nothing at this // module's top level reads it, and the published bundle is a single file. import { resolveFormWidgetType } from './index.js'; +// The field's help line, read in the one order the record form reads it in. +import { fieldHelpText } from './fieldHelpText.js'; import type { I18nLabel } from '@objectstack/spec/ui'; // Aliased per PR #4169's convention — app-shell has its OWN `resolveI18nLabel` // (renamed `resolveKeyedI18nLabel` by objectui#4167) over the translation-KEY @@ -353,17 +355,15 @@ interface RuntimeField { /** * `@objectstack/spec`'s `FieldSchema.inlineHelpText`, the help line drawn * under the field's control (objectui#12213). Read first for a field-backed - * param's `helpText`, the same order the record form reads it in. + * param's `helpText`, through `fieldHelpText`, the read the record form + * shares. */ inlineHelpText?: string; /** - * A SERVED leg, not an authoring key: `FieldSchema` refuses `help` by name. - * An objectstack server older than `@objectstack/spec` 17.7.0 puts a field's - * translated help here; 17.7.0 serves it on `description`. Still read, - * pending the ruling on objectui#12213 (see `fieldHelpText` in - * `@object-ui/plugin-form`'s `sectionFields`, which reads the same order). + * The help line's fallback, read second through `fieldHelpText`. An + * objectstack server on `@objectstack/spec` 17.7.0 or later serves a field's + * translated help text here. */ - help?: string; description?: string; /** * The field's own option list — `select` / `radio` / `checkboxes` metadata as @@ -794,11 +794,11 @@ export function resolveActionParam( options: resolvedOptions, placeholder: param.placeholder ?? field.placeholder, // The param's own `helpText` first; else the help line the field draws - // under its control on the record form, in the record form's order - // (`fieldHelpText` in `@object-ui/plugin-form`, objectui#12213): the - // spec's `inlineHelpText`, the served `help` leg, then `description`. An - // empty string reads as absent, as it does there. - helpText: param.helpText ?? (field.inlineHelpText || field.help || field.description || undefined), + // under its control on the record form, through the one read the record + // form shares (`fieldHelpText`, objectui#12213): the spec's + // `inlineHelpText`, then `description`, an empty string reading as absent. + // A served `help` key is not read. + helpText: param.helpText ?? fieldHelpText(field), defaultValue: rowDefault ?? param.defaultValue ?? field.defaultValue, visible: normaliseVisible(param.visible), // A declaration of the PARAM, never inherited from the field. diff --git a/packages/plugin-form/src/ObjectForm.tsx b/packages/plugin-form/src/ObjectForm.tsx index 61e08c61d4..31a338ec81 100644 --- a/packages/plugin-form/src/ObjectForm.tsx +++ b/packages/plugin-form/src/ObjectForm.tsx @@ -18,7 +18,7 @@ import type { ObjectFormSchema, FormField, FormSchema, DataSource } from '@objec import { SchemaRenderer, useSafeFieldLabel } from '@object-ui/react'; import { pickLocalized, useObjectTranslation } from '@object-ui/i18n'; import { useRecordInvalidation } from './recordInvalidation'; -import { mapFieldTypeToFormType, buildValidationRules, formatFileSize } from '@object-ui/fields'; +import { mapFieldTypeToFormType, buildValidationRules, formatFileSize, fieldHelpText } from '@object-ui/fields'; import { useIsMobile, toast } from '@object-ui/components'; import { resolveSuccessNavigate } from './successBehavior'; import { resolveSubmitRedirect, submitRedirectScope } from './submitRedirect'; @@ -33,7 +33,6 @@ import { warnSectionMemberExcludedByFields, buildSectionFields, sectionEntryName, - fieldHelpText, type SectionFieldsContext, } from './sectionFields'; import { TabbedForm } from './TabbedForm'; diff --git a/packages/plugin-form/src/__tests__/objectFormInlineHelpText-12213.test.tsx b/packages/plugin-form/src/__tests__/objectFormInlineHelpText-12213.test.tsx index 29a69bddbc..33968d84c7 100644 --- a/packages/plugin-form/src/__tests__/objectFormInlineHelpText-12213.test.tsx +++ b/packages/plugin-form/src/__tests__/objectFormInlineHelpText-12213.test.tsx @@ -15,7 +15,7 @@ * writes it. Both of the form's field builders (`ObjectForm`'s own and * `fromObjectSchema`) read `field.help || field.description` and never that * key, so help text an admin typed in Studio never showed on the record form. - * They now share one read, `fieldHelpText`. + * They now share one read, `fieldHelpText` from `@object-ui/fields`. * * Pinned through the public `ObjectForm` entry, with the real registered field * widgets, on three layouts that reach the two builders: @@ -34,9 +34,11 @@ * 2. CONTROL: a field with only `description` still draws it; * 3. `inlineHelpText: ''` reads as absent, so the description is drawn (`||`, * not `??`: the spec declares no setting that hides the help line); - * 4. the served `help` leg is still read (an objectstack server older than - * spec 17.7.0 serves a translated description there); it loses to - * `inlineHelpText`. Whether that leg retires is ruled on objectui#12213; + * 4. a served `help` key is ignored (ruled on objectui#12213: only an + * objectstack server older than spec 17.7.0 serves one, and 17.7.0 serves + * the translated text on `description`). A field carrying `help` draws its + * `description`; one whose only help is `help` draws no help line; and + * `inlineHelpText` still wins over it; * 5. CONTROL: a field with none of the three draws no description. * * Plus, on the flat layout only (the one that appends it): a file field's size @@ -65,13 +67,14 @@ const OBJECT_SCHEMA = { summary: { type: 'text', label: 'Summary', description: 'The summary description' }, blank: { type: 'text', label: 'Blank', inlineHelpText: '', description: 'The blank description' }, served: { type: 'text', label: 'Served', help: 'Served translated help', description: 'Served source description' }, + helpOnly: { type: 'text', label: 'Help only', help: 'Served help alone' }, both: { type: 'text', label: 'Both', inlineHelpText: 'Declared help wins', help: 'Served help loses' }, plain: { type: 'text', label: 'Plain' }, contract: { type: 'file', label: 'Contract', inlineHelpText: 'PDF only', maxSize: 10 * MB }, }, }; -const TEXT_FIELDS = ['note', 'summary', 'blank', 'served', 'both', 'plain'] as const; +const TEXT_FIELDS = ['note', 'summary', 'blank', 'served', 'helpOnly', 'both', 'plain'] as const; const makeDS = () => ({ @@ -130,9 +133,12 @@ describe.each(['flat', 'sectioned', 'tabbed'])( expect(control('Blank')).toHaveAccessibleDescription('The blank description'); }); - it('the served `help` leg is still read, and loses to `inlineHelpText`', async () => { + it('a served `help` key is ignored: `description` is drawn, and `help` alone draws no help line', async () => { await renderForm(layout); - expect(control('Served')).toHaveAccessibleDescription('Served translated help'); + expect(control('Served')).toHaveAccessibleDescription('Served source description'); + expect(screen.queryByText('Served translated help')).toBeNull(); + expect(control('Help only')).toHaveAccessibleDescription(''); + expect(screen.queryByText('Served help alone')).toBeNull(); expect(control('Both')).toHaveAccessibleDescription('Declared help wins'); expect(screen.queryByText('Served help loses')).toBeNull(); }); diff --git a/packages/plugin-form/src/sectionFields.ts b/packages/plugin-form/src/sectionFields.ts index 94a1ae4b03..6a725f0095 100644 --- a/packages/plugin-form/src/sectionFields.ts +++ b/packages/plugin-form/src/sectionFields.ts @@ -40,9 +40,9 @@ * that makes the loser audible. */ -import type { BaseFieldMetadata, FormField } from '@object-ui/types'; +import type { FormField } from '@object-ui/types'; import { evalFieldPredicate, isBlankPredicateText, type FieldRulePredicate } from '@object-ui/core'; -import { mapFieldTypeToFormType, buildValidationRules } from '@object-ui/fields'; +import { mapFieldTypeToFormType, buildValidationRules, fieldHelpText } from '@object-ui/fields'; import { isCreateFormMode, isRequiredInForm } from './schemaDefaults'; import { findCustomFieldMember } from './customFieldsMerge'; import { isInlineFieldDef } from './submitTarget'; @@ -280,68 +280,6 @@ export function warnSectionMemberExcludedByFields( ); } -/** - * The keys {@link fieldHelpText} reads off a field definition as served. - * - * `inlineHelpText` and `description` are the authoring face's own members - * (`BaseFieldMetadata`; the first is typed there by reference to the spec). - * `help` is declared HERE and not on the authoring face: it is a served leg, - * not a key an author may write (see {@link fieldHelpText}). - */ -interface FieldHelpTextSource extends Pick { - /** - * The served leg. An objectstack server older than `@objectstack/spec` - * 17.7.0 puts a field's translated help here. Not an authoring key. - */ - help?: string; -} - -/** - * The help line a field draws under its control on the record form - * (objectui#12213): the ONE read `ObjectForm`'s field builder and - * {@link fromObjectSchema} share, so the flat and the sectioned forms cannot - * drift apart. Both used to spell `field.help || field.description`, and - * neither read the key the spec declares. - * - * First non-empty value wins: - * - * 1. `inlineHelpText`, `@objectstack/spec`'s `FieldSchema.inlineHelpText` - * ("Help text displayed below the field in forms"). Studio's object field - * inspector writes it, and flow screens already draw it under the control. - * 2. `help`, the served leg below. - * 3. `description`, the fallback the form always had. - * - * `resolveActionParam` in `@object-ui/fields` derives a field-backed action - * param's `helpText` in the same order; it cannot import this package, so the - * order is restated there. - * - * ## `||`, not `??` - * - * An empty string reads as absent, so `inlineHelpText: ''` falls back to the - * description instead of hiding it. The spec declares no setting that hides - * the help line, and Studio's inspector stores a cleared box as no key at all - * (`inlineHelpText: v || undefined`). With `??`, an empty string would become - * a second, undeclared way to suppress the description. - * - * ## Why the `help` leg is still read - * - * `FieldSchema` refuses `help` by name (it suggests `inlineHelpText`), so no - * document that passes the write door carries it. The ingestion fold - * (`normalizeSchemaReferenceKeys` in `@object-ui/core`) has no declared twin - * to stamp it onto: its alias probe of `help` matches no key `FieldSchema` - * declares, so the key arrives here as served. One producer still serves it. - * An objectstack server whose `@objectstack/spec` is older than 17.7.0 - * translated a field by putting the bundle's field `help` entry (the - * translated `description`) on the served field's `help` key. 17.7.0 serves - * that entry on `description` instead. Against the older server this leg is - * what keeps the translated text on the form. Whether it retires is the - * decision recorded on objectui#12213, not taken here. - */ -export function fieldHelpText(field: FieldHelpTextSource | null | undefined): string | undefined { - if (!field) return undefined; - return field.inlineHelpText || field.help || field.description || undefined; -} - /** * Build a runtime FormField from object-schema metadata for `fieldName`. * @@ -372,7 +310,7 @@ export function fromObjectSchema(fieldName: string, ctx: SectionFieldsContext): placeholder: field.placeholder, // The help line under the control: the spec's `inlineHelpText`, else // `description`, through the one read `ObjectForm`'s field builder shares - // (`fieldHelpText`, objectui#12213). + // (`fieldHelpText` from `@object-ui/fields`, objectui#12213). description: fieldHelpText(field), validation: buildValidationRules(field), field, diff --git a/packages/types/src/field-types.ts b/packages/types/src/field-types.ts index 10ea484a63..c118240603 100644 --- a/packages/types/src/field-types.ts +++ b/packages/types/src/field-types.ts @@ -65,7 +65,7 @@ export interface BaseFieldMetadata { * below the field in forms"), typed BY REFERENCE (objectui#12213). It is * the key Studio's object field inspector writes. The record form reads it * first and falls back to `description` (`fieldHelpText` in - * `@object-ui/plugin-form`), and flow screens draw it in the same place. + * `@object-ui/fields`), and flow screens draw it in the same place. * * It replaces `help`, which this interface declared until objectui#12213: * the spec refuses `help` by name and suggests `inlineHelpText`, so a field