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:
@@ -1168,4 +1168,115 @@ describe('task-stream (e2e)', () => {
|
||||
history.body.map((h: { actor: { name: string } }) => h.actor.name),
|
||||
).toContain('TaskStream');
|
||||
});
|
||||
|
||||
it('adds custom fields to a project and lets tickets hold and filter by them', async () => {
|
||||
const key = 'C' + 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,
|
||||
);
|
||||
const settings = project.body['settings'];
|
||||
expect(settings.customFields).toEqual([]);
|
||||
|
||||
// REST: define them (a client that leaves them out keeps them).
|
||||
const fields = [
|
||||
{
|
||||
id: 'BROWSER',
|
||||
name: 'Navegador',
|
||||
type: 'SELECT',
|
||||
options: ['Chrome', 'Safari'],
|
||||
},
|
||||
{ id: 'COST', name: 'Coste', type: 'NUMBER' },
|
||||
];
|
||||
expect(
|
||||
(
|
||||
await rest('token-ana', 'PUT', `/projects/${key}/settings`, {
|
||||
...settings,
|
||||
customFields: fields,
|
||||
})
|
||||
).status,
|
||||
).toBe(200);
|
||||
await eventually(
|
||||
() => rest('token-ana', 'GET', `/projects/${key}`),
|
||||
(r) => r.body['settings'].customFields.length === 2,
|
||||
);
|
||||
const { customFields: _left, ...older } = settings;
|
||||
expect(
|
||||
(await rest('token-ana', 'PUT', `/projects/${key}/settings`, older))
|
||||
.status,
|
||||
).toBe(200);
|
||||
|
||||
// REST: a ticket with values; a wrong one is refused.
|
||||
const created = await rest('token-ana', 'POST', `/projects/${key}/issues`, {
|
||||
type: 'BUG',
|
||||
title: 'Pago roto',
|
||||
customFields: { BROWSER: 'Safari', COST: '1,5' },
|
||||
});
|
||||
expect(created.status).toBe(201);
|
||||
const ticket = created.body['key'] as string;
|
||||
const issue = await eventually(
|
||||
() => rest('token-ana', 'GET', `/issues/${ticket}`),
|
||||
(r) => r.status === 200,
|
||||
);
|
||||
expect(issue.body['customFields']).toEqual({
|
||||
BROWSER: 'Safari',
|
||||
COST: '1.5',
|
||||
});
|
||||
expect(
|
||||
(
|
||||
await rest('token-ana', 'PATCH', `/issues/${ticket}`, {
|
||||
customFields: { BROWSER: 'Edge' },
|
||||
})
|
||||
).status,
|
||||
).toBe(400);
|
||||
await rest('token-ana', 'POST', `/projects/${key}/issues`, {
|
||||
type: 'BUG',
|
||||
title: 'Otro',
|
||||
customFields: { BROWSER: 'Chrome' },
|
||||
});
|
||||
|
||||
// GraphQL: change one, clear another; filter by value.
|
||||
await gql(
|
||||
'token-ana',
|
||||
`mutation($i: UpdateIssueInput!) { updateIssue(input: $i) { version } }`,
|
||||
{
|
||||
i: {
|
||||
issueId: issue.body['id'],
|
||||
customFields: [
|
||||
{ fieldId: 'BROWSER', value: 'Chrome' },
|
||||
{ fieldId: 'COST', value: null },
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
const filtered = await eventually(
|
||||
() =>
|
||||
gql(
|
||||
'token-ana',
|
||||
`query($f: IssueFilterInput!) { issues(filter: $f) { total items { key customFields { fieldId value } } } }`,
|
||||
{
|
||||
f: {
|
||||
projectId: project.body['id'],
|
||||
customFields: [{ fieldId: 'BROWSER', value: 'Chrome' }],
|
||||
},
|
||||
},
|
||||
),
|
||||
(d) => d['issues'].total === 2,
|
||||
);
|
||||
expect(
|
||||
filtered['issues'].items.find((i: { key: string }) => i.key === ticket)
|
||||
.customFields,
|
||||
).toEqual([{ fieldId: 'BROWSER', value: 'Chrome' }]);
|
||||
const bySafari = await rest(
|
||||
'token-ana',
|
||||
'GET',
|
||||
`/projects/${key}/issues?field=BROWSER:Safari`,
|
||||
);
|
||||
expect(bySafari.body['total']).toBe(0);
|
||||
const history = await rest('token-ana', 'GET', `/issues/${ticket}/history`);
|
||||
expect(
|
||||
history.body.map((h: { summary: string }) => h.summary).join(' | '),
|
||||
).toContain('los campos personalizados');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user