feat(tasks): add estimate quick-pick submenu to context menu

This commit is contained in:
Johannes Millan 2026-05-05 18:22:31 +02:00
parent 9e11c09562
commit eca8d211ae
8 changed files with 85 additions and 12 deletions

View file

@ -113,7 +113,7 @@
[class.menu-open]="isEstimateMenuOpen()"
(click)="onEstimateMenuClick()"
>
<mat-icon>timer</mat-icon>
<mat-icon>hourglass_empty</mat-icon>
<span>{{
estimateDisplay() || (T.F.TASK.ADD_TASK_BAR.ESTIMATE_BUTTON | translate)
}}</span>

View file

@ -17,16 +17,18 @@ export interface AddTaskBarState {
attachments: TaskAttachment[];
repeatQuickSetting: RepeatQuickSetting | null;
}
const M = 60 * 1000;
const H = 60 * M;
export const ESTIMATE_OPTIONS = [
{ label: '5m', value: '5m' },
{ label: '10m', value: '10m' },
{ label: '15m', value: '15m' },
{ label: '30m', value: '30m' },
{ label: '1h', value: '1h' },
{ label: '2h', value: '2h' },
{ label: '3h', value: '3h' },
{ label: '4h', value: '4h' },
{ label: '8h', value: '8h' },
{ label: '5m', value: '5m', ms: 5 * M },
{ label: '10m', value: '10m', ms: 10 * M },
{ label: '15m', value: '15m', ms: 15 * M },
{ label: '30m', value: '30m', ms: 30 * M },
{ label: '1h', value: '1h', ms: 1 * H },
{ label: '2h', value: '2h', ms: 2 * H },
{ label: '3h', value: '3h', ms: 3 * H },
{ label: '4h', value: '4h', ms: 4 * H },
{ label: '8h', value: '8h', ms: 8 * H },
];
export const INITIAL_ADD_TASK_BAR_STATE: AddTaskBarState = {

View file

@ -35,7 +35,7 @@
@if (task.timeSpent || task.timeEstimate) {
<section class="section">
<h3>
<mat-icon>timer</mat-icon>
<mat-icon>hourglass_empty</mat-icon>
<strong>{{ T.F.TASK.ADDITIONAL_INFO.TIME | translate }}</strong>
</h3>
<div class="time-info">

View file

@ -249,6 +249,21 @@
</button>
}
@if (!task.subTaskIds.length) {
<button
#estimateMenuTrigger="matMenuTrigger"
[matMenuTriggerFor]="estimateMenu"
mat-menu-item
[menuTouchFix]="estimateMenuTrigger"
>
<span class="menuItemLeft">
<mat-icon>hourglass_empty</mat-icon>
{{ T.F.TASK.CMP.ESTIMATE | translate }}
</span>
<span class="key-i">{{ kb.taskOpenEstimationDialog }}</span>
</button>
}
<button
[matMenuTriggerFor]="tagMenu"
mat-menu-item
@ -306,6 +321,45 @@
</button>
</mat-menu>
<mat-menu
#estimateMenu="matMenu"
(closed)="focusRelatedTaskOrNext()"
>
<ng-template matMenuContent>
@for (option of ESTIMATE_OPTIONS; track option.value) {
<button
mat-menu-item
menuTouchFix
(click)="setEstimate(option.ms)"
>
@if (option.ms === task.timeEstimate) {
<mat-icon>check</mat-icon>
}
{{ option.label }}
</button>
}
<mat-divider></mat-divider>
<button
mat-menu-item
menuTouchFix
(click)="estimateTime()"
>
<mat-icon>tune</mat-icon>
{{ T.F.TASK.CMP.CUSTOM_ESTIMATE | translate }}
</button>
@if (task.timeEstimate > 0) {
<button
mat-menu-item
menuTouchFix
(click)="setEstimate(0)"
>
<mat-icon>clear</mat-icon>
{{ T.F.TASK.CMP.CLEAR_ESTIMATE | translate }}
</button>
}
</ng-template>
</mat-menu>
<mat-menu
#projectMenu="matMenu"
(closed)="focusRelatedTaskOrNext()"

View file

@ -21,6 +21,8 @@ import {
MatMenuItem,
MatMenuTrigger,
} from '@angular/material/menu';
import { MatDivider } from '@angular/material/divider';
import { ESTIMATE_OPTIONS } from '../../add-task-bar/add-task-bar.const';
import { Task, TaskCopy, TaskWithSubTasks } from '../../task.model';
import { EMPTY, forkJoin, from, Observable, of, ReplaySubject, Subject } from 'rxjs';
import {
@ -86,6 +88,7 @@ import { DEFAULT_GLOBAL_CONFIG } from 'src/app/features/config/default-global-co
MatMenu,
MatMenuContent,
MatMenuItem,
MatDivider,
TranslateModule,
MatMenuTrigger,
MatIconButton,
@ -118,6 +121,7 @@ export class TaskContextMenuInnerComponent implements AfterViewInit, OnDestroy {
protected readonly isTouchActive = isTouchActive;
protected readonly T = T;
readonly ESTIMATE_OPTIONS = ESTIMATE_OPTIONS;
isAdvancedControls = input<boolean>(false);
todayList = toSignal(this._store.select(selectTodayTaskIds), { initialValue: [] });
@ -433,6 +437,13 @@ export class TaskContextMenuInnerComponent implements AfterViewInit, OnDestroy {
.subscribe(() => this.focusRelatedTaskOrNext());
}
setEstimate(ms: number): void {
if (ms === this.task.timeEstimate) {
return;
}
this._taskService.update(this.task.id, { timeEstimate: ms });
}
addSubTask(): void {
this._taskService.addSubTaskTo(this.task.parentId || this.task.id);
}

View file

@ -88,7 +88,7 @@
class="input-item --estimate"
>
<ng-container input-title>
<mat-icon>timer</mat-icon>
<mat-icon>hourglass_empty</mat-icon>
<span>{{ T.F.TASK.ADDITIONAL_INFO.TIME | translate }}</span>
</ng-container>
<ng-container input-value>

View file

@ -1601,13 +1601,16 @@ const T = {
ADD_SUB_TASK: 'F.TASK.CMP.ADD_SUB_TASK',
ADD_TO_MY_DAY: 'F.TASK.CMP.ADD_TO_MY_DAY',
ADD_TO_PROJECT: 'F.TASK.CMP.ADD_TO_PROJECT',
CLEAR_ESTIMATE: 'F.TASK.CMP.CLEAR_ESTIMATE',
CONVERT_TO_PARENT_TASK: 'F.TASK.CMP.CONVERT_TO_PARENT_TASK',
CUSTOM_ESTIMATE: 'F.TASK.CMP.CUSTOM_ESTIMATE',
DELETE: 'F.TASK.CMP.DELETE',
DELETE_REPEAT_INSTANCE: 'F.TASK.CMP.DELETE_REPEAT_INSTANCE',
DROP_ATTACHMENT: 'F.TASK.CMP.DROP_ATTACHMENT',
DUPLICATE: 'F.TASK.CMP.DUPLICATE',
EDIT_DEADLINE: 'F.TASK.CMP.EDIT_DEADLINE',
EDIT_SCHEDULED: 'F.TASK.CMP.EDIT_SCHEDULED',
ESTIMATE: 'F.TASK.CMP.ESTIMATE',
FOCUS_SESSION: 'F.TASK.CMP.FOCUS_SESSION',
MARK_DONE: 'F.TASK.CMP.MARK_DONE',
MOVE_TO_BACKLOG: 'F.TASK.CMP.MOVE_TO_BACKLOG',

View file

@ -1563,13 +1563,16 @@
"ADD_SUB_TASK": "Add subtask",
"ADD_TO_MY_DAY": "Add to Today",
"ADD_TO_PROJECT": "Add to a project",
"CLEAR_ESTIMATE": "Clear estimate",
"CONVERT_TO_PARENT_TASK": "Convert to parent task",
"CUSTOM_ESTIMATE": "Custom…",
"DELETE": "Delete task",
"DELETE_REPEAT_INSTANCE": "Delete recurring task instance",
"DROP_ATTACHMENT": "Drop here to attach to \"{{title}}\"",
"DUPLICATE": "Duplicate",
"EDIT_DEADLINE": "Edit deadline",
"EDIT_SCHEDULED": "Reschedule",
"ESTIMATE": "Estimate",
"FOCUS_SESSION": "Start focus session",
"MARK_DONE": "Mark as completed",
"MOVE_TO_BACKLOG": "Move to backlog",