Skip to content

feat(task-board): let a column say what it means - #6718

Merged
viktormarinho merged 1 commit into
mainfrom
feat/column-roles
Aug 28, 2026
Merged

feat(task-board): let a column say what it means#6718
viktormarinho merged 1 commit into
mainfrom
feat/column-roles

Conversation

@viktormarinho

@viktormarinho viktormarinho commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

What is this contribution about?

This is the piece @viktormarinho asked for in place of a status mapping: you tell each column what it means, once, instead of maintaining a translation between two vocabularies.

It also closes a hole I flagged in #6716 and would not want anyone to enable the flag over.

The hole

archive-merged.ts wrote { status: "archived" } directly. On a board whose columns are the org's own there is no such column — and nothing catches it:

  • the sweep writes through storage, so assertBoardHasColumn never runs;
  • board_column_org is null for those rows, so the foreign key sleeps.

The card would be filed under a column that does not exist: invisible, not archived. The board decides now, and a board with nowhere to retire a card leaves it in Done — wrong-but-visible beats right-but-gone.

/** Where the sweep retires a finished card, or null when this board has
 *  nowhere to retire one to. */
archiveColumn(): Promise<string | null>;

Same nullable shape as automationFor, for the same reason.

Roles

TASK_BOARD_COLUMN_ROLE_SET is how an org says which of its columns means what. A column mirrored from a tracker is a name and a position — nothing tells us "Code Review" is where review happens, or that "Arquivado" retires a card.

Two roles today (in_review, archived), because two are what automation actually reads. A column carries at most one, and most carry none — that stays the safe default for a column we did not invent.

Studio's own lanes are deliberately not configurable: they already mean what they say, and setRole finds no row to update on a board that has no rows, which the tool turns into a clear error rather than a silent no-op.

It rejects a role on a column the board does not have, for the same reason TASK_BOARD_AUTOMATION_UPSERT does — a rule that can never fire still reads as configured to whoever set it.

How did you verify your code works?

Real-Postgres cases in board-handler.integration.test.ts, one per board:

  • Studio's board retires to archived, as it always did;
  • an org board has nowhere to retire a card until someone says which column that is — asserted across the whole cycle: null, then set a role and it answers that column, then clear it and it goes back to null. The clear half matters because a role you cannot take back is a trap.

The existing archive-sweep integration tests keep passing — and one of them needed real work rather than a workaround: its context was built with only taskBoard, and the sweep now asks the board (which asks the org which board it is). I gave the test the storages instead of moving the lookup to dodge it, the same call as in #6710.

4200 pass / 72 fail across apps/api/src and the board's web tests — the same 72 pre-existing order-dependent failures measured against baseline earlier. Every *.integration.test.ts also run individually in find | sort order, the way CI runs them, all green. bun run check 0 errors, bun run lint at baseline, knip clean (it caught COLUMN_ROLES being exported for no one — narrowed), fmt:check clean.

Not verified, flagging honestly:

  • No UI. Roles are set through the tool, and the debt is now three PRs deep: no screen for automations (feat(task-board): configure what a column runs, per column #6690), for the board mode (feat(task-board): a board whose columns are the org's own #6698), or for roles (here). At some point that stops being deferral and starts being a product with no controls.
  • Only two roles. in_review is defined and settable but nothing reads it yet — the review-phase checks still compare to the literal "in_review". It is here because setting it is half of what makes the sync useful, but I want to be plain that it is inert today; archived is the one that does something.
  • No e2e, and no live check of the archive sweep against an org board — there are no org boards with cards yet.

Screenshots/Demonstration

Not applicable — no visible change. Studio's own board answers exactly as before.

How to Test

  1. On any org today, the archive sweep still retires merged Done cards to Archived.
  2. On an org with org_board_columns on and some columns, confirm the sweep leaves finished cards in Done — it has nowhere to retire them.
  3. TASK_BOARD_COLUMN_ROLE_SET { columnKey: "<one of theirs>", role: "archived" }, then confirm the sweep retires to that column.
  4. Clear it with role: null and confirm it stops again.
  5. Try a role on a column that does not exist, and on an org still using Studio's board — both should be refused with a message that says why.

Migration Notes

None. No schema change — task_board_columns.role has existed since #6698 and this is the first thing to write it.

Review Checklist

  • PR title is clear and descriptive
  • Changes are tested and working
  • Documentation is updated (if needed)
  • No breaking changes

Summary by cubic

Fixes the archive sweep so a finished card is never filed under a column the board doesn't have. The sweep now asks the board where to retire a card instead of writing the literal "archived" — a board with no column marked as such leaves merged cards in Done (wrong-but-visible) instead of filing them under a nonexistent column (invisible).

  • Adds TASK_BOARD_COLUMN_ROLE_SET to mark one of the org's columns as archived or in_review, or clear it with null.
  • Studio's own board keeps its fixed Archived lane; org boards have no archive column until the org says which one that is.

Written for commit 1544edd. Summary will update on new commits.

Review in cubic

The board decides where a finished card retires to, instead of every caller
writing `"archived"` and hoping the board has one.

That literal was a real hole on an org board. The sweep writes through storage,
so neither the write-path check nor the foreign key catches it — a card would
be filed under a column that does not exist, which is invisible rather than
archived. A board with nowhere to retire a card now leaves it in Done, which is
wrong-but-visible instead of right-but-gone.

`TASK_BOARD_COLUMN_ROLE_SET` is how an org says which of its columns means
what. A column mirrored from a tracker is a name and a position; nothing tells
us "Code Review" is where review happens or that "Arquivado" retires a card.
Two roles today, because two are what automation reads. A column carries at
most one and most carry none, which stays the safe default for a column we did
not invent.

Studio's own lanes are not configurable — they already mean what they say, and
`setRole` finds no row to update on a board that has none.

Rejects a role on a column the board does not have, for the same reason the
automation upsert does: a rule that can never fire still reads as configured.
@viktormarinho
viktormarinho merged commit 4acc14d into main Aug 28, 2026
33 checks passed
@viktormarinho
viktormarinho deleted the feat/column-roles branch August 28, 2026 19:52
decocms Bot pushed a commit that referenced this pull request Aug 28, 2026
PR: #6718 feat(task-board): let a column say what it means
Bump type: minor

- decocms (apps/api/package.json): 4.299.1 -> 4.300.0
- @decocms/native (apps/native/package.json): 4.299.1 -> 4.300.0
- @decocms/shared (packages/shared/package.json): 0.72.1 -> 0.73.0

Deploy-Scope: server
pedrofrxncx added a commit that referenced this pull request Aug 29, 2026
Follows #6718. setRole() wrote the new role onto the target column but
left it on whichever column held it before, so re-pointing "archived"
(or "in_review") to a second column left two columns quietly claiming
the same role. archiveColumn()/automationFor() pick the first match by
position, so which one actually fires becomes arbitrary and the old
column silently keeps acting archived/in_review.

Fix: setRole() now clears the role from any other column in the org
that holds it, in the same transaction, before writing it to the
target column.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant