From 21cfe31421509daf3f959bf2e20dfcbcb07af0ee Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 11:43:23 +0000 Subject: [PATCH 1/2] docs(plugin-gantt): state the navConfig comment once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comment block above `navConfig` carried a verbatim repeat of its own last two lines, so the text ran on into a fragment: "…providing its own `navigation` config (e.g., page mode). detail panel inline (no full-page navigation). Schema can override by providing its own `navigation` config (e.g., page mode)." The first three lines already state the sentence completely and in the right order; the two removed lines were a byte-for-byte duplicate of the two before them. Comment-only, no behaviour change. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q --- .changeset/gantt-navconfig-comment-once.md | 6 ++++++ packages/plugin-gantt/src/ObjectGantt.tsx | 2 -- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 .changeset/gantt-navconfig-comment-once.md diff --git a/.changeset/gantt-navconfig-comment-once.md b/.changeset/gantt-navconfig-comment-once.md new file mode 100644 index 0000000000..18a653f6c8 --- /dev/null +++ b/.changeset/gantt-navconfig-comment-once.md @@ -0,0 +1,6 @@ +--- +--- + +Comment-only cleanup in `ObjectGantt`: the block above the `navConfig` default +repeated its own last two lines verbatim, leaving a mid-sentence fragment. The +sentence is now stated once, in full. No published behaviour changes. diff --git a/packages/plugin-gantt/src/ObjectGantt.tsx b/packages/plugin-gantt/src/ObjectGantt.tsx index 22d30b9510..82ede3884e 100644 --- a/packages/plugin-gantt/src/ObjectGantt.tsx +++ b/packages/plugin-gantt/src/ObjectGantt.tsx @@ -1147,8 +1147,6 @@ export const ObjectGantt: React.FC = ({ // Default to a right-side drawer so clicking a task opens an editable // detail panel inline (no full-page navigation). Schema can override by // providing its own `navigation` config (e.g., page mode). - // detail panel inline (no full-page navigation). Schema can override by - // providing its own `navigation` config (e.g., page mode). const navConfig = schema.navigation ?? { mode: 'drawer', width: 'min(960px, 60vw)' }; const navIsOverlay = navConfig.mode === 'drawer' || navConfig.mode === 'modal' || navConfig.mode === 'split' || navConfig.mode === 'popover'; const navigation = useNavigationOverlay({ From ae7898efb0479197eaaf56900cc978b6b1b44a91 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 12:19:04 +0000 Subject: [PATCH 2/2] refactor(plugin-gantt): drop the deprecated `width` from the drawer default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The renderer default was `{ mode: 'drawer', width: 'min(960px, 60vw)' }`. `width` is `@deprecated [#2578 -> size]` in the spec that owns the shape, and `resolveOverlayWidth` gives an explicit `width` priority OVER `size` — so while the default spelled it, a gantt declaring no `navigation` could never be reached by the size buckets, and the deprecated branch stayed load-bearing on the path most gantts take. The default is now `{ mode: 'drawer' }`. `resolveOverlayWidth` returns `undefined` and RecordDetailDrawer's own `width` default supplies the identical `min(960px, 60vw)`, so the resolved overlay width is unchanged on every viewport — measured, not assumed. Deliberately not converged on `size: 'lg'`: that bucket is `min(92vw, 960px)`, which matches the current default only at viewport >= 1600px and is up to 53% wider below it (+11% at 1440px, +25% at 1280px). That is a real layout change across three renderers and stays with the maintainer. Nothing pinned this width before — a repo-wide search returned only producers, zero assertions, and the full 402-test gantt suite stayed green when the value was changed. ObjectGantt.navWidthDefault.test.tsx now pins both halves: that the gantt injects no width of its own, and that the real drawer still resolves to the same value. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q --- .changeset/gantt-navconfig-comment-once.md | 15 ++- .../src/ObjectGantt.navWidthDefault.test.tsx | 106 ++++++++++++++++++ packages/plugin-gantt/src/ObjectGantt.tsx | 19 +++- 3 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx diff --git a/.changeset/gantt-navconfig-comment-once.md b/.changeset/gantt-navconfig-comment-once.md index 18a653f6c8..0390f51d96 100644 --- a/.changeset/gantt-navconfig-comment-once.md +++ b/.changeset/gantt-navconfig-comment-once.md @@ -1,6 +1,15 @@ --- --- -Comment-only cleanup in `ObjectGantt`: the block above the `navConfig` default -repeated its own last two lines verbatim, leaving a mid-sentence fragment. The -sentence is now stated once, in full. No published behaviour changes. +`ObjectGantt` internal cleanup, measured as a zero-pixel change: + +- The comment block above the `navConfig` default repeated its own last two + lines verbatim, leaving a mid-sentence fragment. The sentence is now stated + once, in full. +- The drawer default no longer spells the spec-deprecated `width` + (`@deprecated [#2578 -> size]`). It is now `{ mode: 'drawer' }`, so + `resolveOverlayWidth` returns `undefined` and `RecordDetailDrawer`'s own + `width` default supplies the identical `min(960px, 60vw)`. The resolved + overlay width is unchanged on every viewport, and is now pinned by a test. + +No published behaviour changes. diff --git a/packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx b/packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx new file mode 100644 index 0000000000..28f37a60d3 --- /dev/null +++ b/packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx @@ -0,0 +1,106 @@ +/** + * Pins the drawer width a gantt gets when it declares no `navigation`. + * + * ObjectGantt used to hard-code `{ mode: 'drawer', width: 'min(960px, 60vw)' }` + * as that default. `width` is `@deprecated [#2578 -> size]` and + * `resolveOverlayWidth` gives an explicit `width` priority OVER `size`, so + * spelling it kept the deprecated branch load-bearing on the path most gantts + * take. The default is now `{ mode: 'drawer' }`: `resolveOverlayWidth` returns + * `undefined` and RecordDetailDrawer's own `width` default supplies the + * identical CSS — a zero-pixel change. + * + * That equivalence was previously pinned by NOTHING: a repo-wide search for + * `min(960px, 60vw)` returned only producers, zero assertions, and the whole + * 402-test gantt suite stayed green when the value was changed. Both halves + * below are load-bearing and fail for different reasons: + * + * half 1 — the gantt must stop injecting a width of its own (it has to hand + * `undefined` down, or the drawer's default can never apply); + * half 2 — the width the REAL drawer then resolves must still be that value. + * Without this half the gantt would follow a moved drawer default + * invisibly, which is the regression the indirection introduces. + * + * Both assert the resolved width VALUE — never a `className`, never "it + * renders", either of which passes in both worlds. + */ +import React from 'react'; +import { render, screen, waitFor, fireEvent } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { ObjectGantt } from './ObjectGantt'; + +/** The width the gantt's drawer has always resolved to. Must not drift. */ +const EXPECTED_WIDTH = 'min(960px, 60vw)'; + +vi.mock('sonner', () => ({ toast: { error: vi.fn() } })); + +vi.mock('./GanttView', () => ({ + GanttView: ({ tasks, onTaskClick }: any) => ( +
+ {tasks.map((t: any) => ( + + ))} +
+ ), +})); + +// Record the props ObjectGantt hands down, then delegate to the REAL drawer so +// half 2 measures the actual resolution rather than a stub's idea of it. +let drawerProps: any = null; +vi.mock('@object-ui/plugin-detail', async (importOriginal) => { + const actual = await importOriginal(); + const Real = actual.RecordDetailDrawer; + return { + ...actual, + RecordDetailDrawer: (props: any) => { + drawerProps = props; + return ; + }, + }; +}); + +function makeSchema(): any { + return { + type: 'gantt', + objectName: 'tasks', + gantt: { titleField: 'name', startDateField: 'start_date', endDateField: 'end_date' }, + data: { + provider: 'value', + items: [{ id: '1', name: 'Row', start_date: '2024-01-01', end_date: '2024-01-05' }], + }, + }; +} + +async function openDrawer() { + render(); + await waitFor(() => expect(screen.getByTestId('gv-view-1')).toBeDefined()); + fireEvent.click(screen.getByTestId('gv-view-1')); + await waitFor(() => expect(drawerProps).not.toBeNull()); +} + +describe('gantt drawer width with no declared `navigation`', () => { + beforeEach(() => { + drawerProps = null; + // Cross-test leakage guard: the drawer prefers a drag-resized width + // persisted in localStorage over its prop, which would mask half 2. + try { window.localStorage.clear(); } catch { /* ignore */ } + }); + + it('half 1: the gantt injects no width of its own (so the drawer default applies)', async () => { + await openDrawer(); + expect(drawerProps.width).toBeUndefined(); + }); + + it('half 2: the width the real drawer resolves is still the pinned value', async () => { + await openDrawer(); + // The drawer applies the resolved width as an inline style on its panel, + // as BOTH `width` and `max-width`. happy-dom's CSS parser drops the + // `width` longhand when the value is a `min()` expression but keeps + // `max-width`, so the surviving declaration is what we read — it is the + // same resolved string, not a proxy for it. + const panel = document.querySelector('[role="dialog"]') as HTMLElement | null; + expect(panel, 'drawer panel').not.toBeNull(); + expect(panel!.style.maxWidth).toBe(EXPECTED_WIDTH); + }); +}); diff --git a/packages/plugin-gantt/src/ObjectGantt.tsx b/packages/plugin-gantt/src/ObjectGantt.tsx index 82ede3884e..ae81c5c22e 100644 --- a/packages/plugin-gantt/src/ObjectGantt.tsx +++ b/packages/plugin-gantt/src/ObjectGantt.tsx @@ -1147,7 +1147,24 @@ export const ObjectGantt: React.FC = ({ // Default to a right-side drawer so clicking a task opens an editable // detail panel inline (no full-page navigation). Schema can override by // providing its own `navigation` config (e.g., page mode). - const navConfig = schema.navigation ?? { mode: 'drawer', width: 'min(960px, 60vw)' }; + // + // No width is spelled here on purpose. `width` is `@deprecated [#2578 -> + // size]` in the spec that owns this shape, and `resolveOverlayWidth` gives + // an explicit `width` priority OVER `size` — so spelling it kept the + // deprecated branch load-bearing on the path most gantts take (no declared + // `navigation`), and made the size buckets unreachable there. Omitting both + // leaves `resolveOverlayWidth` returning `undefined`, which is what + // RecordDetailDrawer's own `width` default is for; that default is the + // identical `min(960px, 60vw)`, so this is a zero-pixel change on every + // viewport. Pinned by ObjectGantt.navWidthDefault.test.tsx — both halves, + // because the equivalence now depends on the drawer's default too. + // + // Deliberately NOT converged on `size: 'lg'`: that bucket is + // `min(92vw, 960px)`, which agrees with the above only at viewport >= + // 1600px and is up to 53% wider below it. Whether the renderers should move + // to the bucket is open (#6259), and spans kanban/calendar/RecordDetailDrawer + // as one decision (#6303). + const navConfig = schema.navigation ?? { mode: 'drawer' }; const navIsOverlay = navConfig.mode === 'drawer' || navConfig.mode === 'modal' || navConfig.mode === 'split' || navConfig.mode === 'popover'; const navigation = useNavigationOverlay({ navigation: navConfig,