From 51ae6d97f24c4b9d9b5093e7ec239bef0db92b2d Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 14 Oct 2025 17:03:27 +0200 Subject: [PATCH 01/50] feat(scheduleRightPanel): add first draft for schedule panel --- src/app/core-ui/layout/layout.service.ts | 17 + .../core-ui/layout/store/layout.actions.ts | 5 + .../core-ui/layout/store/layout.effects.ts | 8 +- .../core-ui/layout/store/layout.reducer.ts | 19 + .../desktop-panel-buttons.component.ts | 12 + .../main-header/main-header.component.html | 1 + .../main-header/main-header.component.ts | 1 + .../features/panels/panel-content.service.ts | 9 +- .../right-panel-content.component.html | 2 + .../right-panel-content.component.ts | 7 +- .../right-panel/right-panel.component.ts | 1 + .../schedule-day-panel.component.html | 33 ++ .../schedule-day-panel.component.scss | 37 ++ .../schedule-day-panel.component.ts | 526 ++++++++++++++++++ .../components/_overwrite-material.scss | 36 ++ 15 files changed, 710 insertions(+), 4 deletions(-) create mode 100644 src/app/features/schedule/schedule-day-panel/schedule-day-panel.component.html create mode 100644 src/app/features/schedule/schedule-day-panel/schedule-day-panel.component.scss create mode 100644 src/app/features/schedule/schedule-day-panel/schedule-day-panel.component.ts diff --git a/src/app/core-ui/layout/layout.service.ts b/src/app/core-ui/layout/layout.service.ts index edac6e54e7..bff8916d9f 100644 --- a/src/app/core-ui/layout/layout.service.ts +++ b/src/app/core-ui/layout/layout.service.ts @@ -8,6 +8,8 @@ import { toggleIssuePanel, toggleShowNotes, toggleTaskViewCustomizerPanel, + toggleScheduleDayPanel, + hideScheduleDayPanel, } from './store/layout.actions'; import { Observable } from 'rxjs'; import { select, Store } from '@ngrx/store'; @@ -17,6 +19,7 @@ import { selectIsShowIssuePanel, selectIsShowNotes, selectIsShowTaskViewCustomizerPanel, + selectIsShowScheduleDayPanel, } from './store/layout.reducer'; import { map } from 'rxjs/operators'; import { BreakpointObserver } from '@angular/cdk/layout'; @@ -85,6 +88,11 @@ export class LayoutService { readonly isShowIssuePanel = toSignal(this.isShowIssuePanel$, { initialValue: false }); + readonly isShowScheduleDayPanel = toSignal( + this._store$.pipe(select(selectIsShowScheduleDayPanel)), + { initialValue: false }, + ); + showAddTaskBar(): void { // Store currently focused element if it's a task const activeElement = document.activeElement as HTMLElement; @@ -138,4 +146,13 @@ export class LayoutService { // Trigger the focus signal - components listening to this signal will handle the focus this._focusSideNavTrigger.update((value) => value + 1); } + + // Schedule Day Panel controls + toggleScheduleDayPanel(): void { + this._store$.dispatch(toggleScheduleDayPanel()); + } + + hideScheduleDayPanel(): void { + this._store$.dispatch(hideScheduleDayPanel()); + } } diff --git a/src/app/core-ui/layout/store/layout.actions.ts b/src/app/core-ui/layout/store/layout.actions.ts index 6930d6c831..7dc4787d3f 100644 --- a/src/app/core-ui/layout/store/layout.actions.ts +++ b/src/app/core-ui/layout/store/layout.actions.ts @@ -37,3 +37,8 @@ export const togglePluginPanel = createAction( '[Layout] Toggle PluginPanel', (pluginId: string) => ({ pluginId }), ); + +// Schedule Day Panel +export const toggleScheduleDayPanel = createAction('[Layout] Toggle ScheduleDayPanel'); + +export const hideScheduleDayPanel = createAction('[Layout] Hide ScheduleDayPanel'); diff --git a/src/app/core-ui/layout/store/layout.effects.ts b/src/app/core-ui/layout/store/layout.effects.ts index ae0f73d8c7..e6952ab52a 100644 --- a/src/app/core-ui/layout/store/layout.effects.ts +++ b/src/app/core-ui/layout/store/layout.effects.ts @@ -3,6 +3,7 @@ import { Actions, createEffect, ofType } from '@ngrx/effects'; import { hideNonTaskSidePanelContent } from './layout.actions'; import { filter, mapTo } from 'rxjs/operators'; import { setSelectedTask } from '../../../features/tasks/store/task.actions'; +import { TaskDetailTargetPanel } from '../../../features/tasks/task.model'; import { LayoutService } from '../layout.service'; import { NavigationStart, NavigationEnd, Router } from '@angular/router'; @@ -23,7 +24,12 @@ export class LayoutEffects { hideNotesWhenTaskIsSelected$ = createEffect(() => this.actions$.pipe( ofType(setSelectedTask), - filter(({ id }) => id !== null), + filter(({ id, taskDetailTargetPanel, isSkipToggle }) => { + // Do not hide side content when opening modal (DONT_OPEN_PANEL) or when explicitly skipped + if (id === null) return false; + if (isSkipToggle) return false; + return taskDetailTargetPanel !== TaskDetailTargetPanel.DONT_OPEN_PANEL; + }), mapTo(hideNonTaskSidePanelContent()), ), ); diff --git a/src/app/core-ui/layout/store/layout.reducer.ts b/src/app/core-ui/layout/store/layout.reducer.ts index 2a4fa07e41..905f5661fd 100644 --- a/src/app/core-ui/layout/store/layout.reducer.ts +++ b/src/app/core-ui/layout/store/layout.reducer.ts @@ -9,6 +9,7 @@ import { toggleShowNotes, toggleTaskViewCustomizerPanel, } from './layout.actions'; +import { toggleScheduleDayPanel, hideScheduleDayPanel } from './layout.actions'; import { Action, createFeatureSelector, @@ -27,6 +28,7 @@ export interface LayoutState { isShowTaskViewCustomizerPanel: boolean; isShowPluginPanel: boolean; activePluginId: string | null; + isShowScheduleDayPanel: boolean; } export const INITIAL_LAYOUT_STATE: LayoutState = { @@ -37,6 +39,7 @@ export const INITIAL_LAYOUT_STATE: LayoutState = { isShowTaskViewCustomizerPanel: false, isShowPluginPanel: false, activePluginId: null, + isShowScheduleDayPanel: false, }; export const selectLayoutFeatureState = @@ -77,12 +80,18 @@ export const selectActivePluginId = createSelector( (state) => state.activePluginId, ); +export const selectIsShowScheduleDayPanel = createSelector( + selectLayoutFeatureState, + (state) => state.isShowScheduleDayPanel, +); + const ALL_PANEL_CONTENT_HIDDEN: Partial = { isShowNotes: false, isShowIssuePanel: false, isShowTaskViewCustomizerPanel: false, isShowPluginPanel: false, activePluginId: null, + isShowScheduleDayPanel: false, }; const _reducer = createReducer( @@ -138,6 +147,16 @@ const _reducer = createReducer( activePluginId: isCurrentlyActive ? null : pluginId, }; }), + on(toggleScheduleDayPanel, (state) => ({ + ...state, + ...ALL_PANEL_CONTENT_HIDDEN, + isShowScheduleDayPanel: !state.isShowScheduleDayPanel, + })), + on(hideScheduleDayPanel, (state) => ({ + ...state, + ...ALL_PANEL_CONTENT_HIDDEN, + isShowScheduleDayPanel: false, + })), ); export const layoutReducer = ( diff --git a/src/app/core-ui/main-header/desktop-panel-buttons/desktop-panel-buttons.component.ts b/src/app/core-ui/main-header/desktop-panel-buttons/desktop-panel-buttons.component.ts index 443da1f879..a7cddb8790 100644 --- a/src/app/core-ui/main-header/desktop-panel-buttons/desktop-panel-buttons.component.ts +++ b/src/app/core-ui/main-header/desktop-panel-buttons/desktop-panel-buttons.component.ts @@ -13,6 +13,17 @@ import { KeyboardConfig } from '../../../features/config/keyboard-config.model'; standalone: true, imports: [MatIconButton, MatIcon, MatTooltip, TranslatePipe], template: ` + + +
+ +
+ +} diff --git a/src/app/ui/help-box/help-box.component.scss b/src/app/ui/help-box/help-box.component.scss new file mode 100644 index 0000000000..c055cd635d --- /dev/null +++ b/src/app/ui/help-box/help-box.component.scss @@ -0,0 +1,33 @@ +:host { + display: block; + position: sticky; + top: 16px; + background: var(--bg); + z-index: 200; + margin: 8px; + 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: 4px; + background-color: var(--color-bg-info); + } + + .close-btn { + position: absolute; + top: 4px; + right: 4px; + } + + //.help-box-content {} + + :host-context([dir='rtl']) { + .close-btn { + right: unset; + left: 4px; + } + } +} diff --git a/src/app/ui/help-box/help-box.component.spec.ts b/src/app/ui/help-box/help-box.component.spec.ts new file mode 100644 index 0000000000..822a55cfa0 --- /dev/null +++ b/src/app/ui/help-box/help-box.component.spec.ts @@ -0,0 +1,33 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HelpBoxComponent } from './help-box.component'; +import { provideAnimations } from '@angular/platform-browser/animations'; + +describe('HelpBoxComponent', () => { + let component: HelpBoxComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HelpBoxComponent], + providers: [provideAnimations()], + }).compileComponents(); + + fixture = TestBed.createComponent(HelpBoxComponent); + component = fixture.componentInstance; + component.lsKey = 'TEST_HELP_BOX'; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should show the help box by default', () => { + expect(component.isVisible()).toBe(true); + }); + + it('should hide the help box when close is clicked', () => { + component.onClose(); + expect(component.isVisible()).toBe(false); + }); +}); diff --git a/src/app/ui/help-box/help-box.component.ts b/src/app/ui/help-box/help-box.component.ts new file mode 100644 index 0000000000..d3e341b07b --- /dev/null +++ b/src/app/ui/help-box/help-box.component.ts @@ -0,0 +1,31 @@ +import { ChangeDetectionStrategy, Component, Input, Signal, 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 { + @Input({ required: true }) lsKey!: string; + + isVisible: Signal = signal(true); + + ngOnInit(): void { + // Check localStorage to determine if the help box should be shown + const isDismissed = lsGetBoolean(this.lsKey, false); + (this.isVisible as any).set(!isDismissed); + } + + onClose(): void { + // Set the localStorage key to true to indicate the box was dismissed + lsSetItem(this.lsKey, true); + (this.isVisible as any).set(false); + } +} From cbc7aaa5bef49096113eac6a1d2bc3265ae3bbdd Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Fri, 17 Oct 2025 18:07:50 +0200 Subject: [PATCH 49/50] fix(scheduleDayPanel): animation --- .../schedule-day-panel/schedule-day-panel.component.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/features/schedule/schedule-day-panel/schedule-day-panel.component.scss b/src/app/features/schedule/schedule-day-panel/schedule-day-panel.component.scss index 47faa81c64..7e82ed0f30 100644 --- a/src/app/features/schedule/schedule-day-panel/schedule-day-panel.component.scss +++ b/src/app/features/schedule/schedule-day-panel/schedule-day-panel.component.scss @@ -1,4 +1,6 @@ :host { + display: block; + > * { inset: 4px; margin: 4px; From b43a34710d89b0bf6ed45ee822b9dea5f1f1b1d0 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Fri, 17 Oct 2025 18:30:09 +0200 Subject: [PATCH 50/50] feat(scheduleDayPanel): add message for first time users --- .../schedule-day-panel.component.html | 5 ++++ .../schedule-day-panel.component.scss | 23 +++++++++++++++++++ .../schedule-day-panel.component.ts | 8 +++++-- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/app/features/schedule/schedule-day-panel/schedule-day-panel.component.html b/src/app/features/schedule/schedule-day-panel/schedule-day-panel.component.html index 46e2e75229..0ffbe05bbe 100644 --- a/src/app/features/schedule/schedule-day-panel/schedule-day-panel.component.html +++ b/src/app/features/schedule/schedule-day-panel/schedule-day-panel.component.html @@ -5,6 +5,11 @@ (mousemove)="onDragMove($event)" (touchmove)="onDragMove($event)" > + @if (hasNoEvents()) { +
+
Drop tasks here to schedule them
+
+ } this._eventsAndBeyondBudget().eventsFlat); + hasNoEvents = computed(() => { + const evs = this.events().filter((ev) => ev.type !== SVEType.LunchBreak); + return !evs || evs.length === 0; + }); + private _workStartEndHours = toSignal( this._store.select(selectTimelineWorkStartEndHours), );