fix: unable to set due date of new task in add task bar #5495

This commit is contained in:
Johannes Millan 2025-11-13 19:13:47 +01:00
parent 3d4b7fe2e9
commit 68cfe28bbc
7 changed files with 157 additions and 9 deletions

View file

@ -0,0 +1,89 @@
import { type Locator, type Page } from '@playwright/test';
import { expect, test } from '../../fixtures/test.fixture';
const ADD_TASK_BAR = 'add-task-bar.global';
const ADD_TASK_INPUT = `${ADD_TASK_BAR} input`;
const DUE_BUTTON = `${ADD_TASK_BAR} [data-test="add-task-bar-due-btn"]`;
const SCHEDULE_DIALOG = 'dialog-schedule-task';
const QUICK_ACCESS_BTN = `${SCHEDULE_DIALOG} .quick-access button`;
const ensureGlobalAddTaskBarOpen = async (page: Page): Promise<Locator> => {
const addTaskInput = page.locator(ADD_TASK_INPUT).first();
const isVisible = await addTaskInput.isVisible().catch(() => false);
if (!isVisible) {
const addBtn = page.locator('.tour-addBtn').first();
await addBtn.waitFor({ state: 'visible', timeout: 10000 });
await addBtn.click();
}
await addTaskInput.waitFor({ state: 'visible', timeout: 10000 });
return addTaskInput;
};
const openScheduleDialogFromBar = async (
page: Page,
): Promise<{ dialog: Locator; dueButton: Locator }> => {
const dueButton = page.locator(DUE_BUTTON).first();
await dueButton.waitFor({ state: 'visible', timeout: 10000 });
await dueButton.click();
const dialog = page.locator(SCHEDULE_DIALOG);
await dialog.waitFor({ state: 'visible', timeout: 10000 });
return { dialog, dueButton };
};
test.describe('Add Task Bar date picker', () => {
test.beforeEach(async ({ workViewPage }) => {
await workViewPage.waitForTaskList();
});
test('selecting Today keeps add-task-bar open and sets the date', async ({
page,
testPrefix,
}) => {
const input = await ensureGlobalAddTaskBarOpen(page);
await input.fill(`${testPrefix}-today quick date`);
const { dialog, dueButton } = await openScheduleDialogFromBar(page);
await dialog.locator(QUICK_ACCESS_BTN).first().click();
await dialog.waitFor({ state: 'hidden', timeout: 10000 });
await expect(page.locator(ADD_TASK_INPUT)).toBeVisible();
await expect(dueButton).toHaveClass(/has-value/);
await expect(dueButton).toContainText(/today/i);
});
test('selecting Tomorrow keeps add-task-bar open and sets the date', async ({
page,
testPrefix,
}) => {
const input = await ensureGlobalAddTaskBarOpen(page);
await input.fill(`${testPrefix}-tomorrow quick date`);
const { dialog, dueButton } = await openScheduleDialogFromBar(page);
await dialog.locator(QUICK_ACCESS_BTN).nth(1).click();
await dialog.waitFor({ state: 'hidden', timeout: 10000 });
await expect(page.locator(ADD_TASK_INPUT)).toBeVisible();
await expect(dueButton).toHaveClass(/has-value/);
await expect(dueButton).toContainText(/tomorrow/i);
});
test('canceling the dialog keeps add-task-bar open without setting a date', async ({
page,
testPrefix,
}) => {
const input = await ensureGlobalAddTaskBarOpen(page);
await input.fill(`${testPrefix}-cancel quick date`);
const { dialog, dueButton } = await openScheduleDialogFromBar(page);
await dialog.locator('button:has-text("Cancel")').click();
await dialog.waitFor({ state: 'hidden', timeout: 10000 });
await expect(page.locator(ADD_TASK_INPUT)).toBeVisible();
await expect(dueButton).not.toHaveClass(/has-value/);
await expect(dueButton).toContainText(/due/i);
});
});

View file

@ -28,6 +28,7 @@
<button
mat-button
class="action-btn"
data-test="add-task-bar-due-btn"
(click)="openScheduleDialog()"
[class.has-value]="state().date"
>

View file

@ -496,6 +496,16 @@ describe('AddTaskBarActionsComponent', () => {
expect(dialogConfig?.data?.isSelectDueOnly).toBe(true);
});
});
it('should emit dialog open state changes', async () => {
const emitSpy = spyOn(component.scheduleDialogOpenChange, 'emit');
component.openScheduleDialog();
expect(emitSpy).toHaveBeenCalledWith(true);
await new Promise((resolve) => setTimeout(resolve));
expect(emitSpy).toHaveBeenCalledWith(false);
});
});
describe('Tag Operations', () => {

View file

@ -17,7 +17,7 @@ import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { first } from 'rxjs/operators';
import { ProjectService } from '../../../project/project.service';
import { TagService } from '../../../tag/tag.service';
import { MatDialog } from '@angular/material/dialog';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { DialogScheduleTaskComponent } from '../../../planner/dialog-schedule-task/dialog-schedule-task.component';
import { AddTaskBarStateService } from '../add-task-bar-state.service';
import { AddTaskBarParserService } from '../add-task-bar-parser.service';
@ -66,6 +66,7 @@ export class AddTaskBarActionsComponent {
// Outputs
estimateChanged = output<string>();
refocus = output<void>();
scheduleDialogOpenChange = output<boolean>();
// Menu state
isProjectMenuOpen = signal<boolean>(false);
@ -136,13 +137,20 @@ export class AddTaskBarActionsComponent {
openScheduleDialog(): void {
const state = this.state();
const dialogRef = this._matDialog.open(DialogScheduleTaskComponent, {
data: {
targetDay: state.date || undefined,
targetTime: state.time || undefined,
isSelectDueOnly: true,
},
});
this.scheduleDialogOpenChange.emit(true);
let dialogRef!: MatDialogRef<DialogScheduleTaskComponent>;
try {
dialogRef = this._matDialog.open(DialogScheduleTaskComponent, {
data: {
targetDay: state.date || undefined,
targetTime: state.time || undefined,
isSelectDueOnly: true,
},
});
} catch (err) {
this.scheduleDialogOpenChange.emit(false);
throw err;
}
dialogRef.afterClosed().subscribe((result) => {
if (result && typeof result === 'object' && result.date) {
@ -151,6 +159,11 @@ export class AddTaskBarActionsComponent {
this.stateService.updateRemindOption(result.remindOption);
}
this.refocus.emit();
window.setTimeout(() => {
if (!this._destroyRef.destroyed) {
this.scheduleDialogOpenChange.emit(false);
}
});
});
}

View file

@ -109,6 +109,7 @@
[isHideTagBtn]="!!planForDay()"
[isHideDueBtn]="!!planForDay()"
(refocus)="focusInput()"
(scheduleDialogOpenChange)="onScheduleDialogOpenChange($event)"
></add-task-bar-actions>
}
</div>

View file

@ -531,4 +531,33 @@ describe('AddTaskBarComponent', () => {
expect(defaultProject).toBeUndefined();
});
});
describe('document click handling', () => {
beforeEach(() => {
fixture.detectChanges();
});
it('should emit done when clicking outside and no dialog is open', () => {
const doneSpy = spyOn(component.done, 'emit');
const event = {
target: document.createElement('div'),
} as unknown as MouseEvent;
component.onDocumentClick(event);
expect(doneSpy).toHaveBeenCalled();
});
it('should not emit done when schedule dialog is open', () => {
const doneSpy = spyOn(component.done, 'emit');
component.onScheduleDialogOpenChange(true);
const event = {
target: document.createElement('div'),
} as unknown as MouseEvent;
component.onDocumentClick(event);
expect(doneSpy).not.toHaveBeenCalled();
});
});
});

View file

@ -143,6 +143,7 @@ export class AddTaskBarComponent implements AfterViewInit, OnInit, OnDestroy {
isSearchLoading = signal(false);
activatedSuggestion$ = new BehaviorSubject<AddTaskSuggestion | null>(null);
isMentionListShown = signal(false);
isScheduleDialogOpen = signal(false);
// Computed signals for projects and tags (sorted for consistency)
projects = this._projectService.listSortedForUI;
@ -597,7 +598,7 @@ export class AddTaskBarComponent implements AfterViewInit, OnInit, OnDestroy {
const overlayContainer = target.closest('.cdk-overlay-container');
// If click is outside the component and not on autocomplete or menu options, close it
if (!component && !overlayContainer) {
if (!component && !overlayContainer && !this.isScheduleDialogOpen()) {
this.done.emit();
}
}
@ -761,4 +762,8 @@ export class AddTaskBarComponent implements AfterViewInit, OnInit, OnDestroy {
updateListShown(isShown: boolean): void {
window.setTimeout(() => this.isMentionListShown.set(isShown));
}
onScheduleDialogOpenChange(isOpen: boolean): void {
this.isScheduleDialogOpen.set(isOpen);
}
}