diff --git a/.changeset/6025-declare-plugin-grid-in-gallery-host.md b/.changeset/6025-declare-plugin-grid-in-gallery-host.md
new file mode 100644
index 0000000000..abfbccde32
--- /dev/null
+++ b/.changeset/6025-declare-plugin-grid-in-gallery-host.md
@@ -0,0 +1,17 @@
+---
+---
+
+Docs-site host and catalog test only: `apps/site/app/components/registerCatalogBlocks.ts`
+now declares `@object-ui/plugin-grid` instead of inheriting `object-grid` from
+`@object-ui/plugin-view`'s `import { ObjectGrid }` (`ObjectView.tsx:37`). Nothing renders
+differently — measured: with exactly the eleven packages the host used to carry,
+`object-grid` and every `@object-ui/plugin-form` key already resolved — which is the point.
+A component import in another package was the only reason two catalog tiles could draw, and
+a refactor of `object-view` that stopped it from drawing a grid itself would have turned
+them into OBJUI-001 panels with the cause several files from the symptom.
+
+`catalog-gallery-render.test.tsx` gains the judge that can tell the two apart: every
+`plugin-*` category with catalog entries must be loaded BY NAME in the host's import list,
+derived from the categories rather than enumerated, and leaning on the existing parity case
+that ties `HOST_PACKAGES` to the host file's literal imports. `ComponentRegistry.get()`
+cannot judge this — it is truthy in both worlds.
diff --git a/.changeset/6167-plugin-form-catalog-entries.md b/.changeset/6167-plugin-form-catalog-entries.md
new file mode 100644
index 0000000000..0a31d99a68
--- /dev/null
+++ b/.changeset/6167-plugin-form-catalog-entries.md
@@ -0,0 +1,31 @@
+---
+---
+
+Docs and catalog fixtures only, no published package source touched: the two `plugin-form`
+catalog entries are now real `object-form` nodes instead of hand-built `form` schemas.
+`content/docs/plugins/plugin-form.mdx` mounted `basic-form` and `contact-form` under
+`PluginLoader plugins={['form']}` while both authored a root `type: "form"` with `input` /
+`select` / `checkbox` / `textarea` fields — every one of those registered by
+`@object-ui/components`, none of them by `@object-ui/plugin-form`, whose own keys are
+`object-form`, `embeddable-form`, `form-analytics`, `object-master-detail-form`,
+`record:line_items` and `view:form`. They are replaced by `object-form-record` (one `users`
+record, fields derived from the object's metadata, `fields` / `columns` shaping the grid)
+and `object-form-tabbed-sections` (the same record with `formType: 'tabbed'`, so the
+declared `sections` become tab panels of one form).
+
+The two hand-built forms are legitimate `@object-ui/components` examples filed under the
+wrong plugin, so they are re-seated into `components-form-form` as `basic-form` and
+`demo-request-form` rather than deleted — deleting a catalog entry moves corpus-wide
+counters (`NODE_CENSUS` in `layout-dom-leak-5574.test.tsx` and in
+`form-control-dom-leak-5632.test.tsx`, and the `className`-carrying layout node and `stack`
+node floors in `layout-props-conversion.test.tsx`), and a counter that moves because a
+fixture was deleted is indistinguishable later from one that moved because coverage
+regressed. All of them are unchanged, with no floor edited.
+
+`apps/site/app/components/registerCatalogBlocks.ts` now declares `@object-ui/plugin-form`,
+and its header no longer claims the import list is "exactly the packages that census
+resolves to". The two `OWN_PLUGIN_DEBT` lines in `catalog-gallery-render.test.tsx` are
+deleted, so both entries are held to the pin like every other one, and the pin's
+provenance instrument now reads the fixture record out of a form control's value as well as
+out of the tile's text — a form puts its record in `input.value`, where `textContent`
+cannot see it.
diff --git a/apps/site/app/components/registerCatalogBlocks.ts b/apps/site/app/components/registerCatalogBlocks.ts
index 97d3ae303e..6aa4a78d97 100644
--- a/apps/site/app/components/registerCatalogBlocks.ts
+++ b/apps/site/app/components/registerCatalogBlocks.ts
@@ -36,10 +36,43 @@
* components-disclosure-toggle-group 1 core-schema-renderer 1
*
* The gallery page's PURPOSE is to render the whole catalog, so its baseline is
- * that every type an entry names is registered before the render. The nine
- * imports below are exactly the packages that census resolves to — not a
+ * that every type an entry names is registered before the render. Nine of the
+ * imports below are exactly the packages that census resolved to — not a
* precautionary sweep of the workspace.
*
+ * ## The list is no longer ONLY that census (objectui#6167)
+ *
+ * `@object-ui/plugin-form` is here for a different reason, and the header has
+ * to say so rather than let the sentence above keep describing a property this
+ * file no longer has. The two `plugin-form` catalog entries are real
+ * `object-form` nodes as of objectui#6167, and `object-form` is registered by
+ * `@object-ui/plugin-form` — so the gallery depends on that package by name.
+ *
+ * What it did NOT do is change what resolves. Measured before adding it, with
+ * exactly the eleven imports this file used to carry: `object-form`,
+ * `plugin-form:object-form`, `embeddable-form`, `form-analytics`,
+ * `object-master-detail-form`, `record:line_items` and `view:form` all resolved
+ * already, because `@object-ui/plugin-view` — import #11 below — does
+ * `import { ObjectForm } from '@object-ui/plugin-form'`
+ * (`packages/plugin-view/src/ObjectView.tsx:38`), and importing that entry runs
+ * its `ComponentRegistry.register` calls. The package's graph was therefore
+ * ALREADY in this route's eager closure; the import below adds a declaration,
+ * not a payload. Registration is idempotent for the same reason: an ES module
+ * executes once, so naming it here does not re-run anything.
+ *
+ * `@object-ui/plugin-grid` joins on the same argument and with the same
+ * measured non-effect (objectui#6025). The `plugin-grid` entries have been real
+ * `object-grid` nodes since objectui#5856, and `object-grid` reached this
+ * registry only because `ObjectView.tsx:37` — the line above the one quoted
+ * above — imports `ObjectGrid`. That is a COMPONENT import, not a plugin
+ * dependency: a refactor that stopped `object-view` from drawing a grid itself
+ * would be entirely reasonable, would say nothing about the gallery, and would
+ * turn two catalog tiles into OBJUI-001 panels several files from the cause.
+ *
+ * So the list below is: the census (nine), plus objectui#4600's original two,
+ * plus two packages the gallery's own entries depend on BY NAME. What it is not
+ * is a sweep — nothing is here that the catalog does not render.
+ *
* ## The cost, measured before deciding (objectui#4616 ruling 1)
*
* Eager registration pulls each package's graph into the `/docs/[[...slug]]`
@@ -126,3 +159,5 @@ import '@object-ui/plugin-map';
import '@object-ui/plugin-markdown';
import '@object-ui/plugin-timeline';
import '@object-ui/plugin-view';
+import '@object-ui/plugin-form';
+import '@object-ui/plugin-grid';
diff --git a/content/docs/plugins/plugin-form.mdx b/content/docs/plugins/plugin-form.mdx
index f9a50e9722..f734fad386 100644
--- a/content/docs/plugins/plugin-form.mdx
+++ b/content/docs/plugins/plugin-form.mdx
@@ -17,9 +17,28 @@ npm install @object-ui/plugin-form
## Interactive Examples
-
+Each preview below **is** an `object-form` node drawn by this plugin — the JSON
+in the Code tab is the whole example, and the fields on screen were built from
+the `users` object's own metadata rather than written into that JSON. Both the
+metadata and the record come from the docs site's demo data source, because
+`dataSource` is not a schema key: it is the prop the registered renderer pulls
+off `SchemaRendererProvider` context (see
+[Registration is a side effect of the import](#registration-is-a-side-effect-of-the-import)
+below), so in your own app these same nodes read whatever object your data
+source serves.
-
+A plain `form` node with its fields written out inline is the
+`@object-ui/components` renderer, not this plugin — that is what these two
+examples used to be, and they now live in the catalog under
+`components-form-form`.
+
+### One record, fields from the object
+
+
+
+### Sections as tabs
+
+
## Features
diff --git a/examples/schema-catalog/package.json b/examples/schema-catalog/package.json
index ec9bd46254..c7fb185842 100644
--- a/examples/schema-catalog/package.json
+++ b/examples/schema-catalog/package.json
@@ -39,7 +39,9 @@
"@object-ui/plugin-chatbot": "workspace:*",
"@object-ui/plugin-dashboard": "workspace:*",
"@object-ui/plugin-editor": "workspace:*",
+ "@object-ui/plugin-form": "workspace:*",
"@object-ui/plugin-gantt": "workspace:*",
+ "@object-ui/plugin-grid": "workspace:*",
"@object-ui/plugin-kanban": "workspace:*",
"@object-ui/plugin-map": "workspace:*",
"@object-ui/plugin-markdown": "workspace:*",
diff --git a/examples/schema-catalog/src/catalog-meta.json b/examples/schema-catalog/src/catalog-meta.json
index 83157eedd9..892662de1f 100644
--- a/examples/schema-catalog/src/catalog-meta.json
+++ b/examples/schema-catalog/src/catalog-meta.json
@@ -36,6 +36,26 @@
"verification"
]
},
+ "components-form-form/basic-form": {
+ "title": "Basic Form",
+ "description": "A hand-built `form` node from `@object-ui/components`: name, email, country and newsletter fields declared inline, with no object behind them. For a form generated from an object's own metadata, see the `plugin-form` examples.",
+ "tags": [
+ "form",
+ "input",
+ "select",
+ "checkbox"
+ ]
+ },
+ "components-form-form/demo-request-form": {
+ "title": "Demo Request Form",
+ "description": "A longer hand-built `form`: company details, industry and size selects, a message textarea and a terms checkbox, all declared inline. For a form bound to an object, see the `plugin-form` examples.",
+ "tags": [
+ "form",
+ "select",
+ "textarea",
+ "checkbox"
+ ]
+ },
"components-layout-card/inventory-table-card": {
"title": "Inventory Table Card",
"description": "A card that draws a product inventory table by hand — toolbar buttons, header row, stock-status badges. For a table bound to an object, see the `plugin-grid` examples.",
@@ -109,6 +129,26 @@
"title": "Filtered Dashboard — Target Widgets Allow-list",
"description": "Legacy targetWidgets allow-list: only listed widgets get the default binding; an explicit filterBindings entry still wins"
},
+ "plugin-form/object-form-record": {
+ "title": "Object Form — One Record From Object Metadata",
+ "description": "object-form editing one `users` record: the fields come from the object's own metadata through `getObjectSchema`, the values from `findOne`, and `fields` / `columns` decide which of them appear and how wide the grid is.",
+ "tags": [
+ "object-form",
+ "objectql",
+ "edit",
+ "columns"
+ ]
+ },
+ "plugin-form/object-form-tabbed-sections": {
+ "title": "Object Form — Sections as Tabs",
+ "description": "The same record with `formType: 'tabbed'`: the declared `sections` become tab panels of ONE form, so a single submit spans them and a tab the user leaves keeps its values.",
+ "tags": [
+ "object-form",
+ "tabbed",
+ "sections",
+ "objectql"
+ ]
+ },
"plugin-grid/object-grid-columns": {
"title": "Object Grid — Column Definitions",
"description": "object-grid rendering the users object: `ListColumn` objects carrying label, width, align, cell `type` and `link`, with `sort`, `searchableFields` and `pagination` shaping the query the host's data source answers.",
diff --git a/examples/schema-catalog/src/index.ts b/examples/schema-catalog/src/index.ts
index 11fd014e35..d3bffaee23 100644
--- a/examples/schema-catalog/src/index.ts
+++ b/examples/schema-catalog/src/index.ts
@@ -210,7 +210,9 @@ import components_form_file_upload_images_only from './schemas/components-form-f
import components_form_file_upload_multiple_files from './schemas/components-form-file-upload/multiple-files.json' with { type: 'json' };
import components_form_file_upload_simple_upload from './schemas/components-form-file-upload/simple-upload.json' with { type: 'json' };
import components_form_file_upload_single_file from './schemas/components-form-file-upload/single-file.json' with { type: 'json' };
+import components_form_form_basic_form from './schemas/components-form-form/basic-form.json' with { type: 'json' };
import components_form_form_contact_form from './schemas/components-form-form/contact-form.json' with { type: 'json' };
+import components_form_form_demo_request_form from './schemas/components-form-form/demo-request-form.json' with { type: 'json' };
import components_form_form_login_form from './schemas/components-form-form/login-form.json' with { type: 'json' };
import components_form_form_registration_form from './schemas/components-form-form/registration-form.json' with { type: 'json' };
import components_form_input_basic_input from './schemas/components-form-input/basic-input.json' with { type: 'json' };
@@ -414,8 +416,8 @@ import plugin_dashboard_support_dashboard from './schemas/plugin-dashboard/suppo
import plugin_editor_javascript_editor from './schemas/plugin-editor/javascript-editor.json' with { type: 'json' };
import plugin_editor_python_editor from './schemas/plugin-editor/python-editor.json' with { type: 'json' };
import plugin_editor_read_only_json_viewer from './schemas/plugin-editor/read-only-json-viewer.json' with { type: 'json' };
-import plugin_form_basic_form from './schemas/plugin-form/basic-form.json' with { type: 'json' };
-import plugin_form_contact_form from './schemas/plugin-form/contact-form.json' with { type: 'json' };
+import plugin_form_object_form_record from './schemas/plugin-form/object-form-record.json' with { type: 'json' };
+import plugin_form_object_form_tabbed_sections from './schemas/plugin-form/object-form-tabbed-sections.json' with { type: 'json' };
import plugin_gantt_construction_project_phases from './schemas/plugin-gantt/construction-project-phases.json' with { type: 'json' };
import plugin_gantt_project_timeline_with_dependencies from './schemas/plugin-gantt/project-timeline-with-dependencies.json' with { type: 'json' };
import plugin_gantt_sprint_development_timeline from './schemas/plugin-gantt/sprint-development-timeline.json' with { type: 'json' };
@@ -2238,6 +2240,16 @@ const REGISTRY: Record = {
},
schema: components_form_file_upload_single_file,
},
+ 'components-form-form/basic-form': {
+ id: 'components-form-form/basic-form',
+ meta: {
+ title: "Basic Form",
+ description: "A hand-built `form` node from `@object-ui/components`: name, email, country and newsletter fields declared inline, with no object behind them. For a form generated from an object's own metadata, see the `plugin-form` examples.",
+ category: 'components-form-form',
+ tags: ["form", "input", "select", "checkbox"],
+ },
+ schema: components_form_form_basic_form,
+ },
'components-form-form/contact-form': {
id: 'components-form-form/contact-form',
meta: {
@@ -2247,6 +2259,16 @@ const REGISTRY: Record = {
},
schema: components_form_form_contact_form,
},
+ 'components-form-form/demo-request-form': {
+ id: 'components-form-form/demo-request-form',
+ meta: {
+ title: "Demo Request Form",
+ description: "A longer hand-built `form`: company details, industry and size selects, a message textarea and a terms checkbox, all declared inline. For a form bound to an object, see the `plugin-form` examples.",
+ category: 'components-form-form',
+ tags: ["form", "select", "textarea", "checkbox"],
+ },
+ schema: components_form_form_demo_request_form,
+ },
'components-form-form/login-form': {
id: 'components-form-form/login-form',
meta: {
@@ -4079,23 +4101,25 @@ const REGISTRY: Record = {
},
schema: plugin_editor_read_only_json_viewer,
},
- 'plugin-form/basic-form': {
- id: 'plugin-form/basic-form',
+ 'plugin-form/object-form-record': {
+ id: 'plugin-form/object-form-record',
meta: {
- title: "Basic Form",
- description: "",
+ title: "Object Form — One Record From Object Metadata",
+ description: "object-form editing one `users` record: the fields come from the object's own metadata through `getObjectSchema`, the values from `findOne`, and `fields` / `columns` decide which of them appear and how wide the grid is.",
category: 'plugin-form',
+ tags: ["object-form", "objectql", "edit", "columns"],
},
- schema: plugin_form_basic_form,
+ schema: plugin_form_object_form_record,
},
- 'plugin-form/contact-form': {
- id: 'plugin-form/contact-form',
+ 'plugin-form/object-form-tabbed-sections': {
+ id: 'plugin-form/object-form-tabbed-sections',
meta: {
- title: "Contact Form",
- description: "",
+ title: "Object Form — Sections as Tabs",
+ description: "The same record with `formType: 'tabbed'`: the declared `sections` become tab panels of ONE form, so a single submit spans them and a tab the user leaves keeps its values.",
category: 'plugin-form',
+ tags: ["object-form", "tabbed", "sections", "objectql"],
},
- schema: plugin_form_contact_form,
+ schema: plugin_form_object_form_tabbed_sections,
},
'plugin-gantt/construction-project-phases': {
id: 'plugin-gantt/construction-project-phases',
diff --git a/examples/schema-catalog/src/schemas/plugin-form/basic-form.json b/examples/schema-catalog/src/schemas/components-form-form/basic-form.json
similarity index 100%
rename from examples/schema-catalog/src/schemas/plugin-form/basic-form.json
rename to examples/schema-catalog/src/schemas/components-form-form/basic-form.json
diff --git a/examples/schema-catalog/src/schemas/plugin-form/contact-form.json b/examples/schema-catalog/src/schemas/components-form-form/demo-request-form.json
similarity index 100%
rename from examples/schema-catalog/src/schemas/plugin-form/contact-form.json
rename to examples/schema-catalog/src/schemas/components-form-form/demo-request-form.json
diff --git a/examples/schema-catalog/src/schemas/plugin-form/object-form-record.json b/examples/schema-catalog/src/schemas/plugin-form/object-form-record.json
new file mode 100644
index 0000000000..7f0233776c
--- /dev/null
+++ b/examples/schema-catalog/src/schemas/plugin-form/object-form-record.json
@@ -0,0 +1,9 @@
+{
+ "type": "object-form",
+ "objectName": "users",
+ "mode": "edit",
+ "recordId": "1",
+ "columns": 2,
+ "fields": ["name", "email", "department"],
+ "submitText": "Save changes"
+}
diff --git a/examples/schema-catalog/src/schemas/plugin-form/object-form-tabbed-sections.json b/examples/schema-catalog/src/schemas/plugin-form/object-form-tabbed-sections.json
new file mode 100644
index 0000000000..cb6d7ba75b
--- /dev/null
+++ b/examples/schema-catalog/src/schemas/plugin-form/object-form-tabbed-sections.json
@@ -0,0 +1,13 @@
+{
+ "type": "object-form",
+ "objectName": "users",
+ "mode": "edit",
+ "recordId": "1",
+ "formType": "tabbed",
+ "defaultTab": "identity",
+ "sections": [
+ { "name": "identity", "label": "Identity", "columns": 2, "fields": ["name", "email"] },
+ { "name": "organisation", "label": "Organisation", "fields": ["department", "created_at"] }
+ ],
+ "submitText": "Save changes"
+}
diff --git a/examples/schema-catalog/test/catalog-gallery-render.test.tsx b/examples/schema-catalog/test/catalog-gallery-render.test.tsx
index 34ab74741f..0103ade28a 100644
--- a/examples/schema-catalog/test/catalog-gallery-render.test.tsx
+++ b/examples/schema-catalog/test/catalog-gallery-render.test.tsx
@@ -34,8 +34,10 @@
* components-disclosure-toggle-group 1 core-schema-renderer 1
*
* That is the red this file was written against. `apps/site/app/components/
- * registerCatalogBlocks.ts` now loads the nine further packages that census
- * resolves to, which takes 31 of those 33 tiles from the panel to a drawn
+ * registerCatalogBlocks.ts` loads the nine further packages that census
+ * resolved to — further packages have joined that list since, none of them
+ * from this census (see the host file's own header) — which takes 31 of those
+ * 33 tiles from the panel to a drawn
* component. What registration cannot reach is named — never skipped silently —
* in the tables below. Four classes were defects in the entries, one issue each:
*
@@ -138,6 +140,8 @@ import '@object-ui/plugin-map';
import '@object-ui/plugin-markdown';
import '@object-ui/plugin-timeline';
import '@object-ui/plugin-view';
+import '@object-ui/plugin-form';
+import '@object-ui/plugin-grid';
import { SidebarProvider } from '@object-ui/components';
import { registerLayout } from '@object-ui/layout';
import { ComponentRegistry } from '@object-ui/core';
@@ -172,6 +176,17 @@ const DATASOURCE_REQUIRED = 'DataSource required for object/api providers';
const ALL_DIAGNOSTICS = [UNKNOWN_COMPONENT, FAILED_TO_RENDER, DATASOURCE_REQUIRED];
+/**
+ * The placeholder every `@object-ui/plugin-form` container paints while it
+ * fetches (`ObjectForm.tsx:1092`, and the same string in `TabbedForm`,
+ * `SplitForm`, `WizardForm`, `DrawerForm`). NOT a diagnostic — it is a frame on
+ * the way to the tile — but `renderEntry` has to settle past it, so it is a
+ * literal here for the same reason the three above are: a reworded placeholder
+ * should turn this file red for review rather than silently stop being waited
+ * for.
+ */
+const FORM_LOADING = 'Loading form...';
+
/** The packages the gallery host must load, in the host's own order. */
const HOST_PACKAGES = [
'@object-ui/plugin-dashboard',
@@ -185,6 +200,8 @@ const HOST_PACKAGES = [
'@object-ui/plugin-markdown',
'@object-ui/plugin-timeline',
'@object-ui/plugin-view',
+ '@object-ui/plugin-form',
+ '@object-ui/plugin-grid',
];
/**
@@ -355,6 +372,12 @@ const WRAPPER_ELEMENTS = 2;
interface Rendered {
text: string;
elements: number;
+ /**
+ * The values of the tile's form controls. `textContent` cannot see them, and
+ * for a FORM that is exactly where the data it was given ends up — see
+ * `carriesFixtureRecord` below.
+ */
+ controlValues: string[];
unmount: () => void;
}
@@ -396,9 +419,22 @@ async function renderEntry(schema: unknown): Promise {
drewSomething(container.querySelectorAll('*').length, container.textContent ?? ''),
).toBe(true),
);
+ // A third shape (objectui#6167), and the one that makes the two above
+ // insufficient: a data-bound form holds its first paint behind
+ // `Loading form...` while it fetches the object schema and the record. That
+ // placeholder is 5 elements of real DOM, so `drewSomething` accepts it — and
+ // every assertion after it would then be measured against a frame that was
+ // never the tile. Measured: without this wait the `plugin-form` entries'
+ // DATA-provenance cases PASS in a whole-file run (the 570 renders before them
+ // leave the promises resolved) and FAIL when the file is run with `-t`, which
+ // is the definition of a reading that is not a measurement.
+ await waitFor(() => expect(container.textContent ?? '').not.toContain(FORM_LOADING));
return {
text: container.textContent ?? '',
elements: container.querySelectorAll('*').length,
+ controlValues: Array.from(
+ container.querySelectorAll('input, textarea, select'),
+ ).map((el) => (el as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement).value ?? ''),
unmount,
};
}
@@ -600,8 +636,8 @@ describe('objectui#4616 — every catalog entry renders in the docs gallery', ()
* The objectui#4600 separation, restated because objectui#4616 is exactly
* the change that would tempt someone to collapse it: the per-page demo
* hosts opt into their plugins through `PluginLoader`, and importing this
- * module from them would make every docs page carrying a demo load all
- * eleven graphs eagerly.
+ * module from them would make every docs page carrying a demo load every
+ * graph in that list eagerly.
*/
it.each(['InteractiveDemo.tsx', 'LiveSplitDemo.tsx'])(
'%s still does NOT import it (PluginLoader stays lazy there)',
@@ -658,6 +694,24 @@ describe('objectui#4616 — every catalog entry renders in the docs gallery', ()
* `dataSource.find`, not out of the entry's own JSON. This is
* objectui#5113's original second fact, unweakened.
*
+ * ## What "shows a record" means, after objectui#6167
+ *
+ * The provenance token is looked for in the tile's TEXT **and** in the values
+ * of its form CONTROLS, because those are two different places the same fact
+ * lands. A grid puts a row into text; a form puts the record into
+ * `input.value`, which `textContent` cannot see. Measured on the `plugin-form`
+ * entries this card added: `Alice Johnson` arrives as the value of
+ * `input[name]`, while the whole tile's text reads
+ * `NameEmailDepartmentCancelSave changes`. A text-only reading would have
+ * called a correctly bound form unbound — the blind-instrument failure, on the
+ * very half that does the work.
+ *
+ * The widening is a SUPERSET of the old reading, so it cannot turn a red entry
+ * green by relaxing anything; and `it('the provenance instrument still fails
+ * on a surface with no record behind it')` below is the control that it still
+ * discriminates — the same renderer in `create` mode paints the same controls
+ * with nothing in them, and fails.
+ *
* (3) is the strongest of the three and it does NOT generalize, for a reason
* that is a property of the entries rather than a gap in this file: the other
* eleven categories author their data INLINE (`data`, `staticData`, `messages`,
@@ -708,8 +762,9 @@ describe('objectui#4616 — every catalog entry renders in the docs gallery', ()
* `packages/components/src/renderers/form/form.tsx`, not by
* `@object-ui/plugin-form`, whose own keys are `object-form`,
* `embeddable-form`, `form-analytics` and `object-master-detail-form`. The two
- * `plugin-form` entries are therefore the third instance of the #5113 defect,
- * ledgered in `OWN_PLUGIN_DEBT` below against the card that owns them. An
+ * `plugin-form` entries were therefore the third instance of the #5113 defect,
+ * and were ledgered in `OWN_PLUGIN_DEBT` below until objectui#6167 rewrote them
+ * as real `object-form` nodes; the ledger is empty as of that card. An
* enumerated table would have inherited that mistake and reported them green.
*
* ## Why the set, not a single type
@@ -721,12 +776,17 @@ describe('objectui#4616 — every catalog entry renders in the docs gallery', ()
* least one type this package registers", which one conforming entry satisfies
* while every other entry in the category drifts.
*
- * The types are asserted to be REGISTERED separately from the render, because
- * the gallery's registration of `object-grid` is transitive: the host list
- * (`HOST_PACKAGES`) does not name `@object-ui/plugin-grid`, and the type
- * arrives because `@object-ui/plugin-view` imports `ObjectGrid` from it, which
- * runs that package's `register` calls. That is load-bearing and invisible, so
- * it gets its own assertion.
+ * The types are asserted to be REGISTERED separately from the render, because a
+ * type that resolves to nothing fails every case below with the OBJUI-001 panel
+ * rather than with anything about the entries. That separation was written for
+ * `object-grid`, which used to reach this registry ONLY because
+ * `@object-ui/plugin-view` imports `ObjectGrid` from `@object-ui/plugin-grid` —
+ * load-bearing and invisible. objectui#6025 made both `@object-ui/plugin-grid`
+ * and `@object-ui/plugin-form` declared imports of the gallery host, so
+ * resolution no longer rides on that component import. The transitive path
+ * still exists and still works, which is precisely why "is it registered"
+ * cannot judge whether the declaration is there — see
+ * `objectui#6025 — the gallery DECLARES the packages its entries need` below.
*
* ## No environment exclusions, and that is a result rather than an oversight
*
@@ -827,6 +887,16 @@ const FIXTURE_OBJECTS = new Set([USERS_SCHEMA.name]);
/** The record that exists ONLY in the fixture — the provenance token. */
const FIXTURE_ONLY_RECORD = USERS_ROWS[0].name;
+/**
+ * Did the fixture's record reach the DOM at all? TEXT **or** the value of a
+ * form control — see "What \"shows a record\" means" in the header. A form is
+ * the case that needs the second half: its record lands in `input.value`,
+ * where `textContent` cannot see it.
+ */
+const carriesFixtureRecord = (r: Rendered) =>
+ r.text.includes(FIXTURE_ONLY_RECORD) ||
+ r.controlValues.some((v) => v.includes(FIXTURE_ONLY_RECORD));
+
/** Does the entry bind to an object the fixture serves? Read off its own JSON. */
function bindsFixtureObject(node: unknown): boolean {
if (Array.isArray(node)) return node.some((n) => bindsFixtureObject(n));
@@ -845,19 +915,18 @@ function bindsFixtureObject(node: unknown): boolean {
* fix, and asserted below to STILL FAIL: an entry that starts conforming fails
* this file until its line is deleted, so the ledger cannot rot green.
*
+ * EMPTY as of objectui#6167, and that emptiness is an assertion rather than a
+ * dormant mechanism: the coverage-split case below pins `ledgered-debt` to `[]`,
+ * so the day an entry is added here the split moves and says so. The two lines
+ * it used to carry were `plugin-form/basic-form` and `plugin-form/contact-form`,
+ * deleted by that card together with the entries they described — the ledger is
+ * emptied by fixing entries, never by editing this list.
+ *
* ⛔ This is not an exemption list. Nothing may be added here to make a red
* turn green — a new entry that does not use its own plugin is a defect in that
* entry, and the fix is the entry.
*/
-const OWN_PLUGIN_DEBT: Record = {
- 'plugin-form/basic-form':
- 'objectui#6167 — authors `form`, which `packages/components` registers, not the ' +
- '`object-form` of `@object-ui/plugin-form`. The gallery host does not load ' +
- '`@object-ui/plugin-form` at all, so the fix spans the entries and ' +
- '`registerCatalogBlocks.ts`.',
- 'plugin-form/contact-form':
- 'objectui#6167 — same defect as `basic-form`; see that entry.',
-};
+const OWN_PLUGIN_DEBT: Record = {};
const pluginEntries = entries.filter((e) => PLUGIN_CATEGORIES.includes(e.meta.category));
const isDataBound = (e: (typeof pluginEntries)[number]) => bindsFixtureObject(e.schema);
@@ -894,6 +963,45 @@ describe('objectui#6024 — the derivation this pin is built on', () => {
expect(authoring).toEqual([]);
});
+ /**
+ * THE PROVENANCE INSTRUMENT STILL DISCRIMINATES (objectui#6167).
+ *
+ * `carriesFixtureRecord` reads text OR control values, which is a WIDER
+ * reading than the `r.text` one objectui#5113 wrote. A widened instrument has
+ * to be shown to still fail on the thing it is meant to catch, or the entries
+ * it newly admits are admitted by relaxation.
+ *
+ * The control is the `plugin-form` entries' own shape with ONE thing changed:
+ * a `recordId` the fixture does not serve. Same renderer, same object, same
+ * mode, same harness — so what it measures is the record and nothing else.
+ * Its controls still render, which is asserted first: without that, a form
+ * that painted nothing would satisfy the line below while measuring nothing.
+ *
+ * (`mode: 'create'` was tried first and is NOT usable here: it paints its
+ * shell before the object schema resolves, so `renderEntry` — which settles
+ * on "something was drawn" — returns with zero controls. The edit path holds
+ * its first paint until the fetch lands, which is why this shape settles.)
+ */
+ it('the provenance instrument still fails on a surface with no record behind it', async () => {
+ const r = await renderEntry({
+ type: 'object-form',
+ objectName: 'users',
+ mode: 'edit',
+ recordId: 'no-such-record',
+ fields: ['name', 'email', 'department'],
+ });
+ try {
+ expect(
+ r.controlValues.length,
+ 'the control form rendered no controls at all, so the assertion below would ' +
+ 'pass without measuring anything',
+ ).toBeGreaterThan(0);
+ expect(carriesFixtureRecord(r)).toBe(false);
+ } finally {
+ teardown(r);
+ }
+ });
+
/**
* THE SPLIT, STATED BY THE GATE ITSELF. Breadth is not depth: this case is
* what stops "the pin covers all thirteen categories" from being read as
@@ -924,8 +1032,11 @@ describe('objectui#6024 — the derivation this pin is built on', () => {
categories: 13,
entries: 41,
// The strong half — the tile shows a record only the fixture holds.
- // These are the two categories whose entries bind to `users`.
- 'structure+mount+data': { categories: ['plugin-grid', 'plugin-view'], entries: 5 },
+ // These are the three categories whose entries bind to `users`.
+ 'structure+mount+data': {
+ categories: ['plugin-form', 'plugin-grid', 'plugin-view'],
+ entries: 7,
+ },
// The other eleven author their data inline, so no fixture-only record
// can reach their tiles. They carry STRUCTURE and MOUNT, and nothing here
// claims otherwise.
@@ -945,7 +1056,9 @@ describe('objectui#6024 — the derivation this pin is built on', () => {
entries: 34,
},
// Open defects with an owning card, NOT exemptions. See OWN_PLUGIN_DEBT.
- 'ledgered-debt': ['plugin-form/basic-form', 'plugin-form/contact-form'],
+ // EMPTY as of objectui#6167 — every plugin category is now held to the
+ // rule, and adding a line to the ledger moves this literal.
+ 'ledgered-debt': [],
});
});
@@ -1018,12 +1131,18 @@ describe.each(PLUGIN_CATEGORIES)(
if (dataBound.length > 0) {
it.each(dataBound.map((e) => [e.id, e.schema] as const))(
'%s puts data from the gallery data source on screen',
- async (_id, schema) => {
+ async (id, schema) => {
const r = await renderEntry(schema);
try {
// Not authored anywhere in the catalog — it exists only in the
- // fixture, which the case above pins.
- expect(r.text).toContain(FIXTURE_ONLY_RECORD);
+ // fixture, which the case above pins. Read out of the text OR a
+ // control's value: a grid shows it, a form holds it.
+ expect(
+ carriesFixtureRecord(r),
+ `${id} renders with ${FIXTURE_ONLY_RECORD} nowhere in it — not in the tile's ` +
+ "text and not in a form control's value — so nothing on screen came through " +
+ 'the registered renderer → `dataSource.find`.',
+ ).toBe(true);
} finally {
teardown(r);
}
@@ -1033,6 +1152,60 @@ describe.each(PLUGIN_CATEGORIES)(
},
);
+/**
+ * DECLARED, NOT TRANSITIVE (objectui#6025).
+ *
+ * ## Why this is not the same question as "is the type registered"
+ *
+ * `object-grid` resolved in this registry long before the host declared it, and
+ * it still would if the declaration were deleted: `@object-ui/plugin-view`
+ * imports `ObjectGrid` from `@object-ui/plugin-grid`
+ * (`packages/plugin-view/src/ObjectView.tsx:37`), and importing that entry runs
+ * its `register` calls. The same is true of `@object-ui/plugin-form` one line
+ * below it. Measured on this card's merge-base, importing exactly the eleven
+ * packages the host then carried: `object-grid`, `object-form`,
+ * `plugin-form:object-form`, `embeddable-form`, `form-analytics`,
+ * `object-master-detail-form`, `record:line_items`, `view:form` and
+ * `import-wizard` ALL resolved.
+ *
+ * So `expect(ComponentRegistry.get('object-grid')).toBeTruthy()` — which this
+ * file does assert, for its own reason, in every category's own case — passes
+ * in the declared world and in the transitive one alike. For THIS question it
+ * is a ghost: an assertion that cannot fail in either world.
+ *
+ * What is asserted here instead is the host's DECLARATION, and it is a real
+ * judge because of the case it leans on: the parity case above ties
+ * `HOST_PACKAGES` to the literal import list in `registerCatalogBlocks.ts`.
+ * Remove the import from the host and parity reds; remove it from both and this
+ * case reds. No transitive import can satisfy either, because neither reads the
+ * registry at all.
+ *
+ * The rule is derived, not enumerated: every `plugin-*` CATEGORY that has
+ * catalog entries names a package the gallery renders through, so the host must
+ * load that package by name. A new plugin category with entries picks the
+ * requirement up with no edit here.
+ */
+describe('objectui#6025 — the gallery DECLARES the packages its entries need', () => {
+ it('every plugin category with catalog entries is loaded BY NAME by the host', () => {
+ expect(
+ PLUGIN_CATEGORIES.length,
+ 'no plugin categories were derived, so this case would be vacuous',
+ ).toBeGreaterThanOrEqual(13);
+
+ const undeclared = PLUGIN_CATEGORIES.map((category) => `@object-ui/${category}`).filter(
+ (pkg) => !HOST_PACKAGES.includes(pkg),
+ );
+ expect(
+ undeclared,
+ 'these packages register the types their own category’s entries author, and the ' +
+ 'gallery host does not name them. Whether the types resolve anyway through some ' +
+ 'other package’s component import is a different question and not this one — add ' +
+ 'the side-effect import to `apps/site/app/components/registerCatalogBlocks.ts` and ' +
+ 'mirror it in `HOST_PACKAGES`.',
+ ).toEqual([]);
+ });
+});
+
/**
* HOST PARITY for the fixture, same technique and same reason as the
* registration-set parity above: the mirror at the top of this file is what
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b053973ace..4405a3fae7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -680,9 +680,15 @@ importers:
'@object-ui/plugin-editor':
specifier: workspace:*
version: link:../../packages/plugin-editor
+ '@object-ui/plugin-form':
+ specifier: workspace:*
+ version: link:../../packages/plugin-form
'@object-ui/plugin-gantt':
specifier: workspace:*
version: link:../../packages/plugin-gantt
+ '@object-ui/plugin-grid':
+ specifier: workspace:*
+ version: link:../../packages/plugin-grid
'@object-ui/plugin-kanban':
specifier: workspace:*
version: link:../../packages/plugin-kanban