feat(task-board): a board whose columns are the org's own - #6698
Merged
Conversation
viktormarinho
force-pushed
the
feat/dynamic-board-columns
branch
2 times, most recently
from
August 28, 2026 17:09
aa871df to
a1d2435
Compare
Step three, and the one that makes the interface earn its keep: a second implementation. `org_board_columns` picks between them, in the single place a board is constructed, which is why every caller was moved to asking the board first. Named for who defines the column set, not for where it came from. Today the only source is a Jira board, but mirroring is something the SYNC does — this side only knows the columns are not ours to invent, so a column typed by hand would land here too and nothing would need renaming. An org board reads its columns from rows. Studio's own still answers from the constant. Only `columns` differs — rules hang off a column key either way, so whose board it is has no bearing on what runs where. That is what keying automations by column rather than by lane bought. An org board with no columns yet renders EMPTY, not Studio's lanes. Falling back would quietly re-introduce a vocabulary the org has said it does not use, and file its cards under lanes nobody chose. "Opted in but not yet populated" has to be a state you can be in, which is also why the mode is an explicit flag rather than inferred from whether rows exist. `replaceAll` writes the set whole, because a column deleted upstream has to disappear here. A column that survives keeps its `role`: that is Studio's reading of someone else's column, and the tracker has no idea it exists. No foreign key from `status`. Not because it cannot be conditional — a nullable one can — but because every `ON DELETE` action is wrong for the case that actually happens. A column removed upstream would either take the customer's cards with it (CASCADE), block the sync (RESTRICT), or be impossible (SET NULL on a NOT NULL column). What those cards should do is a policy, and policy belongs in the application. Nothing populates the rows yet — mirroring from Jira is the next step. What lands here is the board that can hold them.
viktormarinho
force-pushed
the
feat/dynamic-board-columns
branch
from
August 28, 2026 17:17
a1d2435 to
cd0473d
Compare
decocms Bot
pushed a commit
that referenced
this pull request
Aug 28, 2026
PR: #6698 feat(task-board): a board whose columns are the org's own Bump type: minor - decocms (apps/api/package.json): 4.296.0 -> 4.297.0 - @decocms/native (apps/native/package.json): 4.296.0 -> 4.297.0 - @decocms/shared (packages/shared/package.json): 0.70.0 -> 0.71.0 Deploy-Scope: server
This was referenced Aug 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is this contribution about?
Step three, and the one that makes the interface earn its keep: a second implementation.
dynamic_board_columnspicks between them, in the single place a board is constructed — which is why #6682 and #6690 moved every caller to asking the board first. Nothing else in the codebase learns that boards can differ.Only
columnsdiffers between them. Rules hang off a column key either way, so which board an org has has no bearing on what runs where — that is exactly what keying automations by column rather than by lane bought us in #6690, and it is why the second implementation is small.The decisions I'd want challenged
A mirrored board with no columns renders empty, not Studio's lanes. Falling back would quietly re-introduce a vocabulary the org has said it does not use, and file its cards under lanes nobody chose. This is the single most load-bearing behaviour in the PR and it is asserted first in the tests.
The mode is an explicit flag, not inferred from whether rows exist. I used "the row's existence is the switch" for automations and it was right there — a missing rule is a safe default. Here it is not: a board converts before its first sync, so "dynamic but not yet populated" has to be a state you can be in. Inferring from rows would make that state indistinguishable from static and undo the point above.
replaceAllwrites the set whole. The caller is mirroring a board it does not own, so a column deleted upstream has to disappear here — a merge would accumulate ghosts. But a surviving column keeps itsrole: that is Studio's reading of someone else's column, and the tracker has no idea it exists, so a re-mirror must not wipe it.Still no table for the canonical board. Its columns remain a constant; the new table stays empty for every org that has not opted in.
How did you verify your code works?
Thirteen real-Postgres cases — the tier this needs, since the whole second implementation is rows and the properties that matter are about what is absent.
The static board's existing cases carry over unchanged (columns left to right, positions agreeing with
LANE_RANK, an unconfigured board doing nothing anywhere, rules org-scoped). New for the mirrored one: renders empty before its columns arrive rather than Studio's lanes; renders the org's own columns in the given order (Backlog / Em Progresso / Code Review, i.e. names that are not ours); a mirrored column has no role until someone says what it means; a role survives a re-mirror while a vanished column is dropped — both halves in one assertion, since either alone would pass a broken implementation; and a rule hung on a tracker-named column fires while a canonical key does not.1039 pass / 0 fail across
apps/api/src/{tools/task-board,jira,storage,core}.bun run check0 type errors,bun run lint19 warnings identical to baseline,knipclean,fmt:checkclean. Migration run against a real database.Not verified, flagging honestly:
replaceAllandsetRolehave no caller outside tests — mirroring from Jira is the next step. I judged that better than shipping a half-mirror, but it does mean the mirrored board is unreachable in the product today: the flag exists, and turning it on gets you an empty board. That is a deliberately inert state, not a working feature.boardFor, so a mirrored org'sTASK_BOARD_ITEM_LISTreturns its own columns, but nothing asserts that over the wire.boardForadds a settings read per call. Three call sites today, one of them the board read. It is a single indexed row and almost certainly noise next to the queries beside it, but I did not measure it.Screenshots/Demonstration
Not applicable — no user-visible change with the flag off, and the flag is off everywhere.
How to Test
dynamic_board_columns: trueon a test org. The board read should return an emptycolumnsarray — not the nine lanes.BoardColumnStorage.replaceAlland confirm they come back in order, with null roles.replaceAlldropping one column, and confirm the surviving column kept its role and the dropped one is gone.Migration Notes
Migration 190 creates
task_board_columns, empty. Reversible, and no existing row is touched.dynamic_board_columnsis a new default-off org flag, so every org keeps the board it has.Review Checklist
Summary by cubic
Adds a second task-board implementation whose columns come from the org's own storage rather than Studio's canonical lanes. A default-off
org_board_columnsorg flag picks between the two at the board's single construction point, so with the flag off nothing changes.Behavior with the flag on
task_board_columnstable; a mirrored board with no columns yet renders empty, not Studio's lanes.replaceAllwrites the column set whole, so a column deleted upstream disappears while a surviving column keeps itsrole.statushas no foreign key totask_board_columnsbecause CASCADE, RESTRICT, and SET NULL are all wrong when a mirrored column disappears, so what its cards do stays app policy.boardForadds one settings read per call across the call sites.Migration
task_board_columnstable; the canonical board's columns stay a constant and the table stays empty for every org that hasn't opted in.Written for commit cd0473d. Summary will update on new commits.