Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions apps/api/src/storage/task-board-columns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,18 +117,37 @@ export class BoardColumnStorage {
});
}

/** Say what one of this board's columns means to Studio, or unsay it. */
/**
* Say what one of this board's columns means to Studio, or unsay it.
*
* A role means one column, not a set of them — `archiveColumn` and
* `automationFor` both read it that way, picking whichever row happens to
* match first. So giving a role to a column strips it from whichever column
* held it before, in the same transaction, rather than leaving two columns
* quietly claiming the same meaning.
*/
async setRole(
organizationId: string,
key: string,
role: string | null,
): Promise<boolean> {
const result = await this.db
.updateTable("task_board_columns")
.set({ role, updated_at: new Date() })
.where("organization_id", "=", organizationId)
.where("key", "=", key)
.executeTakeFirst();
return (result.numUpdatedRows ?? 0n) > 0n;
return await this.db.transaction().execute(async (tx) => {
if (role !== null) {
await tx
.updateTable("task_board_columns")
.set({ role: null, updated_at: new Date() })
.where("organization_id", "=", organizationId)
.where("role", "=", role)
.where("key", "!=", key)
.execute();
}
const result = await tx
.updateTable("task_board_columns")
.set({ role, updated_at: new Date() })
.where("organization_id", "=", organizationId)
.where("key", "=", key)
.executeTakeFirst();
return (result.numUpdatedRows ?? 0n) > 0n;
});
}
}
11 changes: 11 additions & 0 deletions apps/api/src/tools/task-board/board-handler.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,17 @@ describe("boardHandler — a board whose columns are the org's own", () => {
expect(await board().archiveColumn()).toBe(null);
});

/** A role names one column. Re-pointing it must strip it from wherever it
* used to be, or `archiveColumn` picks between two columns that both
* quietly claim "archived" — whichever the query happens to return first. */
it("moves a role rather than duplicating it onto a second column", async () => {
await boardColumns.setRole(ORG_M, "BACKLOG", "archived");
await boardColumns.setRole(ORG_M, "Code Review", "archived");
const columns = await board().columns();
expect(columns.filter((c) => c.role === "archived")).toHaveLength(1);
expect(await board().archiveColumn()).toBe("Code Review");
});

/**
* The obligation the foreign key creates. A column the tracker dropped that
* still holds cards cannot be deleted — RESTRICT refuses — and moving those
Expand Down
Loading