diff --git a/docs/03-api.md b/docs/03-api.md index e082a95..3f98d53 100644 --- a/docs/03-api.md +++ b/docs/03-api.md @@ -309,7 +309,8 @@ stateDiagram-v2 - **Edición en bloque**: `bulkUpdateIssues` / `POST /api/issues/bulk` aplica el mismo cambio (estado, persona o `unassign`, sprint o `noSprint`, release o `noRelease`, prioridad, etiquetas que añadir) a hasta 100 tickets. Cada uno pasa por los comandos normales (permisos, flujo, sprints y releases abiertos) y puede negarse sin parar al resto; - la respuesta dice cuáles. + la respuesta dice cuáles. La web parte selecciones mayores (la Lista enseña hasta 200) en tandas de 100, una tras + otra, y suma el resultado. - **Archivar una release**: `archiveRelease(releaseId, moveTo)` / `POST /api/releases/{id}/archive` (`{ moveTo }`). Sus tickets sin terminar pasan a `moveTo` (otra release abierta del mismo proyecto) o, sin él, se quedan sin release; los terminados se quedan, como registro de lo que salió. Igual que al cerrar un sprint con `carryOverTo`. diff --git a/libs/web/tracker/domain/src/index.ts b/libs/web/tracker/domain/src/index.ts index 9081384..a7e107d 100644 --- a/libs/web/tracker/domain/src/index.ts +++ b/libs/web/tracker/domain/src/index.ts @@ -1,5 +1,6 @@ export * from './lib/models/enums'; export * from './lib/models/models'; +export * from './lib/models/bulk'; export * from './lib/models/inputs'; export * from './lib/models/workflow'; export * from './lib/ports/gateways'; diff --git a/libs/web/tracker/domain/src/lib/models/bulk.spec.ts b/libs/web/tracker/domain/src/lib/models/bulk.spec.ts new file mode 100644 index 0000000..e41816a --- /dev/null +++ b/libs/web/tracker/domain/src/lib/models/bulk.spec.ts @@ -0,0 +1,26 @@ +import { bulkBatches, MAX_BULK, mergeBulkResults } from './bulk'; + +describe('bulk changes', () => { + it('split into batches the API takes, in order', () => { + const ids = Array.from({ length: 230 }, (_, i) => `i${i}`); + const batches = bulkBatches(ids); + expect(batches.map((b) => b.length)).toEqual([MAX_BULK, MAX_BULK, 30]); + expect(batches.flat()).toEqual(ids); + expect(bulkBatches([])).toEqual([]); + }); + + it('add the batches up', () => { + expect( + mergeBulkResults([ + { updated: 2, results: [{ issueId: 'a', ok: true, error: null }] }, + { updated: 0, results: [{ issueId: 'b', ok: false, error: 'No' }] }, + ]), + ).toEqual({ + updated: 2, + results: [ + { issueId: 'a', ok: true, error: null }, + { issueId: 'b', ok: false, error: 'No' }, + ], + }); + }); +}); diff --git a/libs/web/tracker/domain/src/lib/models/bulk.ts b/libs/web/tracker/domain/src/lib/models/bulk.ts new file mode 100644 index 0000000..c2f2c39 --- /dev/null +++ b/libs/web/tracker/domain/src/lib/models/bulk.ts @@ -0,0 +1,20 @@ +import { BulkResult } from './inputs'; + +/** The API takes at most this many tickets per bulk change. */ +export const MAX_BULK = 100; + +/** The tickets in batches the API takes, in order. */ +export function bulkBatches(issueIds: string[], size = MAX_BULK): string[][] { + const batches: string[][] = []; + for (let start = 0; start < issueIds.length; start += size) + batches.push(issueIds.slice(start, start + size)); + return batches; +} + +/** Several batches' results as one. */ +export function mergeBulkResults(results: BulkResult[]): BulkResult { + return { + updated: results.reduce((sum, r) => sum + r.updated, 0), + results: results.flatMap((r) => r.results), + }; +} diff --git a/libs/web/tracker/infrastructure/src/lib/adapters/graphql-issues.gateway.ts b/libs/web/tracker/infrastructure/src/lib/adapters/graphql-issues.gateway.ts index 5d91ddd..a562594 100644 --- a/libs/web/tracker/infrastructure/src/lib/adapters/graphql-issues.gateway.ts +++ b/libs/web/tracker/infrastructure/src/lib/adapters/graphql-issues.gateway.ts @@ -4,6 +4,7 @@ import { BoardFilter, BulkChange, BulkResult, + bulkBatches, ChangeStatusInput, CommandResult, CreateIssueInput, @@ -16,9 +17,10 @@ import { IssueSummary, LinkIssuesInput, MyIssuesFilter, + mergeBulkResults, UpdateIssueInput, } from '@task-stream/web/tracker/domain'; -import { map, Observable } from 'rxjs'; +import { concatMap, from, map, Observable, toArray } from 'rxjs'; import { AddChecklistItemDocument, AssignIssueDocument, @@ -288,7 +290,19 @@ export class GraphqlIssuesGateway extends IssuesGateway { .pipe(map((d) => d.trash.map(toIssueSummary))); } + /** In batches of what the API takes, one after another, added up. */ bulkUpdate(issueIds: string[], change: BulkChange): Observable { + return from(bulkBatches(issueIds)).pipe( + concatMap((batch) => this.bulkBatch(batch, change)), + toArray(), + map(mergeBulkResults), + ); + } + + private bulkBatch( + issueIds: string[], + change: BulkChange, + ): Observable { return this.gql .mutate(BulkUpdateIssuesDocument, { input: {