feat: definition of done per project

A project can ask that tickets reach a DONE status only with their
checklist ticked (settings.checklistBeforeDone, off by default; absent
when saving means unchanged). Members get a 422 telling how many steps are
left; owners and admins may still close it. Settings has the switch and
the ticket's checklist says when the project asks for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 14:39:24 +02:00
parent 5b0396bc10
commit e598f76e1c
25 changed files with 285 additions and 4 deletions

View File

@@ -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);
});
});

View File

@@ -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": [

View File

@@ -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!]!

View File

@@ -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";