mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-26 09:23:50 +00:00
fix: unable to set due date of new task in add task bar #5495
This commit is contained in:
parent
3d4b7fe2e9
commit
68cfe28bbc
7 changed files with 157 additions and 9 deletions
89
e2e/tests/add-task-bar/add-task-bar-date-picker.spec.ts
Normal file
89
e2e/tests/add-task-bar/add-task-bar-date-picker.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -28,6 +28,7 @@
|
|||
<button
|
||||
mat-button
|
||||
class="action-btn"
|
||||
data-test="add-task-bar-due-btn"
|
||||
(click)="openScheduleDialog()"
|
||||
[class.has-value]="state().date"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@
|
|||
[isHideTagBtn]="!!planForDay()"
|
||||
[isHideDueBtn]="!!planForDay()"
|
||||
(refocus)="focusInput()"
|
||||
(scheduleDialogOpenChange)="onScheduleDialogOpenChange($event)"
|
||||
></add-task-bar-actions>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue