Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .changeset/12213-fields-action-param-help.md
Original file line number Diff line number Diff line change
@@ -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.)
9 changes: 9 additions & 0 deletions .changeset/12213-fields-field-help-text-export.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 7 additions & 5 deletions .changeset/12213-plugin-form-inline-help-text.md
Original file line number Diff line number Diff line change
@@ -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.)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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' },
},
};
Expand Down Expand Up @@ -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 () => {
Expand Down
20 changes: 20 additions & 0 deletions packages/fields/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions packages/fields/src/__tests__/fieldHelpText-12213.test.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof fieldHelpText>[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');
});
});
54 changes: 54 additions & 0 deletions packages/fields/src/fieldHelpText.ts
Original file line number Diff line number Diff line change
@@ -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<BaseFieldMetadata, 'inlineHelpText' | 'description'>,
): string | undefined {
return field.inlineHelpText || field.description || undefined;
}
8 changes: 8 additions & 0 deletions packages/fields/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, React.ComponentType<any>>();

Expand Down
24 changes: 12 additions & 12 deletions packages/fields/src/resolveActionParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 1 addition & 2 deletions packages/plugin-form/src/ObjectForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -33,7 +33,6 @@ import {
warnSectionMemberExcludedByFields,
buildSectionFields,
sectionEntryName,
fieldHelpText,
type SectionFieldsContext,
} from './sectionFields';
import { TabbedForm } from './TabbedForm';
Expand Down
Loading
Loading