super-productivity/src/app/features/tasks/task/task.component.html
Claude e2f0dd6925
feat(issue): clear the issue-updated badge when its toggle button is clicked
The accent-colored "issue updated" indicator could previously only be
cleared via the "mark as checked" button inside the issue-content panel,
which renders only once the issue data (re)loads. For Plainspace tasks —
and any provider whose remote issue is gone/unreachable — the data never
loads, so the badge had no way to be dismissed and stayed stuck forever.

Clicking the task's detail-panel toggle button while it shows the accent
update icon (i.e. issueWasUpdated) now also marks the issue update as read.
This is always available and does not depend on the issue data loading, so
the badge can never get stuck. The manual "mark as checked" button stays.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B9SCXSQvTq8TKUL7LabJQS
2026-06-20 00:39:15 +00:00

379 lines
12 KiB
HTML

@let t = task();
<swipe-block
[isDone]="t.isDone"
[canSwipe]="!taskTitleEditEl?.isEditing()"
(swipeStart)="onSwipeStart()"
(swipeRight)="toggleTaskDone()"
(swipeRightTriggered)="onSwipeRightTriggered($event)"
(swipeLeft)="openContextMenu()"
>
<div class="inner-wrapper">
<div class="box"></div>
<div
(mouseenter)="isFirstLineHover = true"
(mouseleave)="isFirstLineHover = false"
class="first-line"
>
<div
class="title-and-left-btns-wrapper"
(click)="titleBarClick($event)"
>
<done-toggle
[isDone]="t.isDone"
[isCurrent]="isCurrent()"
[showDoneAnimation]="showDoneAnimation()"
[showUndoneAnimation]="showUndoneAnimation()"
(toggled)="toggleTaskDone()"
[attr.aria-label]="T.F.TASK.CMP.TOGGLE_DONE | translate"
></done-toggle>
<div class="title-and-tags-wrapper">
@if (!isInSubTaskList() && t.parentId) {
<div class="parent-title">
<div class="title">{{ parentTitle() }}</div>
</div>
}
<task-title
[resetToLastExternalValueTrigger]="t"
[style.pointer-events]="isTouchActive() && t.title.length ? 'none' : 'auto'"
#taskTitleEditEl
(valueEdited)="updateTaskTitleIfChanged($event)"
[value]="t.title"
class="task-title"
></task-title>
@if (t.tagIds?.length || t.projectId || t.repeatCfgId || t.issueId) {
<tag-list
[task]="t"
[isShowProjectTagNever]="isInSubTaskList()"
></tag-list>
}
</div>
</div>
<div class="all-controls-wrapper">
<div
(click)="estimateTime()"
[class.hasNoTimeSpentOrEstimate]="!t.timeSpent && !t.timeEstimate"
[class.isEditable]="!t.subTasks?.length"
class="time-wrapper"
>
@if (!t.subTasks?.length) {
<div class="time">
@if (t.timeSpent) {
<div class="time-val">
<span [innerHTML]="t.timeSpent | msToString"></span>
</div>
<div class="separator">/</div>
}
<div
[innerHTML]="t.timeEstimate | msToString"
class="time-val"
></div>
</div>
}
@if (t.subTasks?.length) {
@let subTaskTimeSpent = t.subTasks | subTaskTotalTimeSpent;
@if (t.timeEstimate || subTaskTimeSpent) {
<div class="time">
<div class="time-val">
<mat-icon
inline="true"
style="margin-top: -6px"
>functions
</mat-icon>
<span [innerHTML]="subTaskTimeSpent | msToString"></span>
</div>
<div class="separator">/</div>
<div class="time-val">
<mat-icon inline="true">hourglass_empty</mat-icon>
{{ t.timeEstimate | msToString }}
</div>
</div>
}
}
</div>
<div class="controls">
@if (isFirstLineHover && !isTouchActive()) {
<task-hover-controls
[isShowAddToToday]="isShowAddToToday()"
[isShowRemoveFromToday]="isShowRemoveFromToday()"
[task]="t"
[isCurrent]="isCurrent()"
[isSelected]="isSelected()"
class="hover-controls"
></task-hover-controls>
}
@if (t.attachments?.length || t.issueAttachmentNr) {
<button
(click)="toggleShowAttachments()"
[title]="T.F.TASK.CMP.TOGGLE_ATTACHMENTS | translate"
class="ico-btn attachment-btn"
color=""
mat-icon-button
>
<mat-icon>attach_file</mat-icon>
</button>
}
@if (t.remindAt) {
<button
(click)="scheduleTask()"
[title]="T.F.TASK.CMP.EDIT_SCHEDULED | translate"
[matTooltip]="
hasTimeConflict() ? (T.F.TASK.CMP.TIME_CONFLICT | translate) : ''
"
matTooltipPosition="above"
[attr.aria-label]="
hasTimeConflict()
? (T.F.TASK.CMP.TIME_CONFLICT | translate)
: (T.F.TASK.CMP.EDIT_SCHEDULED | translate)
"
class="ico-btn schedule-btn"
[color]="isOverdue() ? 'warn' : ''"
mat-icon-button
>
<mat-icon>alarm</mat-icon>
<div
class="time-badge"
[innerHTML]="t.dueWithTime | shortPlannedAt"
></div>
</button>
} @else if (t.dueWithTime) {
<button
(click)="scheduleTask()"
[title]="T.F.TASK.CMP.EDIT_SCHEDULED | translate"
[matTooltip]="
hasTimeConflict() ? (T.F.TASK.CMP.TIME_CONFLICT | translate) : ''
"
matTooltipPosition="above"
[attr.aria-label]="
hasTimeConflict()
? (T.F.TASK.CMP.TIME_CONFLICT | translate)
: (T.F.TASK.CMP.EDIT_SCHEDULED | translate)
"
class="ico-btn schedule-btn"
[color]="isOverdue() ? 'warn' : ''"
mat-icon-button
>
@if (isScheduledToday()) {
<mat-icon>wb_sunny</mat-icon>
} @else {
<mat-icon>schedule</mat-icon>
}
<div
class="time-badge"
[innerHTML]="t.dueWithTime | shortPlannedAt"
></div>
</button>
} @else if (isShowDueDayBtn()) {
<button
(click)="scheduleTask()"
[title]="T.F.TASK.CMP.EDIT_SCHEDULED | translate"
[matTooltip]="
hasTimeConflict() ? (T.F.TASK.CMP.TIME_CONFLICT | translate) : ''
"
matTooltipPosition="above"
[attr.aria-label]="
hasTimeConflict()
? (T.F.TASK.CMP.TIME_CONFLICT | translate)
: (T.F.TASK.CMP.EDIT_SCHEDULED | translate)
"
class="ico-btn schedule-btn"
[color]="isOverdue() ? 'warn' : ''"
mat-icon-button
>
@if (isScheduledToday()) {
<mat-icon>wb_sunny</mat-icon>
} @else {
<mat-icon>today</mat-icon>
}
<div class="time-badge">
{{
t.dueDay | localDateStr: globalTrackingIntervalService.todayDateStr()
}}
</div>
</button>
}
@if (hasDeadline()) {
<button
(click)="openDeadlineDialog()"
[title]="T.F.TASK.ADDITIONAL_INFO.DEADLINE | translate"
class="ico-btn deadline-btn"
[color]="
isDeadlineOverdue() ? 'warn' : isDeadlineApproaching() ? 'accent' : ''
"
mat-icon-button
>
<mat-icon>flag</mat-icon>
@if (t.deadlineWithTime) {
<div
class="time-badge"
[innerHTML]="t.deadlineWithTime | shortPlannedAt"
></div>
} @else if (t.deadlineDay) {
<div class="time-badge">
{{
t.deadlineDay
| localDateStr: globalTrackingIntervalService.todayDateStr()
}}
</div>
}
</button>
}
<!-- Stands in for the notes icon; like it, yields to the close (X) button -->
@if (!isShowCloseButton() && checklistProgress(); as cp) {
<button
(click)="openNotesPanel()"
[matTooltip]="
T.F.TASK.CMP.CHECKLIST_PROGRESS
| translate: { done: cp.done, total: cp.total }
"
[attr.aria-label]="
T.F.TASK.CMP.CHECKLIST_PROGRESS
| translate: { done: cp.done, total: cp.total }
"
[class.isComplete]="cp.done === cp.total"
class="ico-btn checklist-progress-btn"
mat-icon-button
>
<mat-icon>{{ cp.done === cp.total ? 'check_box' : 'checklist' }}</mat-icon>
<div class="checklist-progress-badge">{{ cp.done }}/{{ cp.total }}</div>
</button>
}
<div>
@if (isShowToggleButton()) {
<button
(click)="onToggleDetailPanelBtnClick($event)"
title="{{ T.F.TASK.CMP.TOGGLE_DETAIL_PANEL | translate }} {{
kb.taskToggleDetailPanelOpen
? '[' + kb.taskToggleDetailPanelOpen + ']'
: ''
}}"
class="ico-btn show-additional-info-btn"
color=""
mat-icon-button
>
<mat-icon
[class.updated-icon]="toggleButtonIcon() === 'update'"
[color]="toggleButtonIcon() === 'update' ? 'accent' : ''"
>
{{ toggleButtonIcon() }}
</mat-icon>
</button>
}
</div>
</div>
</div>
</div>
@if (isCurrent()) {
<progress-bar
[cssClass]="'bg-accent'"
[progress]="progress()"
></progress-bar>
}
@if (t.subTasks?.length || isAddSubtaskInputVisible()) {
<div class="sub-tasks">
@if (t.subTasks?.length) {
<button
(click)="toggleSubTaskMode()"
[title]="T.F.TASK.CMP.TOGGLE_SUB_TASK_VISIBILITY | translate"
class="toggle-sub-tasks-btn ico-btn mat-elevation-z2"
color=""
mat-mini-fab
>
@if (t._hideSubTasksMode === ShowSubTasksMode.HideAll) {
<mat-icon>add</mat-icon>
} @else {
<mat-icon
[class.isHideDoneTasks]="
t._hideSubTasksMode === ShowSubTasksMode.HideDone
"
>remove
</mat-icon>
}
</button>
<task-list
[@expandInOnly]
[isHideAll]="!!t._hideSubTasksMode"
[isHideDone]="t._hideSubTasksMode === ShowSubTasksMode.HideDone"
[isSubTaskList]="true"
[listModelId]="t.id"
[parentId]="t.id"
[tasks]="t.subTasks"
listId="SUB"
></task-list>
}
@if (isAddSubtaskInputVisible()) {
<add-subtask-input
[@expandFade]
[parentId]="t.id"
(closed)="onAddSubtaskInputClosed($event)"
></add-subtask-input>
}
</div>
}
@if (isDragOver) {
<div class="bgc-accent drag-over-msg">
<mat-icon>add</mat-icon>
{{ T.F.TASK.CMP.DROP_ATTACHMENT | translate: { title: t.title } }}
</div>
}
</div>
</swipe-block>
<!--*ngIf="isShowContextMenu"-->
@if (isContextMenuLoaded()) {
<task-context-menu
#taskContextMenu
[isAdvancedControls]="true"
[task]="t"
></task-context-menu>
}
@if (isProjectMenuLoaded()) {
<div
#projectMenuTriggerEl
[matMenuTriggerFor]="projectMenu"
style="position: absolute; visibility: hidden; pointer-events: none"
></div>
<mat-menu
#projectMenu="matMenu"
(closed)="focusSelf()"
(menuOpened)="_loadProjectListIfNeeded()"
>
<ng-template matMenuContent>
@if (moveToProjectList()) {
@for (project of moveToProjectList(); track trackByProjectId($index, project)) {
<button
(click)="moveTaskToProject(project.id)"
mat-menu-item
>
<select-option-row
[title]="project.title"
[icon]="project.icon"
[defaultIcon]="DEFAULT_PROJECT_ICON"
[color]="project.theme?.primary"
[folderPath]="projectFolderMap().get(project.id)"
></select-option-row>
</button>
}
}
</ng-template>
</mat-menu>
}
@if (isTagMenuVisible()) {
<tag-toggle-menu-list
#tagToggleMenuList
[task]="t"
(toggleTag)="toggleTag($event)"
(afterClose)="isTagMenuVisible.set(false); focusSelf()"
></tag-toggle-menu-list>
}