mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-03 12:10:31 +00:00
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:
@@ -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'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user