Merge pull request 'KAN-12: Support both ticket link directions' (#174) from feature/KAN-12-bidirectional-ticket-links into main
Reviewed-on: #174
This commit was merged in pull request #174.
This commit is contained in:
@@ -17,18 +17,21 @@ class StoreProjectTicketLink
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{linked_ticket_id: int, ticket_link_type_id: int} $validated
|
* @param array{linked_ticket_id: int, ticket_link_type_id: int, link_direction?: 'source'|'counterpart'} $validated
|
||||||
*/
|
*/
|
||||||
public function handle(Project $project, Ticket $ticket, User $user, array $validated): TicketLink
|
public function handle(Project $project, Ticket $ticket, User $user, array $validated): TicketLink
|
||||||
{
|
{
|
||||||
$linkedTicketId = Arr::get($validated, 'linked_ticket_id');
|
$linkedTicketId = Arr::get($validated, 'linked_ticket_id');
|
||||||
$ticketLinkTypeId = Arr::get($validated, 'ticket_link_type_id');
|
$ticketLinkTypeId = Arr::get($validated, 'ticket_link_type_id');
|
||||||
|
$isSourceTicket = Arr::get($validated, 'link_direction', 'source') === 'source';
|
||||||
|
|
||||||
$linkedTicket = Ticket::query()
|
$linkedTicket = Ticket::query()
|
||||||
->where('project_id', $project->id)
|
->where('project_id', $project->id)
|
||||||
->findOrFail($linkedTicketId);
|
->findOrFail($linkedTicketId);
|
||||||
|
$sourceTicket = $isSourceTicket ? $ticket : $linkedTicket;
|
||||||
|
$targetTicket = $isSourceTicket ? $linkedTicket : $ticket;
|
||||||
|
|
||||||
return DB::transaction(function () use ($linkedTicket, $user, $ticketLinkTypeId, $ticket): TicketLink {
|
return DB::transaction(function () use ($linkedTicket, $user, $ticketLinkTypeId, $ticket, $sourceTicket, $targetTicket, $isSourceTicket): TicketLink {
|
||||||
Ticket::query()
|
Ticket::query()
|
||||||
->whereKey([$ticket->id, $linkedTicket->id])
|
->whereKey([$ticket->id, $linkedTicket->id])
|
||||||
->orderBy('id')
|
->orderBy('id')
|
||||||
@@ -50,31 +53,31 @@ class StoreProjectTicketLink
|
|||||||
|
|
||||||
if ($ticketLink === null) {
|
if ($ticketLink === null) {
|
||||||
$ticketLink = TicketLink::query()->create([
|
$ticketLink = TicketLink::query()->create([
|
||||||
'source_ticket_id' => $ticket->id,
|
'source_ticket_id' => $sourceTicket->id,
|
||||||
'target_ticket_id' => $linkedTicket->id,
|
'target_ticket_id' => $targetTicket->id,
|
||||||
'ticket_link_type_id' => $ticketLinkTypeId,
|
'ticket_link_type_id' => $ticketLinkTypeId,
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
$ticketLink->update([
|
$ticketLink->update([
|
||||||
'source_ticket_id' => $ticket->id,
|
'source_ticket_id' => $sourceTicket->id,
|
||||||
'target_ticket_id' => $linkedTicket->id,
|
'target_ticket_id' => $targetTicket->id,
|
||||||
'ticket_link_type_id' => $ticketLinkTypeId,
|
'ticket_link_type_id' => $ticketLinkTypeId,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$ticketLink->load('ticketLinkType');
|
$ticketLink->load('ticketLinkType');
|
||||||
|
|
||||||
$sourceLabel = $ticketLink->ticketLinkType->labelForSource(true);
|
$ticketLabel = $ticketLink->ticketLinkType->labelForSource($isSourceTicket);
|
||||||
$targetLabel = $ticketLink->ticketLinkType->labelForSource(false);
|
$linkedTicketLabel = $ticketLink->ticketLinkType->labelForSource(! $isSourceTicket);
|
||||||
|
|
||||||
$this->createTicketWorklogEntry->handle(
|
$this->createTicketWorklogEntry->handle(
|
||||||
$ticket,
|
$ticket,
|
||||||
$user,
|
$user,
|
||||||
TicketWorklogEntryType::LinkUpdated,
|
TicketWorklogEntryType::LinkUpdated,
|
||||||
"Linked {$linkedTicket->ticket_key} as {$sourceLabel}.",
|
"Linked {$linkedTicket->ticket_key} as {$ticketLabel}.",
|
||||||
[
|
[
|
||||||
'linked_ticket_key' => $linkedTicket->ticket_key,
|
'linked_ticket_key' => $linkedTicket->ticket_key,
|
||||||
'ticket_link_type' => $sourceLabel,
|
'ticket_link_type' => $ticketLabel,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -82,10 +85,10 @@ class StoreProjectTicketLink
|
|||||||
$linkedTicket,
|
$linkedTicket,
|
||||||
$user,
|
$user,
|
||||||
TicketWorklogEntryType::LinkUpdated,
|
TicketWorklogEntryType::LinkUpdated,
|
||||||
"Linked {$ticket->ticket_key} as {$targetLabel}.",
|
"Linked {$ticket->ticket_key} as {$linkedTicketLabel}.",
|
||||||
[
|
[
|
||||||
'linked_ticket_key' => $ticket->ticket_key,
|
'linked_ticket_key' => $ticket->ticket_key,
|
||||||
'ticket_link_type' => $targetLabel,
|
'ticket_link_type' => $linkedTicketLabel,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -36,11 +36,12 @@ class ProjectTicketLinkController extends ApiController
|
|||||||
|
|
||||||
$ticketLink = $this->storeProjectTicketLink->handle($project, $ticket, $user, $request->validated());
|
$ticketLink = $this->storeProjectTicketLink->handle($project, $ticket, $user, $request->validated());
|
||||||
$ticketLink->setAttribute('current_ticket_id', $ticket->id);
|
$ticketLink->setAttribute('current_ticket_id', $ticket->id);
|
||||||
|
$label = $ticketLink->ticketLinkType->labelForSource($ticketLink->source_ticket_id === $ticket->id);
|
||||||
|
|
||||||
return TicketLinkResource::make($ticketLink->load(['sourceTicket', 'targetTicket', 'ticketLinkType']))
|
return TicketLinkResource::make($ticketLink->load(['sourceTicket', 'targetTicket', 'ticketLinkType']))
|
||||||
->additional([
|
->additional([
|
||||||
'meta' => [
|
'meta' => [
|
||||||
'message' => "{$ticketLink->ticketLinkType->label} link was saved.",
|
'message' => "{$label} link was saved.",
|
||||||
],
|
],
|
||||||
])
|
])
|
||||||
->response()
|
->response()
|
||||||
|
|||||||
@@ -34,9 +34,10 @@ class ProjectTicketLinkController extends Controller
|
|||||||
);
|
);
|
||||||
|
|
||||||
$ticketLink = $this->storeProjectTicketLink->handle($project, $ticket, $user, $request->validated());
|
$ticketLink = $this->storeProjectTicketLink->handle($project, $ticket, $user, $request->validated());
|
||||||
|
$label = $ticketLink->ticketLinkType->labelForSource($ticketLink->source_ticket_id === $ticket->id);
|
||||||
|
|
||||||
return to_route('projects.tickets.show', [$project, $ticket])
|
return to_route('projects.tickets.show', [$project, $ticket])
|
||||||
->with('status', "{$ticketLink->ticketLinkType->label} link was saved.");
|
->with('status', "{$label} link was saved.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function destroy(
|
public function destroy(
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class StoreTicketLinkRequest extends FormRequest
|
|||||||
fn ($query) => $query->where('project_id', $project->id),
|
fn ($query) => $query->where('project_id', $project->id),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
'link_direction' => ['sometimes', 'required', Rule::in(['source', 'counterpart'])],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const canAddLink = computed(
|
|||||||
);
|
);
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
ticket_link_type_id: props.ticketLinkTypes[0]?.id ?? '',
|
ticket_link_type_id: props.ticketLinkTypes[0]?.id ?? '',
|
||||||
|
link_direction: 'source',
|
||||||
linked_ticket_id: '',
|
linked_ticket_id: '',
|
||||||
});
|
});
|
||||||
const protection = useFormProtection(form, {
|
const protection = useFormProtection(form, {
|
||||||
@@ -220,7 +221,7 @@ function submit(): void {
|
|||||||
/>
|
/>
|
||||||
<fieldset
|
<fieldset
|
||||||
:disabled="form.processing"
|
:disabled="form.processing"
|
||||||
class="grid min-w-0 gap-4 sm:grid-cols-[minmax(0,1fr)_minmax(0,2fr)]"
|
class="grid min-w-0 gap-4 sm:grid-cols-3"
|
||||||
>
|
>
|
||||||
<TicketFormField
|
<TicketFormField
|
||||||
id="ticket_link_type_id"
|
id="ticket_link_type_id"
|
||||||
@@ -230,13 +231,33 @@ function submit(): void {
|
|||||||
:options="
|
:options="
|
||||||
ticketLinkTypes.map((type) => ({
|
ticketLinkTypes.map((type) => ({
|
||||||
value: type.id,
|
value: type.id,
|
||||||
label: type.label,
|
label: `${type.label} / ${type.counterpart_label}`,
|
||||||
}))
|
}))
|
||||||
"
|
"
|
||||||
:error="form.errors.ticket_link_type_id"
|
:error="form.errors.ticket_link_type_id"
|
||||||
:disabled="form.processing"
|
:disabled="form.processing"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<TicketFormField
|
||||||
|
id="link_direction"
|
||||||
|
v-model="form.link_direction"
|
||||||
|
kind="select"
|
||||||
|
:label="t('ticketRelationships.direction')"
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
value: 'source',
|
||||||
|
label: selectedType?.label ?? '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'counterpart',
|
||||||
|
label:
|
||||||
|
selectedType?.counterpart_label ?? '',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:error="form.errors.link_direction"
|
||||||
|
:disabled="form.processing"
|
||||||
|
required
|
||||||
|
/>
|
||||||
<TicketFormField
|
<TicketFormField
|
||||||
id="linked_ticket_id"
|
id="linked_ticket_id"
|
||||||
v-model="form.linked_ticket_id"
|
v-model="form.linked_ticket_id"
|
||||||
@@ -267,7 +288,11 @@ function submit(): void {
|
|||||||
<span class="font-mono text-xs">{{
|
<span class="font-mono text-xs">{{
|
||||||
ticket.ticket_key
|
ticket.ticket_key
|
||||||
}}</span
|
}}</span
|
||||||
><span>{{ selectedType.label }}</span
|
><span>{{
|
||||||
|
form.link_direction === 'source'
|
||||||
|
? selectedType.label
|
||||||
|
: selectedType.counterpart_label
|
||||||
|
}}</span
|
||||||
><ArrowRight class="size-4" aria-hidden="true" /><span
|
><ArrowRight class="size-4" aria-hidden="true" /><span
|
||||||
class="font-mono text-xs"
|
class="font-mono text-xs"
|
||||||
>{{ selectedTicket.ticket_key }}</span
|
>{{ selectedTicket.ticket_key }}</span
|
||||||
|
|||||||
@@ -1200,8 +1200,9 @@ export const en = {
|
|||||||
'Open a child ticket to change its parent. Expand a group to see its descendants.',
|
'Open a child ticket to change its parent. Expand a group to see its descendants.',
|
||||||
'ticketRelationships.addLink': 'Add or update a link',
|
'ticketRelationships.addLink': 'Add or update a link',
|
||||||
'ticketRelationships.selectTicket': 'Select a ticket',
|
'ticketRelationships.selectTicket': 'Select a ticket',
|
||||||
|
'ticketRelationships.direction': 'This ticket',
|
||||||
'ticketRelationships.directionHelp':
|
'ticketRelationships.directionHelp':
|
||||||
'The selected relationship points from this ticket to the linked ticket. Saving an existing pair updates its type and direction.',
|
'Choose how this ticket relates to the linked ticket. The reverse label appears on the other ticket. Saving an existing pair updates its type and direction.',
|
||||||
'ticketRelationships.outgoing': 'Outgoing',
|
'ticketRelationships.outgoing': 'Outgoing',
|
||||||
'ticketRelationships.incoming': 'Incoming',
|
'ticketRelationships.incoming': 'Incoming',
|
||||||
'ticketRelationships.removeLink': 'Remove link to {key}',
|
'ticketRelationships.removeLink': 'Remove link to {key}',
|
||||||
|
|||||||
@@ -293,6 +293,49 @@ class TicketApiTest extends TestCase
|
|||||||
$this->assertDatabaseCount('ticket_worklog_entries', 0);
|
$this->assertDatabaseCount('ticket_worklog_entries', 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_counterpart_link_direction_is_reflected_on_both_tickets_in_the_api(): void
|
||||||
|
{
|
||||||
|
[$user, $project] = $this->projectContext();
|
||||||
|
$status = $project->ticketStatuses()->firstOrFail();
|
||||||
|
$blockingTicket = $this->ticket($project, $status);
|
||||||
|
$blockedTicket = $this->ticket($project, $status, 2);
|
||||||
|
$linkType = TicketLinkType::factory()->for($project)->create([
|
||||||
|
'label' => 'Blocks',
|
||||||
|
'counterpart_label' => 'Blocked by',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->withApplicationKey($user)
|
||||||
|
->postJson(route('api.projects.tickets.links.store', [$project, $blockedTicket]), [
|
||||||
|
'linked_ticket_id' => $blockingTicket->id,
|
||||||
|
'ticket_link_type_id' => $linkType->id,
|
||||||
|
'link_direction' => 'counterpart',
|
||||||
|
])
|
||||||
|
->assertCreated()
|
||||||
|
->assertJsonPath('data.source_ticket_id', $blockingTicket->id)
|
||||||
|
->assertJsonPath('data.target_ticket_id', $blockedTicket->id)
|
||||||
|
->assertJsonPath('data.ticket_link_type.label', 'Blocked by')
|
||||||
|
->assertJsonPath('meta.message', 'Blocked by link was saved.');
|
||||||
|
|
||||||
|
$this->withApplicationKey($user)
|
||||||
|
->getJson(route('api.projects.tickets.show', [$project, $blockingTicket]))
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.linkedTickets.0.ticket_link_type.label', 'Blocks');
|
||||||
|
|
||||||
|
$this->withApplicationKey($user)
|
||||||
|
->getJson(route('api.projects.tickets.show', [$project, $blockedTicket]))
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.linkedTickets.0.ticket_link_type.label', 'Blocked by');
|
||||||
|
|
||||||
|
$this->withApplicationKey($user)
|
||||||
|
->postJson(route('api.projects.tickets.links.store', [$project, $blockedTicket]), [
|
||||||
|
'linked_ticket_id' => $blockingTicket->id,
|
||||||
|
'ticket_link_type_id' => $linkType->id,
|
||||||
|
'link_direction' => 'invalid',
|
||||||
|
])
|
||||||
|
->assertUnprocessable()
|
||||||
|
->assertJsonValidationErrors('link_direction');
|
||||||
|
}
|
||||||
|
|
||||||
public function test_ticket_links_and_branch_endpoints_are_available(): void
|
public function test_ticket_links_and_branch_endpoints_are_available(): void
|
||||||
{
|
{
|
||||||
Http::preventStrayRequests();
|
Http::preventStrayRequests();
|
||||||
|
|||||||
@@ -83,6 +83,74 @@ class ProjectTicketLinkControllerTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_counterpart_label_can_be_selected_from_the_current_ticket(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$project = Project::factory()->create(['key' => 'CORE']);
|
||||||
|
|
||||||
|
app(InitializeProjectTicketMetadata::class)->handle($project);
|
||||||
|
|
||||||
|
$priority = $project->ticketPriorities()->firstOrFail();
|
||||||
|
$taskType = $project->ticketTypes()->where('is_parent', false)->firstOrFail();
|
||||||
|
$ticketLinkType = TicketLinkType::factory()->for($project)->create([
|
||||||
|
'label' => 'Blocks',
|
||||||
|
'counterpart_label' => 'Blocked by',
|
||||||
|
]);
|
||||||
|
$blockingTicket = Ticket::factory()->create([
|
||||||
|
'project_id' => $project->id,
|
||||||
|
'sequence_number' => 1,
|
||||||
|
'ticket_key' => 'CORE-1',
|
||||||
|
'ticket_priority_id' => $priority->id,
|
||||||
|
'ticket_type_id' => $taskType->id,
|
||||||
|
]);
|
||||||
|
$blockedTicket = Ticket::factory()->create([
|
||||||
|
'project_id' => $project->id,
|
||||||
|
'sequence_number' => 2,
|
||||||
|
'ticket_key' => 'CORE-2',
|
||||||
|
'ticket_priority_id' => $priority->id,
|
||||||
|
'ticket_type_id' => $taskType->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->syncProjectPermissions($project, $user, ['edit', 'view']);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->post(route('projects.tickets.links.store', [$project, $blockedTicket]), [
|
||||||
|
'linked_ticket_id' => $blockingTicket->id,
|
||||||
|
'ticket_link_type_id' => $ticketLinkType->id,
|
||||||
|
'link_direction' => 'counterpart',
|
||||||
|
])
|
||||||
|
->assertRedirect(route('projects.tickets.show', [$project, $blockedTicket]))
|
||||||
|
->assertSessionHas('status', 'Blocked by link was saved.');
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('ticket_links', [
|
||||||
|
'source_ticket_id' => $blockingTicket->id,
|
||||||
|
'target_ticket_id' => $blockedTicket->id,
|
||||||
|
'ticket_link_type_id' => $ticketLinkType->id,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('ticket_worklog_entries', [
|
||||||
|
'ticket_id' => $blockedTicket->id,
|
||||||
|
'summary' => 'Linked CORE-1 as Blocked by.',
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('ticket_worklog_entries', [
|
||||||
|
'ticket_id' => $blockingTicket->id,
|
||||||
|
'summary' => 'Linked CORE-2 as Blocks.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->post(route('projects.tickets.links.store', [$project, $blockedTicket]), [
|
||||||
|
'linked_ticket_id' => $blockingTicket->id,
|
||||||
|
'ticket_link_type_id' => $ticketLinkType->id,
|
||||||
|
'link_direction' => 'source',
|
||||||
|
])
|
||||||
|
->assertSessionHas('status', 'Blocks link was saved.');
|
||||||
|
|
||||||
|
$this->assertSame(1, TicketLink::query()->count());
|
||||||
|
$this->assertDatabaseHas('ticket_links', [
|
||||||
|
'source_ticket_id' => $blockedTicket->id,
|
||||||
|
'target_ticket_id' => $blockingTicket->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure ticket links cannot target the current ticket or a ticket from another project.
|
* Ensure ticket links cannot target the current ticket or a ticket from another project.
|
||||||
*/
|
*/
|
||||||
@@ -144,6 +212,15 @@ class ProjectTicketLinkControllerTest extends TestCase
|
|||||||
])
|
])
|
||||||
->assertRedirect(route('projects.tickets.show', [$project, $ticket]))
|
->assertRedirect(route('projects.tickets.show', [$project, $ticket]))
|
||||||
->assertSessionHasErrors('linked_ticket_id');
|
->assertSessionHasErrors('linked_ticket_id');
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->from(route('projects.tickets.show', [$project, $ticket]))
|
||||||
|
->post(route('projects.tickets.links.store', [$project, $ticket]), [
|
||||||
|
'linked_ticket_id' => $foreignTicket->id,
|
||||||
|
'ticket_link_type_id' => $ticketLinkType->id,
|
||||||
|
'link_direction' => 'sideways',
|
||||||
|
])
|
||||||
|
->assertSessionHasErrors('link_direction');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -398,6 +398,11 @@ if (serve) {
|
|||||||
} else {
|
} else {
|
||||||
params.delete('noLinks');
|
params.delete('noLinks');
|
||||||
params.delete('linkRejected');
|
params.delete('linkRejected');
|
||||||
|
if (data.link_direction === 'counterpart') {
|
||||||
|
params.set('incoming', '1');
|
||||||
|
} else {
|
||||||
|
params.delete('incoming');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
assert.ok(
|
assert.ok(
|
||||||
@@ -486,6 +491,8 @@ if (serve) {
|
|||||||
return result.body.split('id="main-content"')[1];
|
return result.body.split('id="main-content"')[1];
|
||||||
};
|
};
|
||||||
const html = await render();
|
const html = await render();
|
||||||
|
assert.ok(html.includes('id="link_direction"'));
|
||||||
|
assert.ok(html.includes('Blocked by'));
|
||||||
for (const text of [
|
for (const text of [
|
||||||
ticket.title,
|
ticket.title,
|
||||||
'Keep every existing ticket action available.',
|
'Keep every existing ticket action available.',
|
||||||
|
|||||||
@@ -70,6 +70,15 @@ try {
|
|||||||
assert.equal(await collapse.getAttribute('aria-expanded'), 'true');
|
assert.equal(await collapse.getAttribute('aria-expanded'), 'true');
|
||||||
await control('Link type').focus();
|
await control('Link type').focus();
|
||||||
await page.keyboard.press('Tab');
|
await page.keyboard.press('Tab');
|
||||||
|
assert.equal(
|
||||||
|
await control('This ticket').evaluate(
|
||||||
|
(el) =>
|
||||||
|
el === document.activeElement &&
|
||||||
|
el.matches(':focus-visible'),
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await control('Linked ticket').evaluate(
|
await control('Linked ticket').evaluate(
|
||||||
(el) =>
|
(el) =>
|
||||||
@@ -118,6 +127,7 @@ try {
|
|||||||
await save().click();
|
await save().click();
|
||||||
assert.deepEqual((await request).postDataJSON(), {
|
assert.deepEqual((await request).postDataJSON(), {
|
||||||
ticket_link_type_id: 1,
|
ticket_link_type_id: 1,
|
||||||
|
link_direction: 'source',
|
||||||
linked_ticket_id: 41,
|
linked_ticket_id: 41,
|
||||||
});
|
});
|
||||||
await section().getByText('Outgoing', { exact: true }).waitFor();
|
await section().getByText('Outgoing', { exact: true }).waitFor();
|
||||||
@@ -141,6 +151,25 @@ try {
|
|||||||
document.activeElement?.textContent.includes('Linked tickets'),
|
document.activeElement?.textContent.includes('Linked tickets'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await visit('?noLinks');
|
||||||
|
await control('This ticket').selectOption('counterpart');
|
||||||
|
await control('Linked ticket').selectOption('41');
|
||||||
|
await section()
|
||||||
|
.locator('form')
|
||||||
|
.last()
|
||||||
|
.getByText('Blocked by', { exact: true })
|
||||||
|
.waitFor();
|
||||||
|
request = page.waitForRequest(
|
||||||
|
(r) => r.method() === 'POST' && r.url().endsWith('/links'),
|
||||||
|
);
|
||||||
|
await save().click();
|
||||||
|
assert.deepEqual((await request).postDataJSON(), {
|
||||||
|
ticket_link_type_id: 1,
|
||||||
|
link_direction: 'counterpart',
|
||||||
|
linked_ticket_id: 41,
|
||||||
|
});
|
||||||
|
await section().getByText('Incoming', { exact: true }).waitFor();
|
||||||
|
|
||||||
await visit('?rejectLink');
|
await visit('?rejectLink');
|
||||||
await control('Linked ticket').selectOption('41');
|
await control('Linked ticket').selectOption('41');
|
||||||
await save().click();
|
await save().click();
|
||||||
|
|||||||
Reference in New Issue
Block a user