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
22 changes: 22 additions & 0 deletions .changeset/modal-form-section-group-11542.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'@object-ui/plugin-form': patch
---

The record dialog now draws a `form.sections[].group` section (objectui#11542).

A form view section can declare its members by pointing `group` at one of the
object's `fieldGroups` (the reference form of objectstack#13855). `ObjectForm`
resolved that form, but `ModalForm` did not, and the console's More actions ›
Edit / New dialog and action-opened modals mount `ModalForm` directly with the
form view's sections as authored. A `{ group }` section therefore reached the
dialog with no fields and was dropped: a tabbed form view showed no tab for the
group, a stacked one showed no header, and the fields only that group carries
could not be edited in the dialog.

`ModalForm` now resolves its sections through `resolveSectionGroupReferences`,
the same resolver `ObjectForm` uses, against the object schema it already
loads. The group's section is drawn with the group's label and members in both
content layouts, its members pass the same field-level security gate as
enumerated fields, and an unknown group renders nothing and is reported once,
as it is on `ObjectForm`. A section list that uses no `group` reaches the
dialog unchanged.
6 changes: 5 additions & 1 deletion packages/plugin-form/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,11 @@ it points `group` at one of the object's declared `fieldGroups` and inherits tha
group's members **and** its presentation (objectstack#13855, ADR-0085 §5 — the
spec range that carries it is the `@objectstack/spec` entry in this package's own
`package.json`). `ObjectForm` resolves the reference once, above its routing
fork, so all six layouts inherit it.
fork, so all six layouts inherit it. `ModalForm` resolves its own `sections`
through the same call as well, because hosts mount it directly rather than
through `ObjectForm` — the console's record create / edit dialog and
action-opened modals do (objectui#11542). `DrawerForm` mounted directly does
not resolve them; reach it through `ObjectForm` with `formType: 'drawer'`.

A host with its **own** section builder resolves it with the same function
instead of deriving sections itself (objectui#8641):
Expand Down
32 changes: 28 additions & 4 deletions packages/plugin-form/src/ModalForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*/

import React, { useState, useCallback, useEffect, useMemo, useId, useRef } from 'react';
import type { FormField, FormSchema, DataSource, ObjectFormSchema } from '@object-ui/types';
import type { FormField, FormSchema, DataSource, ObjectFormSchema, ObjectFormSection } from '@object-ui/types';
import {
Dialog,
MobileDialogContent,
Expand Down Expand Up @@ -51,6 +51,7 @@ import {
CONTAINER_GRID_COLS,
} from './autoLayout';
import { deriveFieldGroupSections, projectSectionDivider, resolveSectionCollapse } from './fieldGroups';
import { resolveSectionGroupReferences } from './sectionGroups';
import {
sanitizeFormData,
dirtyEditPayload,
Expand Down Expand Up @@ -325,6 +326,29 @@ export const ModalForm: React.FC<ModalFormProps> = ({
// Stable form id for linking the external submit button to the form element
const formId = useId();

// `form.sections[].group` (objectui#11542): a section that points `group` at
// one of the object's `fieldGroups` becomes the section that group declares,
// through the ONE resolver `ObjectForm`'s `withGroups` uses, so no assembly
// rule lives here. `ObjectForm` resolves above its routing fork, but the
// console's record dialog and an action-opened modal mount THIS component
// directly with the form view's sections as authored, so the dialog resolves
// its own. Resolved once, above both content layouts (tabbed and stacked),
// against the object schema this form already loads; while it loads the
// form shows its skeleton. Returns `schema.sections` itself when no section
// uses `group` — which includes every section list `ObjectForm` hands over,
// already resolved — so no other modal takes a new path. Every resolved
// member still goes through `gateFields` below, like an enumerated one.
const resolvedSections = useMemo(
() =>
resolveSectionGroupReferences(schema.sections as ObjectFormSection[] | undefined, {
objectName: schema.objectName,
formType: schema.formType,
objectDef: objectSchema,
resolvable: typeof dataSource?.getObjectSchema === 'function',
}) as ModalFormSectionConfig[] | undefined,
[schema.sections, schema.objectName, schema.formType, objectSchema, dataSource],
);

// Field-group fallback (object-designer metadata): when the caller passes no
// explicit sections, honor the object's declared `fieldGroups` the same way
// ObjectForm's simple path does — one section per group, with flat-path
Expand All @@ -348,7 +372,7 @@ export const ModalForm: React.FC<ModalFormProps> = ({
return sections.map((s) => ({ ...s, columns })) as ModalFormSectionConfig[];
}, [schema.sections, schema.columns, schema.mode, formFields, objectSchema]);

const effectiveSections = schema.sections?.length ? schema.sections : (derivedSections ?? undefined);
const effectiveSections = resolvedSections?.length ? resolvedSections : (derivedSections ?? undefined);

// Compute auto-layout for flat fields (no sections) to determine inferred columns
// (`customFields` does not switch it off: the members are merged into
Expand Down Expand Up @@ -785,8 +809,8 @@ export const ModalForm: React.FC<ModalFormProps> = ({
// 2+ silently dropped everything the user typed; and in the tabbed variant
// Radix unmounted the inactive panel, destroying that tab's form state
// outright. Same single-form pattern as ObjectForm / DrawerForm.
if (schema.sections?.length) {
const sections = schema.sections;
if (resolvedSections?.length) {
const sections = resolvedSections;
const sectionKey = (sec: ModalFormSectionConfig, i: number) => sec.name || sec.label || String(i);
// Section headers go through the same i18n hook ObjectForm uses, so a
// translated group label wins over the raw metadata label.
Expand Down
Loading
Loading