fix(dashboard): plugin- and dock-rendered cards resolved no column traits at all (#3025)

`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) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-31 01:14:13 -07:00
committed by GitHub
parent f8155cafd7
commit 43463f1a53
4 changed files with 57 additions and 2 deletions

View File

@@ -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.

View File

@@ -375,6 +375,9 @@ export function MainContent({
renderTaskCard: (task: Task | TaskDetail) => (
<TaskCard
task={task}
/* Plugin-rendered cards resolved NO traits before this: every role helper inside the
card fell back to the legacy id for any view using `renderTaskCard`. */
taskColumnFlags={columnFlagsByTaskId?.get(task.id)}
projectId={currentProject?.id}
onOpenDetail={openPluginTaskDetail}
addToast={addToast}

View File

@@ -23,8 +23,19 @@ vi.mock("../../../plugins/PluginDashboardViewHost", () => ({
}));
vi.mock("../../TaskCard", () => ({
TaskCard: ({ task, onOpenDetail }: { task: Task | TaskDetail; onOpenDetail: (task: Task | TaskDetail) => void }) => (
<button type="button" onClick={() => onOpenDetail(task)}>Open rendered task card</button>
TaskCard: ({ task, onOpenDetail, taskColumnFlags }: {
task: Task | TaskDetail;
onOpenDetail: (task: Task | TaskDetail) => void;
taskColumnFlags?: Record<string, boolean | undefined>;
}) => (
<button
type="button"
/* The probe for the trait hand-off: absent means the card resolved nothing. */
data-column-flags={taskColumnFlags ? JSON.stringify(taskColumnFlags) : "none"}
onClick={() => onOpenDetail(task)}
>
Open rendered task card
</button>
),
}));
@@ -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(
<MainContent
{...mainContentProps({
taskView: "graph",
columnFlagsByTaskId: new Map([[graphTask.id, { complete: true }]]),
})}
/>,
);
const card = screen.getByTestId("rendered-task-card").querySelector("button");
expect(card?.getAttribute("data-column-flags")).toBe(JSON.stringify({ complete: true }));
});
});

View File

@@ -174,6 +174,10 @@ export function useRightDockController(input: RightDockControllerInput): RightDo
const renderTaskCard = useCallback((task: Task | TaskDetail) => (
<TaskCard
task={task}
/* Plugin- and dock-rendered cards resolved NO traits before this, so every role helper inside
the card fell back to the legacy id. The map is already in scope for the canonical lookup
below — the card itself was simply never given it. */
taskColumnFlags={input.columnFlagsByTaskId?.get(task.id)}
projectId={input.projectId}
onOpenDetail={(value: Task | TaskDetail) => input.openDetailTask(value)}
onDeleteTask={input.onDeleteTask}