diff --git a/.changeset/27130-gantt-api-source-not-taught.md b/.changeset/27130-gantt-api-source-not-taught.md new file mode 100644 index 00000000000..e551a1ee863 --- /dev/null +++ b/.changeset/27130-gantt-api-source-not-taught.md @@ -0,0 +1,12 @@ +--- +'@object-ui/plugin-gantt': patch +--- + +docs(plugin-gantt): the `object-gantt` registration no longer teaches the retired view-level `api` source + +Clause-②: no +Metadata-protocol: none + +- **Before.** The `data` input said `{ provider: 'api', read }` "reads through that request", and the `filter` input listed "an `api` request" among the record sources the gantt reads. The spec refuses that provider by name and the gantt has no reader for it: a gantt carrying it requests neither URL. +- **Now.** `data` names the two providers the gantt reads, `{ provider: 'value', items }` and `{ provider: 'object', object }`, and says that rows a server computes reach the gantt as an object whose plugin driver computes them, never as a request URL in the view. `filter` names the object and inline rows. +- **Unchanged.** Text only: no input, schema, renderer behaviour or export moves. The page element's `dataSource: { provider: 'api', read, path? }` binding is a different door and is untouched. diff --git a/content/docs/ui/components/plugins.mdx b/content/docs/ui/components/plugins.mdx index c096d469b34..74f5bf31a0c 100644 --- a/content/docs/ui/components/plugins.mdx +++ b/content/docs/ui/components/plugins.mdx @@ -199,9 +199,9 @@ Namespace `plugin-gantt`. | :--- | :--- | :--- | :--- | | `objectName` | `string` | | ObjectQL object name. The record source is one of `data`, `staticData` and `objectName`; the `object-gantt` schema refuses a block that declares none of them, unless the node's `dataSource.object` names the object, which then lands on this key. | | `gantt` | `object` | | Gantt-timeline configuration, the author face — the same block `ListViewSchema.gantt` declares, and the one the renderer validates this node against. Taken WHOLE when present: the flat top-level spelling beside it is ignored. The spec requires `startDateField`, `endDateField` and `titleField`, which name the record fields a task's bar starts at, ends at and is titled by; the other members (`progressField`, `colorField`, `dependenciesField`, `parentField`, `viewMode`, `exportFileName`, …) are optional. | -| `data` | `object` | | A `{ provider, … }` data-source configuration, read FIRST on the record-source ladder: a gantt carrying one never reaches `staticData` and never queries `objectName`. `{ provider: 'value', items }` charts those rows, `{ provider: 'object', object }` queries that object and `{ provider: 'api', read }` reads through that request. A bare array is not this key’s shape and is not a record source: the gantt falls through to `staticData`, then `objectName`, so inline rows belong under `staticData`. | +| `data` | `object` | | A `{ provider, … }` data-source configuration, read FIRST on the record-source ladder: a gantt carrying one never reaches `staticData`. The gantt reads two providers: `{ provider: 'value', items }` charts those rows and never queries `objectName`, and `{ provider: 'object', object }` queries that object in place of `objectName`. Rows a server computes reach the gantt as an object whose plugin driver computes them, never as a request URL in the view. A bare array is not this key’s shape and is not a record source: the gantt falls through to `staticData`, then `objectName`, so inline rows belong under `staticData`. | | `staticData` | `array` | | Inline records, read SECOND on the record-source ladder: a `data` configuration wins and this key is then never reached, while `objectName` is read AFTER it, so a gantt carrying both charts these rows and never queries that object. `filter` and `sort` narrow and order these rows exactly as they do fetched ones. | -| `filter` | `array` | | Base query filter in the rule-array form `[{ field, operator, value }, ...]`, narrowing the tasks the gantt charts — from every record source it reads: the object, an `api` request, or inline rows. Context tokens such as `{current_user_id}` are resolved first, then the filter is lowered to `$filter` on the query. The MongoDB-style record form is not this key’s shape. | +| `filter` | `array` | | Base query filter in the rule-array form `[{ field, operator, value }, ...]`, narrowing the tasks the gantt charts — from every record source it reads: the object or inline rows. Context tokens such as `{current_user_id}` are resolved first, then the filter is lowered to `$filter` on the query. The MongoDB-style record form is not this key’s shape. | | `sort` | `array` | | Task order in `[{ field, order }]` form, ordering the rows the gantt charts. Lowered to `$orderby` on the same query. | | `navigation` | `object` | | What a task click opens — the `{ mode, size, openNewTab, preventNavigation }` block a list view declares. With the key ABSENT a click opens the task's record in a drawer, this renderer's own default. `mode` is `drawer` (the overlay), `new_window`, `page` or `none`, and a block written without `mode` takes the spec's `drawer` default. `drawer` opens the task's record in the drawer; `none` opens nothing. `page` opens the task's record page in the same tab and `new_window` opens it in a new tab. The gantt builds that address itself from the page it is on and does not use a record navigator the host publishes, so a host that publishes one changes nothing: on the object's own list or view route (`…/OBJECT/view/VIEW`) the address is the record page `…/OBJECT/record/ID`, while on any other page, such as a custom page, `/OBJECT/record/ID` is appended to the current address. The object is `data.object` when `data` is the object provider, else `objectName`; on inline rows that name neither there is no record to open, and every mode, the default drawer included, opens nothing. `preventNavigation: true` opens nothing whatever the mode, `openNewTab: true` opens the record page in a new tab and outranks every mode except `none`, and `size` sets the overlay width. | | `label` | `string` \| `object` | | Gantt label — the second link of the exported PNG/PDF file-name chain, after `gantt.exportFileName` and before the bound object's own label. A string is used as written, and an inline locale map (`{ en, "zh-CN", … }`) gives the entry for the display locale. The chart draws it nowhere else. | diff --git a/scripts/sdui-manifest.record.json b/scripts/sdui-manifest.record.json index 47e64df6113..840bca1d7a5 100644 --- a/scripts/sdui-manifest.record.json +++ b/scripts/sdui-manifest.record.json @@ -14,7 +14,7 @@ "source": "built-tree", "generator": "scripts/gen-sdui-manifest-node.mjs", "generatedAt": "2026-10-29", - "sha256": "2ec5933ffaca5ab9cd1767f6d53c0e7eddb3880627c00c3275f71ed03253f3b8", + "sha256": "776bef14f20193239ad50cb76769e3dc366456005e907593041ebb5266c9f444", "components": 106, "inTree": { "modulesRoot": "ui/apps/console", diff --git a/sdui.manifest.json b/sdui.manifest.json index dc63ae89dda..cff4842077a 100644 --- a/sdui.manifest.json +++ b/sdui.manifest.json @@ -1004,7 +1004,7 @@ { "name": "data", "type": "object", - "description": "A `{ provider, … }` data-source configuration, read FIRST on the record-source ladder: a gantt carrying one never reaches `staticData` and never queries `objectName`. `{ provider: 'value', items }` charts those rows, `{ provider: 'object', object }` queries that object and `{ provider: 'api', read }` reads through that request. A bare array is not this key’s shape and is not a record source: the gantt falls through to `staticData`, then `objectName`, so inline rows belong under `staticData`." + "description": "A `{ provider, … }` data-source configuration, read FIRST on the record-source ladder: a gantt carrying one never reaches `staticData`. The gantt reads two providers: `{ provider: 'value', items }` charts those rows and never queries `objectName`, and `{ provider: 'object', object }` queries that object in place of `objectName`. Rows a server computes reach the gantt as an object whose plugin driver computes them, never as a request URL in the view. A bare array is not this key’s shape and is not a record source: the gantt falls through to `staticData`, then `objectName`, so inline rows belong under `staticData`." }, { "name": "staticData", @@ -1014,7 +1014,7 @@ { "name": "filter", "type": "array", - "description": "Base query filter in the rule-array form `[{ field, operator, value }, ...]`, narrowing the tasks the gantt charts — from every record source it reads: the object, an `api` request, or inline rows. Context tokens such as `{current_user_id}` are resolved first, then the filter is lowered to `$filter` on the query. The MongoDB-style record form is not this key’s shape." + "description": "Base query filter in the rule-array form `[{ field, operator, value }, ...]`, narrowing the tasks the gantt charts — from every record source it reads: the object or inline rows. Context tokens such as `{current_user_id}` are resolved first, then the filter is lowered to `$filter` on the query. The MongoDB-style record form is not this key’s shape." }, { "name": "sort", diff --git a/ui/packages/plugin-gantt/src/__tests__/apiSourceNotTaught-27130.test.ts b/ui/packages/plugin-gantt/src/__tests__/apiSourceNotTaught-27130.test.ts new file mode 100644 index 00000000000..5152b561aaf --- /dev/null +++ b/ui/packages/plugin-gantt/src/__tests__/apiSourceNotTaught-27130.test.ts @@ -0,0 +1,58 @@ +/** + * 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. + */ + +/** + * objectstack#27130 — the `object-gantt` registration teaches no view-level + * `api` record source. + * + * The view-level `api` data source is retired in `@objectstack/spec` + * (objectstack#23082): the spec refuses `data: { provider: 'api', … }` by name, + * and `resolveDataSource` has no `api` arm, so a gantt still carrying one never + * requests its `read` / `write` URLs (behaviour pinned by + * `recordSourceInputs-10394.test.tsx`, its "retired api block" row). The + * registration's `data` and `filter` descriptions still named it as a live + * record source, and that published text is what an author copied. Rows a + * server computes reach the gantt as an object behind a plugin driver. + * + * The pin reads the registered inputs, which feed `sdui.manifest.json` and the + * generated component reference, so one row covers all three. + */ + +import { describe, it, expect } from 'vitest'; +import { ComponentRegistry } from '@object-ui/core'; +// Module scope, not a hook: this import IS the registration (AGENTS.md +// test-discipline section). +import '../index'; + +/** `provider: 'api'` in any quoting, or an `api` request/source named in code font. */ +const TEACHES_API_SOURCE = /provider:\s*\\?['"]api\\?['"]|`api`/; + +const descriptions = (): Array<[string, string]> => + (((ComponentRegistry.getConfig('object-gantt', 'plugin-gantt') as any)?.inputs ?? []) as any[]).map( + (input) => [input.name, String(input.description ?? '')], + ); + +describe('objectstack#27130 — object-gantt teaches no retired api record source', () => { + it('control: the matcher fires on the text it replaced', () => { + expect("`{ provider: \\'api\\', read }` reads through that request").toMatch(TEACHES_API_SOURCE); + expect("`{ provider: 'api', read }` reads through that request").toMatch(TEACHES_API_SOURCE); + expect('the object, an `api` request, or inline rows').toMatch(TEACHES_API_SOURCE); + }); + + it('no input description names a provider: api record source', () => { + const rows = descriptions(); + expect(rows.map(([name]) => name)).toEqual(expect.arrayContaining(['data', 'filter'])); + expect(rows.filter(([, text]) => TEACHES_API_SOURCE.test(text)).map(([name]) => name)).toEqual([]); + }); + + it('positive control: data still names the object and value providers', () => { + const data = new Map(descriptions()).get('data') ?? ''; + expect(data).toContain("`{ provider: 'object', object }`"); + expect(data).toContain("`{ provider: 'value', items }`"); + }); +}); diff --git a/ui/packages/plugin-gantt/src/index.tsx b/ui/packages/plugin-gantt/src/index.tsx index b3fac7cc2cf..4d07512dbf0 100644 --- a/ui/packages/plugin-gantt/src/index.tsx +++ b/ui/packages/plugin-gantt/src/index.tsx @@ -182,9 +182,9 @@ ComponentRegistry.register('object-gantt', ObjectGanttRenderer, { inputs: [ { name: 'objectName', type: 'string', description: 'ObjectQL object name. The record source is one of `data`, `staticData` and `objectName`; the `object-gantt` schema refuses a block that declares none of them, unless the node\'s `dataSource.object` names the object, which then lands on this key.' }, { name: 'gantt', type: 'object', description: 'Gantt-timeline configuration, the author face — the same block `ListViewSchema.gantt` declares, and the one the renderer validates this node against. Taken WHOLE when present: the flat top-level spelling beside it is ignored. The spec requires `startDateField`, `endDateField` and `titleField`, which name the record fields a task\'s bar starts at, ends at and is titled by; the other members (`progressField`, `colorField`, `dependenciesField`, `parentField`, `viewMode`, `exportFileName`, …) are optional.' }, - { name: 'data', type: 'object', description: 'A `{ provider, … }` data-source configuration, read FIRST on the record-source ladder: a gantt carrying one never reaches `staticData` and never queries `objectName`. `{ provider: \'value\', items }` charts those rows, `{ provider: \'object\', object }` queries that object and `{ provider: \'api\', read }` reads through that request. A bare array is not this key’s shape and is not a record source: the gantt falls through to `staticData`, then `objectName`, so inline rows belong under `staticData`.' }, + { name: 'data', type: 'object', description: 'A `{ provider, … }` data-source configuration, read FIRST on the record-source ladder: a gantt carrying one never reaches `staticData`. The gantt reads two providers: `{ provider: \'value\', items }` charts those rows and never queries `objectName`, and `{ provider: \'object\', object }` queries that object in place of `objectName`. Rows a server computes reach the gantt as an object whose plugin driver computes them, never as a request URL in the view. A bare array is not this key’s shape and is not a record source: the gantt falls through to `staticData`, then `objectName`, so inline rows belong under `staticData`.' }, { name: 'staticData', type: 'array', description: 'Inline records, read SECOND on the record-source ladder: a `data` configuration wins and this key is then never reached, while `objectName` is read AFTER it, so a gantt carrying both charts these rows and never queries that object. `filter` and `sort` narrow and order these rows exactly as they do fetched ones.' }, - { name: 'filter', type: 'array', description: 'Base query filter in the rule-array form `[{ field, operator, value }, ...]`, narrowing the tasks the gantt charts — from every record source it reads: the object, an `api` request, or inline rows. Context tokens such as `{current_user_id}` are resolved first, then the filter is lowered to `$filter` on the query. The MongoDB-style record form is not this key’s shape.' }, + { name: 'filter', type: 'array', description: 'Base query filter in the rule-array form `[{ field, operator, value }, ...]`, narrowing the tasks the gantt charts — from every record source it reads: the object or inline rows. Context tokens such as `{current_user_id}` are resolved first, then the filter is lowered to `$filter` on the query. The MongoDB-style record form is not this key’s shape.' }, { name: 'sort', type: 'array', description: 'Task order in `[{ field, order }]` form, ordering the rows the gantt charts. Lowered to `$orderby` on the same query.' }, { name: 'navigation', type: 'object', description: OBJECT_GANTT_NAVIGATION_DESCRIPTION }, { name: 'label', type: ['string', 'object'], description: 'Gantt label — the second link of the exported PNG/PDF file-name chain, after `gantt.exportFileName` and before the bound object\'s own label. A string is used as written, and an inline locale map (`{ en, "zh-CN", … }`) gives the entry for the display locale. The chart draws it nowhere else.' },