mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
This commit is contained in:
parent
d02f2271da
commit
6bb0472549
19 changed files with 2 additions and 1035 deletions
|
|
@ -128,7 +128,7 @@ export const cssSelectors = {
|
|||
// TASK DETAIL PANEL SELECTORS
|
||||
// ============================================================================
|
||||
RIGHT_PANEL: '.right-panel',
|
||||
DETAIL_PANEL: 'dialog-task-detail-panel, task-detail-panel',
|
||||
DETAIL_PANEL: 'task-detail-panel',
|
||||
DETAIL_PANEL_BTN: '.show-additional-info-btn',
|
||||
SCHEDULE_TASK_ITEM:
|
||||
'task-detail-item:has(mat-icon:text("alarm")), task-detail-item:has(mat-icon:text("today")), task-detail-item:has(mat-icon:text("schedule"))',
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { fillTimeInput } from './time-input-helper';
|
|||
|
||||
// Selectors for scheduling
|
||||
const DETAIL_PANEL_BTN = '.show-additional-info-btn';
|
||||
const DETAIL_PANEL_SELECTOR = 'dialog-task-detail-panel, task-detail-panel';
|
||||
const DETAIL_PANEL_SELECTOR = 'task-detail-panel';
|
||||
const DETAIL_PANEL_SCHEDULE_ITEM =
|
||||
'task-detail-item:has(mat-icon:text("alarm")), ' +
|
||||
'task-detail-item:has(mat-icon:text("today")), ' +
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ const filesToFix = [
|
|||
'src/app/features/schedule/map-schedule-data/insert-blocked-blocks-view-entries-for-schedule.ts',
|
||||
'src/app/features/schedule/map-schedule-data/map-to-schedule-days.ts',
|
||||
'src/app/features/task-repeat-cfg/sort-repeatable-task-cfg.ts',
|
||||
'src/app/features/tasks/dialog-task-detail-panel/dialog-task-detail-panel.component.ts',
|
||||
'src/app/features/work-context/store/work-context-meta.helper.ts',
|
||||
'src/app/root-store/index.ts',
|
||||
'src/app/ui/duration/input-duration-formly/input-duration-formly.component.ts',
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
:host {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
// Mobile dropdown wrapper
|
||||
.mobile-dropdown-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Mobile dropdown
|
||||
.mobile-dropdown {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
transition: var(--transition-standard);
|
||||
top: 100%;
|
||||
flex-direction: column;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
|
||||
&.isVisible {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
button {
|
||||
transition: var(--transition-standard);
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-top: var(--s);
|
||||
margin-left: 0;
|
||||
|
||||
@for $i from 2 through 7 {
|
||||
&:nth-child(#{$i}) {
|
||||
transform: translateY(#{$i * -100%});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.isVisible button {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Active button styles
|
||||
button.active {
|
||||
background-color: var(--sidenav-bg);
|
||||
|
||||
&.isCustomized {
|
||||
background-color: var(--c-accent);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
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 '@sp/keyboard-config';
|
||||
import { GlobalConfigService } from '../../../features/config/global-config.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import {
|
||||
selectActivePluginId,
|
||||
selectIsShowPluginPanel,
|
||||
} from '../../layout/store/layout.reducer';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { PluginBridgeService } from '../../../plugins/plugin-bridge.service';
|
||||
import { PluginIconComponent } from '../../../plugins/ui/plugin-icon/plugin-icon.component';
|
||||
import { togglePluginPanel } from '../../layout/store/layout.actions';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { filter, map, startWith } from 'rxjs/operators';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
|
||||
@Component({
|
||||
selector: 'mobile-side-panel-menu',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatTooltipModule,
|
||||
MatMenuModule,
|
||||
TranslateModule,
|
||||
PluginIconComponent,
|
||||
TaskViewCustomizerPanelComponent,
|
||||
],
|
||||
template: `
|
||||
<div class="mobile-dropdown-wrapper">
|
||||
<button
|
||||
(click)="toggleMenu()"
|
||||
mat-icon-button
|
||||
[matTooltip]="T.MH.SIDE_PANEL_MENU | translate"
|
||||
>
|
||||
<mat-icon>{{ isShowMobileMenu() ? 'close' : 'view_sidebar' }}</mat-icon>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="mobile-dropdown"
|
||||
[class.isVisible]="isShowMobileMenu()"
|
||||
>
|
||||
<!-- Plugin buttons -->
|
||||
@for (button of sidePanelButtons(); track button.pluginId) {
|
||||
<button
|
||||
mat-mini-fab
|
||||
color=""
|
||||
[matTooltip]="button.label"
|
||||
(click)="onPluginButtonClick(button)"
|
||||
[class.active]="activePluginId() === button.pluginId && isShowPluginPanel()"
|
||||
>
|
||||
<plugin-icon
|
||||
[pluginId]="button.pluginId"
|
||||
[fallbackIcon]="button.icon || 'extension'"
|
||||
></plugin-icon>
|
||||
</button>
|
||||
}
|
||||
|
||||
<!-- Task View Customizer -->
|
||||
<button
|
||||
mat-mini-fab
|
||||
color=""
|
||||
[class.isCustomized]="taskViewCustomizerService.isCustomized()"
|
||||
[disabled]="!isWorkViewPage()"
|
||||
[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>
|
||||
|
||||
@if (isIssuesPanelEnabled()) {
|
||||
<!-- Issue Panel -->
|
||||
<button
|
||||
mat-mini-fab
|
||||
color=""
|
||||
class="e2e-toggle-issue-provider-panel"
|
||||
[class.active]="isShowIssuePanel()"
|
||||
[disabled]="!isRouteWithSidePanel()"
|
||||
(click)="toggleIssuePanel()"
|
||||
[matTooltip]="T.MH.TOGGLE_SHOW_ISSUE_PANEL | translate"
|
||||
>
|
||||
<mat-icon>webhook</mat-icon>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (isProjectNotesEnabled()) {
|
||||
<!-- Notes -->
|
||||
<button
|
||||
mat-mini-fab
|
||||
color=""
|
||||
class="e2e-toggle-notes-btn"
|
||||
[class.active]="isShowNotes()"
|
||||
[disabled]="!isRouteWithSidePanel()"
|
||||
(click)="toggleNotes()"
|
||||
[matTooltip]="T.MH.TOGGLE_SHOW_NOTES | translate"
|
||||
>
|
||||
<mat-icon>comment</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./mobile-side-panel-menu.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MobileSidePanelMenuComponent {
|
||||
readonly T = T;
|
||||
readonly layoutService = inject(LayoutService);
|
||||
readonly taskViewCustomizerService = inject(TaskViewCustomizerService);
|
||||
readonly breakpointObserver = inject(BreakpointObserver);
|
||||
|
||||
private _globalConfigService = inject(GlobalConfigService);
|
||||
private _pluginBridge = inject(PluginBridgeService);
|
||||
private _store = inject(Store);
|
||||
private _router = inject(Router);
|
||||
|
||||
// State signals
|
||||
readonly isShowMobileMenu = signal(false);
|
||||
|
||||
// Convert observables to signals
|
||||
readonly isRouteWithSidePanel = toSignal(
|
||||
this._router.events.pipe(
|
||||
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
|
||||
map((event) => true), // Always true since right-panel is now global
|
||||
startWith(true), // Always true since right-panel is now global
|
||||
),
|
||||
{ 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$/) },
|
||||
);
|
||||
|
||||
readonly kb: KeyboardConfig = this._globalConfigService.cfg()?.keyboard || {};
|
||||
|
||||
// Plugin-related signals
|
||||
readonly sidePanelButtons = this._pluginBridge.sidePanelButtons;
|
||||
readonly activePluginId = toSignal(this._store.select(selectActivePluginId));
|
||||
readonly isShowPluginPanel = toSignal(this._store.select(selectIsShowPluginPanel));
|
||||
|
||||
// Navigation signals
|
||||
readonly currentRoute = toSignal(
|
||||
this._router.events.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
map(() => this._router.url),
|
||||
),
|
||||
{ initialValue: this._router.url },
|
||||
);
|
||||
|
||||
readonly isWorkView = computed(() => {
|
||||
const url = this.currentRoute();
|
||||
return url.includes('/active/') || url.includes('/tag/') || url.includes('/project/');
|
||||
});
|
||||
|
||||
// Panel state signals
|
||||
readonly isShowNotes = computed(() => this.layoutService.isShowNotes());
|
||||
readonly isShowIssuePanel = computed(() => this.layoutService.isShowIssuePanel());
|
||||
readonly isIssuesPanelEnabled = computed(
|
||||
() => this._globalConfigService.appFeatures().isIssuesPanelEnabled,
|
||||
);
|
||||
readonly isProjectNotesEnabled = computed(
|
||||
() => this._globalConfigService.appFeatures().isProjectNotesEnabled,
|
||||
);
|
||||
readonly isShowTaskViewCustomizerPanel = computed(() =>
|
||||
this.layoutService.isShowTaskViewCustomizerPanel(),
|
||||
);
|
||||
|
||||
// Computed signal for active panel
|
||||
readonly hasActivePanel = computed(() => {
|
||||
return !!(
|
||||
this.isShowNotes() ||
|
||||
this.isShowIssuePanel() ||
|
||||
this.isShowTaskViewCustomizerPanel() ||
|
||||
(this.activePluginId() && this.isShowPluginPanel())
|
||||
);
|
||||
});
|
||||
|
||||
toggleMenu(): void {
|
||||
this.isShowMobileMenu.update((v) => !v);
|
||||
}
|
||||
|
||||
onPluginButtonClick(button: {
|
||||
pluginId: string;
|
||||
onClick?: () => void;
|
||||
label?: string;
|
||||
icon?: string;
|
||||
}): void {
|
||||
this._store.dispatch(togglePluginPanel(button.pluginId));
|
||||
|
||||
if (button.onClick) {
|
||||
button.onClick();
|
||||
}
|
||||
|
||||
// Close mobile menu after action
|
||||
this.isShowMobileMenu.set(false);
|
||||
}
|
||||
|
||||
toggleIssuePanel(): void {
|
||||
this.layoutService.toggleAddTaskPanel();
|
||||
this.isShowMobileMenu.set(false);
|
||||
}
|
||||
|
||||
toggleNotes(): void {
|
||||
this.layoutService.toggleNotes();
|
||||
this.isShowMobileMenu.set(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
import { WorkContextThemeCfg } from '../../features/work-context/work-context.model';
|
||||
|
||||
/**
|
||||
* Pre-validated color palette that meets WCAG AA contrast standards (4.5:1)
|
||||
* for both light and dark themes when used on standard backgrounds.
|
||||
*/
|
||||
export interface ValidatedColorPalette {
|
||||
/** Display name for the palette */
|
||||
name: string;
|
||||
/** Theme configuration with validated colors */
|
||||
theme: WorkContextThemeCfg;
|
||||
/** Description for UI display */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection of pre-validated color palettes.
|
||||
* All palettes are tested to meet WCAG AA contrast standards (4.5:1 ratio)
|
||||
* on both light (#f8f8f7) and dark (#131314) backgrounds.
|
||||
*/
|
||||
export const VALIDATED_COLOR_PALETTES: readonly ValidatedColorPalette[] = [
|
||||
{
|
||||
name: 'Purple & Pink (Default)',
|
||||
description: 'The classic Super Productivity theme',
|
||||
theme: {
|
||||
isAutoContrast: true,
|
||||
isDisableBackgroundTint: false,
|
||||
primary: '#8b4a9d', // Darker purple for better contrast (was #a05db1)
|
||||
huePrimary: '500',
|
||||
accent: '#d81b60', // Darker pink for better contrast (was #ff4081)
|
||||
hueAccent: '500',
|
||||
warn: '#c62828', // Darker red for better contrast (was #e11826)
|
||||
hueWarn: '500',
|
||||
backgroundImageDark: null,
|
||||
backgroundImageLight: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Ocean Blue',
|
||||
description: 'Professional blue tones',
|
||||
theme: {
|
||||
isAutoContrast: true,
|
||||
isDisableBackgroundTint: false,
|
||||
primary: '#1976d2', // Material Blue 700
|
||||
huePrimary: '500',
|
||||
accent: '#0277bd', // Light Blue 800
|
||||
hueAccent: '500',
|
||||
warn: '#c62828', // Red 800
|
||||
hueWarn: '500',
|
||||
backgroundImageDark: null,
|
||||
backgroundImageLight: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Forest Green',
|
||||
description: 'Calming natural greens',
|
||||
theme: {
|
||||
isAutoContrast: true,
|
||||
isDisableBackgroundTint: false,
|
||||
primary: '#388e3c', // Green 700
|
||||
huePrimary: '500',
|
||||
accent: '#00796b', // Teal 700
|
||||
hueAccent: '500',
|
||||
warn: '#d32f2f', // Red 700
|
||||
hueWarn: '500',
|
||||
backgroundImageDark: null,
|
||||
backgroundImageLight: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Sunset Orange',
|
||||
description: 'Warm and energetic',
|
||||
theme: {
|
||||
isAutoContrast: true,
|
||||
isDisableBackgroundTint: false,
|
||||
primary: '#e64a19', // Deep Orange 700
|
||||
huePrimary: '500',
|
||||
accent: '#f57c00', // Orange 700
|
||||
hueAccent: '500',
|
||||
warn: '#c62828', // Red 800
|
||||
hueWarn: '500',
|
||||
backgroundImageDark: null,
|
||||
backgroundImageLight: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Deep Teal',
|
||||
description: 'Modern and sophisticated',
|
||||
theme: {
|
||||
isAutoContrast: true,
|
||||
isDisableBackgroundTint: false,
|
||||
primary: '#00796b', // Teal 700
|
||||
huePrimary: '500',
|
||||
accent: '#0097a7', // Cyan 700
|
||||
hueAccent: '500',
|
||||
warn: '#c62828', // Red 800
|
||||
hueWarn: '500',
|
||||
backgroundImageDark: null,
|
||||
backgroundImageLight: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Ruby Red',
|
||||
description: 'Bold and striking',
|
||||
theme: {
|
||||
isAutoContrast: true,
|
||||
isDisableBackgroundTint: false,
|
||||
primary: '#c62828', // Red 800
|
||||
huePrimary: '500',
|
||||
accent: '#ad1457', // Pink 800
|
||||
hueAccent: '500',
|
||||
warn: '#d84315', // Deep Orange 800
|
||||
hueWarn: '500',
|
||||
backgroundImageDark: null,
|
||||
backgroundImageLight: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Indigo Night',
|
||||
description: 'Deep and focused',
|
||||
theme: {
|
||||
isAutoContrast: true,
|
||||
isDisableBackgroundTint: false,
|
||||
primary: '#283593', // Indigo 800
|
||||
huePrimary: '500',
|
||||
accent: '#303f9f', // Indigo 700
|
||||
hueAccent: '500',
|
||||
warn: '#c62828', // Red 800
|
||||
hueWarn: '500',
|
||||
backgroundImageDark: null,
|
||||
backgroundImageLight: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Amber Glow',
|
||||
description: 'Bright and cheerful',
|
||||
theme: {
|
||||
isAutoContrast: true,
|
||||
isDisableBackgroundTint: false,
|
||||
primary: '#f57f17', // Yellow 800
|
||||
huePrimary: '500',
|
||||
accent: '#ef6c00', // Orange 800
|
||||
hueAccent: '500',
|
||||
warn: '#c62828', // Red 800
|
||||
hueWarn: '500',
|
||||
backgroundImageDark: null,
|
||||
backgroundImageLight: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Slate Gray',
|
||||
description: 'Neutral and elegant',
|
||||
theme: {
|
||||
isAutoContrast: true,
|
||||
isDisableBackgroundTint: false,
|
||||
primary: '#455a64', // Blue Gray 700
|
||||
huePrimary: '500',
|
||||
accent: '#546e7a', // Blue Gray 600
|
||||
hueAccent: '500',
|
||||
warn: '#c62828', // Red 800
|
||||
hueWarn: '500',
|
||||
backgroundImageDark: null,
|
||||
backgroundImageLight: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Lime Fresh',
|
||||
description: 'Vibrant and lively',
|
||||
theme: {
|
||||
isAutoContrast: true,
|
||||
isDisableBackgroundTint: false,
|
||||
primary: '#689f38', // Light Green 700
|
||||
huePrimary: '500',
|
||||
accent: '#7cb342', // Light Green 600
|
||||
hueAccent: '500',
|
||||
warn: '#c62828', // Red 800
|
||||
hueWarn: '500',
|
||||
backgroundImageDark: null,
|
||||
backgroundImageLight: null,
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Get a validated palette by name
|
||||
*/
|
||||
export const getValidatedPalette = (name: string): ValidatedColorPalette | undefined => {
|
||||
return VALIDATED_COLOR_PALETTES.find((p) => p.name === name);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the default validated palette
|
||||
*/
|
||||
export const getDefaultValidatedPalette = (): ValidatedColorPalette => {
|
||||
return VALIDATED_COLOR_PALETTES[0];
|
||||
};
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<h1 mat-dialog-title>
|
||||
{{
|
||||
isEdit
|
||||
? (T.F.TAG.D_EDIT.EDIT | translate: { title: title })
|
||||
: (T.F.TAG.D_EDIT.ADD | translate: { title: title })
|
||||
}}
|
||||
</h1>
|
||||
|
||||
<mat-dialog-content>
|
||||
<h3 class="mat-h3"></h3>
|
||||
|
||||
<div class="form-wrapper">
|
||||
<chip-list-input
|
||||
(addItem)="addTag($event)"
|
||||
(addNewItem)="addNewTag($event)"
|
||||
(removeItem)="removeTag($event)"
|
||||
autoFocus
|
||||
[label]="T.F.TAG.D_EDIT.LABEL | translate"
|
||||
[model]="tagIds"
|
||||
[suggestions]="tagSuggestions$ | async"
|
||||
(ctrlEnterSubmit)="close()"
|
||||
></chip-list-input>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button
|
||||
(click)="close()"
|
||||
mat-button
|
||||
type="submit"
|
||||
>
|
||||
{{ T.G.CLOSE | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
@use '../../../../styles/_globals.scss' as *;
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
align-items: stretch;
|
||||
max-width: 600px;
|
||||
|
||||
@include mq(xs) {
|
||||
width: 70vw;
|
||||
}
|
||||
|
||||
:host-context([dir='rtl']) {
|
||||
direction: rtl;
|
||||
}
|
||||
}
|
||||
|
||||
.form-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
import { ChangeDetectionStrategy, Component, inject, OnDestroy } from '@angular/core';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogActions,
|
||||
MatDialogContent,
|
||||
MatDialogRef,
|
||||
MatDialogTitle,
|
||||
} from '@angular/material/dialog';
|
||||
import { T } from '../../../t.const';
|
||||
import { TaskService } from '../../tasks/task.service';
|
||||
import { DialogEditTagsForTaskPayload } from './dialog-edit-tags-for-task.payload';
|
||||
import { truncate } from '../../../util/truncate';
|
||||
import { TagService } from '../tag.service';
|
||||
import { Task } from '../../tasks/task.model';
|
||||
import { unique } from '../../../util/unique';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { Tag } from '../tag.model';
|
||||
import { ChipListInputComponent } from '../../../ui/chip-list-input/chip-list-input.component';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TaskSharedActions } from '../../../root-store/meta/task-shared.actions';
|
||||
import { addTag } from '../store/tag.actions';
|
||||
|
||||
@Component({
|
||||
selector: 'dialog-edit-tags',
|
||||
templateUrl: './dialog-edit-tags-for-task.component.html',
|
||||
styleUrls: ['./dialog-edit-tags-for-task.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
ChipListInputComponent,
|
||||
MatDialogActions,
|
||||
MatButton,
|
||||
AsyncPipe,
|
||||
TranslatePipe,
|
||||
],
|
||||
})
|
||||
export class DialogEditTagsForTaskComponent implements OnDestroy {
|
||||
private _taskService = inject(TaskService);
|
||||
private _tagService = inject(TagService);
|
||||
private _store = inject(Store);
|
||||
private _matDialogRef =
|
||||
inject<MatDialogRef<DialogEditTagsForTaskComponent>>(MatDialogRef);
|
||||
data = inject<DialogEditTagsForTaskPayload>(MAT_DIALOG_DATA);
|
||||
|
||||
T: typeof T = T;
|
||||
title: string = truncate(this.data.task.title, 20);
|
||||
task$: Observable<Task> = this._taskService.getByIdLive$(this.data.task.id);
|
||||
task: Task = this.data.task;
|
||||
tagIds: string[] = [...this.data.task.tagIds];
|
||||
isEdit: boolean = this.data.task.tagIds && this.data.task.tagIds.length > 0;
|
||||
tagSuggestions$: Observable<Tag[]> = this._tagService.tagsNoMyDayAndNoList$;
|
||||
|
||||
private _subs: Subscription = new Subscription();
|
||||
|
||||
constructor() {
|
||||
this._subs.add(
|
||||
this.task$.subscribe((task) => {
|
||||
this.tagIds = task.tagIds;
|
||||
this.task = task;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._subs.unsubscribe();
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this._matDialogRef.close();
|
||||
}
|
||||
|
||||
addTag(id: string): void {
|
||||
this._updateTags(unique([...this.tagIds, id]));
|
||||
}
|
||||
|
||||
addNewTag(title: string): void {
|
||||
const cleanTitle = (t: string): string => {
|
||||
return t.replace('#', '');
|
||||
};
|
||||
|
||||
const tag = this._tagService.createTagObject({ title: cleanTitle(title) });
|
||||
this._store.dispatch(addTag({ tag }));
|
||||
this._store.dispatch(
|
||||
TaskSharedActions.addTagToTask({ tagId: tag.id, taskId: this.task.id }),
|
||||
);
|
||||
}
|
||||
|
||||
removeTag(id: string): void {
|
||||
const updatedTagIds = this.tagIds.filter((tagId) => tagId !== id);
|
||||
this._updateTags(updatedTagIds);
|
||||
}
|
||||
|
||||
private _updateTags(newTagIds: string[]): void {
|
||||
this._taskService.updateTags(this.task, unique(newTagIds));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { Task } from '../../tasks/task.model';
|
||||
|
||||
export interface DialogEditTagsForTaskPayload {
|
||||
task: Task;
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<mat-dialog-content>
|
||||
<!-- <h1>XXX {{(task$|async)?.id}}</h1>-->
|
||||
@if (task$ | async; as task) {
|
||||
<task-detail-panel
|
||||
[task]="task"
|
||||
[isDialogMode]="true"
|
||||
></task-detail-panel>
|
||||
}
|
||||
</mat-dialog-content>
|
||||
|
||||
<!--<mat-dialog-actions align="center">-->
|
||||
<!-- <button-->
|
||||
<!-- mat-flat-button-->
|
||||
<!-- color="primary"-->
|
||||
<!-- mat-dialog-close-->
|
||||
<!-- >-->
|
||||
<!-- <mat-icon>close</mat-icon> {{T.G.CLOSE|translate}}-->
|
||||
<!-- </button>-->
|
||||
<!--</mat-dialog-actions>-->
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
@use '../../../../styles/_globals.scss' as *;
|
||||
|
||||
:host {
|
||||
mat-dialog-content {
|
||||
padding: 0;
|
||||
border-radius: var(--card-border-radius);
|
||||
max-width: 450px;
|
||||
max-height: 75vh;
|
||||
min-width: 340px;
|
||||
|
||||
background: var(--bg-lightest) !important;
|
||||
|
||||
@include mq(xxxs) {
|
||||
min-width: 380px;
|
||||
}
|
||||
@include mq(xs) {
|
||||
min-width: 450px;
|
||||
max-height: 85vh;
|
||||
}
|
||||
}
|
||||
|
||||
&::ng-deep {
|
||||
.task-title-wrapper {
|
||||
padding-right: var(--s) !important;
|
||||
}
|
||||
|
||||
task .hover-controls {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import { ChangeDetectionStrategy, Component, inject, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogContent,
|
||||
MatDialogRef,
|
||||
} from '@angular/material/dialog';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { selectSelectedTask } from '../store/task.selectors';
|
||||
import { T } from 'src/app/t.const';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { setSelectedTask } from '../store/task.actions';
|
||||
import { TaskDetailTargetPanel } from '../task.model';
|
||||
import { skipWhile } from 'rxjs/operators';
|
||||
import { TaskDetailPanelComponent } from '../task-detail-panel/task-detail-panel.component';
|
||||
|
||||
@Component({
|
||||
selector: 'dialog-task-detail-panel',
|
||||
imports: [CommonModule, TranslateModule, MatDialogContent, TaskDetailPanelComponent],
|
||||
templateUrl: './dialog-task-detail-panel.component.html',
|
||||
styleUrl: './dialog-task-detail-panel.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DialogTaskDetailPanelComponent implements OnDestroy {
|
||||
data = inject<{
|
||||
taskId: string;
|
||||
}>(MAT_DIALOG_DATA);
|
||||
private _matDialogRef =
|
||||
inject<MatDialogRef<DialogTaskDetailPanelComponent>>(MatDialogRef);
|
||||
private _store = inject(Store);
|
||||
|
||||
T: typeof T = T;
|
||||
task$ = this._store.select(selectSelectedTask).pipe(skipWhile((v) => !v));
|
||||
|
||||
constructor() {
|
||||
const data = this.data;
|
||||
|
||||
this._store.dispatch(
|
||||
setSelectedTask({
|
||||
id: data.taskId,
|
||||
taskDetailTargetPanel: TaskDetailTargetPanel.DONT_OPEN_PANEL,
|
||||
isSkipToggle: true,
|
||||
}),
|
||||
);
|
||||
// this.task$.subscribe((v) => TaskLog.log(`task$`, v));
|
||||
}
|
||||
|
||||
// close(): void {
|
||||
// this._matDialogRef.close();
|
||||
// }
|
||||
ngOnDestroy(): void {
|
||||
this._store.dispatch(
|
||||
setSelectedTask({
|
||||
id: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { PluginBridgeService } from '../plugin-bridge.service';
|
||||
import { MatMenuItem } from '@angular/material/menu';
|
||||
import { PluginIconComponent } from './plugin-icon/plugin-icon.component';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'plugin-menu',
|
||||
template: `
|
||||
@for (menuEntry of menuEntries(); track menuEntry.pluginId + menuEntry.label) {
|
||||
<button
|
||||
mat-menu-item
|
||||
class="plugin-menu-entry"
|
||||
[class.isActive]="isActive(menuEntry.pluginId)"
|
||||
(click)="menuEntry.onClick()"
|
||||
>
|
||||
<plugin-icon
|
||||
[pluginId]="menuEntry.pluginId"
|
||||
[fallbackIcon]="menuEntry.icon || 'extension'"
|
||||
></plugin-icon>
|
||||
<span>{{ menuEntry.label }}</span>
|
||||
</button>
|
||||
}
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.plugin-menu-entry {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 4px;
|
||||
&.isActive {
|
||||
color: var(--c-primary);
|
||||
font-weight: bold;
|
||||
|
||||
mat-icon {
|
||||
color: var(--c-primary);
|
||||
}
|
||||
}
|
||||
|
||||
mat-icon,
|
||||
plugin-icon {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [MatMenuItem, PluginIconComponent],
|
||||
})
|
||||
export class PluginMenuComponent {
|
||||
private readonly _pluginBridge = inject(PluginBridgeService);
|
||||
private readonly _router = inject(Router);
|
||||
|
||||
readonly currentRoute = toSignal(
|
||||
this._router.events.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
map(() => this._router.url),
|
||||
),
|
||||
{ initialValue: this._router.url },
|
||||
);
|
||||
|
||||
readonly menuEntries = this._pluginBridge.menuEntries;
|
||||
|
||||
isActive(pluginId: string): boolean {
|
||||
return this.currentRoute().includes(pluginId);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
@if (isVisible()) {
|
||||
<div
|
||||
[@expandFade]
|
||||
class="help-box"
|
||||
>
|
||||
<button
|
||||
(click)="onClose()"
|
||||
aria-label="Close help box"
|
||||
class="close-btn"
|
||||
mat-icon-button
|
||||
type="button"
|
||||
>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<div class="help-box-content">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
:host {
|
||||
display: block;
|
||||
position: sticky;
|
||||
top: var(--s2);
|
||||
background: var(--bg);
|
||||
z-index: 200;
|
||||
margin: var(--s);
|
||||
border: 2px solid var(--separator-color);
|
||||
border-radius: var(--card-border-radius);
|
||||
|
||||
.help-box {
|
||||
position: relative;
|
||||
padding: var(--s);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--card-border-radius);
|
||||
background-color: var(--color-bg-info);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: var(--s-half);
|
||||
right: var(--s-half);
|
||||
}
|
||||
|
||||
//.help-box-content {}
|
||||
|
||||
:host-context([dir='rtl']) {
|
||||
.close-btn {
|
||||
right: unset;
|
||||
left: var(--s-half);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
OnInit,
|
||||
WritableSignal,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { expandFadeAnimation } from '../animations/expand.ani';
|
||||
import { lsGetBoolean, lsSetItem } from '../../util/ls-util';
|
||||
|
||||
@Component({
|
||||
selector: 'help-box',
|
||||
templateUrl: './help-box.component.html',
|
||||
styleUrls: ['./help-box.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [expandFadeAnimation],
|
||||
imports: [MatIcon, MatIconButton],
|
||||
})
|
||||
export class HelpBoxComponent implements OnInit {
|
||||
@Input({ required: true }) lsKey!: string;
|
||||
|
||||
isVisible: WritableSignal<boolean> = signal(true);
|
||||
|
||||
ngOnInit(): void {
|
||||
// Check localStorage to determine if the help box should be shown
|
||||
const isDismissed = lsGetBoolean(this.lsKey, false);
|
||||
this.isVisible.set(!isDismissed);
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
// Set the localStorage key to true to indicate the box was dismissed
|
||||
lsSetItem(this.lsKey, true);
|
||||
this.isVisible.set(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { AfterViewInit, Directive, ElementRef, OnDestroy, inject } from '@angular/core';
|
||||
const DEFAULT_PINNED_CLASS = 'is-stuck';
|
||||
|
||||
@Directive({
|
||||
selector: '[stuck]',
|
||||
standalone: true,
|
||||
})
|
||||
export class StuckDirective implements AfterViewInit, OnDestroy {
|
||||
private elRef = inject(ElementRef);
|
||||
|
||||
private _observer?: IntersectionObserver;
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
const el = this.elRef.nativeElement as HTMLElement;
|
||||
this._observer = new IntersectionObserver(
|
||||
// ([e]) => e.target.classList.toggle(DEFAULT_PINNED_CLASS, e.intersectionRatio < 1),
|
||||
([e]) => {
|
||||
e.target.classList.toggle(DEFAULT_PINNED_CLASS, !e.isIntersecting);
|
||||
// Log.log(e.boundingClientRect.top, e);
|
||||
// Log.log('top', e.boundingClientRect.top < 0, e.isIntersecting);
|
||||
// if (e.boundingClientRect.top < 0) {
|
||||
// e.target.classList.toggle(DEFAULT_PINNED_CLASS, e.isIntersecting);
|
||||
// }
|
||||
},
|
||||
{
|
||||
rootMargin: '0px',
|
||||
threshold: 1,
|
||||
},
|
||||
);
|
||||
|
||||
this._observer.observe(el);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (!this._observer) {
|
||||
throw new Error('Observer is not defined');
|
||||
}
|
||||
this._observer.unobserve(this.elRef.nativeElement as HTMLElement);
|
||||
this._observer.disconnect();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'tree-drop-zones',
|
||||
standalone: true,
|
||||
template: `
|
||||
<div
|
||||
class="drop drop--before"
|
||||
data-drop-zone="before"
|
||||
[attr.data-node-id]="nodeId()"
|
||||
[class.is-over]="overBefore()"
|
||||
></div>
|
||||
<div
|
||||
class="drop drop--after"
|
||||
data-drop-zone="after"
|
||||
[attr.data-node-id]="nodeId()"
|
||||
[class.is-over]="overAfter()"
|
||||
></div>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.drop {
|
||||
height: 50%;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
:host-context(.tree.is-dragging) & {
|
||||
pointer-events: all;
|
||||
background: rgba(0, 123, 255, 0.1);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 123, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.drop--before,
|
||||
.drop--after {
|
||||
}
|
||||
|
||||
.drop--before {
|
||||
top: 0px;
|
||||
|
||||
:host-context(.tree.is-dragging) & {
|
||||
background: rgba(255, 165, 0, 0.1); /* Light orange when dragging */
|
||||
}
|
||||
|
||||
&.is-over {
|
||||
background: rgba(
|
||||
255,
|
||||
165,
|
||||
0,
|
||||
0.25
|
||||
) !important; /* Darker orange when hovering */
|
||||
}
|
||||
}
|
||||
|
||||
.drop--after {
|
||||
bottom: 0px;
|
||||
|
||||
:host-context(.tree.is-dragging) & {
|
||||
background: rgba(34, 139, 34, 0.1); /* Light green when dragging */
|
||||
}
|
||||
|
||||
&.is-over {
|
||||
background: rgba(34, 139, 34, 0.25) !important; /* Darker green when hovering */
|
||||
}
|
||||
}
|
||||
`,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TreeDropZonesComponent {
|
||||
readonly nodeId = input.required<string>();
|
||||
readonly overBefore = input(false);
|
||||
readonly overAfter = input(false);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue