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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/6939-objectql-record-source-refinement.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@object-ui/types': patch
---

Repair the `object-map` and `object-gantt` mirrors: `objectName` is optional,
and a refinement requires that at least one of `data`, `staticData`,
`objectName` is present (objectui#6939, maintainer ruling recorded 2026-09-02 —
this is one of the eight groups on that card, dispatched as its own PR per the
ruling).

Both renderers resolve their records from one of three keys, in this order —
`getDataConfig` in `plugin-map/src/ObjectMap.tsx` and
`plugin-gantt/src/ObjectGantt.tsx`: `data`, then `staticData`, then
`objectName`. Both mirrors required `objectName` alone, so a document authored
on `staticData` drew correctly and was refused by `safeValidateSchema` — six
catalog entries, three per component.

- **`object-map`** / **`object-gantt`**: `objectName` becomes optional on the
mirror and on the TypeScript twin in the same stroke, and each member ends in
`requireRecordSource`, whose issue sits at the root path, carries
`params.code = 'RECORD_SOURCE_REQUIRED'` and names the three keys an author
can supply.
- **`object-gantt`** additionally declares `data` (as `ViewDataSchema`, the
spelling `object-map` already used): it is the FIRST key that resolver reads
and was undeclared on both faces, which would have left the refinement naming
a key the validator had never heard of.

**patch, not minor: the accept set only widens toward what already renders.**
Every document that validated before still validates — `objectName` alone,
including an empty one, still parses, because presence is `!== undefined` and
not the renderer's truthiness. The one shape the refinement refuses (none of
the three) was refused before too, when `objectName` was required. Documents
the renderers already draw start validating.
7 changes: 4 additions & 3 deletions content/docs/plugins/plugin-gantt.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,9 +118,10 @@ const schema = {
```plaintext
{
type: 'object-gantt',
objectName?: string, // ObjectQL object name
staticData?: Array<any>, // Static data array
data?: ViewData, // Advanced data configuration
objectName?: string, // ObjectQL object name (read third)
staticData?: Array<any>, // Static data array (read second)
data?: ViewData, // Advanced data configuration (read first)
// At least one of data / staticData / objectName is required
gantt?: GanttConfig, // Gantt-specific configuration
viewMode?: 'day' | 'week' | 'month' | 'quarter' | 'year',
readOnly?: boolean // disable every edit path
Expand Down
7 changes: 4 additions & 3 deletions content/docs/plugins/plugin-map.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,9 +102,10 @@ const schema = {
```plaintext
{
type: 'object-map',
objectName: string, // ObjectQL object name
staticData?: Array<any>, // Static data array
data?: ViewData, // Advanced data configuration
objectName?: string, // ObjectQL object name (read third)
staticData?: Array<any>, // Static data array (read second)
data?: ViewData, // Advanced data configuration (read first)
// At least one of data / staticData / objectName is required
filter?: Array<any>, // Query filter, sent as $filter
sort?: string | SortConfig[], // Sort, sent as $orderby
map?: ObjectMapConfig, // Map-specific configuration
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* objectui#6939, the `object-map` + `object-gantt` group — the GANTT half of
* the render pin. `ObjectGanttSchema` used to require `objectName`, a key the
* renderer reads THIRD (`getDataConfig` in `plugin-gantt/src/ObjectGantt.tsx`:
* `data`, then `staticData`, then `objectName`), so the three `staticData`-only
* catalog entries below drew correctly and were refused by `safeValidateSchema`.
* The repair makes `objectName` optional behind a refinement; the validator-side
* contract is pinned in `packages/types/src/__tests__/objectql-record-source-
* refinement-6939.test.ts`, and the `object-map` tiles are pinned in
* `packages/plugin-map/src/ObjectMap.catalogRecordSource-6939.test.tsx`.
*
* ## Why the render half is the discriminating half
*
* From objectui#6318's triage: a "correction" that renders identically proves
* the SCHEMA was wrong, not the fixture. So the repair has to clear the mirror
* image of that bar — the validator's verdict must change and the renderer's
* output must NOT. The numbers in `PRE_REPAIR` were measured on `origin/main`
* at `d88e20f55`, BEFORE the mirror was touched, through THIS file's harness.
*
* ## The harness, and why it is named
*
* A bare `SchemaRenderer` is not enough here: `ObjectGantt` calls
* `useSchemaContext`, so without a `SchemaRendererProvider` the tile is the
* error boundary — 4 elements reading `Component "object-gantt" failed to
* render` — and an identity pin over THAT is the vacuous pin this file must
* not be. Measured through the provider-wrapped bare renderer below, the three
* tiles draw 407 / 354 / 436 elements. The docs-gallery harness
* (`catalog-gallery-render.test.tsx`, provider + `SidebarProvider` + a padded
* wrapper) gives different absolute counts for the same tile; identity within
* ONE harness is the claim that discriminates. `Date` is frozen because the
* chart carries a Today marker and a date-stamped export name — the pin must
* not move with the calendar.
*
* Three readings per tile, because a count alone cannot tell a swapped element
* from an equal one: element count, a tag census, and the text — the visible
* text as a literal (the chart's own `<style>` block is part of `textContent`
* and is folded into the hash instead, at ~2.7 KB it is not a readable pin),
* plus a SHA-256 of the full `textContent`, style block included.
*/
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
import { render, waitFor } from '@testing-library/react';
import { createHash } from 'node:crypto';
import '@object-ui/components';
import '@object-ui/plugin-gantt';
import { SchemaRenderer, SchemaRendererProvider, toRenderableSchema } from '@object-ui/react';
import { safeValidateSchema } from '@object-ui/types/zod';
import { getExample } from '../src/index.js';

const IDS = [
'plugin-gantt/construction-project-phases',
'plugin-gantt/project-timeline-with-dependencies',
'plugin-gantt/sprint-development-timeline',
] as const;

/**
* Measured on `origin/main` @ `d88e20f55` through `measure()` below, mirror
* untouched. `sha256` is over the full `textContent`; `visibleText` is the
* same string with the chart's `<style>` element's text removed.
*/
const PRE_REPAIR: Record<(typeof IDS)[number], {
elements: number;
tags: Record<string, number>;
sha256: string;
visibleText: string;
}> = {
'plugin-gantt/construction-project-phases': {
elements: 407,
tags: { DIV: 185, STYLE: 1, BUTTON: 26, svg: 19, path: 52, SPAN: 112, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: '19a41af45950a5c6be4b440dfb09c154826f049be3b87e75931348a21b1746cb',
visibleText:
'January 2024DayWeekMonthQuarterYearTodayThis weekThis monthTask NameJan 2024Feb 2024Mar 2024' +
'25T26F27S28S29M30T31W1T2F3S4S5M6T7W8T9F10S11S12M13T14W15T16F17S18S19M20T21W22T23F24S25S26M27T28W29T1F2S3S' +
'Site Preparation2/1 → 2/14Foundation2/15 → 3/15Framing3/16 → 4/30Electrical & Plumbing5/1 → 5/31Interior Finishing6/1 → 7/15' +
'Site Preparation100%Foundation75%Framing30%Electrical & Plumbing0%Interior Finishing0%',
},
'plugin-gantt/project-timeline-with-dependencies': {
elements: 354,
tags: { DIV: 156, STYLE: 1, BUTTON: 24, svg: 17, path: 44, SPAN: 100, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: '3294ccbe33d159b947d01ea52f367b26ce7b4084a1ae89f0ed54661cf36002f8',
visibleText:
'December 2023DayWeekMonthQuarterYearTodayThis weekThis monthTask NameDec 2023Jan 2024Feb 2024' +
'25M26T27W28T29F30S31S1M2T3W4T5F6S7S8M9T10W11T12F13S14S15M16T17W18T19F20S21S22M23T24W25T26F27S28S29M30T31W1T' +
'Design Phase1/1 → 1/15Development1/16 → 2/28Testing3/1 → 3/15' +
'Design Phase100%Development60%Testing0%',
},
'plugin-gantt/sprint-development-timeline': {
elements: 436,
tags: { DIV: 200, STYLE: 1, BUTTON: 27, svg: 20, path: 58, SPAN: 118, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: 'af064c159287029592b508f22314336603aafacaf33bfdc67325e2212c6350d4',
visibleText:
'December 2023DayWeekMonthQuarterYearTodayThis weekThis monthTask NameDec 2023Jan 2024Feb 2024' +
'25M26T27W28T29F30S31S1M2T3W4T5F6S7S8M9T10W11T12F13S14S15M16T17W18T19F20S21S22M23T24W25T26F27S28S29M30T31W1T' +
'Sprint Planning1/1 → 1/3User Authentication1/4 → 1/10Dashboard UI1/4 → 1/12API Integration1/11 → 1/18Testing & QA1/19 → 1/25Deployment1/26 → 1/28' +
'Sprint Planning100%User Authentication100%Dashboard UI85%API Integration40%Testing & QA0%Deployment0%',
},
};

beforeAll(() => {
vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime(new Date('2026-09-03T12:00:00Z'));
});
afterAll(() => vi.useRealTimers());

/** Render one entry through the provider-wrapped bare renderer and measure what it drew. */
async function measure(schema: unknown) {
const { container, unmount } = render(
// No host DataSource: every entry authors `staticData`, which the
// renderer wraps into its own in-memory ValueDataSource. The provider
// itself is load-bearing (`useSchemaContext`).
<SchemaRendererProvider dataSource={undefined}>
<SchemaRenderer schema={toRenderableSchema(schema as never) as never} />
</SchemaRendererProvider>,
);
// The chart loads its inline rows through a ValueDataSource, so its first
// paint is the loading placeholder; measure only once that is gone.
await waitFor(() => expect(container.textContent ?? '').not.toContain('Loading Gantt chart...'));
const nodes = Array.from(container.querySelectorAll('*'));
const text = container.textContent ?? '';
const styleText = Array.from(container.querySelectorAll('style')).map((el) => el.textContent ?? '');
const tags = nodes.reduce<Record<string, number>>((h, el) => ((h[el.tagName] = (h[el.tagName] ?? 0) + 1), h), {});
const out = {
elements: nodes.length,
tags,
sha256: createHash('sha256').update(text).digest('hex'),
visibleText: styleText.reduce((t, s) => t.replace(s, ''), text),
text,
};
unmount();
return out;
}

/** Report the issues rather than `false`, so a red run says what broke. */
function reasons(schema: unknown): string[] {
const r = safeValidateSchema(schema);
return r.success ? [] : r.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`);
}

describe('objectui#6939 — the three gantt entries the mirror refused now validate', () => {
it.each(IDS)('%s validates under safeValidateSchema', (id) => {
// Each reported `: Invalid input` (the union's own top-level issue)
// before this card, for a key the renderer reads third.
expect(reasons(getExample(id).schema)).toEqual([]);
});
});

describe('objectui#6939 — and the repair moved the validator, not the renderer', () => {
it.each(IDS)('%s renders exactly what it rendered before', async (id) => {
const after = await measure(getExample(id).schema);
const before = PRE_REPAIR[id];
expect(after.elements).toBe(before.elements);
expect(after.tags).toEqual(before.tags);
expect(after.visibleText).toBe(before.visibleText);
expect(after.sha256).toBe(before.sha256);
});

it.each(IDS)('%s anti-vacuity: the tile drew its AUTHORED tasks, not an error box', async (id) => {
// An entry that renders nothing — or the error boundary — satisfies
// "identical" trivially. The authored titles on screen prove the rows
// reached the chart through `staticData`, the source the mirror ignored.
const schema = getExample(id).schema as { staticData: Record<string, string>[]; gantt: { titleField: string } };
const m = await measure(schema);
expect(m.elements).toBeGreaterThan(100);
expect(m.text).not.toContain('failed to render');
for (const row of schema.staticData) {
expect(m.visibleText).toContain(row[schema.gantt.titleField]);
}
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/6939-objectql-record-source-refinement.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@object-ui/types': patch
---

Repair the `object-map` and `object-gantt` mirrors: `objectName` is optional,
and a refinement requires that at least one of `data`, `staticData`,
`objectName` is present (objectui#6939, maintainer ruling recorded 2026-09-02 —
this is one of the eight groups on that card, dispatched as its own PR per the
ruling).

Both renderers resolve their records from one of three keys, in this order —
`getDataConfig` in `plugin-map/src/ObjectMap.tsx` and
`plugin-gantt/src/ObjectGantt.tsx`: `data`, then `staticData`, then
`objectName`. Both mirrors required `objectName` alone, so a document authored
on `staticData` drew correctly and was refused by `safeValidateSchema` — six
catalog entries, three per component.

- **`object-map`** / **`object-gantt`**: `objectName` becomes optional on the
mirror and on the TypeScript twin in the same stroke, and each member ends in
`requireRecordSource`, whose issue sits at the root path, carries
`params.code = 'RECORD_SOURCE_REQUIRED'` and names the three keys an author
can supply.
- **`object-gantt`** additionally declares `data` (as `ViewDataSchema`, the
spelling `object-map` already used): it is the FIRST key that resolver reads
and was undeclared on both faces, which would have left the refinement naming
a key the validator had never heard of.

**patch, not minor: the accept set only widens toward what already renders.**
Every document that validated before still validates — `objectName` alone,
including an empty one, still parses, because presence is `!== undefined` and
not the renderer's truthiness. The one shape the refinement refuses (none of
the three) was refused before too, when `objectName` was required. Documents
the renderers already draw start validating.
7 changes: 4 additions & 3 deletions content/docs/plugins/plugin-gantt.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,9 +118,10 @@ const schema = {
```plaintext
{
type: 'object-gantt',
objectName?: string, // ObjectQL object name
staticData?: Array<any>, // Static data array
data?: ViewData, // Advanced data configuration
objectName?: string, // ObjectQL object name (read third)
staticData?: Array<any>, // Static data array (read second)
data?: ViewData, // Advanced data configuration (read first)
// At least one of data / staticData / objectName is required
gantt?: GanttConfig, // Gantt-specific configuration
viewMode?: 'day' | 'week' | 'month' | 'quarter' | 'year',
readOnly?: boolean // disable every edit path
Expand Down
7 changes: 4 additions & 3 deletions content/docs/plugins/plugin-map.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,9 +102,10 @@ const schema = {
```plaintext
{
type: 'object-map',
objectName: string, // ObjectQL object name
staticData?: Array<any>, // Static data array
data?: ViewData, // Advanced data configuration
objectName?: string, // ObjectQL object name (read third)
staticData?: Array<any>, // Static data array (read second)
data?: ViewData, // Advanced data configuration (read first)
// At least one of data / staticData / objectName is required
filter?: Array<any>, // Query filter, sent as $filter
sort?: string | SortConfig[], // Sort, sent as $orderby
map?: ObjectMapConfig, // Map-specific configuration
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* objectui#6939, the `object-map` + `object-gantt` group — the GANTT half of
* the render pin. `ObjectGanttSchema` used to require `objectName`, a key the
* renderer reads THIRD (`getDataConfig` in `plugin-gantt/src/ObjectGantt.tsx`:
* `data`, then `staticData`, then `objectName`), so the three `staticData`-only
* catalog entries below drew correctly and were refused by `safeValidateSchema`.
* The repair makes `objectName` optional behind a refinement; the validator-side
* contract is pinned in `packages/types/src/__tests__/objectql-record-source-
* refinement-6939.test.ts`, and the `object-map` tiles are pinned in
* `packages/plugin-map/src/ObjectMap.catalogRecordSource-6939.test.tsx`.
*
* ## Why the render half is the discriminating half
*
* From objectui#6318's triage: a "correction" that renders identically proves
* the SCHEMA was wrong, not the fixture. So the repair has to clear the mirror
* image of that bar — the validator's verdict must change and the renderer's
* output must NOT. The numbers in `PRE_REPAIR` were measured on `origin/main`
* at `d88e20f55`, BEFORE the mirror was touched, through THIS file's harness.
*
* ## The harness, and why it is named
*
* A bare `SchemaRenderer` is not enough here: `ObjectGantt` calls
* `useSchemaContext`, so without a `SchemaRendererProvider` the tile is the
* error boundary — 4 elements reading `Component "object-gantt" failed to
* render` — and an identity pin over THAT is the vacuous pin this file must
* not be. Measured through the provider-wrapped bare renderer below, the three
* tiles draw 407 / 354 / 436 elements. The docs-gallery harness
* (`catalog-gallery-render.test.tsx`, provider + `SidebarProvider` + a padded
* wrapper) gives different absolute counts for the same tile; identity within
* ONE harness is the claim that discriminates. `Date` is frozen because the
* chart carries a Today marker and a date-stamped export name — the pin must
* not move with the calendar.
*
* Three readings per tile, because a count alone cannot tell a swapped element
* from an equal one: element count, a tag census, and the text — the visible
* text as a literal (the chart's own `<style>` block is part of `textContent`
* and is folded into the hash instead, at ~2.7 KB it is not a readable pin),
* plus a SHA-256 of the full `textContent`, style block included.
*/
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
import { render, waitFor } from '@testing-library/react';
import { createHash } from 'node:crypto';
import '@object-ui/components';
import '@object-ui/plugin-gantt';
import { SchemaRenderer, SchemaRendererProvider, toRenderableSchema } from '@object-ui/react';
import { safeValidateSchema } from '@object-ui/types/zod';
import { getExample } from '../src/index.js';

const IDS = [
'plugin-gantt/construction-project-phases',
'plugin-gantt/project-timeline-with-dependencies',
'plugin-gantt/sprint-development-timeline',
] as const;

/**
* Measured on `origin/main` @ `d88e20f55` through `measure()` below, mirror
* untouched. `sha256` is over the full `textContent`; `visibleText` is the
* same string with the chart's `<style>` element's text removed.
*/
const PRE_REPAIR: Record<(typeof IDS)[number], {
elements: number;
tags: Record<string, number>;
sha256: string;
visibleText: string;
}> = {
'plugin-gantt/construction-project-phases': {
elements: 407,
tags: { DIV: 185, STYLE: 1, BUTTON: 26, svg: 19, path: 52, SPAN: 112, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: '19a41af45950a5c6be4b440dfb09c154826f049be3b87e75931348a21b1746cb',
visibleText:
'January 2024DayWeekMonthQuarterYearTodayThis weekThis monthTask NameJan 2024Feb 2024Mar 2024' +
'25T26F27S28S29M30T31W1T2F3S4S5M6T7W8T9F10S11S12M13T14W15T16F17S18S19M20T21W22T23F24S25S26M27T28W29T1F2S3S' +
'Site Preparation2/1 → 2/14Foundation2/15 → 3/15Framing3/16 → 4/30Electrical & Plumbing5/1 → 5/31Interior Finishing6/1 → 7/15' +
'Site Preparation100%Foundation75%Framing30%Electrical & Plumbing0%Interior Finishing0%',
},
'plugin-gantt/project-timeline-with-dependencies': {
elements: 354,
tags: { DIV: 156, STYLE: 1, BUTTON: 24, svg: 17, path: 44, SPAN: 100, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: '3294ccbe33d159b947d01ea52f367b26ce7b4084a1ae89f0ed54661cf36002f8',
visibleText:
'December 2023DayWeekMonthQuarterYearTodayThis weekThis monthTask NameDec 2023Jan 2024Feb 2024' +
'25M26T27W28T29F30S31S1M2T3W4T5F6S7S8M9T10W11T12F13S14S15M16T17W18T19F20S21S22M23T24W25T26F27S28S29M30T31W1T' +
'Design Phase1/1 → 1/15Development1/16 → 2/28Testing3/1 → 3/15' +
'Design Phase100%Development60%Testing0%',
},
'plugin-gantt/sprint-development-timeline': {
elements: 436,
tags: { DIV: 200, STYLE: 1, BUTTON: 27, svg: 20, path: 58, SPAN: 118, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: 'af064c159287029592b508f22314336603aafacaf33bfdc67325e2212c6350d4',
visibleText:
'December 2023DayWeekMonthQuarterYearTodayThis weekThis monthTask NameDec 2023Jan 2024Feb 2024' +
'25M26T27W28T29F30S31S1M2T3W4T5F6S7S8M9T10W11T12F13S14S15M16T17W18T19F20S21S22M23T24W25T26F27S28S29M30T31W1T' +
'Sprint Planning1/1 → 1/3User Authentication1/4 → 1/10Dashboard UI1/4 → 1/12API Integration1/11 → 1/18Testing & QA1/19 → 1/25Deployment1/26 → 1/28' +
'Sprint Planning100%User Authentication100%Dashboard UI85%API Integration40%Testing & QA0%Deployment0%',
},
};

beforeAll(() => {
vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime(new Date('2026-09-03T12:00:00Z'));
});
afterAll(() => vi.useRealTimers());

/** Render one entry through the provider-wrapped bare renderer and measure what it drew. */
async function measure(schema: unknown) {
const { container, unmount } = render(
// No host DataSource: every entry authors `staticData`, which the
// renderer wraps into its own in-memory ValueDataSource. The provider
// itself is load-bearing (`useSchemaContext`).
<SchemaRendererProvider dataSource={undefined}>
<SchemaRenderer schema={toRenderableSchema(schema as never) as never} />
</SchemaRendererProvider>,
);
// The chart loads its inline rows through a ValueDataSource, so its first
// paint is the loading placeholder; measure only once that is gone.
await waitFor(() => expect(container.textContent ?? '').not.toContain('Loading Gantt chart...'));
const nodes = Array.from(container.querySelectorAll('*'));
const text = container.textContent ?? '';
const styleText = Array.from(container.querySelectorAll('style')).map((el) => el.textContent ?? '');
const tags = nodes.reduce<Record<string, number>>((h, el) => ((h[el.tagName] = (h[el.tagName] ?? 0) + 1), h), {});
const out = {
elements: nodes.length,
tags,
sha256: createHash('sha256').update(text).digest('hex'),
visibleText: styleText.reduce((t, s) => t.replace(s, ''), text),
text,
};
unmount();
return out;
}

/** Report the issues rather than `false`, so a red run says what broke. */
function reasons(schema: unknown): string[] {
const r = safeValidateSchema(schema);
return r.success ? [] : r.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`);
}

describe('objectui#6939 — the three gantt entries the mirror refused now validate', () => {
it.each(IDS)('%s validates under safeValidateSchema', (id) => {
// Each reported `: Invalid input` (the union's own top-level issue)
// before this card, for a key the renderer reads third.
expect(reasons(getExample(id).schema)).toEqual([]);
});
});

describe('objectui#6939 — and the repair moved the validator, not the renderer', () => {
it.each(IDS)('%s renders exactly what it rendered before', async (id) => {
const after = await measure(getExample(id).schema);
const before = PRE_REPAIR[id];
expect(after.elements).toBe(before.elements);
expect(after.tags).toEqual(before.tags);
expect(after.visibleText).toBe(before.visibleText);
expect(after.sha256).toBe(before.sha256);
});

it.each(IDS)('%s anti-vacuity: the tile drew its AUTHORED tasks, not an error box', async (id) => {
// An entry that renders nothing — or the error boundary — satisfies
// "identical" trivially. The authored titles on screen prove the rows
// reached the chart through `staticData`, the source the mirror ignored.
const schema = getExample(id).schema as { staticData: Record<string, string>[]; gantt: { titleField: string } };
const m = await measure(schema);
expect(m.elements).toBeGreaterThan(100);
expect(m.text).not.toContain('failed to render');
for (const row of schema.staticData) {
expect(m.visibleText).toContain(row[schema.gantt.titleField]);
}
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/6939-objectql-record-source-refinement.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@object-ui/types': patch
---

Repair the `object-map` and `object-gantt` mirrors: `objectName` is optional,
and a refinement requires that at least one of `data`, `staticData`,
`objectName` is present (objectui#6939, maintainer ruling recorded 2026-09-02 —
this is one of the eight groups on that card, dispatched as its own PR per the
ruling).

Both renderers resolve their records from one of three keys, in this order —
`getDataConfig` in `plugin-map/src/ObjectMap.tsx` and
`plugin-gantt/src/ObjectGantt.tsx`: `data`, then `staticData`, then
`objectName`. Both mirrors required `objectName` alone, so a document authored
on `staticData` drew correctly and was refused by `safeValidateSchema` — six
catalog entries, three per component.

- **`object-map`** / **`object-gantt`**: `objectName` becomes optional on the
mirror and on the TypeScript twin in the same stroke, and each member ends in
`requireRecordSource`, whose issue sits at the root path, carries
`params.code = 'RECORD_SOURCE_REQUIRED'` and names the three keys an author
can supply.
- **`object-gantt`** additionally declares `data` (as `ViewDataSchema`, the
spelling `object-map` already used): it is the FIRST key that resolver reads
and was undeclared on both faces, which would have left the refinement naming
a key the validator had never heard of.

**patch, not minor: the accept set only widens toward what already renders.**
Every document that validated before still validates — `objectName` alone,
including an empty one, still parses, because presence is `!== undefined` and
not the renderer's truthiness. The one shape the refinement refuses (none of
the three) was refused before too, when `objectName` was required. Documents
the renderers already draw start validating.
7 changes: 4 additions & 3 deletions content/docs/plugins/plugin-gantt.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,9 +118,10 @@ const schema = {
```plaintext
{
type: 'object-gantt',
objectName?: string, // ObjectQL object name
staticData?: Array<any>, // Static data array
data?: ViewData, // Advanced data configuration
objectName?: string, // ObjectQL object name (read third)
staticData?: Array<any>, // Static data array (read second)
data?: ViewData, // Advanced data configuration (read first)
// At least one of data / staticData / objectName is required
gantt?: GanttConfig, // Gantt-specific configuration
viewMode?: 'day' | 'week' | 'month' | 'quarter' | 'year',
readOnly?: boolean // disable every edit path
Expand Down
7 changes: 4 additions & 3 deletions content/docs/plugins/plugin-map.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,9 +102,10 @@ const schema = {
```plaintext
{
type: 'object-map',
objectName: string, // ObjectQL object name
staticData?: Array<any>, // Static data array
data?: ViewData, // Advanced data configuration
objectName?: string, // ObjectQL object name (read third)
staticData?: Array<any>, // Static data array (read second)
data?: ViewData, // Advanced data configuration (read first)
// At least one of data / staticData / objectName is required
filter?: Array<any>, // Query filter, sent as $filter
sort?: string | SortConfig[], // Sort, sent as $orderby
map?: ObjectMapConfig, // Map-specific configuration
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* objectui#6939, the `object-map` + `object-gantt` group — the GANTT half of
* the render pin. `ObjectGanttSchema` used to require `objectName`, a key the
* renderer reads THIRD (`getDataConfig` in `plugin-gantt/src/ObjectGantt.tsx`:
* `data`, then `staticData`, then `objectName`), so the three `staticData`-only
* catalog entries below drew correctly and were refused by `safeValidateSchema`.
* The repair makes `objectName` optional behind a refinement; the validator-side
* contract is pinned in `packages/types/src/__tests__/objectql-record-source-
* refinement-6939.test.ts`, and the `object-map` tiles are pinned in
* `packages/plugin-map/src/ObjectMap.catalogRecordSource-6939.test.tsx`.
*
* ## Why the render half is the discriminating half
*
* From objectui#6318's triage: a "correction" that renders identically proves
* the SCHEMA was wrong, not the fixture. So the repair has to clear the mirror
* image of that bar — the validator's verdict must change and the renderer's
* output must NOT. The numbers in `PRE_REPAIR` were measured on `origin/main`
* at `d88e20f55`, BEFORE the mirror was touched, through THIS file's harness.
*
* ## The harness, and why it is named
*
* A bare `SchemaRenderer` is not enough here: `ObjectGantt` calls
* `useSchemaContext`, so without a `SchemaRendererProvider` the tile is the
* error boundary — 4 elements reading `Component "object-gantt" failed to
* render` — and an identity pin over THAT is the vacuous pin this file must
* not be. Measured through the provider-wrapped bare renderer below, the three
* tiles draw 407 / 354 / 436 elements. The docs-gallery harness
* (`catalog-gallery-render.test.tsx`, provider + `SidebarProvider` + a padded
* wrapper) gives different absolute counts for the same tile; identity within
* ONE harness is the claim that discriminates. `Date` is frozen because the
* chart carries a Today marker and a date-stamped export name — the pin must
* not move with the calendar.
*
* Three readings per tile, because a count alone cannot tell a swapped element
* from an equal one: element count, a tag census, and the text — the visible
* text as a literal (the chart's own `<style>` block is part of `textContent`
* and is folded into the hash instead, at ~2.7 KB it is not a readable pin),
* plus a SHA-256 of the full `textContent`, style block included.
*/
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
import { render, waitFor } from '@testing-library/react';
import { createHash } from 'node:crypto';
import '@object-ui/components';
import '@object-ui/plugin-gantt';
import { SchemaRenderer, SchemaRendererProvider, toRenderableSchema } from '@object-ui/react';
import { safeValidateSchema } from '@object-ui/types/zod';
import { getExample } from '../src/index.js';

const IDS = [
'plugin-gantt/construction-project-phases',
'plugin-gantt/project-timeline-with-dependencies',
'plugin-gantt/sprint-development-timeline',
] as const;

/**
* Measured on `origin/main` @ `d88e20f55` through `measure()` below, mirror
* untouched. `sha256` is over the full `textContent`; `visibleText` is the
* same string with the chart's `<style>` element's text removed.
*/
const PRE_REPAIR: Record<(typeof IDS)[number], {
elements: number;
tags: Record<string, number>;
sha256: string;
visibleText: string;
}> = {
'plugin-gantt/construction-project-phases': {
elements: 407,
tags: { DIV: 185, STYLE: 1, BUTTON: 26, svg: 19, path: 52, SPAN: 112, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: '19a41af45950a5c6be4b440dfb09c154826f049be3b87e75931348a21b1746cb',
visibleText:
'January 2024DayWeekMonthQuarterYearTodayThis weekThis monthTask NameJan 2024Feb 2024Mar 2024' +
'25T26F27S28S29M30T31W1T2F3S4S5M6T7W8T9F10S11S12M13T14W15T16F17S18S19M20T21W22T23F24S25S26M27T28W29T1F2S3S' +
'Site Preparation2/1 → 2/14Foundation2/15 → 3/15Framing3/16 → 4/30Electrical & Plumbing5/1 → 5/31Interior Finishing6/1 → 7/15' +
'Site Preparation100%Foundation75%Framing30%Electrical & Plumbing0%Interior Finishing0%',
},
'plugin-gantt/project-timeline-with-dependencies': {
elements: 354,
tags: { DIV: 156, STYLE: 1, BUTTON: 24, svg: 17, path: 44, SPAN: 100, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: '3294ccbe33d159b947d01ea52f367b26ce7b4084a1ae89f0ed54661cf36002f8',
visibleText:
'December 2023DayWeekMonthQuarterYearTodayThis weekThis monthTask NameDec 2023Jan 2024Feb 2024' +
'25M26T27W28T29F30S31S1M2T3W4T5F6S7S8M9T10W11T12F13S14S15M16T17W18T19F20S21S22M23T24W25T26F27S28S29M30T31W1T' +
'Design Phase1/1 → 1/15Development1/16 → 2/28Testing3/1 → 3/15' +
'Design Phase100%Development60%Testing0%',
},
'plugin-gantt/sprint-development-timeline': {
elements: 436,
tags: { DIV: 200, STYLE: 1, BUTTON: 27, svg: 20, path: 58, SPAN: 118, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: 'af064c159287029592b508f22314336603aafacaf33bfdc67325e2212c6350d4',
visibleText:
'December 2023DayWeekMonthQuarterYearTodayThis weekThis monthTask NameDec 2023Jan 2024Feb 2024' +
'25M26T27W28T29F30S31S1M2T3W4T5F6S7S8M9T10W11T12F13S14S15M16T17W18T19F20S21S22M23T24W25T26F27S28S29M30T31W1T' +
'Sprint Planning1/1 → 1/3User Authentication1/4 → 1/10Dashboard UI1/4 → 1/12API Integration1/11 → 1/18Testing & QA1/19 → 1/25Deployment1/26 → 1/28' +
'Sprint Planning100%User Authentication100%Dashboard UI85%API Integration40%Testing & QA0%Deployment0%',
},
};

beforeAll(() => {
vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime(new Date('2026-09-03T12:00:00Z'));
});
afterAll(() => vi.useRealTimers());

/** Render one entry through the provider-wrapped bare renderer and measure what it drew. */
async function measure(schema: unknown) {
const { container, unmount } = render(
// No host DataSource: every entry authors `staticData`, which the
// renderer wraps into its own in-memory ValueDataSource. The provider
// itself is load-bearing (`useSchemaContext`).
<SchemaRendererProvider dataSource={undefined}>
<SchemaRenderer schema={toRenderableSchema(schema as never) as never} />
</SchemaRendererProvider>,
);
// The chart loads its inline rows through a ValueDataSource, so its first
// paint is the loading placeholder; measure only once that is gone.
await waitFor(() => expect(container.textContent ?? '').not.toContain('Loading Gantt chart...'));
const nodes = Array.from(container.querySelectorAll('*'));
const text = container.textContent ?? '';
const styleText = Array.from(container.querySelectorAll('style')).map((el) => el.textContent ?? '');
const tags = nodes.reduce<Record<string, number>>((h, el) => ((h[el.tagName] = (h[el.tagName] ?? 0) + 1), h), {});
const out = {
elements: nodes.length,
tags,
sha256: createHash('sha256').update(text).digest('hex'),
visibleText: styleText.reduce((t, s) => t.replace(s, ''), text),
text,
};
unmount();
return out;
}

/** Report the issues rather than `false`, so a red run says what broke. */
function reasons(schema: unknown): string[] {
const r = safeValidateSchema(schema);
return r.success ? [] : r.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`);
}

describe('objectui#6939 — the three gantt entries the mirror refused now validate', () => {
it.each(IDS)('%s validates under safeValidateSchema', (id) => {
// Each reported `: Invalid input` (the union's own top-level issue)
// before this card, for a key the renderer reads third.
expect(reasons(getExample(id).schema)).toEqual([]);
});
});

describe('objectui#6939 — and the repair moved the validator, not the renderer', () => {
it.each(IDS)('%s renders exactly what it rendered before', async (id) => {
const after = await measure(getExample(id).schema);
const before = PRE_REPAIR[id];
expect(after.elements).toBe(before.elements);
expect(after.tags).toEqual(before.tags);
expect(after.visibleText).toBe(before.visibleText);
expect(after.sha256).toBe(before.sha256);
});

it.each(IDS)('%s anti-vacuity: the tile drew its AUTHORED tasks, not an error box', async (id) => {
// An entry that renders nothing — or the error boundary — satisfies
// "identical" trivially. The authored titles on screen prove the rows
// reached the chart through `staticData`, the source the mirror ignored.
const schema = getExample(id).schema as { staticData: Record<string, string>[]; gantt: { titleField: string } };
const m = await measure(schema);
expect(m.elements).toBeGreaterThan(100);
expect(m.text).not.toContain('failed to render');
for (const row of schema.staticData) {
expect(m.visibleText).toContain(row[schema.gantt.titleField]);
}
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/6939-objectql-record-source-refinement.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@object-ui/types': patch
---

Repair the `object-map` and `object-gantt` mirrors: `objectName` is optional,
and a refinement requires that at least one of `data`, `staticData`,
`objectName` is present (objectui#6939, maintainer ruling recorded 2026-09-02 —
this is one of the eight groups on that card, dispatched as its own PR per the
ruling).

Both renderers resolve their records from one of three keys, in this order —
`getDataConfig` in `plugin-map/src/ObjectMap.tsx` and
`plugin-gantt/src/ObjectGantt.tsx`: `data`, then `staticData`, then
`objectName`. Both mirrors required `objectName` alone, so a document authored
on `staticData` drew correctly and was refused by `safeValidateSchema` — six
catalog entries, three per component.

- **`object-map`** / **`object-gantt`**: `objectName` becomes optional on the
mirror and on the TypeScript twin in the same stroke, and each member ends in
`requireRecordSource`, whose issue sits at the root path, carries
`params.code = 'RECORD_SOURCE_REQUIRED'` and names the three keys an author
can supply.
- **`object-gantt`** additionally declares `data` (as `ViewDataSchema`, the
spelling `object-map` already used): it is the FIRST key that resolver reads
and was undeclared on both faces, which would have left the refinement naming
a key the validator had never heard of.

**patch, not minor: the accept set only widens toward what already renders.**
Every document that validated before still validates — `objectName` alone,
including an empty one, still parses, because presence is `!== undefined` and
not the renderer's truthiness. The one shape the refinement refuses (none of
the three) was refused before too, when `objectName` was required. Documents
the renderers already draw start validating.
7 changes: 4 additions & 3 deletions content/docs/plugins/plugin-gantt.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,9 +118,10 @@ const schema = {
```plaintext
{
type: 'object-gantt',
objectName?: string, // ObjectQL object name
staticData?: Array<any>, // Static data array
data?: ViewData, // Advanced data configuration
objectName?: string, // ObjectQL object name (read third)
staticData?: Array<any>, // Static data array (read second)
data?: ViewData, // Advanced data configuration (read first)
// At least one of data / staticData / objectName is required
gantt?: GanttConfig, // Gantt-specific configuration
viewMode?: 'day' | 'week' | 'month' | 'quarter' | 'year',
readOnly?: boolean // disable every edit path
Expand Down
7 changes: 4 additions & 3 deletions content/docs/plugins/plugin-map.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,9 +102,10 @@ const schema = {
```plaintext
{
type: 'object-map',
objectName: string, // ObjectQL object name
staticData?: Array<any>, // Static data array
data?: ViewData, // Advanced data configuration
objectName?: string, // ObjectQL object name (read third)
staticData?: Array<any>, // Static data array (read second)
data?: ViewData, // Advanced data configuration (read first)
// At least one of data / staticData / objectName is required
filter?: Array<any>, // Query filter, sent as $filter
sort?: string | SortConfig[], // Sort, sent as $orderby
map?: ObjectMapConfig, // Map-specific configuration
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* objectui#6939, the `object-map` + `object-gantt` group — the GANTT half of
* the render pin. `ObjectGanttSchema` used to require `objectName`, a key the
* renderer reads THIRD (`getDataConfig` in `plugin-gantt/src/ObjectGantt.tsx`:
* `data`, then `staticData`, then `objectName`), so the three `staticData`-only
* catalog entries below drew correctly and were refused by `safeValidateSchema`.
* The repair makes `objectName` optional behind a refinement; the validator-side
* contract is pinned in `packages/types/src/__tests__/objectql-record-source-
* refinement-6939.test.ts`, and the `object-map` tiles are pinned in
* `packages/plugin-map/src/ObjectMap.catalogRecordSource-6939.test.tsx`.
*
* ## Why the render half is the discriminating half
*
* From objectui#6318's triage: a "correction" that renders identically proves
* the SCHEMA was wrong, not the fixture. So the repair has to clear the mirror
* image of that bar — the validator's verdict must change and the renderer's
* output must NOT. The numbers in `PRE_REPAIR` were measured on `origin/main`
* at `d88e20f55`, BEFORE the mirror was touched, through THIS file's harness.
*
* ## The harness, and why it is named
*
* A bare `SchemaRenderer` is not enough here: `ObjectGantt` calls
* `useSchemaContext`, so without a `SchemaRendererProvider` the tile is the
* error boundary — 4 elements reading `Component "object-gantt" failed to
* render` — and an identity pin over THAT is the vacuous pin this file must
* not be. Measured through the provider-wrapped bare renderer below, the three
* tiles draw 407 / 354 / 436 elements. The docs-gallery harness
* (`catalog-gallery-render.test.tsx`, provider + `SidebarProvider` + a padded
* wrapper) gives different absolute counts for the same tile; identity within
* ONE harness is the claim that discriminates. `Date` is frozen because the
* chart carries a Today marker and a date-stamped export name — the pin must
* not move with the calendar.
*
* Three readings per tile, because a count alone cannot tell a swapped element
* from an equal one: element count, a tag census, and the text — the visible
* text as a literal (the chart's own `<style>` block is part of `textContent`
* and is folded into the hash instead, at ~2.7 KB it is not a readable pin),
* plus a SHA-256 of the full `textContent`, style block included.
*/
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
import { render, waitFor } from '@testing-library/react';
import { createHash } from 'node:crypto';
import '@object-ui/components';
import '@object-ui/plugin-gantt';
import { SchemaRenderer, SchemaRendererProvider, toRenderableSchema } from '@object-ui/react';
import { safeValidateSchema } from '@object-ui/types/zod';
import { getExample } from '../src/index.js';

const IDS = [
'plugin-gantt/construction-project-phases',
'plugin-gantt/project-timeline-with-dependencies',
'plugin-gantt/sprint-development-timeline',
] as const;

/**
* Measured on `origin/main` @ `d88e20f55` through `measure()` below, mirror
* untouched. `sha256` is over the full `textContent`; `visibleText` is the
* same string with the chart's `<style>` element's text removed.
*/
const PRE_REPAIR: Record<(typeof IDS)[number], {
elements: number;
tags: Record<string, number>;
sha256: string;
visibleText: string;
}> = {
'plugin-gantt/construction-project-phases': {
elements: 407,
tags: { DIV: 185, STYLE: 1, BUTTON: 26, svg: 19, path: 52, SPAN: 112, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: '19a41af45950a5c6be4b440dfb09c154826f049be3b87e75931348a21b1746cb',
visibleText:
'January 2024DayWeekMonthQuarterYearTodayThis weekThis monthTask NameJan 2024Feb 2024Mar 2024' +
'25T26F27S28S29M30T31W1T2F3S4S5M6T7W8T9F10S11S12M13T14W15T16F17S18S19M20T21W22T23F24S25S26M27T28W29T1F2S3S' +
'Site Preparation2/1 → 2/14Foundation2/15 → 3/15Framing3/16 → 4/30Electrical & Plumbing5/1 → 5/31Interior Finishing6/1 → 7/15' +
'Site Preparation100%Foundation75%Framing30%Electrical & Plumbing0%Interior Finishing0%',
},
'plugin-gantt/project-timeline-with-dependencies': {
elements: 354,
tags: { DIV: 156, STYLE: 1, BUTTON: 24, svg: 17, path: 44, SPAN: 100, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: '3294ccbe33d159b947d01ea52f367b26ce7b4084a1ae89f0ed54661cf36002f8',
visibleText:
'December 2023DayWeekMonthQuarterYearTodayThis weekThis monthTask NameDec 2023Jan 2024Feb 2024' +
'25M26T27W28T29F30S31S1M2T3W4T5F6S7S8M9T10W11T12F13S14S15M16T17W18T19F20S21S22M23T24W25T26F27S28S29M30T31W1T' +
'Design Phase1/1 → 1/15Development1/16 → 2/28Testing3/1 → 3/15' +
'Design Phase100%Development60%Testing0%',
},
'plugin-gantt/sprint-development-timeline': {
elements: 436,
tags: { DIV: 200, STYLE: 1, BUTTON: 27, svg: 20, path: 58, SPAN: 118, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: 'af064c159287029592b508f22314336603aafacaf33bfdc67325e2212c6350d4',
visibleText:
'December 2023DayWeekMonthQuarterYearTodayThis weekThis monthTask NameDec 2023Jan 2024Feb 2024' +
'25M26T27W28T29F30S31S1M2T3W4T5F6S7S8M9T10W11T12F13S14S15M16T17W18T19F20S21S22M23T24W25T26F27S28S29M30T31W1T' +
'Sprint Planning1/1 → 1/3User Authentication1/4 → 1/10Dashboard UI1/4 → 1/12API Integration1/11 → 1/18Testing & QA1/19 → 1/25Deployment1/26 → 1/28' +
'Sprint Planning100%User Authentication100%Dashboard UI85%API Integration40%Testing & QA0%Deployment0%',
},
};

beforeAll(() => {
vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime(new Date('2026-09-03T12:00:00Z'));
});
afterAll(() => vi.useRealTimers());

/** Render one entry through the provider-wrapped bare renderer and measure what it drew. */
async function measure(schema: unknown) {
const { container, unmount } = render(
// No host DataSource: every entry authors `staticData`, which the
// renderer wraps into its own in-memory ValueDataSource. The provider
// itself is load-bearing (`useSchemaContext`).
<SchemaRendererProvider dataSource={undefined}>
<SchemaRenderer schema={toRenderableSchema(schema as never) as never} />
</SchemaRendererProvider>,
);
// The chart loads its inline rows through a ValueDataSource, so its first
// paint is the loading placeholder; measure only once that is gone.
await waitFor(() => expect(container.textContent ?? '').not.toContain('Loading Gantt chart...'));
const nodes = Array.from(container.querySelectorAll('*'));
const text = container.textContent ?? '';
const styleText = Array.from(container.querySelectorAll('style')).map((el) => el.textContent ?? '');
const tags = nodes.reduce<Record<string, number>>((h, el) => ((h[el.tagName] = (h[el.tagName] ?? 0) + 1), h), {});
const out = {
elements: nodes.length,
tags,
sha256: createHash('sha256').update(text).digest('hex'),
visibleText: styleText.reduce((t, s) => t.replace(s, ''), text),
text,
};
unmount();
return out;
}

/** Report the issues rather than `false`, so a red run says what broke. */
function reasons(schema: unknown): string[] {
const r = safeValidateSchema(schema);
return r.success ? [] : r.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`);
}

describe('objectui#6939 — the three gantt entries the mirror refused now validate', () => {
it.each(IDS)('%s validates under safeValidateSchema', (id) => {
// Each reported `: Invalid input` (the union's own top-level issue)
// before this card, for a key the renderer reads third.
expect(reasons(getExample(id).schema)).toEqual([]);
});
});

describe('objectui#6939 — and the repair moved the validator, not the renderer', () => {
it.each(IDS)('%s renders exactly what it rendered before', async (id) => {
const after = await measure(getExample(id).schema);
const before = PRE_REPAIR[id];
expect(after.elements).toBe(before.elements);
expect(after.tags).toEqual(before.tags);
expect(after.visibleText).toBe(before.visibleText);
expect(after.sha256).toBe(before.sha256);
});

it.each(IDS)('%s anti-vacuity: the tile drew its AUTHORED tasks, not an error box', async (id) => {
// An entry that renders nothing — or the error boundary — satisfies
// "identical" trivially. The authored titles on screen prove the rows
// reached the chart through `staticData`, the source the mirror ignored.
const schema = getExample(id).schema as { staticData: Record<string, string>[]; gantt: { titleField: string } };
const m = await measure(schema);
expect(m.elements).toBeGreaterThan(100);
expect(m.text).not.toContain('failed to render');
for (const row of schema.staticData) {
expect(m.visibleText).toContain(row[schema.gantt.titleField]);
}
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/6939-objectql-record-source-refinement.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@object-ui/types': patch
---

Repair the `object-map` and `object-gantt` mirrors: `objectName` is optional,
and a refinement requires that at least one of `data`, `staticData`,
`objectName` is present (objectui#6939, maintainer ruling recorded 2026-09-02 —
this is one of the eight groups on that card, dispatched as its own PR per the
ruling).

Both renderers resolve their records from one of three keys, in this order —
`getDataConfig` in `plugin-map/src/ObjectMap.tsx` and
`plugin-gantt/src/ObjectGantt.tsx`: `data`, then `staticData`, then
`objectName`. Both mirrors required `objectName` alone, so a document authored
on `staticData` drew correctly and was refused by `safeValidateSchema` — six
catalog entries, three per component.

- **`object-map`** / **`object-gantt`**: `objectName` becomes optional on the
mirror and on the TypeScript twin in the same stroke, and each member ends in
`requireRecordSource`, whose issue sits at the root path, carries
`params.code = 'RECORD_SOURCE_REQUIRED'` and names the three keys an author
can supply.
- **`object-gantt`** additionally declares `data` (as `ViewDataSchema`, the
spelling `object-map` already used): it is the FIRST key that resolver reads
and was undeclared on both faces, which would have left the refinement naming
a key the validator had never heard of.

**patch, not minor: the accept set only widens toward what already renders.**
Every document that validated before still validates — `objectName` alone,
including an empty one, still parses, because presence is `!== undefined` and
not the renderer's truthiness. The one shape the refinement refuses (none of
the three) was refused before too, when `objectName` was required. Documents
the renderers already draw start validating.
7 changes: 4 additions & 3 deletions content/docs/plugins/plugin-gantt.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,9 +118,10 @@ const schema = {
```plaintext
{
type: 'object-gantt',
objectName?: string, // ObjectQL object name
staticData?: Array<any>, // Static data array
data?: ViewData, // Advanced data configuration
objectName?: string, // ObjectQL object name (read third)
staticData?: Array<any>, // Static data array (read second)
data?: ViewData, // Advanced data configuration (read first)
// At least one of data / staticData / objectName is required
gantt?: GanttConfig, // Gantt-specific configuration
viewMode?: 'day' | 'week' | 'month' | 'quarter' | 'year',
readOnly?: boolean // disable every edit path
Expand Down
7 changes: 4 additions & 3 deletions content/docs/plugins/plugin-map.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,9 +102,10 @@ const schema = {
```plaintext
{
type: 'object-map',
objectName: string, // ObjectQL object name
staticData?: Array<any>, // Static data array
data?: ViewData, // Advanced data configuration
objectName?: string, // ObjectQL object name (read third)
staticData?: Array<any>, // Static data array (read second)
data?: ViewData, // Advanced data configuration (read first)
// At least one of data / staticData / objectName is required
filter?: Array<any>, // Query filter, sent as $filter
sort?: string | SortConfig[], // Sort, sent as $orderby
map?: ObjectMapConfig, // Map-specific configuration
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* objectui#6939, the `object-map` + `object-gantt` group — the GANTT half of
* the render pin. `ObjectGanttSchema` used to require `objectName`, a key the
* renderer reads THIRD (`getDataConfig` in `plugin-gantt/src/ObjectGantt.tsx`:
* `data`, then `staticData`, then `objectName`), so the three `staticData`-only
* catalog entries below drew correctly and were refused by `safeValidateSchema`.
* The repair makes `objectName` optional behind a refinement; the validator-side
* contract is pinned in `packages/types/src/__tests__/objectql-record-source-
* refinement-6939.test.ts`, and the `object-map` tiles are pinned in
* `packages/plugin-map/src/ObjectMap.catalogRecordSource-6939.test.tsx`.
*
* ## Why the render half is the discriminating half
*
* From objectui#6318's triage: a "correction" that renders identically proves
* the SCHEMA was wrong, not the fixture. So the repair has to clear the mirror
* image of that bar — the validator's verdict must change and the renderer's
* output must NOT. The numbers in `PRE_REPAIR` were measured on `origin/main`
* at `d88e20f55`, BEFORE the mirror was touched, through THIS file's harness.
*
* ## The harness, and why it is named
*
* A bare `SchemaRenderer` is not enough here: `ObjectGantt` calls
* `useSchemaContext`, so without a `SchemaRendererProvider` the tile is the
* error boundary — 4 elements reading `Component "object-gantt" failed to
* render` — and an identity pin over THAT is the vacuous pin this file must
* not be. Measured through the provider-wrapped bare renderer below, the three
* tiles draw 407 / 354 / 436 elements. The docs-gallery harness
* (`catalog-gallery-render.test.tsx`, provider + `SidebarProvider` + a padded
* wrapper) gives different absolute counts for the same tile; identity within
* ONE harness is the claim that discriminates. `Date` is frozen because the
* chart carries a Today marker and a date-stamped export name — the pin must
* not move with the calendar.
*
* Three readings per tile, because a count alone cannot tell a swapped element
* from an equal one: element count, a tag census, and the text — the visible
* text as a literal (the chart's own `<style>` block is part of `textContent`
* and is folded into the hash instead, at ~2.7 KB it is not a readable pin),
* plus a SHA-256 of the full `textContent`, style block included.
*/
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
import { render, waitFor } from '@testing-library/react';
import { createHash } from 'node:crypto';
import '@object-ui/components';
import '@object-ui/plugin-gantt';
import { SchemaRenderer, SchemaRendererProvider, toRenderableSchema } from '@object-ui/react';
import { safeValidateSchema } from '@object-ui/types/zod';
import { getExample } from '../src/index.js';

const IDS = [
'plugin-gantt/construction-project-phases',
'plugin-gantt/project-timeline-with-dependencies',
'plugin-gantt/sprint-development-timeline',
] as const;

/**
* Measured on `origin/main` @ `d88e20f55` through `measure()` below, mirror
* untouched. `sha256` is over the full `textContent`; `visibleText` is the
* same string with the chart's `<style>` element's text removed.
*/
const PRE_REPAIR: Record<(typeof IDS)[number], {
elements: number;
tags: Record<string, number>;
sha256: string;
visibleText: string;
}> = {
'plugin-gantt/construction-project-phases': {
elements: 407,
tags: { DIV: 185, STYLE: 1, BUTTON: 26, svg: 19, path: 52, SPAN: 112, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: '19a41af45950a5c6be4b440dfb09c154826f049be3b87e75931348a21b1746cb',
visibleText:
'January 2024DayWeekMonthQuarterYearTodayThis weekThis monthTask NameJan 2024Feb 2024Mar 2024' +
'25T26F27S28S29M30T31W1T2F3S4S5M6T7W8T9F10S11S12M13T14W15T16F17S18S19M20T21W22T23F24S25S26M27T28W29T1F2S3S' +
'Site Preparation2/1 → 2/14Foundation2/15 → 3/15Framing3/16 → 4/30Electrical & Plumbing5/1 → 5/31Interior Finishing6/1 → 7/15' +
'Site Preparation100%Foundation75%Framing30%Electrical & Plumbing0%Interior Finishing0%',
},
'plugin-gantt/project-timeline-with-dependencies': {
elements: 354,
tags: { DIV: 156, STYLE: 1, BUTTON: 24, svg: 17, path: 44, SPAN: 100, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: '3294ccbe33d159b947d01ea52f367b26ce7b4084a1ae89f0ed54661cf36002f8',
visibleText:
'December 2023DayWeekMonthQuarterYearTodayThis weekThis monthTask NameDec 2023Jan 2024Feb 2024' +
'25M26T27W28T29F30S31S1M2T3W4T5F6S7S8M9T10W11T12F13S14S15M16T17W18T19F20S21S22M23T24W25T26F27S28S29M30T31W1T' +
'Design Phase1/1 → 1/15Development1/16 → 2/28Testing3/1 → 3/15' +
'Design Phase100%Development60%Testing0%',
},
'plugin-gantt/sprint-development-timeline': {
elements: 436,
tags: { DIV: 200, STYLE: 1, BUTTON: 27, svg: 20, path: 58, SPAN: 118, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: 'af064c159287029592b508f22314336603aafacaf33bfdc67325e2212c6350d4',
visibleText:
'December 2023DayWeekMonthQuarterYearTodayThis weekThis monthTask NameDec 2023Jan 2024Feb 2024' +
'25M26T27W28T29F30S31S1M2T3W4T5F6S7S8M9T10W11T12F13S14S15M16T17W18T19F20S21S22M23T24W25T26F27S28S29M30T31W1T' +
'Sprint Planning1/1 → 1/3User Authentication1/4 → 1/10Dashboard UI1/4 → 1/12API Integration1/11 → 1/18Testing & QA1/19 → 1/25Deployment1/26 → 1/28' +
'Sprint Planning100%User Authentication100%Dashboard UI85%API Integration40%Testing & QA0%Deployment0%',
},
};

beforeAll(() => {
vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime(new Date('2026-09-03T12:00:00Z'));
});
afterAll(() => vi.useRealTimers());

/** Render one entry through the provider-wrapped bare renderer and measure what it drew. */
async function measure(schema: unknown) {
const { container, unmount } = render(
// No host DataSource: every entry authors `staticData`, which the
// renderer wraps into its own in-memory ValueDataSource. The provider
// itself is load-bearing (`useSchemaContext`).
<SchemaRendererProvider dataSource={undefined}>
<SchemaRenderer schema={toRenderableSchema(schema as never) as never} />
</SchemaRendererProvider>,
);
// The chart loads its inline rows through a ValueDataSource, so its first
// paint is the loading placeholder; measure only once that is gone.
await waitFor(() => expect(container.textContent ?? '').not.toContain('Loading Gantt chart...'));
const nodes = Array.from(container.querySelectorAll('*'));
const text = container.textContent ?? '';
const styleText = Array.from(container.querySelectorAll('style')).map((el) => el.textContent ?? '');
const tags = nodes.reduce<Record<string, number>>((h, el) => ((h[el.tagName] = (h[el.tagName] ?? 0) + 1), h), {});
const out = {
elements: nodes.length,
tags,
sha256: createHash('sha256').update(text).digest('hex'),
visibleText: styleText.reduce((t, s) => t.replace(s, ''), text),
text,
};
unmount();
return out;
}

/** Report the issues rather than `false`, so a red run says what broke. */
function reasons(schema: unknown): string[] {
const r = safeValidateSchema(schema);
return r.success ? [] : r.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`);
}

describe('objectui#6939 — the three gantt entries the mirror refused now validate', () => {
it.each(IDS)('%s validates under safeValidateSchema', (id) => {
// Each reported `: Invalid input` (the union's own top-level issue)
// before this card, for a key the renderer reads third.
expect(reasons(getExample(id).schema)).toEqual([]);
});
});

describe('objectui#6939 — and the repair moved the validator, not the renderer', () => {
it.each(IDS)('%s renders exactly what it rendered before', async (id) => {
const after = await measure(getExample(id).schema);
const before = PRE_REPAIR[id];
expect(after.elements).toBe(before.elements);
expect(after.tags).toEqual(before.tags);
expect(after.visibleText).toBe(before.visibleText);
expect(after.sha256).toBe(before.sha256);
});

it.each(IDS)('%s anti-vacuity: the tile drew its AUTHORED tasks, not an error box', async (id) => {
// An entry that renders nothing — or the error boundary — satisfies
// "identical" trivially. The authored titles on screen prove the rows
// reached the chart through `staticData`, the source the mirror ignored.
const schema = getExample(id).schema as { staticData: Record<string, string>[]; gantt: { titleField: string } };
const m = await measure(schema);
expect(m.elements).toBeGreaterThan(100);
expect(m.text).not.toContain('failed to render');
for (const row of schema.staticData) {
expect(m.visibleText).toContain(row[schema.gantt.titleField]);
}
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/6939-objectql-record-source-refinement.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@object-ui/types': patch
---

Repair the `object-map` and `object-gantt` mirrors: `objectName` is optional,
and a refinement requires that at least one of `data`, `staticData`,
`objectName` is present (objectui#6939, maintainer ruling recorded 2026-09-02 —
this is one of the eight groups on that card, dispatched as its own PR per the
ruling).

Both renderers resolve their records from one of three keys, in this order —
`getDataConfig` in `plugin-map/src/ObjectMap.tsx` and
`plugin-gantt/src/ObjectGantt.tsx`: `data`, then `staticData`, then
`objectName`. Both mirrors required `objectName` alone, so a document authored
on `staticData` drew correctly and was refused by `safeValidateSchema` — six
catalog entries, three per component.

- **`object-map`** / **`object-gantt`**: `objectName` becomes optional on the
mirror and on the TypeScript twin in the same stroke, and each member ends in
`requireRecordSource`, whose issue sits at the root path, carries
`params.code = 'RECORD_SOURCE_REQUIRED'` and names the three keys an author
can supply.
- **`object-gantt`** additionally declares `data` (as `ViewDataSchema`, the
spelling `object-map` already used): it is the FIRST key that resolver reads
and was undeclared on both faces, which would have left the refinement naming
a key the validator had never heard of.

**patch, not minor: the accept set only widens toward what already renders.**
Every document that validated before still validates — `objectName` alone,
including an empty one, still parses, because presence is `!== undefined` and
not the renderer's truthiness. The one shape the refinement refuses (none of
the three) was refused before too, when `objectName` was required. Documents
the renderers already draw start validating.
7 changes: 4 additions & 3 deletions content/docs/plugins/plugin-gantt.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,9 +118,10 @@ const schema = {
```plaintext
{
type: 'object-gantt',
objectName?: string, // ObjectQL object name
staticData?: Array<any>, // Static data array
data?: ViewData, // Advanced data configuration
objectName?: string, // ObjectQL object name (read third)
staticData?: Array<any>, // Static data array (read second)
data?: ViewData, // Advanced data configuration (read first)
// At least one of data / staticData / objectName is required
gantt?: GanttConfig, // Gantt-specific configuration
viewMode?: 'day' | 'week' | 'month' | 'quarter' | 'year',
readOnly?: boolean // disable every edit path
Expand Down
7 changes: 4 additions & 3 deletions content/docs/plugins/plugin-map.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,9 +102,10 @@ const schema = {
```plaintext
{
type: 'object-map',
objectName: string, // ObjectQL object name
staticData?: Array<any>, // Static data array
data?: ViewData, // Advanced data configuration
objectName?: string, // ObjectQL object name (read third)
staticData?: Array<any>, // Static data array (read second)
data?: ViewData, // Advanced data configuration (read first)
// At least one of data / staticData / objectName is required
filter?: Array<any>, // Query filter, sent as $filter
sort?: string | SortConfig[], // Sort, sent as $orderby
map?: ObjectMapConfig, // Map-specific configuration
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* objectui#6939, the `object-map` + `object-gantt` group — the GANTT half of
* the render pin. `ObjectGanttSchema` used to require `objectName`, a key the
* renderer reads THIRD (`getDataConfig` in `plugin-gantt/src/ObjectGantt.tsx`:
* `data`, then `staticData`, then `objectName`), so the three `staticData`-only
* catalog entries below drew correctly and were refused by `safeValidateSchema`.
* The repair makes `objectName` optional behind a refinement; the validator-side
* contract is pinned in `packages/types/src/__tests__/objectql-record-source-
* refinement-6939.test.ts`, and the `object-map` tiles are pinned in
* `packages/plugin-map/src/ObjectMap.catalogRecordSource-6939.test.tsx`.
*
* ## Why the render half is the discriminating half
*
* From objectui#6318's triage: a "correction" that renders identically proves
* the SCHEMA was wrong, not the fixture. So the repair has to clear the mirror
* image of that bar — the validator's verdict must change and the renderer's
* output must NOT. The numbers in `PRE_REPAIR` were measured on `origin/main`
* at `d88e20f55`, BEFORE the mirror was touched, through THIS file's harness.
*
* ## The harness, and why it is named
*
* A bare `SchemaRenderer` is not enough here: `ObjectGantt` calls
* `useSchemaContext`, so without a `SchemaRendererProvider` the tile is the
* error boundary — 4 elements reading `Component "object-gantt" failed to
* render` — and an identity pin over THAT is the vacuous pin this file must
* not be. Measured through the provider-wrapped bare renderer below, the three
* tiles draw 407 / 354 / 436 elements. The docs-gallery harness
* (`catalog-gallery-render.test.tsx`, provider + `SidebarProvider` + a padded
* wrapper) gives different absolute counts for the same tile; identity within
* ONE harness is the claim that discriminates. `Date` is frozen because the
* chart carries a Today marker and a date-stamped export name — the pin must
* not move with the calendar.
*
* Three readings per tile, because a count alone cannot tell a swapped element
* from an equal one: element count, a tag census, and the text — the visible
* text as a literal (the chart's own `<style>` block is part of `textContent`
* and is folded into the hash instead, at ~2.7 KB it is not a readable pin),
* plus a SHA-256 of the full `textContent`, style block included.
*/
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
import { render, waitFor } from '@testing-library/react';
import { createHash } from 'node:crypto';
import '@object-ui/components';
import '@object-ui/plugin-gantt';
import { SchemaRenderer, SchemaRendererProvider, toRenderableSchema } from '@object-ui/react';
import { safeValidateSchema } from '@object-ui/types/zod';
import { getExample } from '../src/index.js';

const IDS = [
'plugin-gantt/construction-project-phases',
'plugin-gantt/project-timeline-with-dependencies',
'plugin-gantt/sprint-development-timeline',
] as const;

/**
* Measured on `origin/main` @ `d88e20f55` through `measure()` below, mirror
* untouched. `sha256` is over the full `textContent`; `visibleText` is the
* same string with the chart's `<style>` element's text removed.
*/
const PRE_REPAIR: Record<(typeof IDS)[number], {
elements: number;
tags: Record<string, number>;
sha256: string;
visibleText: string;
}> = {
'plugin-gantt/construction-project-phases': {
elements: 407,
tags: { DIV: 185, STYLE: 1, BUTTON: 26, svg: 19, path: 52, SPAN: 112, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: '19a41af45950a5c6be4b440dfb09c154826f049be3b87e75931348a21b1746cb',
visibleText:
'January 2024DayWeekMonthQuarterYearTodayThis weekThis monthTask NameJan 2024Feb 2024Mar 2024' +
'25T26F27S28S29M30T31W1T2F3S4S5M6T7W8T9F10S11S12M13T14W15T16F17S18S19M20T21W22T23F24S25S26M27T28W29T1F2S3S' +
'Site Preparation2/1 → 2/14Foundation2/15 → 3/15Framing3/16 → 4/30Electrical & Plumbing5/1 → 5/31Interior Finishing6/1 → 7/15' +
'Site Preparation100%Foundation75%Framing30%Electrical & Plumbing0%Interior Finishing0%',
},
'plugin-gantt/project-timeline-with-dependencies': {
elements: 354,
tags: { DIV: 156, STYLE: 1, BUTTON: 24, svg: 17, path: 44, SPAN: 100, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: '3294ccbe33d159b947d01ea52f367b26ce7b4084a1ae89f0ed54661cf36002f8',
visibleText:
'December 2023DayWeekMonthQuarterYearTodayThis weekThis monthTask NameDec 2023Jan 2024Feb 2024' +
'25M26T27W28T29F30S31S1M2T3W4T5F6S7S8M9T10W11T12F13S14S15M16T17W18T19F20S21S22M23T24W25T26F27S28S29M30T31W1T' +
'Design Phase1/1 → 1/15Development1/16 → 2/28Testing3/1 → 3/15' +
'Design Phase100%Development60%Testing0%',
},
'plugin-gantt/sprint-development-timeline': {
elements: 436,
tags: { DIV: 200, STYLE: 1, BUTTON: 27, svg: 20, path: 58, SPAN: 118, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: 'af064c159287029592b508f22314336603aafacaf33bfdc67325e2212c6350d4',
visibleText:
'December 2023DayWeekMonthQuarterYearTodayThis weekThis monthTask NameDec 2023Jan 2024Feb 2024' +
'25M26T27W28T29F30S31S1M2T3W4T5F6S7S8M9T10W11T12F13S14S15M16T17W18T19F20S21S22M23T24W25T26F27S28S29M30T31W1T' +
'Sprint Planning1/1 → 1/3User Authentication1/4 → 1/10Dashboard UI1/4 → 1/12API Integration1/11 → 1/18Testing & QA1/19 → 1/25Deployment1/26 → 1/28' +
'Sprint Planning100%User Authentication100%Dashboard UI85%API Integration40%Testing & QA0%Deployment0%',
},
};

beforeAll(() => {
vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime(new Date('2026-09-03T12:00:00Z'));
});
afterAll(() => vi.useRealTimers());

/** Render one entry through the provider-wrapped bare renderer and measure what it drew. */
async function measure(schema: unknown) {
const { container, unmount } = render(
// No host DataSource: every entry authors `staticData`, which the
// renderer wraps into its own in-memory ValueDataSource. The provider
// itself is load-bearing (`useSchemaContext`).
<SchemaRendererProvider dataSource={undefined}>
<SchemaRenderer schema={toRenderableSchema(schema as never) as never} />
</SchemaRendererProvider>,
);
// The chart loads its inline rows through a ValueDataSource, so its first
// paint is the loading placeholder; measure only once that is gone.
await waitFor(() => expect(container.textContent ?? '').not.toContain('Loading Gantt chart...'));
const nodes = Array.from(container.querySelectorAll('*'));
const text = container.textContent ?? '';
const styleText = Array.from(container.querySelectorAll('style')).map((el) => el.textContent ?? '');
const tags = nodes.reduce<Record<string, number>>((h, el) => ((h[el.tagName] = (h[el.tagName] ?? 0) + 1), h), {});
const out = {
elements: nodes.length,
tags,
sha256: createHash('sha256').update(text).digest('hex'),
visibleText: styleText.reduce((t, s) => t.replace(s, ''), text),
text,
};
unmount();
return out;
}

/** Report the issues rather than `false`, so a red run says what broke. */
function reasons(schema: unknown): string[] {
const r = safeValidateSchema(schema);
return r.success ? [] : r.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`);
}

describe('objectui#6939 — the three gantt entries the mirror refused now validate', () => {
it.each(IDS)('%s validates under safeValidateSchema', (id) => {
// Each reported `: Invalid input` (the union's own top-level issue)
// before this card, for a key the renderer reads third.
expect(reasons(getExample(id).schema)).toEqual([]);
});
});

describe('objectui#6939 — and the repair moved the validator, not the renderer', () => {
it.each(IDS)('%s renders exactly what it rendered before', async (id) => {
const after = await measure(getExample(id).schema);
const before = PRE_REPAIR[id];
expect(after.elements).toBe(before.elements);
expect(after.tags).toEqual(before.tags);
expect(after.visibleText).toBe(before.visibleText);
expect(after.sha256).toBe(before.sha256);
});

it.each(IDS)('%s anti-vacuity: the tile drew its AUTHORED tasks, not an error box', async (id) => {
// An entry that renders nothing — or the error boundary — satisfies
// "identical" trivially. The authored titles on screen prove the rows
// reached the chart through `staticData`, the source the mirror ignored.
const schema = getExample(id).schema as { staticData: Record<string, string>[]; gantt: { titleField: string } };
const m = await measure(schema);
expect(m.elements).toBeGreaterThan(100);
expect(m.text).not.toContain('failed to render');
for (const row of schema.staticData) {
expect(m.visibleText).toContain(row[schema.gantt.titleField]);
}
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/6939-objectql-record-source-refinement.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@object-ui/types': patch
---

Repair the `object-map` and `object-gantt` mirrors: `objectName` is optional,
and a refinement requires that at least one of `data`, `staticData`,
`objectName` is present (objectui#6939, maintainer ruling recorded 2026-09-02 —
this is one of the eight groups on that card, dispatched as its own PR per the
ruling).

Both renderers resolve their records from one of three keys, in this order —
`getDataConfig` in `plugin-map/src/ObjectMap.tsx` and
`plugin-gantt/src/ObjectGantt.tsx`: `data`, then `staticData`, then
`objectName`. Both mirrors required `objectName` alone, so a document authored
on `staticData` drew correctly and was refused by `safeValidateSchema` — six
catalog entries, three per component.

- **`object-map`** / **`object-gantt`**: `objectName` becomes optional on the
mirror and on the TypeScript twin in the same stroke, and each member ends in
`requireRecordSource`, whose issue sits at the root path, carries
`params.code = 'RECORD_SOURCE_REQUIRED'` and names the three keys an author
can supply.
- **`object-gantt`** additionally declares `data` (as `ViewDataSchema`, the
spelling `object-map` already used): it is the FIRST key that resolver reads
and was undeclared on both faces, which would have left the refinement naming
a key the validator had never heard of.

**patch, not minor: the accept set only widens toward what already renders.**
Every document that validated before still validates — `objectName` alone,
including an empty one, still parses, because presence is `!== undefined` and
not the renderer's truthiness. The one shape the refinement refuses (none of
the three) was refused before too, when `objectName` was required. Documents
the renderers already draw start validating.
7 changes: 4 additions & 3 deletions content/docs/plugins/plugin-gantt.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,9 +118,10 @@ const schema = {
```plaintext
{
type: 'object-gantt',
objectName?: string, // ObjectQL object name
staticData?: Array<any>, // Static data array
data?: ViewData, // Advanced data configuration
objectName?: string, // ObjectQL object name (read third)
staticData?: Array<any>, // Static data array (read second)
data?: ViewData, // Advanced data configuration (read first)
// At least one of data / staticData / objectName is required
gantt?: GanttConfig, // Gantt-specific configuration
viewMode?: 'day' | 'week' | 'month' | 'quarter' | 'year',
readOnly?: boolean // disable every edit path
Expand Down
7 changes: 4 additions & 3 deletions content/docs/plugins/plugin-map.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,9 +102,10 @@ const schema = {
```plaintext
{
type: 'object-map',
objectName: string, // ObjectQL object name
staticData?: Array<any>, // Static data array
data?: ViewData, // Advanced data configuration
objectName?: string, // ObjectQL object name (read third)
staticData?: Array<any>, // Static data array (read second)
data?: ViewData, // Advanced data configuration (read first)
// At least one of data / staticData / objectName is required
filter?: Array<any>, // Query filter, sent as $filter
sort?: string | SortConfig[], // Sort, sent as $orderby
map?: ObjectMapConfig, // Map-specific configuration
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* objectui#6939, the `object-map` + `object-gantt` group — the GANTT half of
* the render pin. `ObjectGanttSchema` used to require `objectName`, a key the
* renderer reads THIRD (`getDataConfig` in `plugin-gantt/src/ObjectGantt.tsx`:
* `data`, then `staticData`, then `objectName`), so the three `staticData`-only
* catalog entries below drew correctly and were refused by `safeValidateSchema`.
* The repair makes `objectName` optional behind a refinement; the validator-side
* contract is pinned in `packages/types/src/__tests__/objectql-record-source-
* refinement-6939.test.ts`, and the `object-map` tiles are pinned in
* `packages/plugin-map/src/ObjectMap.catalogRecordSource-6939.test.tsx`.
*
* ## Why the render half is the discriminating half
*
* From objectui#6318's triage: a "correction" that renders identically proves
* the SCHEMA was wrong, not the fixture. So the repair has to clear the mirror
* image of that bar — the validator's verdict must change and the renderer's
* output must NOT. The numbers in `PRE_REPAIR` were measured on `origin/main`
* at `d88e20f55`, BEFORE the mirror was touched, through THIS file's harness.
*
* ## The harness, and why it is named
*
* A bare `SchemaRenderer` is not enough here: `ObjectGantt` calls
* `useSchemaContext`, so without a `SchemaRendererProvider` the tile is the
* error boundary — 4 elements reading `Component "object-gantt" failed to
* render` — and an identity pin over THAT is the vacuous pin this file must
* not be. Measured through the provider-wrapped bare renderer below, the three
* tiles draw 407 / 354 / 436 elements. The docs-gallery harness
* (`catalog-gallery-render.test.tsx`, provider + `SidebarProvider` + a padded
* wrapper) gives different absolute counts for the same tile; identity within
* ONE harness is the claim that discriminates. `Date` is frozen because the
* chart carries a Today marker and a date-stamped export name — the pin must
* not move with the calendar.
*
* Three readings per tile, because a count alone cannot tell a swapped element
* from an equal one: element count, a tag census, and the text — the visible
* text as a literal (the chart's own `<style>` block is part of `textContent`
* and is folded into the hash instead, at ~2.7 KB it is not a readable pin),
* plus a SHA-256 of the full `textContent`, style block included.
*/
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
import { render, waitFor } from '@testing-library/react';
import { createHash } from 'node:crypto';
import '@object-ui/components';
import '@object-ui/plugin-gantt';
import { SchemaRenderer, SchemaRendererProvider, toRenderableSchema } from '@object-ui/react';
import { safeValidateSchema } from '@object-ui/types/zod';
import { getExample } from '../src/index.js';

const IDS = [
'plugin-gantt/construction-project-phases',
'plugin-gantt/project-timeline-with-dependencies',
'plugin-gantt/sprint-development-timeline',
] as const;

/**
* Measured on `origin/main` @ `d88e20f55` through `measure()` below, mirror
* untouched. `sha256` is over the full `textContent`; `visibleText` is the
* same string with the chart's `<style>` element's text removed.
*/
const PRE_REPAIR: Record<(typeof IDS)[number], {
elements: number;
tags: Record<string, number>;
sha256: string;
visibleText: string;
}> = {
'plugin-gantt/construction-project-phases': {
elements: 407,
tags: { DIV: 185, STYLE: 1, BUTTON: 26, svg: 19, path: 52, SPAN: 112, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: '19a41af45950a5c6be4b440dfb09c154826f049be3b87e75931348a21b1746cb',
visibleText:
'January 2024DayWeekMonthQuarterYearTodayThis weekThis monthTask NameJan 2024Feb 2024Mar 2024' +
'25T26F27S28S29M30T31W1T2F3S4S5M6T7W8T9F10S11S12M13T14W15T16F17S18S19M20T21W22T23F24S25S26M27T28W29T1F2S3S' +
'Site Preparation2/1 → 2/14Foundation2/15 → 3/15Framing3/16 → 4/30Electrical & Plumbing5/1 → 5/31Interior Finishing6/1 → 7/15' +
'Site Preparation100%Foundation75%Framing30%Electrical & Plumbing0%Interior Finishing0%',
},
'plugin-gantt/project-timeline-with-dependencies': {
elements: 354,
tags: { DIV: 156, STYLE: 1, BUTTON: 24, svg: 17, path: 44, SPAN: 100, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: '3294ccbe33d159b947d01ea52f367b26ce7b4084a1ae89f0ed54661cf36002f8',
visibleText:
'December 2023DayWeekMonthQuarterYearTodayThis weekThis monthTask NameDec 2023Jan 2024Feb 2024' +
'25M26T27W28T29F30S31S1M2T3W4T5F6S7S8M9T10W11T12F13S14S15M16T17W18T19F20S21S22M23T24W25T26F27S28S29M30T31W1T' +
'Design Phase1/1 → 1/15Development1/16 → 2/28Testing3/1 → 3/15' +
'Design Phase100%Development60%Testing0%',
},
'plugin-gantt/sprint-development-timeline': {
elements: 436,
tags: { DIV: 200, STYLE: 1, BUTTON: 27, svg: 20, path: 58, SPAN: 118, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: 'af064c159287029592b508f22314336603aafacaf33bfdc67325e2212c6350d4',
visibleText:
'December 2023DayWeekMonthQuarterYearTodayThis weekThis monthTask NameDec 2023Jan 2024Feb 2024' +
'25M26T27W28T29F30S31S1M2T3W4T5F6S7S8M9T10W11T12F13S14S15M16T17W18T19F20S21S22M23T24W25T26F27S28S29M30T31W1T' +
'Sprint Planning1/1 → 1/3User Authentication1/4 → 1/10Dashboard UI1/4 → 1/12API Integration1/11 → 1/18Testing & QA1/19 → 1/25Deployment1/26 → 1/28' +
'Sprint Planning100%User Authentication100%Dashboard UI85%API Integration40%Testing & QA0%Deployment0%',
},
};

beforeAll(() => {
vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime(new Date('2026-09-03T12:00:00Z'));
});
afterAll(() => vi.useRealTimers());

/** Render one entry through the provider-wrapped bare renderer and measure what it drew. */
async function measure(schema: unknown) {
const { container, unmount } = render(
// No host DataSource: every entry authors `staticData`, which the
// renderer wraps into its own in-memory ValueDataSource. The provider
// itself is load-bearing (`useSchemaContext`).
<SchemaRendererProvider dataSource={undefined}>
<SchemaRenderer schema={toRenderableSchema(schema as never) as never} />
</SchemaRendererProvider>,
);
// The chart loads its inline rows through a ValueDataSource, so its first
// paint is the loading placeholder; measure only once that is gone.
await waitFor(() => expect(container.textContent ?? '').not.toContain('Loading Gantt chart...'));
const nodes = Array.from(container.querySelectorAll('*'));
const text = container.textContent ?? '';
const styleText = Array.from(container.querySelectorAll('style')).map((el) => el.textContent ?? '');
const tags = nodes.reduce<Record<string, number>>((h, el) => ((h[el.tagName] = (h[el.tagName] ?? 0) + 1), h), {});
const out = {
elements: nodes.length,
tags,
sha256: createHash('sha256').update(text).digest('hex'),
visibleText: styleText.reduce((t, s) => t.replace(s, ''), text),
text,
};
unmount();
return out;
}

/** Report the issues rather than `false`, so a red run says what broke. */
function reasons(schema: unknown): string[] {
const r = safeValidateSchema(schema);
return r.success ? [] : r.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`);
}

describe('objectui#6939 — the three gantt entries the mirror refused now validate', () => {
it.each(IDS)('%s validates under safeValidateSchema', (id) => {
// Each reported `: Invalid input` (the union's own top-level issue)
// before this card, for a key the renderer reads third.
expect(reasons(getExample(id).schema)).toEqual([]);
});
});

describe('objectui#6939 — and the repair moved the validator, not the renderer', () => {
it.each(IDS)('%s renders exactly what it rendered before', async (id) => {
const after = await measure(getExample(id).schema);
const before = PRE_REPAIR[id];
expect(after.elements).toBe(before.elements);
expect(after.tags).toEqual(before.tags);
expect(after.visibleText).toBe(before.visibleText);
expect(after.sha256).toBe(before.sha256);
});

it.each(IDS)('%s anti-vacuity: the tile drew its AUTHORED tasks, not an error box', async (id) => {
// An entry that renders nothing — or the error boundary — satisfies
// "identical" trivially. The authored titles on screen prove the rows
// reached the chart through `staticData`, the source the mirror ignored.
const schema = getExample(id).schema as { staticData: Record<string, string>[]; gantt: { titleField: string } };
const m = await measure(schema);
expect(m.elements).toBeGreaterThan(100);
expect(m.text).not.toContain('failed to render');
for (const row of schema.staticData) {
expect(m.visibleText).toContain(row[schema.gantt.titleField]);
}
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/6939-objectql-record-source-refinement.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@object-ui/types': patch
---

Repair the `object-map` and `object-gantt` mirrors: `objectName` is optional,
and a refinement requires that at least one of `data`, `staticData`,
`objectName` is present (objectui#6939, maintainer ruling recorded 2026-09-02 —
this is one of the eight groups on that card, dispatched as its own PR per the
ruling).

Both renderers resolve their records from one of three keys, in this order —
`getDataConfig` in `plugin-map/src/ObjectMap.tsx` and
`plugin-gantt/src/ObjectGantt.tsx`: `data`, then `staticData`, then
`objectName`. Both mirrors required `objectName` alone, so a document authored
on `staticData` drew correctly and was refused by `safeValidateSchema` — six
catalog entries, three per component.

- **`object-map`** / **`object-gantt`**: `objectName` becomes optional on the
mirror and on the TypeScript twin in the same stroke, and each member ends in
`requireRecordSource`, whose issue sits at the root path, carries
`params.code = 'RECORD_SOURCE_REQUIRED'` and names the three keys an author
can supply.
- **`object-gantt`** additionally declares `data` (as `ViewDataSchema`, the
spelling `object-map` already used): it is the FIRST key that resolver reads
and was undeclared on both faces, which would have left the refinement naming
a key the validator had never heard of.

**patch, not minor: the accept set only widens toward what already renders.**
Every document that validated before still validates — `objectName` alone,
including an empty one, still parses, because presence is `!== undefined` and
not the renderer's truthiness. The one shape the refinement refuses (none of
the three) was refused before too, when `objectName` was required. Documents
the renderers already draw start validating.
7 changes: 4 additions & 3 deletions content/docs/plugins/plugin-gantt.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,9 +118,10 @@ const schema = {
```plaintext
{
type: 'object-gantt',
objectName?: string, // ObjectQL object name
staticData?: Array<any>, // Static data array
data?: ViewData, // Advanced data configuration
objectName?: string, // ObjectQL object name (read third)
staticData?: Array<any>, // Static data array (read second)
data?: ViewData, // Advanced data configuration (read first)
// At least one of data / staticData / objectName is required
gantt?: GanttConfig, // Gantt-specific configuration
viewMode?: 'day' | 'week' | 'month' | 'quarter' | 'year',
readOnly?: boolean // disable every edit path
Expand Down
7 changes: 4 additions & 3 deletions content/docs/plugins/plugin-map.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,9 +102,10 @@ const schema = {
```plaintext
{
type: 'object-map',
objectName: string, // ObjectQL object name
staticData?: Array<any>, // Static data array
data?: ViewData, // Advanced data configuration
objectName?: string, // ObjectQL object name (read third)
staticData?: Array<any>, // Static data array (read second)
data?: ViewData, // Advanced data configuration (read first)
// At least one of data / staticData / objectName is required
filter?: Array<any>, // Query filter, sent as $filter
sort?: string | SortConfig[], // Sort, sent as $orderby
map?: ObjectMapConfig, // Map-specific configuration
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* objectui#6939, the `object-map` + `object-gantt` group — the GANTT half of
* the render pin. `ObjectGanttSchema` used to require `objectName`, a key the
* renderer reads THIRD (`getDataConfig` in `plugin-gantt/src/ObjectGantt.tsx`:
* `data`, then `staticData`, then `objectName`), so the three `staticData`-only
* catalog entries below drew correctly and were refused by `safeValidateSchema`.
* The repair makes `objectName` optional behind a refinement; the validator-side
* contract is pinned in `packages/types/src/__tests__/objectql-record-source-
* refinement-6939.test.ts`, and the `object-map` tiles are pinned in
* `packages/plugin-map/src/ObjectMap.catalogRecordSource-6939.test.tsx`.
*
* ## Why the render half is the discriminating half
*
* From objectui#6318's triage: a "correction" that renders identically proves
* the SCHEMA was wrong, not the fixture. So the repair has to clear the mirror
* image of that bar — the validator's verdict must change and the renderer's
* output must NOT. The numbers in `PRE_REPAIR` were measured on `origin/main`
* at `d88e20f55`, BEFORE the mirror was touched, through THIS file's harness.
*
* ## The harness, and why it is named
*
* A bare `SchemaRenderer` is not enough here: `ObjectGantt` calls
* `useSchemaContext`, so without a `SchemaRendererProvider` the tile is the
* error boundary — 4 elements reading `Component "object-gantt" failed to
* render` — and an identity pin over THAT is the vacuous pin this file must
* not be. Measured through the provider-wrapped bare renderer below, the three
* tiles draw 407 / 354 / 436 elements. The docs-gallery harness
* (`catalog-gallery-render.test.tsx`, provider + `SidebarProvider` + a padded
* wrapper) gives different absolute counts for the same tile; identity within
* ONE harness is the claim that discriminates. `Date` is frozen because the
* chart carries a Today marker and a date-stamped export name — the pin must
* not move with the calendar.
*
* Three readings per tile, because a count alone cannot tell a swapped element
* from an equal one: element count, a tag census, and the text — the visible
* text as a literal (the chart's own `<style>` block is part of `textContent`
* and is folded into the hash instead, at ~2.7 KB it is not a readable pin),
* plus a SHA-256 of the full `textContent`, style block included.
*/
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
import { render, waitFor } from '@testing-library/react';
import { createHash } from 'node:crypto';
import '@object-ui/components';
import '@object-ui/plugin-gantt';
import { SchemaRenderer, SchemaRendererProvider, toRenderableSchema } from '@object-ui/react';
import { safeValidateSchema } from '@object-ui/types/zod';
import { getExample } from '../src/index.js';

const IDS = [
'plugin-gantt/construction-project-phases',
'plugin-gantt/project-timeline-with-dependencies',
'plugin-gantt/sprint-development-timeline',
] as const;

/**
* Measured on `origin/main` @ `d88e20f55` through `measure()` below, mirror
* untouched. `sha256` is over the full `textContent`; `visibleText` is the
* same string with the chart's `<style>` element's text removed.
*/
const PRE_REPAIR: Record<(typeof IDS)[number], {
elements: number;
tags: Record<string, number>;
sha256: string;
visibleText: string;
}> = {
'plugin-gantt/construction-project-phases': {
elements: 407,
tags: { DIV: 185, STYLE: 1, BUTTON: 26, svg: 19, path: 52, SPAN: 112, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: '19a41af45950a5c6be4b440dfb09c154826f049be3b87e75931348a21b1746cb',
visibleText:
'January 2024DayWeekMonthQuarterYearTodayThis weekThis monthTask NameJan 2024Feb 2024Mar 2024' +
'25T26F27S28S29M30T31W1T2F3S4S5M6T7W8T9F10S11S12M13T14W15T16F17S18S19M20T21W22T23F24S25S26M27T28W29T1F2S3S' +
'Site Preparation2/1 → 2/14Foundation2/15 → 3/15Framing3/16 → 4/30Electrical & Plumbing5/1 → 5/31Interior Finishing6/1 → 7/15' +
'Site Preparation100%Foundation75%Framing30%Electrical & Plumbing0%Interior Finishing0%',
},
'plugin-gantt/project-timeline-with-dependencies': {
elements: 354,
tags: { DIV: 156, STYLE: 1, BUTTON: 24, svg: 17, path: 44, SPAN: 100, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: '3294ccbe33d159b947d01ea52f367b26ce7b4084a1ae89f0ed54661cf36002f8',
visibleText:
'December 2023DayWeekMonthQuarterYearTodayThis weekThis monthTask NameDec 2023Jan 2024Feb 2024' +
'25M26T27W28T29F30S31S1M2T3W4T5F6S7S8M9T10W11T12F13S14S15M16T17W18T19F20S21S22M23T24W25T26F27S28S29M30T31W1T' +
'Design Phase1/1 → 1/15Development1/16 → 2/28Testing3/1 → 3/15' +
'Design Phase100%Development60%Testing0%',
},
'plugin-gantt/sprint-development-timeline': {
elements: 436,
tags: { DIV: 200, STYLE: 1, BUTTON: 27, svg: 20, path: 58, SPAN: 118, circle: 2, line: 5, rect: 1, defs: 1, marker: 3 },
sha256: 'af064c159287029592b508f22314336603aafacaf33bfdc67325e2212c6350d4',
visibleText:
'December 2023DayWeekMonthQuarterYearTodayThis weekThis monthTask NameDec 2023Jan 2024Feb 2024' +
'25M26T27W28T29F30S31S1M2T3W4T5F6S7S8M9T10W11T12F13S14S15M16T17W18T19F20S21S22M23T24W25T26F27S28S29M30T31W1T' +
'Sprint Planning1/1 → 1/3User Authentication1/4 → 1/10Dashboard UI1/4 → 1/12API Integration1/11 → 1/18Testing & QA1/19 → 1/25Deployment1/26 → 1/28' +
'Sprint Planning100%User Authentication100%Dashboard UI85%API Integration40%Testing & QA0%Deployment0%',
},
};

beforeAll(() => {
vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime(new Date('2026-09-03T12:00:00Z'));
});
afterAll(() => vi.useRealTimers());

/** Render one entry through the provider-wrapped bare renderer and measure what it drew. */
async function measure(schema: unknown) {
const { container, unmount } = render(
// No host DataSource: every entry authors `staticData`, which the
// renderer wraps into its own in-memory ValueDataSource. The provider
// itself is load-bearing (`useSchemaContext`).
<SchemaRendererProvider dataSource={undefined}>
<SchemaRenderer schema={toRenderableSchema(schema as never) as never} />
</SchemaRendererProvider>,
);
// The chart loads its inline rows through a ValueDataSource, so its first
// paint is the loading placeholder; measure only once that is gone.
await waitFor(() => expect(container.textContent ?? '').not.toContain('Loading Gantt chart...'));
const nodes = Array.from(container.querySelectorAll('*'));
const text = container.textContent ?? '';
const styleText = Array.from(container.querySelectorAll('style')).map((el) => el.textContent ?? '');
const tags = nodes.reduce<Record<string, number>>((h, el) => ((h[el.tagName] = (h[el.tagName] ?? 0) + 1), h), {});
const out = {
elements: nodes.length,
tags,
sha256: createHash('sha256').update(text).digest('hex'),
visibleText: styleText.reduce((t, s) => t.replace(s, ''), text),
text,
};
unmount();
return out;
}

/** Report the issues rather than `false`, so a red run says what broke. */
function reasons(schema: unknown): string[] {
const r = safeValidateSchema(schema);
return r.success ? [] : r.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`);
}

describe('objectui#6939 — the three gantt entries the mirror refused now validate', () => {
it.each(IDS)('%s validates under safeValidateSchema', (id) => {
// Each reported `: Invalid input` (the union's own top-level issue)
// before this card, for a key the renderer reads third.
expect(reasons(getExample(id).schema)).toEqual([]);
});
});

describe('objectui#6939 — and the repair moved the validator, not the renderer', () => {
it.each(IDS)('%s renders exactly what it rendered before', async (id) => {
const after = await measure(getExample(id).schema);
const before = PRE_REPAIR[id];
expect(after.elements).toBe(before.elements);
expect(after.tags).toEqual(before.tags);
expect(after.visibleText).toBe(before.visibleText);
expect(after.sha256).toBe(before.sha256);
});

it.each(IDS)('%s anti-vacuity: the tile drew its AUTHORED tasks, not an error box', async (id) => {
// An entry that renders nothing — or the error boundary — satisfies
// "identical" trivially. The authored titles on screen prove the rows
// reached the chart through `staticData`, the source the mirror ignored.
const schema = getExample(id).schema as { staticData: Record<string, string>[]; gantt: { titleField: string } };
const m = await measure(schema);
expect(m.elements).toBeGreaterThan(100);
expect(m.text).not.toContain('failed to render');
for (const row of schema.staticData) {
expect(m.visibleText).toContain(row[schema.gantt.titleField]);
}
});
});
Loading
Loading