Skip to content

Commit 44f8f02

Browse files
renemadsenclaude
andcommitted
fix(calendar): default new task to the last-activated board
The create-task modal pre-selected the lowest-id board (the first-created board, e.g. "Miljøtilsyn") whenever more than one board was active in the sidebar, ignoring the board the user had just marked active. The sidebar "active" state is a transient multi-select visibility filter with no persisted default-board concept, and the modal only honoured it when exactly one board was checked. Track the most-recently-activated board in memory: set it when the user turns a board ON in the sidebar (and seed it to the auto-selected board on load), and default the create-task modal to it while it stays active. Falls back to the existing behaviour when that board is no longer active. No DB/API change. Adds a Playwright e2e (n/calendar-default-board.spec.ts) covering the default and the fallback, and an id on the board select for the test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c95f53f commit 44f8f02

3 files changed

Lines changed: 133 additions & 1 deletion

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { test, expect } from '@playwright/test';
2+
import { LoginPage } from '../../../Page objects/Login.page';
3+
import { generateRandmString } from '../../../helper-functions';
4+
import { CalendarUiEnhancementsPage } from '../calendar-ui-enhancements.page';
5+
import { BackendConfigurationPropertiesPage, PropertyCreateUpdate } from '../BackendConfigurationProperties.page';
6+
7+
// Regression for the user report: the create-task modal pre-selected the
8+
// lowest-id board (first created) whenever more than one board was active,
9+
// ignoring the board the user had just activated. PropertyCreateUpdate fields:
10+
// name?, chrNumber?, cvrNumber?, address?, workOrderFlow?.
11+
const property: PropertyCreateUpdate = {
12+
name: 'cal-board-' + generateRandmString(5),
13+
chrNumber: generateRandmString(5),
14+
address: generateRandmString(5),
15+
cvrNumber: '1111111',
16+
};
17+
18+
// Board A is created first (lower id, the "Miljøtilsyn" analogue); board B is
19+
// created + activated last. With both active, the modal must default to B.
20+
const boardA = 'A-' + generateRandmString(5);
21+
const boardB = 'B-' + generateRandmString(5);
22+
23+
// --- helpers --------------------------------------------------------------
24+
25+
async function createBoard(page: import('@playwright/test').Page, name: string) {
26+
await page.locator('a.sidebar-action-link', { hasText: 'Opret tavle' }).click();
27+
const dialog = page.locator('mat-dialog-container');
28+
await dialog.locator('input[formcontrolname="name"]').fill(name);
29+
await dialog.locator('button.btn-primary', { hasText: 'Opret' }).click();
30+
await dialog.waitFor({ state: 'detached', timeout: 10000 });
31+
await expect(page.locator('.board-list .board-name', { hasText: name })).toBeVisible({ timeout: 10000 });
32+
}
33+
34+
function boardItem(page: import('@playwright/test').Page, name: string) {
35+
return page.locator('.board-item', { has: page.locator('.board-name', { hasText: name }) });
36+
}
37+
38+
async function activateBoard(page: import('@playwright/test').Page, name: string) {
39+
await boardItem(page, name).locator('.board-name').click();
40+
await expect(boardItem(page, name).locator('.board-checkbox.active')).toBeVisible({ timeout: 5000 });
41+
}
42+
43+
async function deactivateBoard(page: import('@playwright/test').Page, name: string) {
44+
await boardItem(page, name).locator('.board-name').click();
45+
await expect(boardItem(page, name).locator('.board-checkbox.active')).toHaveCount(0, { timeout: 5000 });
46+
}
47+
48+
// The mtx-select displays the selected board's name as its value label.
49+
async function selectedBoardLabel(page: import('@playwright/test').Page): Promise<string> {
50+
return (await page.locator('#calendarEventBoard .mtx-select__value, #calendarEventBoard .ng-value-label')
51+
.first().innerText()).trim();
52+
}
53+
54+
// --- tests ----------------------------------------------------------------
55+
56+
test.describe.serial('Calendar new-task default board', () => {
57+
test.beforeEach(async ({ page }) => {
58+
await page.goto('http://localhost:4200');
59+
await new LoginPage(page).login();
60+
});
61+
62+
test('seed: property + two boards, activate A then B', async ({ page }) => {
63+
const propertiesPage = new BackendConfigurationPropertiesPage(page);
64+
await propertiesPage.goToProperties();
65+
await propertiesPage.createProperty(property);
66+
67+
const calendarPage = new CalendarUiEnhancementsPage(page);
68+
await calendarPage.goToCalendar();
69+
await calendarPage.selectProperty(property.name as string);
70+
71+
await createBoard(page, boardA);
72+
await createBoard(page, boardB);
73+
74+
// Activate A first, then B — both stay checked, B is the last activated.
75+
await activateBoard(page, boardA);
76+
await activateBoard(page, boardB);
77+
await expect(boardItem(page, boardA).locator('.board-checkbox.active')).toBeVisible();
78+
await expect(boardItem(page, boardB).locator('.board-checkbox.active')).toBeVisible();
79+
});
80+
81+
test('create modal defaults to the last-activated board (B), not the lowest-id board (A)', async ({ page }) => {
82+
const calendarPage = new CalendarUiEnhancementsPage(page);
83+
await calendarPage.goToCalendar();
84+
await calendarPage.selectProperty(property.name as string);
85+
86+
// Re-activate A then B (a fresh page load reset the in-memory filter).
87+
await activateBoard(page, boardA);
88+
await activateBoard(page, boardB);
89+
90+
await calendarPage.clickEmptyTimeSlot(1, 9); // Tuesday 09:00 next week
91+
await expect(page.locator('#calendarEventTitle')).toBeVisible({ timeout: 10000 });
92+
93+
expect(await selectedBoardLabel(page)).toBe(boardB);
94+
});
95+
96+
test('falls back when the last-activated board is deactivated', async ({ page }) => {
97+
const calendarPage = new CalendarUiEnhancementsPage(page);
98+
await calendarPage.goToCalendar();
99+
await calendarPage.selectProperty(property.name as string);
100+
101+
await activateBoard(page, boardA);
102+
await activateBoard(page, boardB);
103+
await deactivateBoard(page, boardB); // B (the last-activated) is no longer active
104+
105+
await calendarPage.clickEmptyTimeSlot(1, 9);
106+
await expect(page.locator('#calendarEventTitle')).toBeVisible({ timeout: 10000 });
107+
108+
// The guard drops the no-longer-active last-activated board: the modal must
109+
// NOT default to B. It falls back to the existing behavior (the lowest-id
110+
// board — here the property's auto-created "Default" board, which stays
111+
// active alongside A). The board select is [clearable]="false", so a real
112+
// board is always shown — assert it's non-empty and specifically not B.
113+
const label = await selectedBoardLabel(page);
114+
expect(label.length).toBeGreaterThan(0);
115+
expect(label).not.toBe(boardB);
116+
});
117+
});

eform-client/src/app/plugins/modules/backend-configuration-pn/modules/calendar/components/calendar-container/calendar-container.component.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ export class CalendarContainerComponent implements OnInit, OnDestroy {
7272
currentDate: string = (() => { const d = new Date(); return `${d.getFullYear()}-${(d.getMonth()+1).toString().padStart(2,'0')}-${d.getDate().toString().padStart(2,'0')}`; })();
7373
viewMode: 'week' | 'day' | 'schedule' = 'week';
7474
activeBoardIds: number[] = [];
75+
// The board the user most recently turned ON in the sidebar. Transient
76+
// (in-memory only) — used to default the create-task modal to that board
77+
// even when several boards stay checked. Re-seeded on board load.
78+
lastActivatedBoardId: number | null = null;
7579
activeSiteIds: number[] = [];
7680
activeTeamIds: number[] = [];
7781
activeTagNames: string[] = [];
@@ -174,6 +178,7 @@ export class CalendarContainerComponent implements OnInit, OnDestroy {
174178
if (autoSelectDefault && this.boards.length > 0) {
175179
const defaultBoard = this.boards.reduce((min, b) => b.id < min.id ? b : min);
176180
this.stateService.setActiveBoardIds([defaultBoard.id]);
181+
this.lastActivatedBoardId = defaultBoard.id;
177182
}
178183
this.propertiesService.getLinkedFolderDtos(propertyId).subscribe(folderRes => {
179184
if (folderRes && folderRes.success) {
@@ -348,7 +353,10 @@ export class CalendarContainerComponent implements OnInit, OnDestroy {
348353
date: event.date,
349354
startHour: event.startHour,
350355
boards: this.boards,
351-
selectedBoardId: this.activeBoardIds.length === 1 ? this.activeBoardIds[0] : undefined,
356+
selectedBoardId:
357+
(this.lastActivatedBoardId != null && this.activeBoardIds.includes(this.lastActivatedBoardId))
358+
? this.lastActivatedBoardId
359+
: (this.activeBoardIds.length === 1 ? this.activeBoardIds[0] : undefined),
352360
employees: this.employees,
353361
tags: this.tags.map(t => t.name),
354362
propertyId: this.currentPropertyId!,
@@ -466,6 +474,12 @@ export class CalendarContainerComponent implements OnInit, OnDestroy {
466474
}
467475

468476
onBoardToggled(boardId: number) {
477+
// The store update is async, so activeBoardIds here still reflects the
478+
// pre-toggle state: if the board is not currently active, this click is
479+
// turning it ON — remember it as the default for new tasks.
480+
if (!this.activeBoardIds.includes(boardId)) {
481+
this.lastActivatedBoardId = boardId;
482+
}
469483
this.stateService.toggleBoard(boardId);
470484
this.loadTasks();
471485
}

eform-client/src/app/plugins/modules/backend-configuration-pn/modules/calendar/modals/task-create-edit-modal/task-create-edit-modal.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@
446446
<mat-form-field class="w-100">
447447
<mat-label>{{ 'My boards' | translate }}</mat-label>
448448
<mtx-select
449+
id="calendarEventBoard"
449450
[formControl]="boardControl"
450451
[items]="filteredBoards"
451452
bindValue="id"

0 commit comments

Comments
 (0)