feat(web): bulk-mutation API client method + types (TASK-1669) (#671)

* feat(web): bulk-mutation API client method + types (TASK-1669)

Add api.items.bulk(ws, data) hitting POST /workspaces/{ws}/items/bulk
(TASK-1668), plus types in lib/types: BulkItemOp, a discriminated-union
BulkItemsRequest (each verb requires only its own params), and
BulkItemOutcome / BulkItemFailure / BulkItemsResponse for the per-row
outcome envelope. Wire contract matches the backend struct field-for-
field; `details` is unknown (server json.RawMessage).

Unblocks the lane-header bulk-action UI (TASK-1672).

* fix(web): assign bulk op ids are string-only, not nullable per Codex review (round 1)

The server treats JSON null as absent (mirrors ItemUpdate) and rejects an
assign with no real id and no clear flag, so a nullable type would let a
caller type-check `assigned_user_id: null` into a 400. Type these string-
only and document that clearing goes through the clear flags.
This commit is contained in:
xarmian
2026-05-30 20:16:04 -04:00
committed by GitHub
parent 57995c5898
commit 2721d890ec
2 changed files with 90 additions and 0 deletions
+22
View File
@@ -7,6 +7,8 @@ import type {
CollectionUpdate,
Backlink,
Item,
BulkItemsRequest,
BulkItemsResponse,
ItemChangeRow,
ItemChangesResponse,
ItemCreate,
@@ -710,6 +712,26 @@ export const api = {
method: 'DELETE'
}),
/**
* Apply one mutation verb (archive / move / tag / untag /
* set-priority / assign) to many items in a single request
* (TASK-1668 / TASK-1669). Editor/owner gated.
*
* Emits ONE `items_bulk_updated` SSE event per affected collection
* + one webhook instead of per-item fan-out — used by the lane-
* header bulk actions, which operate on a whole filtered lane.
*
* The call resolves 200 even when some rows fail: inspect the
* `failed` array (each carries `error` and, for structured
* rejections like `open_children`, `code` + `details`). `updated`
* lists the rows that changed and `total === updated + failed`.
*/
bulk: (ws: string, data: BulkItemsRequest) =>
request<BulkItemsResponse>(`/workspaces/${ws}/items/bulk`, {
method: 'POST',
body: JSON.stringify(data)
}),
restore: (ws: string, slug: string) =>
request<Item>(`/workspaces/${ws}/items/${slug}/restore`, {
method: 'POST'
+68
View File
@@ -583,6 +583,74 @@ export interface ItemUpdate {
force?: boolean;
}
// ─── Bulk mutation (TASK-1668 / TASK-1669) ───────────────────────────────────
// The verbs the bulk endpoint accepts. One mutation applied to many items
// in a single request (POST /workspaces/{ws}/items/bulk), emitting one SSE
// event per affected collection + one webhook instead of per-item fan-out.
export type BulkItemOp =
| 'archive'
| 'move'
| 'tag'
| 'untag'
| 'set-priority'
| 'assign';
// `BulkItemsRequest` is a discriminated union on `op` so each verb only
// accepts (and requires) its own params. `ids` are item refs (TASK-5) or
// UUIDs. `force` overrides the open-children guard on status-bearing moves
// (mirrors ItemUpdate.force). `move` requires status and/or collection
// (target slug) — kept both-optional here; the server validates the
// at-least-one rule.
export type BulkItemsRequest =
| { op: 'archive'; ids: string[]; force?: boolean }
| {
op: 'move';
ids: string[];
status?: string;
collection?: string;
force?: boolean;
}
| { op: 'set-priority'; ids: string[]; priority: string; force?: boolean }
| { op: 'tag' | 'untag'; ids: string[]; tags: string[]; force?: boolean }
| {
op: 'assign';
ids: string[];
// Set an assignee/role by id. To CLEAR, use the clear flags —
// the server treats JSON null as absent (mirrors ItemUpdate),
// so `assigned_user_id: null` would be rejected, not a clear.
assigned_user_id?: string;
agent_role_id?: string;
clear_assigned_user?: boolean;
clear_agent_role?: boolean;
force?: boolean;
};
// One successfully-mutated row.
export interface BulkItemOutcome {
ref: string;
id: string;
}
// One row that failed. `code`/`details` carry the structured server error
// when present (e.g. `open_children` with the blocking-child list); `error`
// is always the human-readable message.
export interface BulkItemFailure {
ref: string;
error: string;
code?: string;
details?: unknown;
}
// Per-row outcome envelope. The HTTP call resolves 200 even on partial
// failure — branch on `failed` to react. `total === updated + failed`.
export interface BulkItemsResponse {
op: BulkItemOp;
updated: BulkItemOutcome[];
failed: BulkItemFailure[];
total: number;
}
// ─── Versions ────────────────────────────────────────────────────────────────
export interface Version {