diff --git a/apps/api/e2e/tracker.e2e-spec.ts b/apps/api/e2e/tracker.e2e-spec.ts index 43b7cc1..ad278f1 100644 --- a/apps/api/e2e/tracker.e2e-spec.ts +++ b/apps/api/e2e/tracker.e2e-spec.ts @@ -1511,4 +1511,60 @@ describe('task-stream (e2e)', () => { ); expect((await rest('token-ana', 'GET', '/me/views')).body).toEqual([]); }); + + it('keeps unfinished checklists out of DONE when the project asks', async () => { + const key = 'H' + Date.now().toString(36).slice(-5).toUpperCase(); + await rest('token-ana', 'POST', '/projects', { key, name: key }); + const project = await eventually( + () => rest('token-ana', 'GET', `/projects/${key}`), + (r) => r.status === 200, + ); + expect(project.body['settings'].checklistBeforeDone).toBe(false); + await rest('token-ana', 'POST', `/projects/${key}/members`, { + userId: '102', + }); + expect( + ( + await rest('token-ana', 'PUT', `/projects/${key}/settings`, { + ...project.body['settings'], + checklistBeforeDone: true, + }) + ).status, + ).toBe(200); + const issue = ( + await rest('token-bob', 'POST', `/projects/${key}/issues`, { + type: 'TASK', + title: 'Con pasos', + }) + ).body['key']; + await eventually( + () => rest('token-bob', 'GET', `/issues/${issue}`), + (r) => r.status === 200, + ); + await rest('token-bob', 'POST', `/issues/${issue}/checklist`, { + text: 'Probar en Safari', + }); + for (const status of [ + 'TODO', + 'IN_PROGRESS', + 'IN_REVIEW', + 'READY_FOR_QA', + 'IN_QA', + ]) + await rest('token-bob', 'PUT', `/issues/${issue}/status`, { status }); + + const refused = await rest('token-bob', 'PUT', `/issues/${issue}/status`, { + status: 'DONE', + }); + expect(refused.status).toBe(422); + expect(refused.body['message']).toMatch(/queda 1 paso/); + // The owner may close it anyway. + expect( + ( + await rest('token-ana', 'PUT', `/issues/${issue}/status`, { + status: 'DONE', + }) + ).status, + ).toBe(200); + }); }); diff --git a/apps/api/openapi.json b/apps/api/openapi.json index f79e9b8..7b8ab0a 100644 --- a/apps/api/openapi.json +++ b/apps/api/openapi.json @@ -10982,6 +10982,10 @@ "items": { "$ref": "#/components/schemas/CustomFieldDefinitionDto" } + }, + "checklistBeforeDone": { + "type": "boolean", + "description": "Definition of done: tickets reach DONE only with their checklist ticked" } }, "required": [ @@ -10989,7 +10993,8 @@ "transitions", "issueTypes", "areas", - "customFields" + "customFields", + "checklistBeforeDone" ] }, "ProjectDto": { @@ -11379,6 +11384,10 @@ "items": { "$ref": "#/components/schemas/CustomFieldDefinitionBodyDto" } + }, + "checklistBeforeDone": { + "type": "boolean", + "description": "Definition of done: tickets reach a DONE status only with their checklist ticked (owners and admins may override). Absent: unchanged" } }, "required": [ diff --git a/apps/api/schema.gql b/apps/api/schema.gql index 51316b5..901b59c 100644 --- a/apps/api/schema.gql +++ b/apps/api/schema.gql @@ -1185,6 +1185,11 @@ Statuses (board order), transitions, ticket types, areas and custom fields """ type ProjectSettings { areas: [AreaOption!]! + + """ + Definition of done: tickets reach DONE only with their checklist ticked (owners and admins may override) + """ + checklistBeforeDone: Boolean! customFields: [CustomFieldDefinition!]! issueTypes: [IssueTypeOption!]! statuses: [WorkflowStatus!]! @@ -1197,6 +1202,11 @@ The whole configuration (replaces the previous one). Statuses go in board order. input ProjectSettingsInput { areas: [AreaOptionInput!]! + """ + Definition of done: DONE only with the checklist ticked. Absent: unchanged + """ + checklistBeforeDone: Boolean + """Absent: unchanged. Fields cannot be removed: archive them.""" customFields: [CustomFieldDefinitionInput!] issueTypes: [IssueTypeOptionInput!]! diff --git a/apps/mcp/src/app/api/taskstream.schema.ts b/apps/mcp/src/app/api/taskstream.schema.ts index 2a9638f..fd4e0d6 100644 --- a/apps/mcp/src/app/api/taskstream.schema.ts +++ b/apps/mcp/src/app/api/taskstream.schema.ts @@ -1747,6 +1747,8 @@ export interface components { issueTypes: components["schemas"]["IssueTypeOptionDto"][]; areas: components["schemas"]["AreaOptionDto"][]; customFields: components["schemas"]["CustomFieldDefinitionDto"][]; + /** @description Definition of done: tickets reach DONE only with their checklist ticked */ + checklistBeforeDone: boolean; }; ProjectDto: { /** Format: uuid */ @@ -1895,6 +1897,8 @@ export interface components { areas: components["schemas"]["AreaOptionBodyDto"][]; /** @description Absent: unchanged. Fields cannot be removed: archive them. */ customFields?: components["schemas"]["CustomFieldDefinitionBodyDto"][]; + /** @description Definition of done: tickets reach a DONE status only with their checklist ticked (owners and admins may override). Absent: unchanged */ + checklistBeforeDone?: boolean; }; /** @enum {string} */ IssuePriority: "LOWEST" | "LOW" | "MEDIUM" | "HIGH" | "CRITICAL"; diff --git a/docs/03-api.md b/docs/03-api.md index 490f68b..4973508 100644 --- a/docs/03-api.md +++ b/docs/03-api.md @@ -118,6 +118,12 @@ nuevo proyecto se quedan fuera). Las etiquetas no tienen catálogo, así que no Los proyectos creados antes de que existiera la configuración usan los valores por defecto (sus eventos no la llevan). Al arrancar, la API rellena la categoría de los tickets del read model que no la tienen. +### Definición de «Hecho» + +`settings.checklistBeforeDone` (desactivada por defecto): con ella, un ticket no pasa a un estado de categoría `DONE` +mientras le quede algún paso de la checklist sin marcar (`422`, «quedan N pasos…»). El propietario y los admins sí +pueden cerrarlo; las automatizaciones, no. Al guardar la configuración, si no viene, se queda como estaba. + ### Campos personalizados Datos propios del proyecto en cada ticket («Navegador», «Cliente», «Coste»…), en `settings.customFields`: `id`, `name`, diff --git a/docs/04-cliente-web.md b/docs/04-cliente-web.md index ac5e8a0..6f6048f 100644 --- a/docs/04-cliente-web.md +++ b/docs/04-cliente-web.md @@ -294,7 +294,7 @@ visualización de la plataforma: | `/p/:key/roadmap` | Hoja de ruta: épicas en el tiempo, con sprints y releases; flechas «bloquea» / «depende de» entre épicas y aviso si una empieza antes de que acabe la que la bloquea | | `/p/:key/dashboard` | Seguimiento: cifras, flujo por estado, lo que ha cambiado desde tu última visita (guardada en este navegador), sprint en curso, tickets en riesgo, previsión de las próximas releases, antigüedad de lo que está en curso, cuellos de botella, salud del backlog (cada número abre la lista filtrada: `?missing=estimate`, `area`, `assignee` o `?staleDays=90`), calidad (bugs por prioridad y por semana, QA por área, bugs tras cada release), abiertos por un campo de lista a elegir, equipo con sus puntos abiertos y tipos. Se recarga solo con cada cambio | | `/p/:key/analytics` | Estadísticas: creados vs. resueltos, flujo acumulado, burnup de una release, tiempos de ciclo y entrega, QA y resueltos por persona (30 días a 1 año) | -| `/p/:key/settings` | Configuración (propietario y admins; pestaña oculta para el resto): estados (orden, categoría, color, motivo), transiciones, tipos, áreas y campos personalizados; archivar en lugar de borrar | +| `/p/:key/settings` | Configuración (propietario y admins; pestaña oculta para el resto): estados (orden, categoría, color, motivo), transiciones, tipos, áreas, definición de «Hecho» y campos personalizados; archivar en lugar de borrar | | `/p/:key/activity` | Actividad en vivo | | `/p/:key/members` | Miembros | diff --git a/docs/10-futuro.md b/docs/10-futuro.md index e63c9a2..a74e09b 100644 --- a/docs/10-futuro.md +++ b/docs/10-futuro.md @@ -37,7 +37,6 @@ Nada: la tanda está hecha (ver 10.5). - **Sincronizar con issues de Gitea o GitHub**: que un issue del repositorio cree o actualice un ticket, y al revés. - **Búsqueda con lenguaje de consulta**: `estado:abierto asignado:yo navegador:Safari prioridad>=alta`, guardable como vista. -- **Definición de «Hecho»**: no pasar a un estado `DONE` con la lista de comprobación a medias (salvo un admin). - **Entornos de despliegue**: en qué entorno está cada ticket (pruebas, producción), desde los avisos de la integración continua. - **Equipos dentro del proyecto**: asignar a un equipo y mencionarlo (`@frontend`). @@ -119,3 +118,5 @@ Mucho trabajo para lo que aportan hoy; se pueden recuperar si alguien los necesi lugar de devolver un 409. - Vistas guardadas en «Mi trabajo»: filtros propios entre proyectos (proyectos, asignados a mí, que sigo, estados, prioridades, solo bugs, vencidos, texto), en una barra encima de «Mi trabajo». +- Definición de «Hecho» por proyecto: con ella activada, un ticket no se cierra con la checklist a medias (el + propietario y los admins sí pueden). diff --git a/docs/TaskStream-Documentacion.docx b/docs/TaskStream-Documentacion.docx index 1a6ed9f..c8c76f8 100644 Binary files a/docs/TaskStream-Documentacion.docx and b/docs/TaskStream-Documentacion.docx differ diff --git a/docs/TaskStream-Documentacion.pdf b/docs/TaskStream-Documentacion.pdf index 8cd4ae2..9d50cea 100644 Binary files a/docs/TaskStream-Documentacion.pdf and b/docs/TaskStream-Documentacion.pdf differ diff --git a/libs/api/tracker/application/src/lib/commands/change-issue-status.command-handler.ts b/libs/api/tracker/application/src/lib/commands/change-issue-status.command-handler.ts index 26960e6..7904689 100644 --- a/libs/api/tracker/application/src/lib/commands/change-issue-status.command-handler.ts +++ b/libs/api/tracker/application/src/lib/commands/change-issue-status.command-handler.ts @@ -14,11 +14,14 @@ export class ChangeIssueStatusCommandHandler command.issueId, command.actorId, ); + const role = project.roleOf(command.actorId); issue.changeStatus( command.actorId, command.status, command.reason, project.workflow, + // The definition of done holds back members, not who runs the project. + role === 'OWNER' || role === 'ADMIN', ); await issue.commitAndWait(); return ack(issue); diff --git a/libs/api/tracker/domain/src/lib/issue/issue.ts b/libs/api/tracker/domain/src/lib/issue/issue.ts index 1f06207..1117a2e 100644 --- a/libs/api/tracker/domain/src/lib/issue/issue.ts +++ b/libs/api/tracker/domain/src/lib/issue/issue.ts @@ -26,7 +26,7 @@ import { IssueUpdatedEvent } from '../events/issue-updated.event'; import { IssueUnwatchedEvent } from '../events/issue-unwatched.event'; import { IssueWatchedEvent } from '../events/issue-watched.event'; import { IssueVotedEvent } from '../events/issue-voted.event'; -import { ProjectWorkflow } from '../project/project-settings'; +import { ProjectWorkflow, StatusCategory } from '../project/project-settings'; import { IssueAttachment, MAX_ATTACHMENTS_PER_ISSUE, @@ -200,6 +200,8 @@ export class Issue extends VersionedAggregateRoot implements IssueProps { to: string, reason?: string | null, workflow: ProjectWorkflow = ProjectWorkflow.default(), + /** Owners and admins may close a ticket its definition of done holds back. */ + canOverride = false, ): void { const from = this.status; if (from === to) return; @@ -214,6 +216,16 @@ export class Issue extends VersionedAggregateRoot implements IssueProps { }`, ); } + const pending = this.checklist.filter((item) => !item.done).length; + if ( + workflow.checklistBeforeDone && + workflow.categoryOf(to) === StatusCategory.DONE && + pending > 0 && + !canOverride + ) + throw new BusinessRuleError( + `A ${this.key} le ${pending === 1 ? 'queda 1 paso' : `quedan ${pending} pasos`} de la lista por marcar, y el proyecto pide terminarla antes de darlo por hecho`, + ); const trimmedReason = reason?.trim() || null; if (workflow.requiresReason(to) && !trimmedReason) { throw new BusinessRuleError( diff --git a/libs/api/tracker/domain/src/lib/project/project-settings.spec.ts b/libs/api/tracker/domain/src/lib/project/project-settings.spec.ts index 891ff5a..c68f2b4 100644 --- a/libs/api/tracker/domain/src/lib/project/project-settings.spec.ts +++ b/libs/api/tracker/domain/src/lib/project/project-settings.spec.ts @@ -353,4 +353,54 @@ describe('Project settings', () => { }); }); }); + + it('holds a ticket with an unticked checklist out of DONE, if asked', () => { + const project = newProject(); + const issue = new IssueFactory().create( + { + projectId: project.id, + projectKey: 'WEB', + number: 1, + title: 'Pago', + type: 'TASK', + reporterId: 'dev', + }, + project.workflow, + ); + issue.addChecklistItem('dev', 'a', 'Probar en Safari'); + issue.addChecklistItem('dev', 'b', 'Probar en Chrome'); + for (const to of [ + 'TODO', + 'IN_PROGRESS', + 'IN_REVIEW', + 'READY_FOR_QA', + 'IN_QA', + ]) + issue.changeStatus('dev', to, null, project.workflow); + + // Off by default: nothing changes for projects that do not ask. + const done = () => + issue.changeStatus('dev', 'DONE', null, project.workflow); + expect(project.workflow.checklistBeforeDone).toBe(false); + + project.updateSettings('owner', { + ...defaultProjectSettings(), + checklistBeforeDone: true, + }); + expect(done).toThrow(/quedan 2 pasos/); + // A client that does not know about it keeps it on. + project.updateSettings('owner', defaultProjectSettings()); + expect(project.workflow.checklistBeforeDone).toBe(true); + issue.updateChecklistItem('dev', 'a', { done: true }); + expect(done).toThrow(/queda 1 paso/); + // An owner or admin may close it anyway. + issue.changeStatus('owner', 'DONE', null, project.workflow, true); + expect(issue.status).toBe('DONE'); + + project.updateSettings('owner', { + ...defaultProjectSettings(), + checklistBeforeDone: false, + }); + expect(project.workflow.checklistBeforeDone).toBe(false); + }); }); diff --git a/libs/api/tracker/domain/src/lib/project/project-settings.ts b/libs/api/tracker/domain/src/lib/project/project-settings.ts index fc2bc06..0f0bc56 100644 --- a/libs/api/tracker/domain/src/lib/project/project-settings.ts +++ b/libs/api/tracker/domain/src/lib/project/project-settings.ts @@ -134,6 +134,12 @@ export interface ProjectSettings { * unchanged (clients that predate them keep them). */ customFields?: CustomFieldDefinition[]; + /** + * Definition of done: a ticket only reaches a DONE status with its + * checklist ticked (owners and admins may still close it). Absent when + * off; when saving, absent means unchanged. + */ + checklistBeforeDone?: boolean; } const status = ( @@ -323,6 +329,11 @@ export class ProjectWorkflow { throw new BusinessRuleError(`Ticket type ${type.name} is archived`); } + /** Tickets reach DONE only with their checklist ticked (definition of done). */ + get checklistBeforeDone(): boolean { + return !!this.settings.checklistBeforeDone; + } + /** The project's custom fields, archived ones included. */ get customFields(): CustomFieldDefinition[] { return this.settings.customFields ?? []; @@ -503,6 +514,9 @@ export function validateProjectSettings( areas, // Only when there are some: settings saved unchanged must compare equal. ...(customFields.length ? { customFields } : {}), + ...((next.checklistBeforeDone ?? previous.checklistBeforeDone) + ? { checklistBeforeDone: true } + : {}), }; } diff --git a/libs/api/tracker/presenters/src/lib/common/settings.ts b/libs/api/tracker/presenters/src/lib/common/settings.ts index d5bed5a..625a78e 100644 --- a/libs/api/tracker/presenters/src/lib/common/settings.ts +++ b/libs/api/tracker/presenters/src/lib/common/settings.ts @@ -27,6 +27,8 @@ export interface SettingsPayload { template?: { description?: string | null; checklist?: string[] } | null; }[]; areas: { id: string; name: string; archived: boolean }[]; + /** Definition of done. Absent when saving: unchanged. */ + checklistBeforeDone?: boolean; /** Absent when saving: unchanged. */ customFields?: { id: string; @@ -41,6 +43,7 @@ export function toSettingsPayload(settings: ProjectSettings): SettingsPayload { return { ...settings, customFields: settings.customFields ?? [], + checklistBeforeDone: !!settings.checklistBeforeDone, transitions: settings.statuses.map((s) => ({ from: s.id, to: settings.transitions[s.id] ?? [], @@ -72,6 +75,9 @@ export function fromSettingsPayload(payload: SettingsPayload): ProjectSettings { : {}), })), areas: payload.areas, + ...(payload.checklistBeforeDone != null + ? { checklistBeforeDone: payload.checklistBeforeDone } + : {}), ...(payload.customFields ? { customFields: payload.customFields.map((f) => ({ diff --git a/libs/api/tracker/presenters/src/lib/graphql/inputs.ts b/libs/api/tracker/presenters/src/lib/graphql/inputs.ts index a002c14..082bdf6 100644 --- a/libs/api/tracker/presenters/src/lib/graphql/inputs.ts +++ b/libs/api/tracker/presenters/src/lib/graphql/inputs.ts @@ -772,6 +772,15 @@ export class ProjectSettingsInput { @ValidateNested({ each: true }) @Type(() => CustomFieldDefinitionInput) customFields?: CustomFieldDefinitionInput[]; + + @Field({ + nullable: true, + description: + 'Definition of done: DONE only with the checklist ticked. Absent: unchanged', + }) + @IsOptional() + @IsBoolean() + checklistBeforeDone?: boolean; } @InputType({ diff --git a/libs/api/tracker/presenters/src/lib/graphql/types.ts b/libs/api/tracker/presenters/src/lib/graphql/types.ts index 46b6513..c7d1949 100644 --- a/libs/api/tracker/presenters/src/lib/graphql/types.ts +++ b/libs/api/tracker/presenters/src/lib/graphql/types.ts @@ -115,6 +115,11 @@ export class ProjectSettingsGql { @Field(() => [AreaOptionGql]) areas!: AreaOptionGql[]; @Field(() => [CustomFieldDefinitionGql]) customFields!: CustomFieldDefinitionGql[]; + @Field({ + description: + 'Definition of done: tickets reach DONE only with their checklist ticked (owners and admins may override)', + }) + checklistBeforeDone!: boolean; } @ObjectType('Project') diff --git a/libs/api/tracker/presenters/src/lib/http/dto/requests.dto.ts b/libs/api/tracker/presenters/src/lib/http/dto/requests.dto.ts index e425258..781c55e 100644 --- a/libs/api/tracker/presenters/src/lib/http/dto/requests.dto.ts +++ b/libs/api/tracker/presenters/src/lib/http/dto/requests.dto.ts @@ -318,6 +318,14 @@ export class ProjectSettingsBodyDto { @ValidateNested({ each: true }) @Type(() => CustomFieldDefinitionBodyDto) customFields?: CustomFieldDefinitionBodyDto[]; + + @ApiPropertyOptional({ + description: + 'Definition of done: tickets reach a DONE status only with their checklist ticked (owners and admins may override). Absent: unchanged', + }) + @IsOptional() + @IsBoolean() + checklistBeforeDone?: boolean; } export class AnalyticsQueryDto { diff --git a/libs/api/tracker/presenters/src/lib/http/dto/responses.dto.ts b/libs/api/tracker/presenters/src/lib/http/dto/responses.dto.ts index 889aced..1c80878 100644 --- a/libs/api/tracker/presenters/src/lib/http/dto/responses.dto.ts +++ b/libs/api/tracker/presenters/src/lib/http/dto/responses.dto.ts @@ -103,6 +103,11 @@ export class ProjectSettingsDto { @ApiProperty({ type: [AreaOptionDto] }) areas!: AreaOptionDto[]; @ApiProperty({ type: [CustomFieldDefinitionDto] }) customFields!: CustomFieldDefinitionDto[]; + @ApiProperty({ + description: + 'Definition of done: tickets reach DONE only with their checklist ticked', + }) + checklistBeforeDone!: boolean; } export class ProjectDto { diff --git a/libs/web/tracker/domain/src/lib/models/workflow.ts b/libs/web/tracker/domain/src/lib/models/workflow.ts index 51234ae..898a44a 100644 --- a/libs/web/tracker/domain/src/lib/models/workflow.ts +++ b/libs/web/tracker/domain/src/lib/models/workflow.ts @@ -97,6 +97,8 @@ export interface ProjectSettings { issueTypes: IssueTypeOption[]; areas: AreaOption[]; customFields: CustomFieldDefinition[]; + /** Definition of done: a DONE status only with the checklist ticked. */ + checklistBeforeDone?: boolean; } export const FINISHED_CATEGORIES: readonly StatusCategory[] = [ diff --git a/libs/web/tracker/infrastructure/src/lib/adapters/mappers.ts b/libs/web/tracker/infrastructure/src/lib/adapters/mappers.ts index b819697..961783b 100644 --- a/libs/web/tracker/infrastructure/src/lib/adapters/mappers.ts +++ b/libs/web/tracker/infrastructure/src/lib/adapters/mappers.ts @@ -112,5 +112,6 @@ export function toSettingsInput( options: f.options, archived: f.archived, })), + checklistBeforeDone: !!settings.checklistBeforeDone, }; } diff --git a/libs/web/tracker/infrastructure/src/lib/graphql/fragments.graphql b/libs/web/tracker/infrastructure/src/lib/graphql/fragments.graphql index fd3daed..4fca5f4 100644 --- a/libs/web/tracker/infrastructure/src/lib/graphql/fragments.graphql +++ b/libs/web/tracker/infrastructure/src/lib/graphql/fragments.graphql @@ -58,6 +58,7 @@ fragment ProjectFields on Project { options archived } + checklistBeforeDone } } diff --git a/libs/web/tracker/infrastructure/src/lib/graphql/generated.ts b/libs/web/tracker/infrastructure/src/lib/graphql/generated.ts index ea240c5..f6fa162 100644 --- a/libs/web/tracker/infrastructure/src/lib/graphql/generated.ts +++ b/libs/web/tracker/infrastructure/src/lib/graphql/generated.ts @@ -273,6 +273,8 @@ export type ProjectRole = 'ADMIN' | 'MEMBER' | 'OWNER' | 'VIEWER'; /** The whole configuration (replaces the previous one). Statuses go in board order. */ export type ProjectSettingsInput = { areas: Array; + /** Definition of done: DONE only with the checklist ticked. Absent: unchanged */ + checklistBeforeDone?: boolean | null | undefined; /** Absent: unchanged. Fields cannot be removed: archive them. */ customFields?: Array | null | undefined; issueTypes: Array; @@ -530,6 +532,7 @@ export type ProjectFieldsFragment = { owner: { id: string; name: string }; members: Array<{ role: ProjectRole; user: { id: string; name: string } }>; settings: { + checklistBeforeDone: boolean; statuses: Array<{ id: string; name: string; @@ -1524,6 +1527,7 @@ export type MyProjectsQuery = { owner: { id: string; name: string }; members: Array<{ role: ProjectRole; user: { id: string; name: string } }>; settings: { + checklistBeforeDone: boolean; statuses: Array<{ id: string; name: string; @@ -1574,6 +1578,7 @@ export type ProjectQuery = { owner: { id: string; name: string }; members: Array<{ role: ProjectRole; user: { id: string; name: string } }>; settings: { + checklistBeforeDone: boolean; statuses: Array<{ id: string; name: string; @@ -1734,6 +1739,7 @@ export type PublicProjectsQuery = { owner: { id: string; name: string }; members: Array<{ role: ProjectRole; user: { id: string; name: string } }>; settings: { + checklistBeforeDone: boolean; statuses: Array<{ id: string; name: string; @@ -2409,6 +2415,7 @@ export type ProjectChangedSubscription = { owner: { id: string; name: string }; members: Array<{ role: ProjectRole; user: { id: string; name: string } }>; settings: { + checklistBeforeDone: boolean; statuses: Array<{ id: string; name: string; @@ -2462,6 +2469,7 @@ export type MyProjectsChangedSubscription = { owner: { id: string; name: string }; members: Array<{ role: ProjectRole; user: { id: string; name: string } }>; settings: { + checklistBeforeDone: boolean; statuses: Array<{ id: string; name: string; @@ -2874,6 +2882,10 @@ export const ProjectFieldsFragmentDoc = { ], }, }, + { + kind: 'Field', + name: { kind: 'Name', value: 'checklistBeforeDone' }, + }, ], }, }, @@ -9615,6 +9627,10 @@ export const MyProjectsDocument = { ], }, }, + { + kind: 'Field', + name: { kind: 'Name', value: 'checklistBeforeDone' }, + }, ], }, }, @@ -9858,6 +9874,10 @@ export const ProjectDocument = { ], }, }, + { + kind: 'Field', + name: { kind: 'Name', value: 'checklistBeforeDone' }, + }, ], }, }, @@ -10952,6 +10972,10 @@ export const PublicProjectsDocument = { ], }, }, + { + kind: 'Field', + name: { kind: 'Name', value: 'checklistBeforeDone' }, + }, ], }, }, @@ -14334,6 +14358,10 @@ export const ProjectChangedDocument = { ], }, }, + { + kind: 'Field', + name: { kind: 'Name', value: 'checklistBeforeDone' }, + }, ], }, }, @@ -14564,6 +14592,10 @@ export const MyProjectsChangedDocument = { ], }, }, + { + kind: 'Field', + name: { kind: 'Name', value: 'checklistBeforeDone' }, + }, ], }, }, diff --git a/libs/web/tracker/presenters/src/lib/pages/issue.page.scss b/libs/web/tracker/presenters/src/lib/pages/issue.page.scss index f33b3c8..0aecdcd 100644 --- a/libs/web/tracker/presenters/src/lib/pages/issue.page.scss +++ b/libs/web/tracker/presenters/src/lib/pages/issue.page.scss @@ -231,6 +231,12 @@ a.ts-key { white-space: nowrap; } +.ts-checklist__rule { + margin: 0 0 0.5rem; + color: var(--ts-muted); + font-size: 0.8125rem; +} + .ts-checklist, .ts-children { display: grid; diff --git a/libs/web/tracker/presenters/src/lib/pages/issue.page.ts b/libs/web/tracker/presenters/src/lib/pages/issue.page.ts index 9307975..7fce730 100644 --- a/libs/web/tracker/presenters/src/lib/pages/issue.page.ts +++ b/libs/web/tracker/presenters/src/lib/pages/issue.page.ts @@ -268,6 +268,14 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024; > } + @if ( + project.workflow().settings.checklistBeforeDone && + facade.checklistProgress().done < facade.checklistProgress().total + ) { +

+ Este proyecto pide marcarla entera antes de darlo por hecho. +

+ } @if (issue.checklist.length) {
    @for (item of issue.checklist; track item.id) { diff --git a/libs/web/tracker/presenters/src/lib/pages/settings.page.ts b/libs/web/tracker/presenters/src/lib/pages/settings.page.ts index 5b0c966..92f1954 100644 --- a/libs/web/tracker/presenters/src/lib/pages/settings.page.ts +++ b/libs/web/tracker/presenters/src/lib/pages/settings.page.ts @@ -530,6 +530,33 @@ function idFrom(name: string): string { + +
    +
    +
    +

    Definición de «Hecho»

    +

    + Un ticket no pasa a un estado «Hecho» con su checklist a + medias. El propietario y los admins sí pueden cerrarlo. +

    +
    + +
    +
    +
    @@ -1396,6 +1423,12 @@ export class SettingsPage { ); } + protected setChecklistBeforeDone(on: boolean): void { + this.patch((d) => { + d.checklistBeforeDone = on; + }); + } + protected addField(): void { this.patch((d) => d.customFields.push({