Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { setupStimulusTest, type StimulusTestContext } from 'core-stimulus/test-
import type SplitViewSyncControllerType from './split-view-sync.controller';

const MOVED_EVENT = 'op-dispatched:backlogs:work-package-moved';
const SORTABLE_MOVED_EVENT = 'sortable-lists:moved';

describe('Backlogs split-view-sync controller', () => {
let ctx:StimulusTestContext;
Expand Down Expand Up @@ -77,7 +78,7 @@ describe('Backlogs split-view-sync controller', () => {
async function renderHost() {
await ctx.mount(`
<div data-controller="backlogs--split-view-sync"
data-action="${MOVED_EVENT}@document->backlogs--split-view-sync#onWorkPackageMoved"></div>
data-action="${MOVED_EVENT}@document->backlogs--split-view-sync#onWorkPackageMoved ${SORTABLE_MOVED_EVENT}@document->backlogs--split-view-sync#onSortableListsMoved"></div>
`);
const host = ctx.container.querySelector<HTMLElement>('[data-controller="backlogs--split-view-sync"]')!;

Expand All @@ -91,6 +92,10 @@ describe('Backlogs split-view-sync controller', () => {
document.dispatchEvent(new CustomEvent(MOVED_EVENT, { detail }));
}

function dispatchSortableMoved(detail:object) {
document.dispatchEvent(new CustomEvent(SORTABLE_MOVED_EVENT, { detail }));
}

it('refreshes the moved work package cache when it is loaded', async () => {
await renderHost();

Expand Down Expand Up @@ -158,4 +163,25 @@ describe('Backlogs split-view-sync controller', () => {

expect(refresh).not.toHaveBeenCalled();
});

it('refreshes a cached work package on sortable-lists:moved', async () => {
await renderHost();

dispatchSortableMoved({ itemId: '1234' });

await waitFor(() => {
expect(state).toHaveBeenCalledWith('1234');
expect(id).toHaveBeenCalledWith('1234');
expect(refresh).toHaveBeenCalled();
});
});

it('ignores a sortable-lists:moved event without an item id', async () => {
await renderHost();

dispatchSortableMoved({});
await ctx.nextFrame();

expect(refresh).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ import { useAngularServices, type ServiceKey } from 'core-stimulus/mixins/use-an

// A split view open on a moved work package caches the lock_version it fetched
// on opening, so the next edit after a move would fail with a
// conflicting-modifications error. The server signals every successful move via
// a document event; this controller refreshes the moved work package in the
// Angular cache so the split view stays editable.
// conflicting-modifications error. Every successful move is signalled via a
// document event, either dispatched by the server (cross-list moves) or by
// the client-side sortable-lists controller (same-list moves, which resolve
// with a 204 and never reach the server-dispatched event); this controller
// refreshes the moved work package in the Angular cache so the split view
// stays editable.
export default class SplitViewSyncController extends Controller {
static services:ServiceKey[] = ['apiV3Service'];

Expand All @@ -55,11 +58,25 @@ export default class SplitViewSyncController extends Controller {
// is correct as well.
onWorkPackageMoved(event:CustomEvent<{ work_package_id?:number }>):void {
const workPackageId = event.detail?.work_package_id;
// apiV3Service is wired asynchronously via useAngularServices, so it may be absent
// if the event somehow fires before the services resolve.
if (workPackageId === undefined || !this.apiV3Service) { return; }
if (workPackageId === undefined) { return; }
this.refreshWorkPackage(workPackageId.toString());
}

// Bound to the client-dispatched `sortable-lists:moved` document event. Same
// refresh as onWorkPackageMoved: optimistic same-list moves answer with 204,
// so no server-dispatched moved event exists on that path. On cross-list
// moves both events fire; the double refresh is idempotent.
onSortableListsMoved(event:CustomEvent<{ itemId?:string }>):void {
const itemId = event.detail?.itemId;
if (itemId === undefined) { return; }
this.refreshWorkPackage(itemId);
}

private refreshWorkPackage(id:string):void {
// apiV3Service is wired asynchronously via useAngularServices, so it may be
// absent if an event somehow fires before the services resolve.
if (!this.apiV3Service) { return; }

const id = workPackageId.toString();
const { work_packages: workPackages } = this.apiV3Service;

if (workPackages.cache.state(id).hasValue()) {
Expand Down
Loading
Loading