@object-ui/plugin-gantt 17.6.0 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,7 +19,7 @@ Gantt chart plugin for Object UI - Visualize project timelines and task dependen
19
19
  ### Create / Edit / Delete / View
20
20
 
21
21
  When used through `ObjectGantt` (the wiring the framework uses for the
22
- `gantt` view type) the full CRUD lifecycle is wired automatically:
22
+ `gantt` stored view type) the full CRUD lifecycle is wired automatically:
23
23
 
24
24
  - **Create** — click the toolbar "+ New Task" button. A small dialog opens
25
25
  pre-filled with start/end (today → +7 days). On submit the component calls
@@ -44,26 +44,70 @@ When used through `ObjectGantt` (the wiring the framework uses for the
44
44
  fetched by `DetailView` itself when `dataSource.getObjectSchema` is
45
45
  available).
46
46
 
47
- Override by setting `navigation` on the schema, e.g.
48
- `{ mode: 'page', basePath: '/console/apps/.../campaign' }` to route
49
- to the standalone detail page instead.
47
+ Override it with `navigation` in the `properties` bag — the spec's
48
+ `NavigationConfig`, `{ mode, size, openNewTab, preventNavigation }`. What a
49
+ task click does with each member, measured through the real `SchemaRenderer`
50
+ (objectui#11168 slice 4):
51
+
52
+ - **absent** — the drawer above, this renderer's own default;
53
+ - `drawer`, `modal`, `popover` — the task's record in that overlay; `split` —
54
+ the record beside the chart, which stays drawn;
55
+ - `page`, and a block written without `mode` (the spec's `page` default) —
56
+ the record page in the same tab; `new_window` — the same page in a new tab;
57
+ - `none` — nothing; `preventNavigation: true` — nothing, whatever the mode;
58
+ `openNewTab: true` — the record page in a new tab, outranking every mode
59
+ except `none`; `size` — the overlay width.
60
+
61
+ ```json
62
+ { "type": "object-gantt", "properties": { "objectName": "project_task", "gantt": { "startDateField": "start_date", "endDateField": "end_date", "titleField": "name" }, "navigation": { "mode": "page" } } }
63
+ ```
64
+
65
+ The record-page address is the gantt's own. It derives it from the page it
66
+ is on and does not use a record navigator the host publishes
67
+ (objectui#11293), so mounting one changes nothing. On the object's own list
68
+ or view route (`…/project_task/view/all`) the address is the record page
69
+ `…/project_task/record/ID`; on any other page, such as a custom page, the
70
+ gantt appends `/project_task/record/ID` to the current address. The object is
71
+ `data.object` when `data` is the object provider, else `objectName`; on
72
+ inline rows that name neither, no mode opens anything, the default drawer
73
+ included.
74
+
75
+ *Which* detail layout opens is not authorable here: assign a `record` page
76
+ to the object and let `isDefault` pick the one that opens. Page assignment
77
+ is what resolves a detail layout; this block only decides **how** that
78
+ detail is surfaced. `navigation`'s schema refuses any key it does not
79
+ declare: an undeclared key rejects the whole config, so the `mode` beside it
80
+ never takes effect either. `@objectstack/spec`'s `NavigationConfigSchema`
81
+ owns the member list.
50
82
 
51
83
 
52
84
  ### Drag-and-drop rescheduling
53
85
 
54
86
  When the renderer is used through `ObjectGantt` (the standard wiring used by
55
- the framework's `gantt` view type) drag is enabled automatically: each bar
87
+ the framework's `gantt` stored view type) drag is enabled automatically: each bar
56
88
  shows a grab cursor; the body drags the entire task, and the two thin edge
57
89
  zones (≈6px) resize start or end. Pointer motion snaps to whole days using
58
- the current column width. On release `ObjectGantt` issues an optimistic local
59
- patch and a `dataSource.update(objectName, recordId, { [startDateField]: …,
90
+ the current column width. A move shifts the start and the end by the same
91
+ number of calendar days and keeps each one's time of day, so across a
92
+ daylight-saving change a day task keeps its length in days and a 10:00 start
93
+ stays at 10:00; dragging a summary moves every task beneath it the same way.
94
+ With shift bands configured (`timeSegments`), a day-view drag moves by bands
95
+ instead and keeps the bar's elapsed length. On release `ObjectGantt` issues an
96
+ optimistic local patch and a `dataSource.update(objectName, recordId, { [startDateField]: …,
60
97
  [endDateField]: … })`. If the request fails the local state is reverted.
61
98
 
62
99
  When you embed the lower-level `<GanttView>` directly, pass `onTaskUpdate`
63
100
  to opt in:
64
101
 
65
102
  ```tsx
66
- <GanttView
103
+ import { GanttView } from '@object-ui/plugin-gantt';
104
+ import type { GanttTask } from '@object-ui/plugin-gantt';
105
+
106
+ // `tasks` is whatever you loaded; `save` stands in for your own write path.
107
+ declare const tasks: GanttTask[];
108
+ declare const save: (id: GanttTask['id'], changes: { start: Date; end: Date }) => void;
109
+
110
+ const editableChart = <GanttView
67
111
  tasks={tasks}
68
112
  onTaskUpdate={(task, { start, end }) => {
69
113
  // `changes` is a Partial<Pick<GanttTask, 'title' | 'start' | 'end' | 'progress'>>:
@@ -73,7 +117,7 @@ to opt in:
73
117
  if (!start || !end) return;
74
118
  save(task.id, { start, end });
75
119
  }}
76
- />
120
+ />;
77
121
  ```
78
122
 
79
123
  ## Installation
@@ -92,14 +136,19 @@ import '@object-ui/plugin-gantt';
92
136
 
93
137
  // Now you can use gantt types in your schemas.
94
138
  // The gantt is RECORD-DRIVEN: it names a data source and the fields to read.
95
- // It does not take a task array — see "Schema API" below.
139
+ // It does not take a task array — see "Schema API" below. Its props go in the
140
+ // node's `properties` bag, and its field mapping in the bag's `gantt` block.
96
141
  const schema = {
97
- type: 'gantt',
98
- objectName: 'project_tasks',
99
- titleField: 'name',
100
- startDateField: 'start_date',
101
- endDateField: 'end_date',
102
- progressField: 'completion_percentage'
142
+ type: 'object-gantt',
143
+ properties: {
144
+ objectName: 'project_tasks',
145
+ gantt: {
146
+ titleField: 'name',
147
+ startDateField: 'start_date',
148
+ endDateField: 'end_date',
149
+ progressField: 'completion_percentage'
150
+ }
151
+ }
103
152
  };
104
153
  ```
105
154
 
@@ -114,11 +163,27 @@ claim these schema types:
114
163
  | Schema `type` | Namespaced key | Renderer |
115
164
  | --- | --- | --- |
116
165
  | `object-gantt` | `plugin-gantt:object-gantt` | `ObjectGanttRenderer` |
117
- | `gantt` | `view:gantt` | `ObjectGanttRenderer` |
118
166
 
119
- Both spellings resolve — `register` stores the namespaced key *and* a bare-`type`
120
- fallback. Both keys declare the same two inputs: `objectName` (required) and the
121
- `gantt` configuration object.
167
+ Both spellings of the surviving key resolve — `register` stores the namespaced key
168
+ *and* a bare-`type` fallback. It declares four inputs: `objectName`, the `gantt`
169
+ configuration object, and the two other record sources, `data` and `staticData`.
170
+ `objectName` is not a required input: the record source is one of `data`,
171
+ `staticData` and `objectName` (read in that order), and the `object-gantt` schema
172
+ refuses a block that declares none of them, unless the node's `dataSource.object`
173
+ names the object, which the registration then lands on `objectName`. `data` is a
174
+ `{ provider, … }` data-source configuration, never a bare array; inline rows go
175
+ under `staticData`.
176
+
177
+ > **The bare `gantt` key is retired** (objectui#8008, ruled 2026-09-09). This
178
+ > table used to carry a second row, `gantt` / `view:gantt`, on the same renderer.
179
+ > The registry accepted both spellings while the published declaration admitted
180
+ > only one — `ObjectGanttSchema.type` is the literal `'object-gantt'` — so an
181
+ > author who annotated their node could not write the key the registry took.
182
+ > `object-gantt` is now the one spelling.
183
+ >
184
+ > ⚠️ The **stored view type** `"gantt"` — what a saved `listViews[].type` holds —
185
+ > is a **different layer and is unchanged**. `ObjectView` maps a stored `gantt`
186
+ > view onto the `object-gantt` node type, so no saved view moves.
122
187
 
123
188
  `ObjectGanttRenderer` is a thin wrapper: it pulls `dataSource` off the renderer
124
189
  context and hands the schema to `ObjectGantt`.
@@ -130,7 +195,7 @@ The package exports components, helpers and their types — not a registry map:
130
195
  ```typescript
131
196
  import {
132
197
  ObjectGantt, // ObjectQL-integrated gantt: loads records, writes back edits
133
- ObjectGanttRenderer, // the registered renderer for `object-gantt` / `gantt`
198
+ ObjectGanttRenderer, // the registered renderer for `object-gantt`
134
199
  GanttView, // the standalone timeline component
135
200
  QuickFilterBar, // the toolbar's quick-filter dropdowns
136
201
  ResourceWorkload, // resource × period workload grid
@@ -191,6 +256,16 @@ The gantt node is **record-driven**: it names *where records come from* and
191
256
  objects further down this page are the component's runtime shape, produced by
192
257
  `ObjectGantt` from each record.
193
258
 
259
+ An authored node takes its props in its `properties` bag, whose members are
260
+ `@objectstack/spec`'s `ComponentPropsMap['object-gantt']` row, and its field
261
+ mapping in the bag's `gantt` block. `objectui validate` judges the bag against
262
+ that row and refuses a prop written flat on the node by name, naming where it
263
+ goes (`Did you mean objectName → properties.objectName?`, or
264
+ `startDateField → properties.gantt.startDateField`), as the spec's own page
265
+ component does (objectui#10859). `SchemaRenderer` hoists the bag onto the node
266
+ before `ObjectGantt` runs, so the reads below are the same for both: the
267
+ renderer sees the hoisted node.
268
+
194
269
  `ObjectGantt` decides what to render from exactly two reads
195
270
  (`src/ObjectGantt.tsx`):
196
271
 
@@ -199,58 +274,94 @@ that is present wins; if none is present there is no data config at all and the
199
274
  chart renders empty:
200
275
 
201
276
  ```typescript
202
- {
203
- type: 'gantt',
204
-
205
- // Pick ONE of the three:
206
- objectName: 'project_tasks', // load through the host DataSource
207
- // data: { provider: 'value', items: [ /* records */ ] }, // inline records
208
- // staticData: [ /* records */ ], // shorthand for the above
209
- }
277
+ const recordSource = {
278
+ type: 'object-gantt',
279
+ properties: {
280
+ // Pick ONE of the three:
281
+ objectName: 'project_tasks', // load through the host DataSource
282
+ // data: { provider: 'value', items: [ /* records */ ] }, // inline records
283
+ // staticData: [ /* records */ ], // shorthand for the above
284
+ },
285
+ // …or name the object in the node's `dataSource` binding instead of `objectName`:
286
+ // dataSource: { object: 'project_tasks' },
287
+ };
210
288
  ```
211
289
 
212
- `data` is the spec's `ViewData` union — `{ provider: 'object', object }`,
213
- `{ provider: 'value', items }`, `{ provider: 'api', read, write }` or
214
- `{ provider: 'schema', schemaId }`.
290
+ `data` is the spec's `ViewData` union. This chart reads two of its arms:
291
+ `{ provider: 'object', object }` and `{ provider: 'value', items }`.
292
+ `{ provider: 'schema', schemaId }` is accepted by the spec and is not a source
293
+ this chart reads: measured, a gantt carrying it queries `objectName` instead.
294
+ The view-level `{ provider: 'api', read, write }` arm is retired in the spec
295
+ (objectstack#23082) and nothing here reads it either: a gantt still carrying
296
+ one is answered the same way, and its `read` / `write` URLs are never
297
+ requested. A metadata-declared API call is the page element's `dataSource`
298
+ binding.
299
+
300
+ **The provider does not change which query keys apply.** An authored `filter`
301
+ and `sort` narrow and order the rows on **every** provider, inline ones
302
+ included, and the platform row ceiling (2,000 drawn rows, with a footnote
303
+ naming both numbers) applies to all of them — inline rows cost the browser what
304
+ fetched rows cost. Inline rows go through the same in-memory adapter the other
305
+ providers go through, so `filter` is evaluated with the same matcher and the
306
+ ceiling is applied to the **filtered** set, never to the raw one. Before
307
+ objectui#8769 the inline provider skipped that query and drew every authored
308
+ row with an authored `filter` silently dropped.
309
+
310
+ The same holds for the full-text pair: `search` is sent as `$search`, and
311
+ `searchableFields` as `$searchFields` alongside it (never without a term). A
312
+ list view's toolbar Search box writes both onto the gantt node it composes — the
313
+ chart runs its own query, so the node is the only way the term reaches it. They
314
+ are not authored keys: the spec's row does not declare them, so `objectui
315
+ validate` refuses them in the bag.
215
316
 
216
317
  **2. How the fields map — `getGanttConfig`.** Two spellings, checked in order.
217
- Top-level keys are used **only when `startDateField` and `endDateField` are both
218
- present**; otherwise the whole `gantt` block is read instead:
318
+ The **`gantt` block wins whenever it is present**, and it is taken WHOLE — the
319
+ flat top-level keys are not merged into it. The flat spelling is read only when
320
+ there is no `gantt` block, and then only when `startDateField` and
321
+ `endDateField` are both present. A node carrying both spellings renders the
322
+ block's values and gets a dev-mode warning naming the ignored top-level keys.
323
+ **Author the `gantt` block**: the flat spelling is the runtime handoff
324
+ `ObjectView` and `ListView` write onto the node they compose, and an authored
325
+ node cannot carry it — `objectui validate` refuses each flat key and names
326
+ `properties.gantt.KEY`, and the spec's row refuses it inside the bag too.
327
+
328
+ Precedence follows the maintainer ruling on objectui#5018 (2026-08-17), which
329
+ settled the identical two-faces shape for `plugin-map`; objectui#6469 inherited
330
+ it here. Before that flip the flat branch returned first, so an authored `gantt`
331
+ block was discarded silently.
219
332
 
220
333
  ```typescript
221
- {
222
- type: 'gantt',
223
- objectName: 'project_tasks',
224
-
225
- // (a) flat spelling — requires BOTH date fields to be taken
226
- startDateField: 'start_date',
227
- endDateField: 'end_date',
228
- titleField: 'name', // defaults to 'name'
229
- progressField: 'completion_percentage',
230
- dependenciesField: 'dependent_task_ids',
231
- colorField: 'bar_color',
232
- parentField: 'parent_task',
233
- typeField: 'task_kind',
234
- viewMode: 'week', // 'day'|'week'|'month'|'quarter'|'year'
235
-
236
- // (b) …or the same configuration as one block:
237
- // gantt: { startDateField: 'start_date', endDateField: 'end_date', … }
238
- }
334
+ const fieldMapping = {
335
+ type: 'object-gantt',
336
+ properties: {
337
+ objectName: 'project_tasks',
338
+ gantt: {
339
+ startDateField: 'start_date',
340
+ endDateField: 'end_date',
341
+ titleField: 'name',
342
+ progressField: 'completion_percentage',
343
+ dependenciesField: 'dependent_task_ids',
344
+ colorField: 'bar_color',
345
+ parentField: 'parent_task',
346
+ typeField: 'task_kind',
347
+ viewMode: 'week', // 'day'|'week'|'month'|'quarter'|'year'
348
+ },
349
+ },
350
+ };
239
351
  ```
240
352
 
241
- `viewMode` is real authoring surface (`ObjectGanttSchema`, derived from the
242
- spec's `GanttConfigSchema.viewMode`) and is honoured by **both** renderer
243
- branches — the timeline and the resource-workload grid. It reaches the renderer
244
- through `getGanttConfig`, so it only takes effect alongside a taken gantt
245
- config: as a top-level key it needs `startDateField` + `endDateField` beside it,
246
- or it can sit inside the `gantt` block. Omitting it is meaningful — a persisted
247
- layout then seeds the granularity before the renderer's `'day'` fallback.
353
+ `viewMode` is real authoring surface (a member of the spec's `GanttConfigSchema`,
354
+ so of the `gantt` block) and is honoured by **both** renderer branches — the
355
+ timeline and the resource-workload grid. It reaches the renderer through
356
+ `getGanttConfig`, so it only takes effect alongside a taken gantt config.
357
+ Omitting it is meaningful — a persisted layout then seeds the granularity before
358
+ the renderer's `'day'` fallback.
248
359
 
249
360
  #### Keys this page used to teach that the renderer never reads
250
361
 
251
362
  Earlier revisions of this README showed a task-array schema. Those keys have no
252
363
  read site anywhere in `src/` — a schema built from them renders an **empty
253
- chart with no diagnostic**, because `type: 'gantt'` *is* a registered type, so
364
+ chart with no diagnostic**, because `type: 'object-gantt'` *is* a registered type, so
254
365
  the node mounts and simply finds nothing to draw:
255
366
 
256
367
  | Key shown before | Status | Use instead |
@@ -260,15 +371,111 @@ the node mounts and simply finds nothing to draw:
260
371
  | `nameField` | never read | `titleField` |
261
372
  | `startField` | never read | `startDateField` |
262
373
  | `endField` | never read | `endDateField` |
263
- | `fields: { name, start, end, … }` | never read | the flat `*Field` keys, or the `gantt` block |
374
+ | `fields: { name, start, end, … }` | never read | the `gantt` block |
264
375
  | `onTaskClick`, `onTaskUpdate` | never read *off the schema* | React props on `<ObjectGantt>` / `<GanttView>` — functions do not belong in serializable metadata |
265
376
  | `className` | never read off the schema | a React prop on `<ObjectGantt>` |
266
377
 
267
- Keys that **are** read but only through a cast, so they are easy to miss when
268
- grepping — all of them genuine, all optional: `readOnly` (disables every edit
269
- path — drag/resize/inline/delete/link/undo), `mobileReadOnly`, `markers`,
270
- `navigation`, `skipWeekends`, `holidays`, `criticalPath`, `showBaselines`,
271
- `persistLayout` / `viewName`, `label`.
378
+ The keys the renderer reads beside `gantt` — `readOnly`, `mobileReadOnly`,
379
+ `markers`, `navigation`, `skipWeekends`, `holidays`, `criticalPath`,
380
+ `showBaselines`, `persistLayout` / `viewName` and `label` — are all optional,
381
+ all declared by the spec's `object-gantt` row, and all published by the
382
+ registration since objectui#11168 slice 4. Each is described in the table
383
+ below.
384
+
385
+ #### Full field-mapping reference (`GanttConfig` members)
386
+
387
+ `titleField` / `startDateField` / `endDateField` are required; everything below
388
+ is optional. Set them INSIDE the `gantt` block — beside a `gantt` block a
389
+ top-level copy is ignored (with a dev-mode warning naming the losing keys), and
390
+ on an authored node `objectui validate` refuses it.
391
+
392
+ | Key (under `gantt`) | Effect |
393
+ | --- | --- |
394
+ | `parentField` | Builds the summary tree: parents roll up their children's span and weighted progress. |
395
+ | `typeField` | Distinguishes `task` / `summary` (aliases `project`, `phase`) / `milestone` / `group` (alias `folder`). A `group` row renders as a **pure tree header with no timeline bar** — expandable like a summary but never scheduled, for grouping-only levels that have no dates of their own. `summary` still renders a bar-carrying rollup bracket. |
396
+ | `lockField` | Marks a row **view-only** when the field is truthy: no bar drag/resize, no progress drag, no dependency connector dot, and inline-edit plus context-menu edit/delete are hidden. Clicking still works (open drawer / jump). Independent of `readOnly`, so one level of a tree can be frozen while its siblings stay editable. |
397
+ | `defaultCollapsedDepth` | Auto-collapse every tree node at or below this 0-indexed depth that has children, on first render. Roots are depth 0; the user can still expand any of them, this only seeds the initial state. In a 4-level tree, `2` starts with every level-2 node (and its children) folded. Omit to start fully expanded. |
398
+ | `dependenciesField` | Draws the dependency arrows. Accepts CSV, an id array, or `[{ id, type: 'fs'\|'ss'\|'ff'\|'sf' }]`. Setting it also makes links **editable** unless `readOnly`: drag a bar's connector dot to create an FS link, right-click a link to switch its type or remove it, right-click a bar to add a predecessor or successor. Every change is written back to the field, which is auto-promoted to the `[{ id, type }]` form the moment a non-FS link is stored. |
399
+ | `tooltipFields` | The hover detail. Each entry is a field name or `{ field, label }`, formatted by field type. |
400
+ | `baselineStartField` / `baselineEndField` | Draw a thin planned-vs-actual baseline strip under each bar. |
401
+ | `groupByField` | Swimlane the rows by any field — a select/lookup label or a raw value; empty values fall into an "ungrouped" bucket. |
402
+ | `colorField` | Field whose value picks the bar colour. |
403
+ | `resourceView: true` | Render the **resource / workload view** instead of the task grid: one row per resource with a per-column load histogram. Requires `assigneeField` to bucket tasks. |
404
+ | `assigneeField` / `effortField` / `capacity` | Resource bucketing (required for `resourceView`), per-task workload weight (default `1`), and the per-resource capacity ceiling (default `1`). Any column whose summed load exceeds `capacity` is painted as over-allocated. |
405
+ | `quickFilters: [{ field, label?, options? }]` | Render a quick-filter bar above the grid — one multi-select dropdown per entry, narrowing the visible bars by that field (AND across dimensions). Option lists resolve in priority order: explicit `options` → the object schema's `select`/`enum` options (the full domain) → a `lookup` / `master_detail`'s referenced records (pulled in full through the data source, so values with **no** tasks still appear) → distinct values from the loaded data. Lookup values match on the embedded record id. Selecting every option of a dimension collapses to "no constraint". |
406
+ | `autoZoomToFilter` | When a quick filter narrows the set, re-derive the timeline range from the **remaining** tasks so the axis zooms to the filtered span (default `true`). Set `false` to pin the axis to the full task span, so bars keep their absolute position while filtering. |
407
+ | `viewMode` | Initial granularity, `'day'` (default) / `'week'` / `'month'` / `'quarter'` / `'year'`; the toolbar switches it live. `year` widens the axis to one column per year with a decade band above. |
408
+
409
+ With links present, dragging a bar into a position that violates a dependency
410
+ raises a reschedule confirmation: accepting runs a topological forward pass
411
+ (link-type aware; summaries stay fixed rollups) over the affected tasks,
412
+ declining keeps the manual placement. This is on by default whenever
413
+ `dependenciesField` is set, and suppressed under `readOnly`.
414
+
415
+ #### Node-level display and behaviour options
416
+
417
+ True siblings of `gantt` in the `properties` bag, not `GanttConfig` members.
418
+
419
+ | Key (in `properties`, beside `gantt`) | Effect |
420
+ | --- | --- |
421
+ | `criticalPath: true` | Start with the critical-path (zero-slack chain) highlight on; the toolbar toggle stays available. |
422
+ | `showBaselines: false` | Hide the baseline strips even when baseline fields are mapped (default `true`). |
423
+ | `skipWeekends: true` | Working-calendar math: auto-schedule and critical path count working days only, and reschedules snap off Sat/Sun. In **day mode** this also folds weekend columns out of the timeline — Friday sits against Monday and a one-column drag advances one working day. Coarser scales stay linear. |
424
+ | `holidays: ["yyyy-mm-dd", …]` | Extra non-working days for the working calendar, combined with or instead of `skipWeekends`. In day mode these columns fold out of the axis too. |
425
+ | `markers: [{ date, label?, color? }]` | Extra vertical marker lines, like the Today line. `label` is drawn against the line and `color` paints it (the theme's primary colour when omitted); a date outside the chart's range draws no line. |
426
+ | `persistLayout: false` | Disable layout persistence. By default the toolbar's save-layout button snapshots the current granularity, zoom and task-list collapse state, plus the quick-filter chips, to `localStorage` (key `gantt-layout:OBJECT:VIEW`) and restores it on the next load. `OBJECT` is `objectName`, else `data.object`, and `gantt` on inline rows that name neither. With `false` there is no save-layout button and nothing is stored. |
427
+ | `viewName: "board"` | The `VIEW` half of that key (default `default`). Two gantts on the same object with different names keep separate layouts, and each restores only its own. |
428
+ | `readOnly: true` | **Disable every edit path** — no bar drag/resize/progress, no inline edit, no delete, no dependency-link drag, no reorder, no auto-schedule, and the Undo/Redo buttons are hidden. A read-only badge shows in the toolbar and the right-click menu drops to view-only (or is suppressed when nothing is actionable). Task click and granularity switching still work. Use it for dashboards and shared read-only views. |
429
+ | `mobileReadOnly: false` | When the chart is narrow (under 640 px of its own measured width, or of the viewport until that is measured) it auto-enters read-only, giving touch users a clean scrollable thumbnail — the chart's write paths are gated as under `readOnly`, applied only while narrow. The record drawer is not: it locks under `readOnly` alone, so a task's record still opens writable on a narrow chart. Enabled by default; `false` keeps a narrow chart editable, and `readOnly: true` locks the chart and the drawer at any width. |
430
+ | `label` | Names the exported PNG/PDF file: the link after `gantt.exportFileName` and before the object's own label, then `objectName`. A string, or an inline locale map (`{ "en": …, "zh-CN": … }`) resolved to the display locale. The chart draws it nowhere else. |
431
+ | `navigation` | What a task click opens — see **Create / Edit / Delete / View** above. |
432
+
433
+ The toolbar also carries **navigation** (jump-to-today / this-week / this-month
434
+ buttons that scroll the timeline to the start of that period) and **export**
435
+ (PNG, plus a dependency-free single-page PDF of the whole chart), always
436
+ available regardless of `readOnly`. Each task-list row carries a **locate** icon
437
+ by its End cell that smooth-scrolls the timeline to centre that row's bar and
438
+ pulses it — useful in deep or long trees.
439
+
440
+ #### A maximal node, for reference
441
+
442
+ ```json
443
+ {
444
+ "type": "object-gantt",
445
+ "properties": {
446
+ "objectName": "project_task",
447
+ "gantt": {
448
+ "titleField": "name",
449
+ "startDateField": "start_date",
450
+ "endDateField": "end_date",
451
+ "progressField": "progress",
452
+ "parentField": "parent_id",
453
+ "dependenciesField": "depends_on",
454
+ "typeField": "item_type",
455
+ "lockField": "is_locked",
456
+ "defaultCollapsedDepth": 2,
457
+ "colorField": "status",
458
+ "baselineStartField": "planned_start",
459
+ "baselineEndField": "planned_end",
460
+ "tooltipFields": [{ "field": "owner", "label": "Owner" }, "status", "effort"],
461
+ "groupByField": "owner",
462
+ "assigneeField": "owner",
463
+ "effortField": "effort",
464
+ "quickFilters": [
465
+ { "field": "status", "label": "Status" },
466
+ { "field": "project", "label": "Project" },
467
+ { "field": "priority", "label": "Priority", "options": ["high", "medium", "low"] }
468
+ ],
469
+ "autoZoomToFilter": true
470
+ },
471
+ "criticalPath": true,
472
+ "skipWeekends": true,
473
+ "holidays": ["2026-01-01", "2026-12-25"],
474
+ "readOnly": false
475
+ },
476
+ "bind": "project_task"
477
+ }
478
+ ```
272
479
 
273
480
  ### Task Structure
274
481
 
@@ -276,7 +483,10 @@ path — drag/resize/inline/delete/link/undo), `mobileReadOnly`, `markers`,
276
483
  what `ObjectGantt` produces from each record. Dates are real `Date` objects and
277
484
  the label field is `title` (`src/GanttView.tsx`):
278
485
 
486
+ <!-- readme-exports: partial GanttTask — deliberate excerpt: `fields` and `hasOwnDates` are populated by ObjectGantt itself, as the paragraph below the block says -->
279
487
  ```typescript
488
+ import type { GanttDependency, GanttTaskType } from '@object-ui/plugin-gantt';
489
+
280
490
  interface GanttTask {
281
491
  id: string | number;
282
492
  title: string;
@@ -304,60 +514,64 @@ the full declaration.
304
514
  ### Basic Gantt Chart
305
515
 
306
516
  Inline records, no backend — `data.items` carries the records and the `*Field`
307
- keys say which of their fields the chart reads. Note that the record field
308
- names are yours; only the `*Field` keys are fixed vocabulary.
517
+ keys of the `gantt` block say which of their fields the chart reads. Note that
518
+ the record field names are yours; only the `*Field` keys are fixed vocabulary.
309
519
 
310
520
  ```typescript
311
521
  const schema = {
312
- type: 'gantt',
313
- viewMode: 'week',
314
- startDateField: 'start',
315
- endDateField: 'end',
316
- titleField: 'name',
317
- progressField: 'progress',
318
- dependenciesField: 'dependencies',
319
- colorField: 'color',
320
- data: {
321
- provider: 'value',
322
- items: [
323
- {
324
- id: '1',
325
- name: 'Project Planning',
326
- start: '2024-01-01',
327
- end: '2024-01-07',
328
- progress: 100,
329
- // bar fill goes straight into an inline `backgroundColor` —
330
- // it must be a CSS color, NOT a Tailwind class
331
- color: '#3b82f6'
332
- },
333
- {
334
- id: '2',
335
- name: 'Design Phase',
336
- start: '2024-01-08',
337
- end: '2024-01-21',
338
- progress: 75,
339
- dependencies: ['1'],
340
- color: '#a855f7'
341
- },
342
- {
343
- id: '3',
344
- name: 'Development',
345
- start: '2024-01-22',
346
- end: '2024-02-15',
347
- progress: 30,
348
- dependencies: ['2'],
349
- color: '#22c55e'
350
- },
351
- {
352
- id: '4',
353
- name: 'Testing',
354
- start: '2024-02-16',
355
- end: '2024-02-28',
356
- progress: 0,
357
- dependencies: ['3'],
358
- color: '#f97316'
359
- }
360
- ]
522
+ type: 'object-gantt',
523
+ properties: {
524
+ gantt: {
525
+ viewMode: 'week',
526
+ startDateField: 'start',
527
+ endDateField: 'end',
528
+ titleField: 'name',
529
+ progressField: 'progress',
530
+ dependenciesField: 'dependencies',
531
+ colorField: 'color',
532
+ },
533
+ data: {
534
+ provider: 'value',
535
+ items: [
536
+ {
537
+ id: '1',
538
+ name: 'Project Planning',
539
+ start: '2024-01-01',
540
+ end: '2024-01-07',
541
+ progress: 100,
542
+ // bar fill goes straight into an inline `backgroundColor` —
543
+ // it must be a CSS color, NOT a Tailwind class
544
+ color: '#3b82f6'
545
+ },
546
+ {
547
+ id: '2',
548
+ name: 'Design Phase',
549
+ start: '2024-01-08',
550
+ end: '2024-01-21',
551
+ progress: 75,
552
+ dependencies: ['1'],
553
+ color: '#a855f7'
554
+ },
555
+ {
556
+ id: '3',
557
+ name: 'Development',
558
+ start: '2024-01-22',
559
+ end: '2024-02-15',
560
+ progress: 30,
561
+ dependencies: ['2'],
562
+ color: '#22c55e'
563
+ },
564
+ {
565
+ id: '4',
566
+ name: 'Testing',
567
+ start: '2024-02-16',
568
+ end: '2024-02-28',
569
+ progress: 0,
570
+ dependencies: ['3'],
571
+ color: '#f97316'
572
+ }
573
+ ]
574
+ }
361
575
  }
362
576
  };
363
577
  ```
@@ -368,14 +582,22 @@ Callbacks are **not** schema keys — the schema is serializable metadata, and a
368
582
  function cannot survive it. Through the registered `gantt` / `object-gantt`
369
583
  types the whole CRUD lifecycle is already wired to the host `DataSource`
370
584
  (create/edit/drag/delete/detail drawer), so there is usually nothing to pass.
371
- When you render the component yourself, hand the callbacks in as React props:
585
+ When you render the component yourself, hand the callbacks in as React props.
586
+ A component mounted without `SchemaRenderer` receives the node as it reads it,
587
+ so its `schema` prop keeps its props on the node: nothing hoists a `properties`
588
+ bag there.
372
589
 
373
590
  ```tsx
374
591
  import { ObjectGantt } from '@object-ui/plugin-gantt';
592
+ import type { DataSource } from '@object-ui/types';
593
+
594
+ // The adapter your host already talks to — see "Integration with Data
595
+ // Sources" below for where it comes from.
596
+ declare const dataSource: DataSource;
375
597
 
376
- <ObjectGantt
598
+ const interactiveChart = <ObjectGantt
377
599
  schema={{
378
- type: 'gantt',
600
+ type: 'object-gantt',
379
601
  objectName: 'project_tasks',
380
602
  startDateField: 'start_date',
381
603
  endDateField: 'end_date',
@@ -383,23 +605,28 @@ import { ObjectGantt } from '@object-ui/plugin-gantt';
383
605
  }}
384
606
  dataSource={dataSource}
385
607
  onTaskClick={(record) => console.log('Task clicked:', record)}
386
- />
608
+ />;
387
609
  ```
388
610
 
389
- To turn editing off from the metadata instead, set `readOnly: true` on the
390
- schema — that one *is* read.
611
+ To turn editing off from the metadata instead, set `readOnly: true` — in the
612
+ `properties` bag of an authored node, or on the schema you mount — that one *is*
613
+ read.
391
614
 
392
615
  ### With ObjectQL Integration
393
616
 
394
617
  ```typescript
395
618
  const schema = {
396
619
  type: 'object-gantt',
397
- objectName: 'project_tasks',
398
- titleField: 'name',
399
- startDateField: 'start_date',
400
- endDateField: 'end_date',
401
- progressField: 'completion_percentage',
402
- dependenciesField: 'dependent_task_ids'
620
+ properties: {
621
+ objectName: 'project_tasks',
622
+ gantt: {
623
+ titleField: 'name',
624
+ startDateField: 'start_date',
625
+ endDateField: 'end_date',
626
+ progressField: 'completion_percentage',
627
+ dependenciesField: 'dependent_task_ids'
628
+ }
629
+ }
403
630
  };
404
631
  ```
405
632
 
@@ -421,17 +648,36 @@ Drag snapping follows the active scale: bars snap to days in day view, weeks
421
648
  in week view, and whole calendar months/quarters (duration preserved) in the
422
649
  coarse views.
423
650
 
424
- Set the initial scale with `viewMode`. It is read through the gantt config, so
425
- it needs the field mapping beside it (or a `gantt` block of its own):
651
+ ### The toolbar period label and its steppers
652
+
653
+ The label between the `‹` / `›` buttons names the period **currently on screen**,
654
+ not the extent of the data: it reads the date at the left edge of the viewport
655
+ and snaps it to the same tier the band header groups by — a month under day and
656
+ week view, a year under month and quarter view, a decade under year view, and
657
+ the shift-day under shift-segmented day view. So the label and the band header
658
+ directly beneath it always name the same period, and the label moves as the
659
+ chart is scrolled.
660
+
661
+ `‹` / `›` step the visible window by one of those periods (one month in day and
662
+ week view, one year in month and quarter view, a decade in year view), clamped
663
+ to the ends of the timeline. They step the *label's* tier rather than a single
664
+ column, so one click always changes what the label says.
665
+
666
+ Set the initial scale with `viewMode`, a member of the `gantt` block, beside
667
+ the field mapping:
426
668
 
427
669
  ```typescript
428
670
  const schema = {
429
- type: 'gantt',
430
- viewMode: 'month',
431
- objectName: 'project_tasks',
432
- startDateField: 'start_date',
433
- endDateField: 'end_date',
434
- titleField: 'name'
671
+ type: 'object-gantt',
672
+ properties: {
673
+ objectName: 'project_tasks',
674
+ gantt: {
675
+ viewMode: 'month',
676
+ startDateField: 'start_date',
677
+ endDateField: 'end_date',
678
+ titleField: 'name'
679
+ }
680
+ }
435
681
  };
436
682
  ```
437
683
 
@@ -504,16 +750,22 @@ Two more chrome features ship with it:
504
750
  - **Custom markers** — vertical reference lines beyond the Today marker:
505
751
 
506
752
  ```tsx
507
- <GanttView
753
+ import { GanttView } from '@object-ui/plugin-gantt';
754
+ import type { GanttTask } from '@object-ui/plugin-gantt';
755
+
756
+ declare const tasks: GanttTask[];
757
+
758
+ const chartWithMarkers = <GanttView
508
759
  tasks={tasks}
509
760
  markers={[
510
761
  { date: '2026-07-01', label: 'Code freeze', color: '#ef4444' },
511
762
  { date: '2026-07-15', label: 'Release' }, // defaults to the primary theme color
512
763
  ]}
513
- />
764
+ />;
514
765
  ```
515
766
 
516
- Through the schema, pass the same array as `markers` on the gantt node.
767
+ Through the schema, pass the same array as `markers` in the gantt node's
768
+ `properties` bag.
517
769
 
518
770
  ## Task Dependencies
519
771
 
@@ -578,18 +830,22 @@ be a CSV string (`"task1, task2"`), an array of ids, or an array of objects —
578
830
 
579
831
  The adapter is **not** a schema key — it reaches the renderer through the
580
832
  renderer context (or as an explicit `dataSource` prop), while the schema names
581
- the object and the fields:
833
+ the object and the fields. Mounted directly as below, `ObjectGantt` takes the
834
+ node as it reads it, typed `ObjectGanttSchema`, with its props on the node;
835
+ through `SchemaRenderer` the same props go in the authored node's `properties`
836
+ bag:
582
837
 
583
838
  ```tsx
584
839
  import { createObjectStackAdapter } from '@object-ui/data-objectstack';
585
840
  import { ObjectGantt } from '@object-ui/plugin-gantt';
841
+ import type { ObjectGanttSchema } from '@object-ui/types';
586
842
 
587
843
  const dataSource = createObjectStackAdapter({
588
844
  baseUrl: 'https://api.example.com',
589
845
  token: 'your-auth-token'
590
846
  });
591
847
 
592
- const schema = {
848
+ const schema: ObjectGanttSchema = {
593
849
  type: 'object-gantt',
594
850
  objectName: 'tasks',
595
851
  titleField: 'task_name',
@@ -598,7 +854,7 @@ const schema = {
598
854
  progressField: 'progress_percent'
599
855
  };
600
856
 
601
- <ObjectGantt schema={schema} dataSource={dataSource} />
857
+ const ganttWithAdapter = <ObjectGantt schema={schema} dataSource={dataSource} />;
602
858
  ```
603
859
 
604
860
  ⚠️ A `dataSource` key **on the schema node** is a different thing with the same
@@ -632,25 +888,39 @@ const props: GanttViewProps = {
632
888
  };
633
889
  ```
634
890
 
635
- **The authored (JSON metadata) types** live in `@object-ui/types` — this package
636
- imports them and does not re-export them. There is no `GanttSchema`; the
637
- component schema is `ObjectGanttSchema`, and it is **record-driven**: it names an
638
- object and the fields to read, it does not carry a task array.
891
+ **The authored (JSON metadata) node** is `{ type: 'object-gantt', properties }`,
892
+ and the members of `properties` are `@objectstack/spec`'s
893
+ `ComponentPropsMap['object-gantt']` row (its `ObjectGanttProps`). It is
894
+ **record-driven**: it names an object and the fields to read, it does not carry
895
+ a task array. There is no `GanttSchema`. `ObjectGanttSchema` in `@object-ui/types`
896
+ — which this package imports and does not re-export — types the node as
897
+ `ObjectGantt` reads it: after `SchemaRenderer` hoists the bag, or as you mount it
898
+ yourself.
639
899
 
640
900
  ```typescript
641
- import type { ObjectGanttSchema } from '@object-ui/types';
901
+ import type { ObjectGanttBlockNode } from '@object-ui/types';
642
902
 
643
- const gantt: ObjectGanttSchema = {
903
+ const gantt: ObjectGanttBlockNode = {
644
904
  type: 'object-gantt',
645
- objectName: 'project_tasks',
646
- titleField: 'name',
647
- startDateField: 'start_date',
648
- endDateField: 'end_date',
649
- progressField: 'completion_percentage',
650
- dependencyField: 'dependent_task_ids',
905
+ properties: {
906
+ objectName: 'project_tasks',
907
+ gantt: {
908
+ titleField: 'name',
909
+ startDateField: 'start_date',
910
+ endDateField: 'end_date',
911
+ progressField: 'completion_percentage',
912
+ dependenciesField: 'dependent_task_ids',
913
+ },
914
+ },
651
915
  };
652
916
  ```
653
917
 
918
+ `dependenciesField` (plural) is the spec's spelling and the one to author. The
919
+ singular `dependencyField` is a `@deprecated` legacy alias: `ObjectGantt` still
920
+ reads it (`dependenciesField || dependencyField`) on a node built in code, but an
921
+ authored node cannot carry it — `objectui validate` refuses it and names
922
+ `properties.gantt.dependenciesField`.
923
+
654
924
  For a list view served under the `gantt` view type, the same configuration is a
655
925
  `gantt` block on `ListViewSchema` (typed by `GanttConfig`, also from
656
926
  `@object-ui/types`) rather than top-level keys — `ObjectGantt` reads either