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:
@@ -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[] = [
|
||||
|
||||
@@ -112,5 +112,6 @@ export function toSettingsInput(
|
||||
options: f.options,
|
||||
archived: f.archived,
|
||||
})),
|
||||
checklistBeforeDone: !!settings.checklistBeforeDone,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ fragment ProjectFields on Project {
|
||||
options
|
||||
archived
|
||||
}
|
||||
checklistBeforeDone
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<AreaOptionInput>;
|
||||
/** 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<CustomFieldDefinitionInput> | null | undefined;
|
||||
issueTypes: Array<IssueTypeOptionInput>;
|
||||
@@ -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' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -268,6 +268,14 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024;
|
||||
>
|
||||
}
|
||||
</h2>
|
||||
@if (
|
||||
project.workflow().settings.checklistBeforeDone &&
|
||||
facade.checklistProgress().done < facade.checklistProgress().total
|
||||
) {
|
||||
<p class="ts-checklist__rule">
|
||||
Este proyecto pide marcarla entera antes de darlo por hecho.
|
||||
</p>
|
||||
}
|
||||
@if (issue.checklist.length) {
|
||||
<ul class="ts-checklist">
|
||||
@for (item of issue.checklist; track item.id) {
|
||||
|
||||
@@ -530,6 +530,33 @@ function idFrom(name: string): string {
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<!-- Definition of done -->
|
||||
<section class="ts-panel ts-block" aria-labelledby="dod-title">
|
||||
<header class="ts-block__head">
|
||||
<div>
|
||||
<h3 id="dod-title">Definición de «Hecho»</h3>
|
||||
<p class="ts-muted">
|
||||
Un ticket no pasa a un estado «Hecho» con su checklist a
|
||||
medias. El propietario y los admins sí pueden cerrarlo.
|
||||
</p>
|
||||
</div>
|
||||
<label class="ts-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
[checked]="!!d.checklistBeforeDone"
|
||||
(change)="
|
||||
setChecklistBeforeDone($any($event.target).checked)
|
||||
"
|
||||
/>
|
||||
<span class="ts-switch__track" aria-hidden="true"></span>
|
||||
<span>{{
|
||||
d.checklistBeforeDone ? 'Activada' : 'Desactivada'
|
||||
}}</span>
|
||||
</label>
|
||||
</header>
|
||||
</section>
|
||||
|
||||
<!-- Custom fields -->
|
||||
<section class="ts-panel ts-block" aria-labelledby="cf-title">
|
||||
<header class="ts-block__head">
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user