mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
feat(work-view): show recurring tasks in project view
Add a collapsible "Recurring Tasks" section at the bottom of the project work-view showing repeatable task configs for the active project. Clicking a row opens the edit dialog, hovering shows next due date. The section is hidden on tag views, when no repeat configs exist for the project, and when task view filters/grouping are active. Closes #4933
This commit is contained in:
parent
9ef05b4fab
commit
564ca17254
10 changed files with 193 additions and 0 deletions
|
|
@ -49,6 +49,7 @@ export enum LS {
|
|||
DONE_TASKS_HIDDEN = 'DONE_TASKS_HIDDEN',
|
||||
LATER_TODAY_TASKS_HIDDEN = 'LATER_TODAY_TASKS_HIDDEN',
|
||||
OVERDUE_TASKS_HIDDEN = 'OVERDUE_TASKS_HIDDEN',
|
||||
REPEAT_CFGS_HIDDEN = 'REPEAT_CFGS_HIDDEN',
|
||||
|
||||
// Magic side nav
|
||||
NAV_SIDEBAR_EXPANDED = 'SUP_NAV_SIDEBAR_EXPANDED',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
<div
|
||||
(click)="editTaskRepeatCfg()"
|
||||
[matTooltip]="nextDueTooltip()"
|
||||
>
|
||||
<mat-icon svgIcon="repeat"></mat-icon>
|
||||
<div class="title">{{ repeatCfg().title }}</div>
|
||||
<div class="info">{{ getRepeatInfoText() }}</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
@use '../../../../styles/_globals.scss' as *;
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
margin-bottom: var(--s-half);
|
||||
}
|
||||
|
||||
:host > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: var(--s-quarter);
|
||||
padding-bottom: var(--s-quarter);
|
||||
border: 1px solid var(--extra-border-color);
|
||||
border-radius: var(--task-border-radius);
|
||||
opacity: 0.9;
|
||||
cursor: pointer;
|
||||
min-height: var(--planner-item-height);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--c-primary);
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
margin-left: var(--s);
|
||||
margin-right: var(--s);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info {
|
||||
white-space: nowrap;
|
||||
margin-left: var(--s);
|
||||
margin-right: var(--s);
|
||||
opacity: 0.7;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import { TaskRepeatCfg } from '../task-repeat-cfg.model';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { DialogEditTaskRepeatCfgComponent } from '../dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component';
|
||||
import { getTaskRepeatInfoText } from '../../tasks/task-detail-panel/get-task-repeat-info-text.util';
|
||||
import { T } from '../../../t.const';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { DateTimeFormatService } from '../../../core/date-time-format/date-time-format.service';
|
||||
import { getNextRepeatOccurrence } from '../store/get-next-repeat-occurrence.util';
|
||||
import { formatMonthDay } from '../../../util/format-month-day.util';
|
||||
|
||||
@Component({
|
||||
selector: 'repeat-cfg-preview',
|
||||
templateUrl: './repeat-cfg-preview.component.html',
|
||||
styleUrl: './repeat-cfg-preview.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [MatIcon, TranslatePipe, MatTooltip],
|
||||
})
|
||||
export class RepeatCfgPreviewComponent {
|
||||
private _matDialog = inject(MatDialog);
|
||||
private _translateService = inject(TranslateService);
|
||||
private _dateTimeFormatService = inject(DateTimeFormatService);
|
||||
|
||||
repeatCfg = input.required<TaskRepeatCfg>();
|
||||
|
||||
T = T;
|
||||
|
||||
nextDueTooltip = computed(() => {
|
||||
const cfg = this.repeatCfg();
|
||||
const nextDate = getNextRepeatOccurrence(cfg, new Date());
|
||||
if (!nextDate) {
|
||||
return '';
|
||||
}
|
||||
const locale = this._dateTimeFormatService.currentLocale();
|
||||
const formatted = formatMonthDay(nextDate, locale);
|
||||
const nextLabel = this._translateService.instant(T.SCHEDULE.NEXT);
|
||||
return `${nextLabel} ${formatted}`;
|
||||
});
|
||||
|
||||
editTaskRepeatCfg(): void {
|
||||
this._matDialog.open(DialogEditTaskRepeatCfgComponent, {
|
||||
restoreFocus: false,
|
||||
data: { repeatCfg: this.repeatCfg() },
|
||||
});
|
||||
}
|
||||
|
||||
getRepeatInfoText(): string {
|
||||
const [key, params] = getTaskRepeatInfoText(
|
||||
this.repeatCfg(),
|
||||
this._dateTimeFormatService.currentLocale(),
|
||||
this._dateTimeFormatService,
|
||||
);
|
||||
return this._translateService.instant(key, params);
|
||||
}
|
||||
}
|
||||
|
|
@ -154,6 +154,14 @@ export const selectAllUnprocessedTaskRepeatCfgs = createSelector(
|
|||
);
|
||||
},
|
||||
);
|
||||
export const selectTaskRepeatCfgsByProjectId = createSelector(
|
||||
selectAllTaskRepeatCfgs,
|
||||
(taskRepeatCfgs: TaskRepeatCfg[], props: { projectId: string }): TaskRepeatCfg[] => {
|
||||
return taskRepeatCfgs
|
||||
.filter((cfg) => cfg.projectId === props.projectId)
|
||||
.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
|
||||
},
|
||||
);
|
||||
export const selectTaskRepeatCfgByIdAllowUndefined = createSelector(
|
||||
selectTaskRepeatCfgFeatureState,
|
||||
(state: TaskRepeatCfgState, props: { id: string }): TaskRepeatCfg | undefined =>
|
||||
|
|
|
|||
|
|
@ -310,6 +310,31 @@
|
|||
</div>
|
||||
}
|
||||
|
||||
@if (isShowRepeatCfgsPanel()) {
|
||||
<div
|
||||
@expand
|
||||
class="collapsible-section"
|
||||
>
|
||||
<collapsible
|
||||
[title]="
|
||||
(T.WW.RECURRING_TASKS | translate) +
|
||||
' (' +
|
||||
repeatCfgsForProject().length +
|
||||
')'
|
||||
"
|
||||
[isExpanded]="!isRepeatCfgsHidden()"
|
||||
(isExpandedChange)="isRepeatCfgsHidden.set(!$event)"
|
||||
[isIconBefore]="true"
|
||||
>
|
||||
<div class="repeat-cfg-list">
|
||||
@for (repeatCfg of repeatCfgsForProject(); track repeatCfg.id) {
|
||||
<repeat-cfg-preview [repeatCfg]="repeatCfg"></repeat-cfg-preview>
|
||||
}
|
||||
</div>
|
||||
</collapsible>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!isPlanningMode() && isOnTodayList()) {
|
||||
<finish-day-btn [hasDoneTasks]="hasDoneTasks()"></finish-day-btn>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -327,3 +327,7 @@ finish-day-btn,
|
|||
padding-bottom: 8px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.repeat-cfg-list {
|
||||
padding: var(--s-half);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
from,
|
||||
fromEvent,
|
||||
Observable,
|
||||
of,
|
||||
ReplaySubject,
|
||||
Subscription,
|
||||
timer,
|
||||
|
|
@ -63,6 +64,9 @@ import { TODAY_TAG } from '../tag/tag.const';
|
|||
import { LS } from '../../core/persistence/storage-keys.const';
|
||||
import { FinishDayBtnComponent } from './finish-day-btn/finish-day-btn.component';
|
||||
import { ScheduledDateGroupPipe } from '../../ui/pipes/scheduled-date-group.pipe';
|
||||
import { selectTaskRepeatCfgsByProjectId } from '../task-repeat-cfg/store/task-repeat-cfg.selectors';
|
||||
import { TaskRepeatCfg } from '../task-repeat-cfg/task-repeat-cfg.model';
|
||||
import { RepeatCfgPreviewComponent } from '../task-repeat-cfg/repeat-cfg-preview/repeat-cfg-preview.component';
|
||||
|
||||
@Component({
|
||||
selector: 'work-view',
|
||||
|
|
@ -94,6 +98,7 @@ import { ScheduledDateGroupPipe } from '../../ui/pipes/scheduled-date-group.pipe
|
|||
CommonModule,
|
||||
FinishDayBtnComponent,
|
||||
ScheduledDateGroupPipe,
|
||||
RepeatCfgPreviewComponent,
|
||||
],
|
||||
})
|
||||
export class WorkViewComponent implements OnInit, OnDestroy {
|
||||
|
|
@ -140,6 +145,30 @@ export class WorkViewComponent implements OnInit, OnDestroy {
|
|||
isDoneHidden = signal(!!localStorage.getItem(LS.DONE_TASKS_HIDDEN));
|
||||
isLaterTodayHidden = signal(!!localStorage.getItem(LS.LATER_TODAY_TASKS_HIDDEN));
|
||||
isOverdueHidden = signal(!!localStorage.getItem(LS.OVERDUE_TASKS_HIDDEN));
|
||||
isRepeatCfgsHidden = signal(!!localStorage.getItem(LS.REPEAT_CFGS_HIDDEN));
|
||||
|
||||
isActiveContextProject = toSignal(this.workContextService.isActiveWorkContextProject$, {
|
||||
initialValue: false,
|
||||
});
|
||||
repeatCfgsForProject = toSignal(
|
||||
this.workContextService.activeWorkContextTypeAndId$.pipe(
|
||||
switchMap(({ activeType, activeId }) =>
|
||||
activeType === 'PROJECT'
|
||||
? this._store.select(selectTaskRepeatCfgsByProjectId, {
|
||||
projectId: activeId,
|
||||
})
|
||||
: of([] as TaskRepeatCfg[]),
|
||||
),
|
||||
),
|
||||
{ initialValue: [] as TaskRepeatCfg[] },
|
||||
);
|
||||
|
||||
isShowRepeatCfgsPanel = computed(
|
||||
() =>
|
||||
this.isActiveContextProject() &&
|
||||
!this.customizerService.isCustomized() &&
|
||||
this.repeatCfgsForProject().length > 0,
|
||||
);
|
||||
|
||||
isShowOverduePanel = computed(
|
||||
() => this.isOnTodayList() && this.overdueTasks().length > 0,
|
||||
|
|
@ -231,6 +260,15 @@ export class WorkViewComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
const isHidden = this.isRepeatCfgsHidden();
|
||||
if (isHidden) {
|
||||
localStorage.setItem(LS.REPEAT_CFGS_HIDDEN, 'true');
|
||||
} else {
|
||||
localStorage.removeItem(LS.REPEAT_CFGS_HIDDEN);
|
||||
}
|
||||
});
|
||||
|
||||
afterNextRender(() => this._initScrollTracking());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2603,6 +2603,7 @@ const T = {
|
|||
TIME_ESTIMATED: 'WW.TIME_ESTIMATED',
|
||||
TODAY_REMAINING: 'WW.TODAY_REMAINING',
|
||||
WITHOUT_BREAK: 'WW.WITHOUT_BREAK',
|
||||
RECURRING_TASKS: 'WW.RECURRING_TASKS',
|
||||
WORKING_TODAY: 'WW.WORKING_TODAY',
|
||||
WORKING_TODAY_ARCHIVED: 'WW.WORKING_TODAY_ARCHIVED',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2548,6 +2548,7 @@
|
|||
"TIME_ESTIMATED": "Time estimated:",
|
||||
"TODAY_REMAINING": "Today remaining:",
|
||||
"WITHOUT_BREAK": "Without break:",
|
||||
"RECURRING_TASKS": "Recurring Tasks",
|
||||
"WORKING_TODAY": "Working today:",
|
||||
"WORKING_TODAY_ARCHIVED": "Time worked today on archived tasks"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue