feat: custom fields per project

Text, number, date or option-list fields defined in the project settings
(archived, never removed; their type is fixed once saved). Tickets hold
them, edited in the side panel, listed under the title and filtered by
option in the list view; also over GraphQL, REST (?field=ID:value) and MCP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 09:31:12 +02:00
parent 233c46e9fa
commit f7e850068b
44 changed files with 1909 additions and 96 deletions

View File

@@ -90,6 +90,7 @@ const FIELD: Record<string, string> = {
storyPoints: 'los puntos',
startDate: 'la fecha de inicio',
dueDate: 'la fecha límite',
customFields: 'los campos personalizados',
};
const list = (items: string[]) =>

View File

@@ -57,6 +57,7 @@ export class IssueCreatedEventHandler
storyPoints: issue.storyPoints,
startDate: issue.startDate ?? null,
dueDate: issue.dueDate,
customFields: issue.customFields ?? {},
createdAt: issue.createdAt,
updatedAt: issue.updatedAt,
version: versionOf(event),

View File

@@ -355,6 +355,7 @@ export class IssuesService {
storyPoints: issue.storyPoints,
startDate: issue.startDate,
dueDate: issue.dueDate,
customFields: issue.customFields,
parentId: issue.parentId,
// The original's text is the content, not the type's template.
useTemplate: false,

View File

@@ -33,6 +33,8 @@ export interface IssueFilter {
dueTo?: string;
/** Only tickets in no sprint (wins over `sprintId`). */
noSprint?: boolean;
/** Custom field id → the value it must hold. */
customFields?: Record<string, string>;
/** Only the ones the viewer follows (the query handler turns it into `watcherId`). */
watching?: boolean;
/** Only the ones this person follows. */