feat(KAN-33): redesign ticket relationships and hierarchy
CI/CD / test_and_check_app (pull_request) Canceled after 0s
CI/CD / Publish immutable image (pull_request) Canceled after 0s
CI/CD / preconditions (pull_request) Canceled after 5s

This commit is contained in:
2026-09-14 09:45:45 +00:00
parent 463cb7fe36
commit 1fe0c0b2d9
8 changed files with 740 additions and 229 deletions
+85 -38
View File
@@ -182,30 +182,35 @@ function makePage(url) {
encryptHistory: false,
props: {
name: 'KanbanGit',
errors: params.has('rejected')
errors: params.has('linkRejected')
? {
title: 'This title is not allowed.',
...(params.has('allErrors')
? {
description: 'Invalid description.',
ticket_type_id: 'Invalid type.',
ticket_priority_id: 'Invalid priority.',
ticket_status_id: 'Invalid status.',
assignee_user_id: 'Invalid assignee.',
author_user_id: 'Invalid author.',
parent_ticket_id: 'Invalid parent.',
story_points: 'Invalid points.',
'acceptance_criteria.0.text':
'Invalid checklist.',
blocked_comment: 'Invalid blocker.',
'field_values.0.value_number':
'Invalid number.',
'field_values.1.value_user_id':
'Invalid reviewer.',
}
: {}),
ticket_link_type_id: 'Invalid relationship type.',
linked_ticket_id: 'Choose another ticket.',
}
: {},
: params.has('rejected')
? {
title: 'This title is not allowed.',
...(params.has('allErrors')
? {
description: 'Invalid description.',
ticket_type_id: 'Invalid type.',
ticket_priority_id: 'Invalid priority.',
ticket_status_id: 'Invalid status.',
assignee_user_id: 'Invalid assignee.',
author_user_id: 'Invalid author.',
parent_ticket_id: 'Invalid parent.',
story_points: 'Invalid points.',
'acceptance_criteria.0.text':
'Invalid checklist.',
blocked_comment: 'Invalid blocker.',
'field_values.0.value_number':
'Invalid number.',
'field_values.1.value_user_id':
'Invalid reviewer.',
}
: {}),
}
: {},
sidebarOpen: true,
auth: {
user,
@@ -250,25 +255,45 @@ function makePage(url) {
? [
{
...linked,
ticket_type: { is_parent: false },
children: [],
ticket_type_name: 'Epic',
children: [
{
id: 43,
ticket_key: 'ENG-43',
title: 'Nested child with a long title that must wrap on mobile',
ticket_type_name: 'Task',
children: [],
},
],
children_count: 0,
},
]
: [],
linkedTickets: params.has('empty')
linkedTickets:
params.has('empty') || params.has('noLinks')
? []
: [
{
id: 3,
source_ticket_id: params.has('incoming')
? 41
: 42,
target_ticket_id: params.has('incoming')
? 42
: 41,
ticket_link_type: {
id: 1,
label: params.has('incoming')
? 'Blocked by'
: 'Blocks',
},
linked_ticket: linked,
},
],
ticketLinkOptions: params.has('noTargets') ? [] : [linked],
ticketLinkTypes: params.has('noTypes')
? []
: [
{
id: 3,
ticket_link_type: { id: 1, label: 'Blocks' },
linked_ticket: linked,
},
],
ticketLinkOptions: [linked],
ticketLinkTypes: [
{ id: 1, label: 'Blocks', counterpart_label: 'Blocked by' },
],
: [{ id: 1, label: 'Blocks', counterpart_label: 'Blocked by' }],
repositoryBranches: params.has('empty')
? []
: [
@@ -317,7 +342,7 @@ if (serve) {
for await (const chunk of request) {
body += chunk;
}
const data = JSON.parse(body);
const data = body ? JSON.parse(body) : {};
const previous = new URL(request.headers.referer);
const params = previous.searchParams;
if (params.has('serverError')) {
@@ -350,8 +375,17 @@ if (serve) {
} else if (request.url.includes('/links')) {
assert.ok(
request.url.endsWith('/links/3?_method=DELETE') ||
request.method === 'DELETE' ||
Number(data.linked_ticket_id) === 41,
);
if (params.has('rejectLink')) {
params.set('linkRejected', '1');
} else if (request.method === 'DELETE') {
params.set('noLinks', '1');
} else {
params.delete('noLinks');
params.delete('linkRejected');
}
} else {
assert.ok(
(request.method === 'POST' || 'field_values' in data) &&
@@ -492,6 +526,15 @@ if (serve) {
assert.match(html, /Add criterion/);
assert.match(html, /Create branch/);
assert.match(html, /Save link/);
assert.match(html, /Outgoing/);
const incoming = await render('?incoming');
assert.match(incoming, /Incoming/);
assert.match(incoming, /Blocked by/);
assert.match(await render('?noTypes'), /No link types are configured/);
assert.match(
await render('?noTargets'),
/No other tickets are available/,
);
assert.match(html, /Remove/);
const readOnly = await render('?readonly');
assert.doesNotMatch(
@@ -517,7 +560,11 @@ if (serve) {
]) {
assert.ok(empty.includes(text), `Missing empty state: ${text}`);
}
assert.match(await render('?parent'), /Child tickets/);
const hierarchy = await render('?parent');
assert.match(hierarchy, /Child tickets/);
assert.match(hierarchy, /Platform delivery/);
assert.match(hierarchy, /Nested child/);
assert.match(hierarchy, /Change parent/);
assert.match(await render('?noAccess'), /Open Git connections/);
assert.match(
await render('?gitError'),