Skip to content

Commit 7a5750e

Browse files
os-zhuangclaude
andauthored
chore(lint): clear baseline lint errors in plugin-gantt (objectui#2713 Wave 3) (#2744)
18 of 21 errors were in the demo harness; no behavior change. - static-components (demo x8): hoist the Swatch legend cell to module scope - rules-of-hooks (demo x9): move App's ?quickfilter=1 early return below all hooks (the branch renders <QuickFilterDemo/> regardless) - purity (demo x1): demo render-timer reads performance.now() in render -> justified scoped disable - no-synthetic-event-trigger (GanttView.interactions.test): Escape test now uses fireEvent.keyDown(window,...) instead of a raw KeyboardEvent dispatch - no-useless-assignment (GanttView, ObjectGantt): drop two dead initializers Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent af1b0db commit 7a5750e

5 files changed

Lines changed: 51 additions & 9 deletions

File tree

.changeset/lint-baseline-gantt.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@object-ui/plugin-gantt": patch
3+
---
4+
5+
chore(lint): clear the baseline lint errors in plugin-gantt (objectui#2713 Wave 3)
6+
7+
Wave 3 of the #2713 lint-gate restoration. `@object-ui/plugin-gantt` was red at
8+
baseline on `main`; cleared every **error** (no behavior change; warnings out of
9+
scope). 18 of the 21 were in the demo harness:
10+
11+
- **`react-hooks/static-components` (demo, ×8)** — the `Swatch` legend cell was
12+
defined inside `ManufacturingLegend`; hoisted to module scope (purely
13+
props-driven, so nothing from render scope is captured).
14+
- **`react-hooks/rules-of-hooks` (demo, ×9)**`App` had a `?quickfilter=1`
15+
early return before ~9 hooks; moved that route below all hooks so hook order
16+
is stable (the quick-filter branch renders `<QuickFilterDemo />` regardless).
17+
- **`react-hooks/purity` (demo, ×1)** — the demo render-timer necessarily reads
18+
`performance.now()` during render (paired with an effect that measures elapsed
19+
ms); justified scoped disable, demo-only.
20+
- **`object-ui/no-synthetic-event-trigger`** (`GanttView.interactions.test`) —
21+
the Escape-closes-menu test dispatched a raw `window` `KeyboardEvent`; switched
22+
to `fireEvent.keyDown(window, { key: 'Escape' })` (the pattern already used
23+
elsewhere in the same file). The window-level Escape listener behaves
24+
identically.
25+
- **`no-useless-assignment`** (`GanttView`, `ObjectGantt`) — dropped two dead
26+
initializers (`ok`, `options`) that their exhaustive `try`/`catch` and
27+
`if`/`else` branches overwrite before reading.

packages/plugin-gantt/demo/main.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -394,14 +394,20 @@ function QuickFilterDemo() {
394394
);
395395
}
396396

397-
/** 状态色图例 (3.4.3) — decodes the 排产计划 / 派工单 bar colors for the mfg demo. */
398-
function ManufacturingLegend() {
399-
const Swatch = ({ color, label, hollow }: { color: string; label: string; hollow?: boolean }) => (
397+
/** One color swatch in the manufacturing legend. Hoisted to module scope so it
398+
* is a stable component reference (react-hooks/static-components); purely
399+
* props-driven, so nothing from the legend's render scope is captured. */
400+
function Swatch({ color, label, hollow }: { color: string; label: string; hollow?: boolean }) {
401+
return (
400402
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
401403
<span style={{ width: 12, height: 12, borderRadius: 3, background: hollow ? 'transparent' : color, border: hollow ? `1px solid ${color}` : 'none', display: 'inline-block' }} />
402404
{label}
403405
</span>
404406
);
407+
}
408+
409+
/** 状态色图例 (3.4.3) — decodes the 排产计划 / 派工单 bar colors for the mfg demo. */
410+
function ManufacturingLegend() {
405411
return (
406412
<div
407413
data-testid="mfg-legend"
@@ -453,7 +459,6 @@ const shiftConfig = (showMidnight: boolean) => ({
453459

454460
function App() {
455461
const params = new URLSearchParams(window.location.search);
456-
if (params.get('quickfilter') === '1') return <QuickFilterDemo />;
457462
const perf = Number(params.get('perf') || 0);
458463
const edge = params.has('edge');
459464
const mfg = params.has('mfg');
@@ -492,6 +497,10 @@ function App() {
492497
return { key: String(v), label: String(v) };
493498
};
494499
}, [resourceMode, resourceField]);
500+
// Demo-only render timer: capturing the render-start timestamp necessarily
501+
// reads performance.now() during render (paired with the effect below that
502+
// measures elapsed ms). Intentional and demo-only.
503+
// eslint-disable-next-line react-hooks/purity
495504
const t0 = React.useMemo(() => performance.now(), []);
496505
const [tasks, setTasks] = React.useState<GanttTask[]>(() => {
497506
const base = perf > 0 ? perfFixture(perf) : edge ? edgeFixture() : mfg ? manufacturingFixture() : projectFixture();
@@ -515,6 +524,10 @@ function App() {
515524
(window as unknown as { __ganttTasks?: GanttTask[] }).__ganttTasks = tasks;
516525
}, [tasks]);
517526

527+
// Route to the quick-filter demo AFTER all hooks above run, so the hook order
528+
// stays stable across renders (React requires the same hooks every render).
529+
if (params.get('quickfilter') === '1') return <QuickFilterDemo />;
530+
518531
return (
519532
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
520533
<div

packages/plugin-gantt/src/GanttView.interactions.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,9 @@ describe('GanttView context menu', () => {
129129
const { container } = renderView([A()], { onTaskDelete });
130130
fireEvent.contextMenu(container.querySelector('[data-testid="gantt-task-bar-a"]')!, { clientX: 10, clientY: 10 });
131131
expect(document.querySelector('[data-testid="gantt-context-menu"]')).toBeTruthy();
132-
act(() => {
133-
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
134-
});
132+
// Dispatch through RTL's fireEvent (not a raw synthetic KeyboardEvent) —
133+
// the Escape listener is on window; mirrors the existing pattern below.
134+
fireEvent.keyDown(window, { key: 'Escape' });
135135
expect(document.querySelector('[data-testid="gantt-context-menu"]')).toBeFalsy();
136136
expect(onTaskDelete).not.toHaveBeenCalled();
137137
});

packages/plugin-gantt/src/GanttView.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1042,7 +1042,8 @@ export function GanttView({
10421042
if (onBeforeTaskUpdate) {
10431043
const kept: typeof candidates = [];
10441044
for (const u of candidates) {
1045-
let ok = true;
1045+
// Assigned by both the try and the catch below before it's read.
1046+
let ok: boolean;
10461047
try {
10471048
ok = (await onBeforeTaskUpdate(u.task, u.changes)) !== false;
10481049
} catch {

packages/plugin-gantt/src/ObjectGantt.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -954,7 +954,8 @@ export const ObjectGantt: React.FC<ObjectGanttProps> = ({
954954
return quickFilterDefs.map((def) => {
955955
const fd = fieldDefs[def.field] ?? fieldDefs[def.field.split('.')[0]];
956956
const label = def.label ?? fd?.label ?? humanizeLabel(def.field);
957-
let options: QuickFilterOption[] = [];
957+
// Assigned by every branch of the exhaustive if/else chain below.
958+
let options: QuickFilterOption[];
958959

959960
if (def.options?.length) {
960961
options = def.options.map((o) =>

0 commit comments

Comments
 (0)