From 43463f1a5367d0e5f9352062da64d9724885520b Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 31 Jul 2026 01:14:13 -0700 Subject: [PATCH] fix(dashboard): plugin- and dock-rendered cards resolved no column traits at all (#3025) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `renderTaskCard` is how a plugin view or the right dock draws a real task card. **Both** producers built a `TaskCard` without `taskColumnFlags`, so every role helper inside that card fell back to the legacy id — archive/revert affordances, progress, the elapsed-time indicator, the planning badge — for every plugin view on every board. Both already had the per-task map in scope. `MainContent` uses it **two lines away** for the near-duplicate canonical lookup; `useRightDockController` reads `input.columnFlagsByTaskId` for the same purpose. The card was simply never given it. ## One affordance, two producers Fixed together rather than one-plus-a-follow-up — that's what the Surface Enumeration rule is for. Finding the second producer is the only thing that makes this an invariant fix rather than a repro-shaped one. ## Measured | check | result | |---|---| | new case in the existing MainContent plugin-host harness | the rendered card must carry its resolved traits; dropping the argument from either producer reads `"none"` | | mutation on MainContent's producer | fails exactly that case | | `dashboard/__tests__` + `useRightDockController` | **35 tests green** | | census · inert-seam · lane-wiring · FNXC | green; lint and `tsc` clean | ## Not done — and my earlier reason for it was wrong `fusion-plugin-dependency-graph` still calls `isTaskStuck` without flags, exempted in the inert-seam gate by #3002. I filed #3003 claiming supply needed a **published-API change**. That's wrong: `PluginDashboardViewContext` is dashboard-internal and `@fusion/plugin-sdk` is `private: true`. I checked this time instead of asserting it — which is how I found the actual obstacle. The real blocker is that the plugin compiles against **different dashboard type declarations** than the dashboard source does: it sees a `PluginDashboardViewContext` without the field I added and an `isTaskStuck` accepting only three arguments. I built the full chain (context field → host supply → plugin consumption), hit those three errors, and reverted the plugin half rather than guess at the type plumbing inside a behaviour fix. So the exemption stands with a corrected reason, and #3003 is updated. That's the second filing rationale of mine to turn out wrong on inspection this session — after #3020, which I ended up fixing in #3022. Co-authored-by: Claude Opus 5 (1M context) --- .../plugin-rendered-cards-column-flags.md | 7 +++ .../app/components/dashboard/MainContent.tsx | 3 ++ .../MainContent.graph-popout.test.tsx | 45 ++++++++++++++++++- .../app/components/useRightDockController.tsx | 4 ++ 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 .changeset/plugin-rendered-cards-column-flags.md diff --git a/.changeset/plugin-rendered-cards-column-flags.md b/.changeset/plugin-rendered-cards-column-flags.md new file mode 100644 index 0000000000..1fc565ea39 --- /dev/null +++ b/.changeset/plugin-rendered-cards-column-flags.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Cards drawn by plugin views and the right dock now use the board's own lane names. +category: fix +dev: Both `renderTaskCard` producers built a `TaskCard` without `taskColumnFlags` despite having the per-task map in scope. diff --git a/packages/dashboard/app/components/dashboard/MainContent.tsx b/packages/dashboard/app/components/dashboard/MainContent.tsx index c399b2f888..99f0bd0fee 100644 --- a/packages/dashboard/app/components/dashboard/MainContent.tsx +++ b/packages/dashboard/app/components/dashboard/MainContent.tsx @@ -375,6 +375,9 @@ export function MainContent({ renderTaskCard: (task: Task | TaskDetail) => ( ({ })); vi.mock("../../TaskCard", () => ({ - TaskCard: ({ task, onOpenDetail }: { task: Task | TaskDetail; onOpenDetail: (task: Task | TaskDetail) => void }) => ( - + TaskCard: ({ task, onOpenDetail, taskColumnFlags }: { + task: Task | TaskDetail; + onOpenDetail: (task: Task | TaskDetail) => void; + taskColumnFlags?: Record; + }) => ( + ), })); @@ -301,4 +312,34 @@ describe("MainContent graph task pop-out wiring", () => { act(() => result.current.popOut(otherTask)); expect(result.current.tasks.map((task) => task.id)).toEqual(["FN-GRAPH", "FN-OTHER"]); }); + /* + FNXC:WorkflowResolvedColumns 2026-07-31-05:20: + A PLUGIN-RENDERED CARD RESOLVED NO COLUMN TRAITS AT ALL. + + `renderTaskCard` is how a plugin view draws a real task card. It built a `TaskCard` without + `taskColumnFlags`, so every role helper inside that card fell back to the legacy id — archive and + revert affordances, progress, the elapsed-time indicator, the planning badge — for every plugin + view on every board. The map was already in this component's scope; the card was simply never + given it. + + The same omission existed in `useRightDockController`'s `renderTaskCard`, which also had the map in + scope. Both are fixed together: this is one affordance with two producers, which is the shape the + Surface Enumeration rule exists for. + + REVERT CHECK: drop `taskColumnFlags` from either `renderTaskCard` and this reads "none". + */ + it("hands a plugin-rendered card its own resolved column traits", () => { + hostContexts.length = 0; + render( + , + ); + + const card = screen.getByTestId("rendered-task-card").querySelector("button"); + expect(card?.getAttribute("data-column-flags")).toBe(JSON.stringify({ complete: true })); + }); }); diff --git a/packages/dashboard/app/components/useRightDockController.tsx b/packages/dashboard/app/components/useRightDockController.tsx index 57031ecfc1..627ff6baff 100644 --- a/packages/dashboard/app/components/useRightDockController.tsx +++ b/packages/dashboard/app/components/useRightDockController.tsx @@ -174,6 +174,10 @@ export function useRightDockController(input: RightDockControllerInput): RightDo const renderTaskCard = useCallback((task: Task | TaskDetail) => ( input.openDetailTask(value)} onDeleteTask={input.onDeleteTask}