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

@@ -46,6 +46,12 @@ const issueFields = {
.optional()
.describe('ISO date when work should start (roadmap)'),
dueDate: z.string().optional().describe('ISO date, e.g. 2026-11-14'),
customFields: z
.record(z.string(), z.string().nullable())
.optional()
.describe(
'Custom field id → value (see get_project settings.customFields): SELECT one of its options, DATE YYYY-MM-DD, NUMBER a number. Only the fields to change; null clears one',
),
};
type IssueFields = {
@@ -57,6 +63,7 @@ type IssueFields = {
storyPoints?: number;
startDate?: string;
dueDate?: string;
customFields?: Record<string, string | null>;
};
@Resolver()
@@ -142,6 +149,12 @@ export class IssuesResolver {
.describe('Only the tickets the user follows'),
dueFrom: z.string().optional().describe('Due on or after (ISO date)'),
dueTo: z.string().optional().describe('Due on or before (ISO date)'),
customFields: z
.record(z.string(), z.string())
.optional()
.describe(
'Only tickets holding these custom values (field id → value)',
),
sort: z
.enum(['RECENT', 'RANK'])
.default('RECENT')
@@ -170,6 +183,7 @@ export class IssuesResolver {
watching?: boolean;
dueFrom?: string;
dueTo?: string;
customFields?: Record<string, string>;
sort: 'RECENT' | 'RANK';
limit: number;
offset: number;
@@ -198,6 +212,11 @@ export class IssuesResolver {
watching: params.watching,
dueFrom: params.dueFrom,
dueTo: params.dueTo,
field: params.customFields
? Object.entries(params.customFields).map(
([id, value]) => `${id}:${value}`,
)
: undefined,
sort: params.sort,
limit: params.limit,
offset: params.offset,