mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
fix(task-view-customizer): persist sort, group, and filter settings to localStorage
Resolves issue #6095 where Task View Customizer settings (sort, group, filter) were being reset on app restart or day change. Changes: - Add localStorage keys for sort/group/filter settings - Initialize signals from localStorage with default fallbacks - Add effects to auto-persist signal changes to localStorage - Add 7 comprehensive unit tests for persistence behavior Settings now persist across app restarts, work context changes, and day boundaries. Invalid localStorage data gracefully falls back to defaults.
This commit is contained in:
parent
623971eacd
commit
337afed482
3 changed files with 208 additions and 4 deletions
|
|
@ -55,6 +55,11 @@ export enum LS {
|
|||
NAV_SIDEBAR_EXPANDED = 'SUP_NAV_SIDEBAR_EXPANDED',
|
||||
NAV_SIDEBAR_WIDTH = 'SUP_NAV_SIDEBAR_WIDTH',
|
||||
RIGHT_PANEL_WIDTH = 'SUP_RIGHT_PANEL_WIDTH',
|
||||
|
||||
// Task view customizer
|
||||
TASK_VIEW_CUSTOMIZER_SORT = 'SUP_TASK_VIEW_CUSTOMIZER_SORT',
|
||||
TASK_VIEW_CUSTOMIZER_GROUP = 'SUP_TASK_VIEW_CUSTOMIZER_GROUP',
|
||||
TASK_VIEW_CUSTOMIZER_FILTER = 'SUP_TASK_VIEW_CUSTOMIZER_FILTER',
|
||||
}
|
||||
|
||||
// SESSION STORAGE
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import {
|
|||
} from './types';
|
||||
import { DateAdapter } from '@angular/material/core';
|
||||
import { DEFAULT_FIRST_DAY_OF_WEEK } from 'src/app/core/locale.constants';
|
||||
import { LS } from '../../core/persistence/storage-keys.const';
|
||||
|
||||
describe('TaskViewCustomizerService', () => {
|
||||
let service: TaskViewCustomizerService;
|
||||
|
|
@ -114,6 +115,9 @@ describe('TaskViewCustomizerService', () => {
|
|||
];
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear localStorage before each test
|
||||
localStorage.clear();
|
||||
|
||||
mockWorkContextService = {
|
||||
activeWorkContextId: null,
|
||||
activeWorkContextType: null,
|
||||
|
|
@ -498,4 +502,176 @@ describe('TaskViewCustomizerService', () => {
|
|||
expect(service.selectedFilter()).toEqual(DEFAULT_OPTIONS.filter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('localStorage persistence', () => {
|
||||
it('should initialize with default values when localStorage is empty', () => {
|
||||
expect(service.selectedSort()).toEqual(DEFAULT_OPTIONS.sort);
|
||||
expect(service.selectedGroup()).toEqual(DEFAULT_OPTIONS.group);
|
||||
expect(service.selectedFilter()).toEqual(DEFAULT_OPTIONS.filter);
|
||||
});
|
||||
|
||||
it('should restore sort option from localStorage on initialization', () => {
|
||||
const savedSort: SortOption = {
|
||||
type: SORT_OPTION_TYPE.name,
|
||||
order: SORT_ORDER.ASC,
|
||||
label: 'Name',
|
||||
};
|
||||
localStorage.setItem(LS.TASK_VIEW_CUSTOMIZER_SORT, JSON.stringify(savedSort));
|
||||
|
||||
// Reset TestBed to create a new service instance
|
||||
TestBed.resetTestingModule();
|
||||
const dateAdapter = jasmine.createSpyObj<DateAdapter<Date>>('DateAdapter', [], {
|
||||
getFirstDayOfWeek: () => DEFAULT_FIRST_DAY_OF_WEEK,
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
TaskViewCustomizerService,
|
||||
{ provide: DateAdapter, useValue: dateAdapter },
|
||||
{ provide: WorkContextService, useValue: mockWorkContextService },
|
||||
{ provide: ProjectService, useValue: { update: projectUpdateSpy } },
|
||||
{ provide: TagService, useValue: { updateTag: tagUpdateSpy } },
|
||||
provideMockStore({
|
||||
selectors: [
|
||||
{ selector: selectAllProjects, value: mockProjects },
|
||||
{ selector: selectAllTags, value: mockTags },
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const newService = TestBed.inject(TaskViewCustomizerService);
|
||||
(newService as any)._allProjects = mockProjects;
|
||||
(newService as any)._allTags = mockTags;
|
||||
|
||||
expect(newService.selectedSort()).toEqual(savedSort);
|
||||
});
|
||||
|
||||
it('should restore group option from localStorage on initialization', () => {
|
||||
const savedGroup: GroupOption = { type: GROUP_OPTION_TYPE.tag, label: 'Tag' };
|
||||
localStorage.setItem(LS.TASK_VIEW_CUSTOMIZER_GROUP, JSON.stringify(savedGroup));
|
||||
|
||||
TestBed.resetTestingModule();
|
||||
const dateAdapter = jasmine.createSpyObj<DateAdapter<Date>>('DateAdapter', [], {
|
||||
getFirstDayOfWeek: () => DEFAULT_FIRST_DAY_OF_WEEK,
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
TaskViewCustomizerService,
|
||||
{ provide: DateAdapter, useValue: dateAdapter },
|
||||
{ provide: WorkContextService, useValue: mockWorkContextService },
|
||||
{ provide: ProjectService, useValue: { update: projectUpdateSpy } },
|
||||
{ provide: TagService, useValue: { updateTag: tagUpdateSpy } },
|
||||
provideMockStore({
|
||||
selectors: [
|
||||
{ selector: selectAllProjects, value: mockProjects },
|
||||
{ selector: selectAllTags, value: mockTags },
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const newService = TestBed.inject(TaskViewCustomizerService);
|
||||
(newService as any)._allProjects = mockProjects;
|
||||
(newService as any)._allTags = mockTags;
|
||||
|
||||
expect(newService.selectedGroup()).toEqual(savedGroup);
|
||||
});
|
||||
|
||||
it('should restore filter option from localStorage on initialization', () => {
|
||||
const savedFilter: FilterOption = {
|
||||
type: FILTER_OPTION_TYPE.tag,
|
||||
preset: 'Tag A',
|
||||
label: 'Tag',
|
||||
};
|
||||
localStorage.setItem(LS.TASK_VIEW_CUSTOMIZER_FILTER, JSON.stringify(savedFilter));
|
||||
|
||||
TestBed.resetTestingModule();
|
||||
const dateAdapter = jasmine.createSpyObj<DateAdapter<Date>>('DateAdapter', [], {
|
||||
getFirstDayOfWeek: () => DEFAULT_FIRST_DAY_OF_WEEK,
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
TaskViewCustomizerService,
|
||||
{ provide: DateAdapter, useValue: dateAdapter },
|
||||
{ provide: WorkContextService, useValue: mockWorkContextService },
|
||||
{ provide: ProjectService, useValue: { update: projectUpdateSpy } },
|
||||
{ provide: TagService, useValue: { updateTag: tagUpdateSpy } },
|
||||
provideMockStore({
|
||||
selectors: [
|
||||
{ selector: selectAllProjects, value: mockProjects },
|
||||
{ selector: selectAllTags, value: mockTags },
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const newService = TestBed.inject(TaskViewCustomizerService);
|
||||
(newService as any)._allProjects = mockProjects;
|
||||
(newService as any)._allTags = mockTags;
|
||||
|
||||
expect(newService.selectedFilter()).toEqual(savedFilter);
|
||||
});
|
||||
|
||||
it('should persist sort option to localStorage when changed', (done) => {
|
||||
const newSort: SortOption = {
|
||||
type: SORT_OPTION_TYPE.name,
|
||||
order: SORT_ORDER.ASC,
|
||||
label: 'Name',
|
||||
};
|
||||
service.setSort(newSort);
|
||||
|
||||
// Wait for effect to run
|
||||
setTimeout(() => {
|
||||
const stored = localStorage.getItem(LS.TASK_VIEW_CUSTOMIZER_SORT);
|
||||
expect(stored).toBeTruthy();
|
||||
expect(JSON.parse(stored!)).toEqual(newSort);
|
||||
done();
|
||||
}, 50);
|
||||
});
|
||||
|
||||
it('should persist group option to localStorage when changed', (done) => {
|
||||
const newGroup: GroupOption = { type: GROUP_OPTION_TYPE.tag, label: 'Tag' };
|
||||
service.setGroup(newGroup);
|
||||
|
||||
setTimeout(() => {
|
||||
const stored = localStorage.getItem(LS.TASK_VIEW_CUSTOMIZER_GROUP);
|
||||
expect(stored).toBeTruthy();
|
||||
expect(JSON.parse(stored!)).toEqual(newGroup);
|
||||
done();
|
||||
}, 50);
|
||||
});
|
||||
|
||||
it('should persist filter option to localStorage when changed', (done) => {
|
||||
const newFilter: FilterOption = {
|
||||
type: FILTER_OPTION_TYPE.tag,
|
||||
preset: 'Tag A',
|
||||
label: 'Tag',
|
||||
};
|
||||
service.setFilter(newFilter);
|
||||
|
||||
setTimeout(() => {
|
||||
const stored = localStorage.getItem(LS.TASK_VIEW_CUSTOMIZER_FILTER);
|
||||
expect(stored).toBeTruthy();
|
||||
expect(JSON.parse(stored!)).toEqual(newFilter);
|
||||
done();
|
||||
}, 50);
|
||||
});
|
||||
|
||||
it('should fallback to defaults when localStorage contains invalid JSON', () => {
|
||||
localStorage.setItem(LS.TASK_VIEW_CUSTOMIZER_SORT, 'invalid json{');
|
||||
localStorage.setItem(LS.TASK_VIEW_CUSTOMIZER_GROUP, '{broken');
|
||||
localStorage.setItem(LS.TASK_VIEW_CUSTOMIZER_FILTER, 'not json');
|
||||
|
||||
const newService = TestBed.inject(TaskViewCustomizerService);
|
||||
(newService as any)._allProjects = mockProjects;
|
||||
(newService as any)._allTags = mockTags;
|
||||
|
||||
expect(newService.selectedSort()).toEqual(DEFAULT_OPTIONS.sort);
|
||||
expect(newService.selectedGroup()).toEqual(DEFAULT_OPTIONS.group);
|
||||
expect(newService.selectedFilter()).toEqual(DEFAULT_OPTIONS.filter);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Injectable, signal, inject } from '@angular/core';
|
||||
import { Injectable, signal, inject, effect } from '@angular/core';
|
||||
import { Observable, animationFrameScheduler, combineLatest } from 'rxjs';
|
||||
import { map, observeOn, take } from 'rxjs/operators';
|
||||
import { TaskWithSubTasks } from '../tasks/task.model';
|
||||
|
|
@ -28,6 +28,8 @@ import {
|
|||
FILTER_COMMON,
|
||||
} from './types';
|
||||
import { DateAdapter } from '@angular/material/core';
|
||||
import { lsGetJSON, lsSetJSON } from '../../util/ls-util';
|
||||
import { LS } from '../../core/persistence/storage-keys.const';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TaskViewCustomizerService {
|
||||
|
|
@ -37,9 +39,18 @@ export class TaskViewCustomizerService {
|
|||
private _projectService = inject(ProjectService);
|
||||
private _tagService = inject(TagService);
|
||||
|
||||
public selectedSort = signal<SortOption>(DEFAULT_OPTIONS.sort);
|
||||
public selectedGroup = signal<GroupOption>(DEFAULT_OPTIONS.group);
|
||||
public selectedFilter = signal<FilterOption>(DEFAULT_OPTIONS.filter);
|
||||
public selectedSort = signal<SortOption>(
|
||||
lsGetJSON<SortOption>(LS.TASK_VIEW_CUSTOMIZER_SORT, DEFAULT_OPTIONS.sort) ??
|
||||
DEFAULT_OPTIONS.sort,
|
||||
);
|
||||
public selectedGroup = signal<GroupOption>(
|
||||
lsGetJSON<GroupOption>(LS.TASK_VIEW_CUSTOMIZER_GROUP, DEFAULT_OPTIONS.group) ??
|
||||
DEFAULT_OPTIONS.group,
|
||||
);
|
||||
public selectedFilter = signal<FilterOption>(
|
||||
lsGetJSON<FilterOption>(LS.TASK_VIEW_CUSTOMIZER_FILTER, DEFAULT_OPTIONS.filter) ??
|
||||
DEFAULT_OPTIONS.filter,
|
||||
);
|
||||
|
||||
isCustomized = computed(() => {
|
||||
return [
|
||||
|
|
@ -52,6 +63,18 @@ export class TaskViewCustomizerService {
|
|||
constructor() {
|
||||
this._initProjects();
|
||||
this._initTags();
|
||||
|
||||
effect(() => {
|
||||
lsSetJSON(LS.TASK_VIEW_CUSTOMIZER_SORT, this.selectedSort());
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
lsSetJSON(LS.TASK_VIEW_CUSTOMIZER_GROUP, this.selectedGroup());
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
lsSetJSON(LS.TASK_VIEW_CUSTOMIZER_FILTER, this.selectedFilter());
|
||||
});
|
||||
}
|
||||
|
||||
private _allProjects: Project[] = [];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue