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:
@@ -1666,12 +1666,29 @@ export interface components {
|
||||
name: string;
|
||||
archived: boolean;
|
||||
};
|
||||
/**
|
||||
* @description DATE values are YYYY-MM-DD
|
||||
* @enum {string}
|
||||
*/
|
||||
CustomFieldType: "TEXT" | "NUMBER" | "DATE" | "SELECT";
|
||||
CustomFieldDefinitionDto: {
|
||||
/** @example BROWSER */
|
||||
id: string;
|
||||
/** @example Navegador */
|
||||
name: string;
|
||||
/** @description DATE values are YYYY-MM-DD */
|
||||
type: components["schemas"]["CustomFieldType"];
|
||||
/** @description The choices of a SELECT field */
|
||||
options: string[];
|
||||
archived: boolean;
|
||||
};
|
||||
ProjectSettingsDto: {
|
||||
/** @description In board order */
|
||||
statuses: components["schemas"]["WorkflowStatusDto"][];
|
||||
transitions: components["schemas"]["StatusTransitionsDto"][];
|
||||
issueTypes: components["schemas"]["IssueTypeOptionDto"][];
|
||||
areas: components["schemas"]["AreaOptionDto"][];
|
||||
customFields: components["schemas"]["CustomFieldDefinitionDto"][];
|
||||
};
|
||||
ProjectDto: {
|
||||
/** Format: uuid */
|
||||
@@ -1788,12 +1805,33 @@ export interface components {
|
||||
/** @default false */
|
||||
archived: boolean;
|
||||
};
|
||||
CustomFieldDefinitionBodyDto: {
|
||||
/** @example BROWSER */
|
||||
id: string;
|
||||
/** @example Navegador */
|
||||
name: string;
|
||||
/** @description Cannot change once the field exists. DATE: YYYY-MM-DD */
|
||||
type: components["schemas"]["CustomFieldType"];
|
||||
/**
|
||||
* @description The choices of a SELECT field
|
||||
* @example [
|
||||
* "Chrome",
|
||||
* "Safari",
|
||||
* "Firefox"
|
||||
* ]
|
||||
*/
|
||||
options?: string[];
|
||||
/** @default false */
|
||||
archived: boolean;
|
||||
};
|
||||
ProjectSettingsBodyDto: {
|
||||
/** @description In board order. Values cannot be removed: archive them instead. */
|
||||
statuses: components["schemas"]["WorkflowStatusBodyDto"][];
|
||||
transitions: components["schemas"]["StatusTransitionsBodyDto"][];
|
||||
issueTypes: components["schemas"]["IssueTypeOptionBodyDto"][];
|
||||
areas: components["schemas"]["AreaOptionBodyDto"][];
|
||||
/** @description Absent: unchanged. Fields cannot be removed: archive them. */
|
||||
customFields?: components["schemas"]["CustomFieldDefinitionBodyDto"][];
|
||||
};
|
||||
/** @enum {string} */
|
||||
IssuePriority: "LOWEST" | "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
|
||||
@@ -1930,6 +1968,15 @@ export interface components {
|
||||
startDate: string | null;
|
||||
/** Format: date */
|
||||
dueDate: string | null;
|
||||
/**
|
||||
* @description Custom field id → value (only those set)
|
||||
* @example {
|
||||
* "BROWSER": "Safari"
|
||||
* }
|
||||
*/
|
||||
customFields: {
|
||||
[key: string]: string;
|
||||
};
|
||||
links: components["schemas"]["IssueLinkDto"][];
|
||||
attachments: components["schemas"]["AttachmentDto"][];
|
||||
/** @description People following it */
|
||||
@@ -2098,6 +2145,15 @@ export interface components {
|
||||
startDate?: string | null;
|
||||
/** Format: date */
|
||||
dueDate?: string | null;
|
||||
/**
|
||||
* @description Custom field id → value. Only the fields to change; null or '' clears one
|
||||
* @example {
|
||||
* "BROWSER": "Safari"
|
||||
* }
|
||||
*/
|
||||
customFields?: {
|
||||
[key: string]: string | null;
|
||||
};
|
||||
/**
|
||||
* @description A ticket type id of the project's settings
|
||||
* @example BUG
|
||||
@@ -2146,6 +2202,15 @@ export interface components {
|
||||
startDate?: string | null;
|
||||
/** Format: date */
|
||||
dueDate?: string | null;
|
||||
/**
|
||||
* @description Custom field id → value. Only the fields to change; null or '' clears one
|
||||
* @example {
|
||||
* "BROWSER": "Safari"
|
||||
* }
|
||||
*/
|
||||
customFields?: {
|
||||
[key: string]: string | null;
|
||||
};
|
||||
/**
|
||||
* @description A ticket type id
|
||||
* @example FEATURE
|
||||
@@ -4365,6 +4430,8 @@ export interface operations {
|
||||
dueFrom?: string;
|
||||
/** @description Due on or before this day */
|
||||
dueTo?: string;
|
||||
/** @description Custom values the tickets must hold, as FIELD:value. Repeat the parameter for several */
|
||||
field?: string[];
|
||||
/** @description RECENT (default): last updated first. RANK: backlog order. */
|
||||
sort?: components["schemas"]["IssueSort"];
|
||||
/** @description Matches key, title or description */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -27,6 +27,7 @@ export const issueLine = (issue: Issue) => ({
|
||||
votes: issue.votes,
|
||||
storyPoints: issue.storyPoints,
|
||||
dueDate: issue.dueDate,
|
||||
customFields: issue.customFields,
|
||||
labels: issue.labels,
|
||||
updatedAt: issue.updatedAt,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user