mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
fix(task-repeat): allow selecting day-of-month recurrence (#8896)
* fix(task-repeat): allow selecting day-of-month mode * fix(task-repeat): clarify monthly recurrence pattern * test(task-repeat): cover day-of-month persistence * docs(task-repeat): explain custom monthly patterns
This commit is contained in:
parent
762b3c5d8d
commit
c68929d1fb
7 changed files with 109 additions and 6 deletions
|
|
@ -27,6 +27,7 @@ You can also open the dialog from a repeating task instance or from a repeat pro
|
|||
- Schedule type (only for intervals > 1): Fixed schedule (every X from start date) or After completion (X after I finish).
|
||||
- Start date — when the pattern begins (required).
|
||||
- For weekly cycle: check the weekdays (Monday–Sunday) on which the task should recur.
|
||||
- For monthly cycle: choose a **Monthly pattern**. **Day of month** uses the day shown in **Start date**; first, second, third, fourth, and last patterns also require a **Weekday**.
|
||||
5. Optionally set **Tags to add** (chip list).
|
||||
6. Optionally set **Scheduled start time** (e.g. 15:00; leave blank for all-day). If you set a time, **Remind at** appears — choose when to be reminded (e.g. when it starts, 5 minutes before).
|
||||
7. Optionally set **Default Estimate** (duration). **Order** (number) controls creation order when several recurring tasks are created at the same time; see the field description in the dialog.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@ import {
|
|||
MatDialogRef,
|
||||
} from '@angular/material/dialog';
|
||||
import { DateAdapter, MatNativeDateModule } from '@angular/material/core';
|
||||
import { MatFormFieldHarness } from '@angular/material/form-field/testing';
|
||||
import { MatSelectHarness } from '@angular/material/select/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { Observable, of, Subject } from 'rxjs';
|
||||
|
|
@ -74,6 +77,7 @@ describe('DialogEditTaskRepeatCfgComponent', () => {
|
|||
getRepeatCfgReturnValue?:
|
||||
| Observable<TaskRepeatCfg | undefined>
|
||||
| Subject<TaskRepeatCfg>,
|
||||
renderTemplate = false,
|
||||
): Promise<ComponentFixture<DialogEditTaskRepeatCfgComponent>> => {
|
||||
mockDialogRef = jasmine.createSpyObj('MatDialogRef', ['close']);
|
||||
mockMatDialog = jasmine.createSpyObj('MatDialog', ['open']);
|
||||
|
|
@ -117,7 +121,7 @@ describe('DialogEditTaskRepeatCfgComponent', () => {
|
|||
formatTime: () => '12:00 PM',
|
||||
});
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
const testModule = TestBed.configureTestingModule({
|
||||
imports: [
|
||||
DialogEditTaskRepeatCfgComponent,
|
||||
MatDialogModule,
|
||||
|
|
@ -140,16 +144,20 @@ describe('DialogEditTaskRepeatCfgComponent', () => {
|
|||
{ provide: DateService, useValue: mockDateService },
|
||||
{ provide: DateAdapter, useClass: CustomDateAdapter },
|
||||
],
|
||||
})
|
||||
.overrideComponent(DialogEditTaskRepeatCfgComponent, {
|
||||
});
|
||||
|
||||
if (!renderTemplate) {
|
||||
testModule.overrideComponent(DialogEditTaskRepeatCfgComponent, {
|
||||
set: {
|
||||
// Use a minimal template to avoid @ngx-formly/material select rendering,
|
||||
// which triggers a compareWith validation error with Angular Material 21+.
|
||||
// These tests verify component signals/logic, not template rendering.
|
||||
template: '<div></div>',
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
}
|
||||
|
||||
await testModule.compileComponents();
|
||||
|
||||
return TestBed.createComponent(DialogEditTaskRepeatCfgComponent);
|
||||
};
|
||||
|
|
@ -158,6 +166,72 @@ describe('DialogEditTaskRepeatCfgComponent', () => {
|
|||
TestBed.resetTestingModule();
|
||||
});
|
||||
|
||||
it('keeps Day of month selected after switching from an Nth weekday (#8886)', async () => {
|
||||
const monthlyNthWeekdayCfg: TaskRepeatCfg = {
|
||||
...DEFAULT_TASK_REPEAT_CFG,
|
||||
id: 'repeat-cfg-monthly-nth-weekday',
|
||||
title: 'Monthly task',
|
||||
quickSetting: 'CUSTOM',
|
||||
repeatCycle: 'MONTHLY',
|
||||
startDate: '2026-06-09',
|
||||
monthlyWeekOfMonth: 2,
|
||||
monthlyWeekday: 1,
|
||||
};
|
||||
const fixture = await setupTestBed(
|
||||
{ repeatCfg: monthlyNthWeekdayCfg },
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const loader = TestbedHarnessEnvironment.loader(fixture);
|
||||
const selects = await loader.getAllHarnesses(MatSelectHarness);
|
||||
const formFieldsBeforeSwitch = await loader.getAllHarnesses(MatFormFieldHarness);
|
||||
const labelsBeforeSwitch = await Promise.all(
|
||||
formFieldsBeforeSwitch.map((formField) => formField.getLabel()),
|
||||
);
|
||||
expect(labelsBeforeSwitch).toContain(T.F.TASK_REPEAT.F.WEEKDAY);
|
||||
let monthlyPatternSelect: MatSelectHarness | undefined;
|
||||
let dayOfMonthOptionText = '';
|
||||
|
||||
for (const select of selects) {
|
||||
await select.open();
|
||||
const [dayOfMonthOption] = await select.getOptions({
|
||||
text: /MONTHLY_MODE_DAY_OF_MONTH/,
|
||||
});
|
||||
if (dayOfMonthOption) {
|
||||
monthlyPatternSelect = select;
|
||||
dayOfMonthOptionText = await dayOfMonthOption.getText();
|
||||
await dayOfMonthOption.click();
|
||||
break;
|
||||
}
|
||||
await select.close();
|
||||
}
|
||||
|
||||
expect(monthlyPatternSelect).toBeDefined();
|
||||
expect(await monthlyPatternSelect!.getValueText()).toBe(dayOfMonthOptionText);
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(fixture.componentInstance.repeatCfg().monthlyWeekOfMonth).toBeNull();
|
||||
const formFieldsAfterSwitch = await loader.getAllHarnesses(MatFormFieldHarness);
|
||||
const labelsAfterSwitch = await Promise.all(
|
||||
formFieldsAfterSwitch.map((formField) => formField.getLabel()),
|
||||
);
|
||||
expect(labelsAfterSwitch).not.toContain(T.F.TASK_REPEAT.F.WEEKDAY);
|
||||
|
||||
fixture.componentInstance.save();
|
||||
|
||||
const changes =
|
||||
mockTaskRepeatCfgService.updateTaskRepeatCfg.calls.mostRecent().args[1];
|
||||
expect(
|
||||
Object.prototype.hasOwnProperty.call(changes, 'monthlyWeekOfMonth'),
|
||||
).toBeTrue();
|
||||
expect(changes.monthlyWeekOfMonth).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('isLoading signal', () => {
|
||||
it('should be false when repeatCfg is provided directly (sync path)', async () => {
|
||||
const fixture = await setupTestBed({ repeatCfg: mockRepeatCfg });
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ import { getDateTimeFromClockString } from '../../../util/get-date-time-from-clo
|
|||
import { remindOptionToMilliseconds } from '../../tasks/util/remind-option-to-milliseconds';
|
||||
import { isValidSplitTime } from '../../../util/is-valid-split-time';
|
||||
import { DateService } from '../../../core/date/date.service';
|
||||
import { MAT_SELECT_CONFIG } from '@angular/material/select';
|
||||
|
||||
// Fields whose change requires offering "Update all task instances?" — covers
|
||||
// what propagates to existing tasks (vs. schedule fields, which only affect
|
||||
|
|
@ -88,6 +89,12 @@ const WEEKDAY_KEYS: (keyof TaskRepeatCfgCopy)[] = [
|
|||
templateUrl: './dialog-edit-task-repeat-cfg.component.html',
|
||||
styleUrls: ['./dialog-edit-task-repeat-cfg.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [
|
||||
{
|
||||
provide: MAT_SELECT_CONFIG,
|
||||
useValue: { canSelectNullableOptions: true },
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
MatDialogTitle,
|
||||
TranslatePipe,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {
|
|||
TASK_REPEAT_CFG_ADVANCED_FORM_CFG,
|
||||
TASK_REPEAT_CFG_ESSENTIAL_FORM_CFG,
|
||||
} from './task-repeat-cfg-form.const';
|
||||
import { T } from '../../../t.const';
|
||||
|
||||
describe('TaskRepeatCfgFormConfig', () => {
|
||||
it('should not contain startDate in essential form fields', () => {
|
||||
|
|
@ -23,6 +24,22 @@ describe('TaskRepeatCfgFormConfig', () => {
|
|||
expect(remindAtField).toBeUndefined();
|
||||
});
|
||||
|
||||
it('explains that Day of month uses the start date (#8886)', () => {
|
||||
const repeatContainer = TASK_REPEAT_CFG_ESSENTIAL_FORM_CFG.find(
|
||||
(field) => field.fieldGroupClassName === 'repeat-config-container',
|
||||
);
|
||||
const monthlyAnchor = repeatContainer?.fieldGroup?.find(
|
||||
(field) => field.fieldGroupClassName === 'monthly-anchor',
|
||||
);
|
||||
const monthlyPattern = monthlyAnchor?.fieldGroup?.find(
|
||||
(field) => field.key === 'monthlyWeekOfMonth',
|
||||
);
|
||||
|
||||
expect(monthlyPattern?.templateOptions?.description).toBe(
|
||||
T.F.TASK_REPEAT.F.MONTHLY_MODE_DAY_OF_MONTH_DESCRIPTION,
|
||||
);
|
||||
});
|
||||
|
||||
describe('weekdays group visibility (issue #8025)', () => {
|
||||
const repeatContainer = TASK_REPEAT_CFG_ESSENTIAL_FORM_CFG.find(
|
||||
(field) => field.fieldGroupClassName === 'repeat-config-container',
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ export const TASK_REPEAT_CFG_ESSENTIAL_FORM_CFG: FormlyFieldConfig[] = [
|
|||
defaultValue: null,
|
||||
templateOptions: {
|
||||
label: T.F.TASK_REPEAT.F.WEEK_OF_MONTH,
|
||||
description: T.F.TASK_REPEAT.F.MONTHLY_MODE_DAY_OF_MONTH_DESCRIPTION,
|
||||
options: [
|
||||
{ value: null, label: T.F.TASK_REPEAT.F.MONTHLY_MODE_DAY_OF_MONTH },
|
||||
{ value: 1, label: T.F.TASK_REPEAT.F.ORD_FIRST },
|
||||
|
|
|
|||
|
|
@ -2054,6 +2054,8 @@ const T = {
|
|||
INHERIT_SUBTASKS_DESCRIPTION: 'F.TASK_REPEAT.F.INHERIT_SUBTASKS_DESCRIPTION',
|
||||
MONDAY: 'F.TASK_REPEAT.F.MONDAY',
|
||||
MONTHLY_MODE_DAY_OF_MONTH: 'F.TASK_REPEAT.F.MONTHLY_MODE_DAY_OF_MONTH',
|
||||
MONTHLY_MODE_DAY_OF_MONTH_DESCRIPTION:
|
||||
'F.TASK_REPEAT.F.MONTHLY_MODE_DAY_OF_MONTH_DESCRIPTION',
|
||||
NOTES: 'F.TASK_REPEAT.F.NOTES',
|
||||
ORD_FIRST: 'F.TASK_REPEAT.F.ORD_FIRST',
|
||||
ORD_FIRST_NTH: 'F.TASK_REPEAT.F.ORD_FIRST_NTH',
|
||||
|
|
|
|||
|
|
@ -1997,6 +1997,7 @@
|
|||
"INHERIT_SUBTASKS_DESCRIPTION": "When enabled, subtasks from the most recent task instance will be recreated with the recurring task",
|
||||
"MONDAY": "Monday",
|
||||
"MONTHLY_MODE_DAY_OF_MONTH": "Day of month",
|
||||
"MONTHLY_MODE_DAY_OF_MONTH_DESCRIPTION": "\"Day of month\" uses the day shown in Start date.",
|
||||
"NOTES": "Default notes",
|
||||
"ORD_FIRST": "first",
|
||||
"ORD_FIRST_NTH": "first",
|
||||
|
|
@ -2038,7 +2039,7 @@
|
|||
"WAIT_FOR_COMPLETION": "Only create next task after completion",
|
||||
"WAIT_FOR_COMPLETION_DESCRIPTION": "Prevents multiple pending recurring tasks from piling up. The next instance will only be created once the current task is completed",
|
||||
"WEDNESDAY": "Wednesday",
|
||||
"WEEK_OF_MONTH": "Week of month",
|
||||
"WEEK_OF_MONTH": "Monthly pattern",
|
||||
"WEEKDAY": "Weekday",
|
||||
"WEEKDAY_REQUIRED": "Select at least one weekday"
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue