mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-08-03 13:02:26 +00:00
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:
commit
45b3f2930f
14 changed files with 479 additions and 210 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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$);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) || {};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -16,5 +16,6 @@
|
|||
@use './planner-shared';
|
||||
@use './mentions';
|
||||
@use './bottom-panel';
|
||||
@use './customizer-menu';
|
||||
|
||||
//@import '../../app/ui/custom-datetime-picker/sass/picker';
|
||||
|
|
|
|||
41
src/styles/components/_customizer-menu.scss
Normal file
41
src/styles/components/_customizer-menu.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue