Merge branch 'feat/filter-panel-as-context-menu'

* feat/filter-panel-as-context-menu:
  feat(customizerMenu): polish new filter menu button and menu
  feat(customizerMenu): change color of active filter button
  feat(customizerMenu): move filter button to right of project title
  feat(customizerMenu): move filter button left of play button
  feat(customizerMenu): try to move checkmark to the right
  feat(customizerMenu): second draft for component
  feat(customizerMenu): first draft for component

# Conflicts:
#	src/app/core-ui/main-header/desktop-panel-buttons/desktop-panel-buttons.component.ts
#	src/app/core-ui/main-header/main-header.component.html
#	src/app/features/panels/panel-content.service.ts
#	src/app/features/right-panel/right-panel-content.component.ts
This commit is contained in:
Johannes Millan 2025-10-21 17:42:27 +02:00
commit 45b3f2930f
14 changed files with 479 additions and 210 deletions

View file

@ -4,9 +4,9 @@ import { MatIcon } from '@angular/material/icon';
import { MatTooltip } from '@angular/material/tooltip';
import { TranslatePipe } from '@ngx-translate/core';
import { LayoutService } from '../../layout/layout.service';
import { TaskViewCustomizerService } from '../../../features/task-view-customizer/task-view-customizer.service';
import { T } from '../../../t.const';
import { KeyboardConfig } from '../../../features/config/keyboard-config.model';
import { TaskViewCustomizerService } from '../../../features/task-view-customizer/task-view-customizer.service';
@Component({
selector: 'desktop-panel-buttons',

View file

@ -145,12 +145,12 @@ export class MainHeaderComponent implements OnDestroy {
enabledSimpleCounters = toSignal(this.simpleCounterService.enabledSimpleCounters$, {
initialValue: [],
});
isShowTaskViewCustomizerPanel = computed(() =>
this.layoutService.isShowTaskViewCustomizerPanel(),
);
isShowIssuePanel = computed(() => this.layoutService.isShowIssuePanel());
isShowNotes = computed(() => this.layoutService.isShowNotes());
isShowScheduleDayPanel = computed(() => this.layoutService.isShowScheduleDayPanel());
isShowTaskViewCustomizerPanel = computed(() =>
this.layoutService.isShowTaskViewCustomizerPanel(),
);
syncIsEnabledAndReady = toSignal(this.syncWrapperService.isEnabledAndReady$);
syncState = toSignal(this.syncWrapperService.syncState$);
isSyncInProgress = toSignal(this.syncWrapperService.isSyncInProgress$);

View file

@ -3,9 +3,11 @@ import { CommonModule } from '@angular/common';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatMenuModule } from '@angular/material/menu';
import { TranslateModule } from '@ngx-translate/core';
import { LayoutService } from '../../layout/layout.service';
import { TaskViewCustomizerService } from '../../../features/task-view-customizer/task-view-customizer.service';
import { TaskViewCustomizerPanelComponent } from '../../../features/task-view-customizer/task-view-customizer-panel/task-view-customizer-panel.component';
import { T } from '../../../t.const';
import { KeyboardConfig } from '../../../features/config/keyboard-config.model';
import { GlobalConfigService } from '../../../features/config/global-config.service';
@ -31,8 +33,10 @@ import { BreakpointObserver } from '@angular/cdk/layout';
MatIconModule,
MatButtonModule,
MatTooltipModule,
MatMenuModule,
TranslateModule,
PluginIconComponent,
TaskViewCustomizerPanelComponent,
],
template: `
<div class="mobile-dropdown-wrapper">
@ -68,15 +72,16 @@ import { BreakpointObserver } from '@angular/cdk/layout';
<button
mat-mini-fab
color=""
[class.active]="isShowTaskViewCustomizerPanel()"
[class.isCustomized]="taskViewCustomizerService.isCustomized()"
[disabled]="!isWorkViewPage()"
(click)="toggleTaskViewCustomizer()"
[matMenuTriggerFor]="customizerPanel.menu"
[matTooltip]="T.GCF.KEYBOARD.TOGGLE_TASK_VIEW_CUSTOMIZER_PANEL | translate"
>
<mat-icon>filter_list</mat-icon>
</button>
<task-view-customizer-panel #customizerPanel></task-view-customizer-panel>
<!-- Issue Panel -->
<button
mat-mini-fab
@ -199,11 +204,6 @@ export class MobileSidePanelMenuComponent {
this.isShowMobileMenu.set(false);
}
toggleTaskViewCustomizer(): void {
this.layoutService.toggleTaskViewCustomizerPanel();
this.isShowMobileMenu.set(false);
}
toggleIssuePanel(): void {
this.layoutService.toggleAddTaskPanel();
this.isShowMobileMenu.set(false);

View file

@ -12,6 +12,10 @@ import { BreakpointObserver } from '@angular/cdk/layout';
import { toSignal } from '@angular/core/rxjs-interop';
import { filter, map, startWith } from 'rxjs/operators';
import { WorkContextService } from '../../../features/work-context/work-context.service';
import { TaskViewCustomizerService } from '../../../features/task-view-customizer/task-view-customizer.service';
import { TaskViewCustomizerPanelComponent } from '../../../features/task-view-customizer/task-view-customizer-panel/task-view-customizer-panel.component';
import { GlobalConfigService } from '../../../features/config/global-config.service';
import { KeyboardConfig } from '../../../features/config/keyboard-config.model';
@Component({
selector: 'page-title',
@ -26,6 +30,7 @@ import { WorkContextService } from '../../../features/work-context/work-context.
MatMenuContent,
MatMenuTrigger,
WorkContextMenuComponent,
TaskViewCustomizerPanelComponent,
TranslatePipe,
],
template: `
@ -39,14 +44,33 @@ import { WorkContextService } from '../../../features/work-context/work-context.
{{ displayTitle() }}
</div>
@if (!isXxxs()) {
<button
[mat-menu-trigger-for]="activeWorkContextMenu"
[matTooltip]="T.MH.PROJECT_MENU | translate"
class="project-settings-btn"
mat-icon-button
>
<mat-icon>more_vert</mat-icon>
</button>
<div class="page-title-actions">
<button
[mat-menu-trigger-for]="activeWorkContextMenu"
[matTooltip]="T.MH.PROJECT_MENU | translate"
class="project-settings-btn"
mat-icon-button
>
<mat-icon>more_vert</mat-icon>
</button>
<button
class="task-filter-btn"
[disabled]="!isWorkViewPage()"
[class.isCustomized]="taskViewCustomizerService.isCustomized()"
[matMenuTriggerFor]="customizerPanel.menu"
mat-icon-button
matTooltip="{{
T.GCF.KEYBOARD.TOGGLE_TASK_VIEW_CUSTOMIZER_PANEL | translate
}} {{
kb.toggleTaskViewCustomizerPanel
? '[' + kb.toggleTaskViewCustomizerPanel + ']'
: ''
}}"
>
<mat-icon>filter_list</mat-icon>
</button>
<task-view-customizer-panel #customizerPanel></task-view-customizer-panel>
</div>
}
<mat-menu #activeWorkContextMenu="matMenu">
<ng-template matMenuContent>
@ -84,21 +108,56 @@ import { WorkContextService } from '../../../features/work-context/work-context.
}
}
.page-title-actions {
display: flex;
align-items: center;
gap: var(--s-quarter);
margin-left: calc(-1 * var(--s));
margin-right: var(--s2);
}
.project-settings-btn {
display: none;
@media (min-width: 600px) {
opacity: 1;
/*display: none;*/
/*@media (min-width: 600px) {*/
/* display: block;*/
/* transition: var(--transition-standard);*/
/* opacity: 0;*/
/* position: relative;*/
/* z-index: 1;*/
/*}*/
/*&:hover,*/
/*.page-title:hover + .page-title-actions &,*/
/*.page-title-actions:hover & {*/
/* opacity: 1;*/
/*}*/
}
.task-filter-btn {
position: relative;
transition: all 0.2s ease;
overflow: visible !important;
.mat-icon {
transition: transform 0.2s ease;
display: block;
transition: var(--transition-standard);
opacity: 0;
margin-right: var(--s2);
margin-left: calc(-1 * var(--s));
position: relative;
z-index: 1;
}
&:hover,
.page-title:hover + & {
opacity: 1;
&.isCustomized {
color: var(--c-accent);
box-shadow: none;
}
&:hover:not(.isCustomized):not(:disabled) {
background-color: var(--hover-color, rgba(0, 0, 0, 0.04));
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
background: transparent !important;
}
}
`,
@ -109,6 +168,8 @@ export class PageTitleComponent {
private _breakpointObserver = inject(BreakpointObserver);
private _router = inject(Router);
private _workContextService = inject(WorkContextService);
readonly taskViewCustomizerService = inject(TaskViewCustomizerService);
private readonly _configService = inject(GlobalConfigService);
readonly T = T;
@ -140,6 +201,13 @@ export class PageTitleComponent {
);
isBoardsSection = toSignal(this._isBoardsSection$, { initialValue: false });
private _isWorkViewPage$ = this._router.events.pipe(
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
map((event) => !!event.urlAfterRedirects.match(/tasks$/)),
startWith(!!this._router.url.match(/tasks$/)),
);
isWorkViewPage = toSignal(this._isWorkViewPage$, { initialValue: false });
// Override title for special routes
displayTitle = computed(() => {
if (this.isScheduleSection()) {
@ -158,4 +226,8 @@ export class PageTitleComponent {
isXxxs = toSignal(this._isXxxs$.pipe(map((result) => result.matches)), {
initialValue: false,
});
get kb(): KeyboardConfig {
return (this._configService.cfg()?.keyboard as KeyboardConfig) || {};
}
}

View file

@ -62,17 +62,6 @@
</button>
}
<button
mat-menu-item
[disabled]="!isWorkViewPage()"
[class.active]="isShowTaskViewCustomizerPanel()"
[class.isCustomized]="taskViewCustomizerService.isCustomized()"
(click)="toggleTaskViewCustomizer()"
>
<mat-icon>filter_list</mat-icon>
<span>{{ T.BN.SHOW_TASK_VIEW_CUSTOMIZER_PANEL | translate }}</span>
</button>
<button
mat-menu-item
[disabled]="!isRouteWithSidePanel()"

View file

@ -10,7 +10,6 @@ import { toSignal } from '@angular/core/rxjs-interop';
import { filter, map, startWith } from 'rxjs/operators';
import { LayoutService } from '../layout/layout.service';
import { TaskViewCustomizerService } from '../../features/task-view-customizer/task-view-customizer.service';
import { PluginBridgeService } from '../../plugins/plugin-bridge.service';
import { PluginIconComponent } from '../../plugins/ui/plugin-icon/plugin-icon.component';
import { Store } from '@ngrx/store';
@ -43,7 +42,6 @@ import { WorkContextService } from '../../features/work-context/work-context.ser
export class MobileBottomNavComponent {
private readonly _router = inject(Router);
private readonly _layoutService = inject(LayoutService);
private readonly _taskViewCustomizerService = inject(TaskViewCustomizerService);
private readonly _pluginBridge = inject(PluginBridgeService);
private readonly _store = inject(Store);
private readonly _workContextService = inject(WorkContextService);
@ -55,7 +53,6 @@ export class MobileBottomNavComponent {
// Services for template access
readonly layoutService = this._layoutService;
readonly taskViewCustomizerService = this._taskViewCustomizerService;
// Output events
toggleMobileNavEvent = output<void>();
@ -88,20 +85,9 @@ export class MobileBottomNavComponent {
{ initialValue: true },
);
readonly isWorkViewPage = toSignal(
this._router.events.pipe(
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
map((event) => !!event.urlAfterRedirects.match(/tasks$/)),
startWith(!!this._router.url.match(/tasks$/)),
),
{ initialValue: !!this._router.url.match(/tasks$/) },
);
// Panel state signals from layout service
readonly isShowNotes = this._layoutService.isShowNotes;
readonly isShowIssuePanel = this._layoutService.isShowIssuePanel;
readonly isShowTaskViewCustomizerPanel =
this._layoutService.isShowTaskViewCustomizerPanel;
// Navigation methods
showAddTaskBar(): void {
@ -126,10 +112,6 @@ export class MobileBottomNavComponent {
}
}
toggleTaskViewCustomizer(): void {
this._layoutService.toggleTaskViewCustomizerPanel();
}
toggleIssuePanel(): void {
this._layoutService.toggleAddTaskPanel();
}

View file

@ -2,14 +2,6 @@
<issue-panel [@slideInFromRight]></issue-panel>
} @else if (panelContent() === 'NOTES') {
<notes [@slideInFromRight]></notes>
} @else if (panelContent() === 'TASK_VIEW_CUSTOMIZER_PANEL') {
<!-- Wrap TVC to stabilize layout during panel width transitions -->
<div
class="panel-wrapper"
[@slideInFromRight]
>
<task-view-customizer-panel></task-view-customizer-panel>
</div>
} @else if (panelContent() === 'PLUGIN') {
@for (key of pluginPanelKeys(); track key) {
<plugin-panel-container [@slideInFromRight]></plugin-panel-container>

View file

@ -18,7 +18,6 @@ import { taskDetailPanelTaskChangeAnimation } from '../tasks/task-detail-panel/t
import { IssuePanelComponent } from '../issue-panel/issue-panel.component';
import { NotesComponent } from '../note/notes/notes.component';
import { TaskDetailPanelComponent } from '../tasks/task-detail-panel/task-detail-panel.component';
import { TaskViewCustomizerPanelComponent } from '../task-view-customizer/task-view-customizer-panel/task-view-customizer-panel.component';
import { PluginService } from '../../plugins/plugin.service';
import { PluginPanelContainerComponent } from '../../plugins/ui/plugin-panel-container/plugin-panel-container.component';
import { ScheduleDayPanelComponent } from '../schedule/schedule-day-panel/schedule-day-panel.component';
@ -56,7 +55,6 @@ export type RightPanelContentPanelType = PanelContentType;
IssuePanelComponent,
NotesComponent,
TaskDetailPanelComponent,
TaskViewCustomizerPanelComponent,
PluginPanelContainerComponent,
ScheduleDayPanelComponent,
],

View file

@ -1,113 +1,288 @@
<div class="customizer-panel">
<h3>{{ T.F.TASK_VIEW.CUSTOMIZER.TITLE | translate }}</h3>
<div class="form-group">
<label>{{ T.F.TASK_VIEW.CUSTOMIZER.SORT_BY | translate }}</label>
<mat-form-field>
<mat-select
[ngModel]="customizerService.selectedSort()"
(ngModelChange)="customizerService.setSort($event)"
>
@for (opt of sortOptions; track opt.value) {
<mat-option [value]="opt.value">{{ opt.label | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="form-group">
<label>{{ T.F.TASK_VIEW.CUSTOMIZER.GROUP_BY | translate }}</label>
<mat-form-field>
<mat-select
[ngModel]="customizerService.selectedGroup()"
(ngModelChange)="customizerService.setGroup($event)"
>
@for (opt of groupOptions; track opt.value) {
<mat-option [value]="opt.value">{{ opt.label | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="form-group">
<label>{{ T.F.TASK_VIEW.CUSTOMIZER.FILTER_BY | translate }}</label>
<mat-form-field>
<mat-select
[ngModel]="customizerService.selectedFilter()"
(ngModelChange)="customizerService.setFilter($event)"
>
@for (opt of filterOptions; track opt.value) {
<mat-option [value]="opt.value">{{ opt.label | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
@if (customizerService.selectedFilter() === 'tag') {
<mat-form-field>
<input
matInput
[ngModel]="customizerService.filterInputValue()"
(ngModelChange)="customizerService.setFilterInputValue($event)"
[placeholder]="T.F.TASK_VIEW.CUSTOMIZER.ENTER_TAG | translate"
/>
</mat-form-field>
}
@if (customizerService.selectedFilter() === 'project') {
<mat-form-field>
<input
matInput
[ngModel]="customizerService.filterInputValue()"
(ngModelChange)="customizerService.setFilterInputValue($event)"
[placeholder]="T.F.TASK_VIEW.CUSTOMIZER.ENTER_PROJECT | translate"
/>
</mat-form-field>
}
@if (customizerService.selectedFilter() === 'scheduledDate') {
<mat-form-field>
<mat-select
[ngModel]="customizerService.filterInputValue()"
(ngModelChange)="customizerService.setFilterInputValue($event)"
>
@for (option of scheduledPresets; track option.value) {
<mat-option [value]="option.value">{{ option.label | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
}
@if (customizerService.selectedFilter() === 'estimatedTime') {
<mat-form-field>
<mat-select
[ngModel]="customizerService.filterInputValue()"
(ngModelChange)="customizerService.setFilterInputValue($event)"
>
@for (option of timePresets; track option.value) {
<mat-option [value]="option.value">{{ option.label | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
}
@if (customizerService.selectedFilter() === 'timeSpent') {
<mat-form-field>
<mat-select
[ngModel]="customizerService.filterInputValue()"
(ngModelChange)="customizerService.setFilterInputValue($event)"
>
@for (option of timePresets; track option.value) {
<mat-option [value]="option.value">{{ option.label | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
}
</div>
<mat-menu
#customizerMenu="matMenu"
class="customizer-menu"
>
<!-- Sort By -->
<button
mat-raised-button
color="primary"
mat-menu-item
[matMenuTriggerFor]="sortMenu"
>
<mat-icon>sort</mat-icon>
<span>{{ T.F.TASK_VIEW.CUSTOMIZER.SORT_BY | translate }}</span>
@if (customizerService.selectedSort() !== 'default') {
<span class="current-value">
{{ getSortLabel(customizerService.selectedSort()) | translate }}
</span>
}
</button>
<!-- Group By -->
<button
mat-menu-item
[matMenuTriggerFor]="groupMenu"
>
<mat-icon>group_work</mat-icon>
<span>{{ T.F.TASK_VIEW.CUSTOMIZER.GROUP_BY | translate }}</span>
@if (customizerService.selectedGroup() !== 'default') {
<span class="current-value">
{{ getGroupLabel(customizerService.selectedGroup()) | translate }}
</span>
}
</button>
<!-- Filter By -->
<button
mat-menu-item
[matMenuTriggerFor]="filterMenu"
>
<mat-icon>filter_alt</mat-icon>
<span>{{ T.F.TASK_VIEW.CUSTOMIZER.FILTER_BY | translate }}</span>
@if (customizerService.selectedFilter() !== 'default') {
<span class="current-value">
{{ getFilterLabel(customizerService.selectedFilter()) | translate }}
</span>
}
</button>
<mat-divider></mat-divider>
<!-- Reset All -->
<button
mat-menu-item
(click)="onResetAll()"
>
{{ T.F.TASK_VIEW.CUSTOMIZER.RESET_ALL | translate }}
<mat-icon>refresh</mat-icon>
<span>{{ T.F.TASK_VIEW.CUSTOMIZER.RESET_ALL | translate }}</span>
</button>
</div>
</mat-menu>
<!-- Sort submenu -->
<mat-menu #sortMenu="matMenu">
@for (opt of sortOptions; track opt.value) {
<button
mat-menu-item
(click)="customizerService.setSort(opt.value)"
[class.active]="customizerService.selectedSort() === opt.value"
>
<span class="menu-item-content">
<span>{{ opt.label | translate }}</span>
@if (customizerService.selectedSort() === opt.value) {
<mat-icon class="check-icon">check</mat-icon>
}
</span>
</button>
}
</mat-menu>
<!-- Group submenu -->
<mat-menu #groupMenu="matMenu">
@for (opt of groupOptions; track opt.value) {
<button
mat-menu-item
(click)="customizerService.setGroup(opt.value)"
[class.active]="customizerService.selectedGroup() === opt.value"
>
<span class="menu-item-content">
<span>{{ opt.label | translate }}</span>
@if (customizerService.selectedGroup() === opt.value) {
<mat-icon class="check-icon">check</mat-icon>
}
</span>
</button>
}
</mat-menu>
<!-- Filter submenu -->
<mat-menu #filterMenu="matMenu">
<button
mat-menu-item
(click)="onFilterSelect('default')"
[class.active]="customizerService.selectedFilter() === 'default'"
>
<span class="menu-item-content">
<span>{{ T.F.TASK_VIEW.CUSTOMIZER.FILTER_DEFAULT | translate }}</span>
@if (customizerService.selectedFilter() === 'default') {
<mat-icon class="check-icon">check</mat-icon>
}
</span>
</button>
<button
mat-menu-item
[matMenuTriggerFor]="filterTagMenu"
[class.active]="customizerService.selectedFilter() === 'tag'"
>
<span class="menu-item-content">
<span>{{ T.F.TASK_VIEW.CUSTOMIZER.FILTER_TAG | translate }}</span>
@if (customizerService.selectedFilter() === 'tag') {
<mat-icon class="check-icon">check</mat-icon>
}
</span>
</button>
<button
mat-menu-item
[matMenuTriggerFor]="filterProjectMenu"
[class.active]="customizerService.selectedFilter() === 'project'"
>
<span class="menu-item-content">
<span>{{ T.F.TASK_VIEW.CUSTOMIZER.FILTER_PROJECT | translate }}</span>
@if (customizerService.selectedFilter() === 'project') {
<mat-icon class="check-icon">check</mat-icon>
}
</span>
</button>
<button
mat-menu-item
[matMenuTriggerFor]="filterScheduledDateMenu"
[class.active]="customizerService.selectedFilter() === 'scheduledDate'"
>
<span class="menu-item-content">
<span>{{ T.F.TASK_VIEW.CUSTOMIZER.FILTER_SCHEDULED_DATE | translate }}</span>
@if (customizerService.selectedFilter() === 'scheduledDate') {
<mat-icon class="check-icon">check</mat-icon>
}
</span>
</button>
<button
mat-menu-item
[matMenuTriggerFor]="filterEstimatedTimeMenu"
[class.active]="customizerService.selectedFilter() === 'estimatedTime'"
>
<span class="menu-item-content">
<span>{{ T.F.TASK_VIEW.CUSTOMIZER.FILTER_ESTIMATED_TIME | translate }}</span>
@if (customizerService.selectedFilter() === 'estimatedTime') {
<mat-icon class="check-icon">check</mat-icon>
}
</span>
</button>
<button
mat-menu-item
[matMenuTriggerFor]="filterTimeSpentMenu"
[class.active]="customizerService.selectedFilter() === 'timeSpent'"
>
<span class="menu-item-content">
<span>{{ T.F.TASK_VIEW.CUSTOMIZER.FILTER_TIME_SPENT | translate }}</span>
@if (customizerService.selectedFilter() === 'timeSpent') {
<mat-icon class="check-icon">check</mat-icon>
}
</span>
</button>
</mat-menu>
<!-- Filter: Tag submenu -->
<mat-menu #filterTagMenu="matMenu">
<div
class="menu-input-wrapper"
(click)="$event.stopPropagation()"
>
<mat-form-field
class="menu-input"
subscriptSizing="dynamic"
>
<input
matInput
[ngModel]="customizerService.filterInputValue()"
(ngModelChange)="onFilterInputChange('tag', $event)"
[placeholder]="T.F.TASK_VIEW.CUSTOMIZER.ENTER_TAG | translate"
(keydown.enter)="$event.stopPropagation()"
/>
</mat-form-field>
</div>
</mat-menu>
<!-- Filter: Project submenu -->
<mat-menu #filterProjectMenu="matMenu">
<div
class="menu-input-wrapper"
(click)="$event.stopPropagation()"
>
<mat-form-field
class="menu-input"
subscriptSizing="dynamic"
>
<input
matInput
[ngModel]="customizerService.filterInputValue()"
(ngModelChange)="onFilterInputChange('project', $event)"
[placeholder]="T.F.TASK_VIEW.CUSTOMIZER.ENTER_PROJECT | translate"
(keydown.enter)="$event.stopPropagation()"
/>
</mat-form-field>
</div>
</mat-menu>
<!-- Filter: Scheduled Date submenu -->
<mat-menu #filterScheduledDateMenu="matMenu">
@for (option of scheduledPresets; track option.value) {
<button
mat-menu-item
(click)="onFilterWithValue('scheduledDate', option.value)"
[class.active]="
customizerService.selectedFilter() === 'scheduledDate' &&
customizerService.filterInputValue() === option.value
"
>
<span class="menu-item-content">
<span>{{ option.label | translate }}</span>
@if (
customizerService.selectedFilter() === 'scheduledDate' &&
customizerService.filterInputValue() === option.value
) {
<mat-icon class="check-icon">check</mat-icon>
}
</span>
</button>
}
</mat-menu>
<!-- Filter: Estimated Time submenu -->
<mat-menu #filterEstimatedTimeMenu="matMenu">
@for (option of timePresets; track option.value) {
<button
mat-menu-item
(click)="onFilterWithValue('estimatedTime', option.value)"
[class.active]="
customizerService.selectedFilter() === 'estimatedTime' &&
customizerService.filterInputValue() === option.value
"
>
<span class="menu-item-content">
<span>{{ option.label | translate }}</span>
@if (
customizerService.selectedFilter() === 'estimatedTime' &&
customizerService.filterInputValue() === option.value
) {
<mat-icon class="check-icon">check</mat-icon>
}
</span>
</button>
}
</mat-menu>
<!-- Filter: Time Spent submenu -->
<mat-menu #filterTimeSpentMenu="matMenu">
@for (option of timePresets; track option.value) {
<button
mat-menu-item
(click)="onFilterWithValue('timeSpent', option.value)"
[class.active]="
customizerService.selectedFilter() === 'timeSpent' &&
customizerService.filterInputValue() === option.value
"
>
<span class="menu-item-content">
<span>{{ option.label | translate }}</span>
@if (
customizerService.selectedFilter() === 'timeSpent' &&
customizerService.filterInputValue() === option.value
) {
<mat-icon class="check-icon">check</mat-icon>
}
</span>
</button>
}
</mat-menu>

View file

@ -1,33 +1,11 @@
.customizer-panel {
// Align layout with other right-panel contents (e.g. issue-panel)
// Fill the container and let the parent handle scrolling
position: absolute;
inset: 8px 16px;
padding: 8px 0;
display: flex;
flex-direction: column;
height: 100%;
// Menu styles are in global styles: src/styles/components/_customizer-menu.scss
// This is necessary because mat-menu renders in a global overlay container
h3 {
text-align: center;
margin-bottom: 20px;
}
.menu-input-wrapper {
padding: 8px 16px;
min-width: 250px;
.form-group {
margin-bottom: 15px;
display: flex;
flex-direction: column;
label {
margin-bottom: 5px;
}
mat-form-field {
width: 100%;
}
mat-slide-toggle {
align-self: flex-start;
}
.menu-input {
width: 100%;
}
}

View file

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { ChangeDetectionStrategy, Component, inject, ViewChild } from '@angular/core';
import { OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@ -7,6 +7,9 @@ import { MatSelectModule } from '@angular/material/select';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatMenuModule, MatMenu } from '@angular/material/menu';
import { MatIconModule } from '@angular/material/icon';
import { MatDividerModule } from '@angular/material/divider';
import { TaskViewCustomizerService } from '../task-view-customizer.service';
import { TranslatePipe } from '@ngx-translate/core';
import { T } from 'src/app/t.const';
@ -17,6 +20,7 @@ import { T } from 'src/app/t.const';
styleUrls: ['./task-view-customizer-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
exportAs: 'customizerMenu',
imports: [
CommonModule,
FormsModule,
@ -25,12 +29,18 @@ import { T } from 'src/app/t.const';
MatInputModule,
MatButtonModule,
MatSlideToggleModule,
MatMenuModule,
MatIconModule,
MatDividerModule,
TranslatePipe,
],
})
export class TaskViewCustomizerPanelComponent implements OnInit {
customizerService = inject(TaskViewCustomizerService);
@ViewChild('customizerMenu', { static: false })
menu!: MatMenu;
T = T;
selectedSort: string = 'default';
selectedGroup: string = 'default';
@ -89,6 +99,37 @@ export class TaskViewCustomizerPanelComponent implements OnInit {
this.filterInputValue = this.customizerService.filterInputValue();
}
getSortLabel(value: string): string {
const option = this.sortOptions.find((opt) => opt.value === value);
return option ? option.label : '';
}
getGroupLabel(value: string): string {
const option = this.groupOptions.find((opt) => opt.value === value);
return option ? option.label : '';
}
getFilterLabel(value: string): string {
const option = this.filterOptions.find((opt) => opt.value === value);
return option ? option.label : '';
}
onFilterSelect(filterType: string): void {
this.customizerService.setFilter(filterType);
}
onFilterInputChange(filterType: string, value: string): void {
if (this.customizerService.selectedFilter() !== filterType) {
this.customizerService.setFilter(filterType);
}
this.customizerService.setFilterInputValue(value);
}
onFilterWithValue(filterType: string, value: string): void {
this.customizerService.setFilter(filterType);
this.customizerService.setFilterInputValue(value);
}
onResetAll(): void {
this.customizerService.resetAll();
}

View file

@ -1465,8 +1465,8 @@
},
"TASK_VIEW": {
"CUSTOMIZER": {
"ENTER_PROJECT": "Enter project",
"ENTER_TAG": "Enter tag",
"ENTER_PROJECT": "Filter Projects",
"ENTER_TAG": "Filter Tag",
"ESTIMATED_TIME": "Estimated Time",
"FILTER_BY": "Filter By",
"FILTER_DEFAULT": "No Filter",

View file

@ -16,5 +16,6 @@
@use './planner-shared';
@use './mentions';
@use './bottom-panel';
@use './customizer-menu';
//@import '../../app/ui/custom-datetime-picker/sass/picker';

View file

@ -0,0 +1,41 @@
// TASK VIEW CUSTOMIZER MENU
// Styles for the task view customizer menu component
// Menu renders in global overlay, so styles must be global
.menu-item-content {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
gap: 16px;
> span:first-child {
flex: 1;
}
.check-icon {
flex-shrink: 0;
margin-left: auto;
margin-right: 0 !important;
}
}
.customizer-menu {
.mat-mdc-menu-item {
&.active {
background-color: rgba(0, 0, 0, 0.08);
font-weight: 500;
}
}
.current-value {
padding-left: 16px;
font-size: 0.875em;
opacity: 0.7;
font-style: italic;
}
mat-icon:first-child:not(.check-icon) {
margin-right: 8px;
}
}