mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:41:09 +02:00
feat(shared-stores): add createViewStore factory for view/filter/sort
Generic factory that eliminates ~110 LOC boilerplate per module for view mode, sort, filters, and saved filter presets with localStorage persistence. Includes 23 tests covering all store operations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
de8335277a
commit
934f3337e3
7 changed files with 548 additions and 3 deletions
|
|
@ -9,11 +9,16 @@
|
||||||
".": "./src/index.ts"
|
".": "./src/index.ts"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"type-check": "echo 'Skipping: shared-stores uses Svelte 5 runes, type-checked at build time'"
|
"type-check": "echo 'Skipping: shared-stores uses Svelte 5 runes, type-checked at build time'",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||||
|
"jsdom": "^29.0.1",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0",
|
||||||
|
"vitest": "^4.1.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@manacore/local-store": "workspace:*",
|
"@manacore/local-store": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,13 @@ export {
|
||||||
type AppSettingsStore,
|
type AppSettingsStore,
|
||||||
type AppSettingsStoreOptions,
|
type AppSettingsStoreOptions,
|
||||||
} from './settings.svelte';
|
} from './settings.svelte';
|
||||||
|
export {
|
||||||
|
createViewStore,
|
||||||
|
type ViewStore,
|
||||||
|
type ViewStoreConfig,
|
||||||
|
type SortOption,
|
||||||
|
type SavedFilter,
|
||||||
|
} from './view.svelte';
|
||||||
export {
|
export {
|
||||||
createSimpleNavigationStores,
|
createSimpleNavigationStores,
|
||||||
type SimpleNavigationStores,
|
type SimpleNavigationStores,
|
||||||
|
|
|
||||||
5
packages/shared-stores/src/test/mock-environment.ts
Normal file
5
packages/shared-stores/src/test/mock-environment.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
/** Mock for $app/environment — always acts as browser in tests */
|
||||||
|
export const browser = true;
|
||||||
|
export const building = false;
|
||||||
|
export const dev = true;
|
||||||
|
export const version = 'test';
|
||||||
335
packages/shared-stores/src/view.svelte.test.ts
Normal file
335
packages/shared-stores/src/view.svelte.test.ts
Normal file
|
|
@ -0,0 +1,335 @@
|
||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
|
||||||
|
// Must dynamically import to get fresh $state per test
|
||||||
|
async function freshStore() {
|
||||||
|
vi.resetModules();
|
||||||
|
const mod = await import('./view.svelte');
|
||||||
|
return mod.createViewStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestViewMode = 'list' | 'grid' | 'table';
|
||||||
|
|
||||||
|
interface TestFilters {
|
||||||
|
search?: string;
|
||||||
|
tagIds?: string[];
|
||||||
|
status?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createViewStore', () => {
|
||||||
|
describe('defaults', () => {
|
||||||
|
it('initializes with default view mode', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
expect(store.viewMode).toBe('list');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializes with default sort', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
expect(store.sort).toEqual({ field: 'name', direction: 'asc' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('starts with empty filters', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
expect(store.activeFilters).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('starts with empty saved filters', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
expect(store.savedFilters).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('viewMode', () => {
|
||||||
|
it('updates view mode', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
store.setViewMode('grid');
|
||||||
|
expect(store.viewMode).toBe('grid');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('persists view mode to localStorage', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
store.setViewMode('table');
|
||||||
|
expect(JSON.parse(localStorage.getItem('test_view_mode')!)).toBe('table');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sort', () => {
|
||||||
|
it('updates sort', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
store.setSort({ field: 'date', direction: 'desc' });
|
||||||
|
expect(store.sort).toEqual({ field: 'date', direction: 'desc' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('persists sort to localStorage', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
store.setSort({ field: 'date', direction: 'desc' });
|
||||||
|
expect(JSON.parse(localStorage.getItem('test_sort')!)).toEqual({
|
||||||
|
field: 'date',
|
||||||
|
direction: 'desc',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filters', () => {
|
||||||
|
it('sets filters', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
store.setFilters({ search: 'hello', tagIds: ['t1'] });
|
||||||
|
expect(store.activeFilters).toEqual({ search: 'hello', tagIds: ['t1'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates a single filter key', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
store.setFilters({ search: 'hello' });
|
||||||
|
store.updateFilter('tagIds', ['t1', 't2']);
|
||||||
|
expect(store.activeFilters).toEqual({ search: 'hello', tagIds: ['t1', 't2'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears all filters', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
store.setFilters({ search: 'hello', tagIds: ['t1'] });
|
||||||
|
store.clearFilters();
|
||||||
|
expect(store.activeFilters).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasActiveFilters', () => {
|
||||||
|
it('returns false when no filters are set', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
expect(store.hasActiveFilters).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses default heuristic when no custom fn provided', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
store.setFilters({ search: 'hello' });
|
||||||
|
expect(store.hasActiveFilters).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores empty arrays in default heuristic', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
store.setFilters({ tagIds: [] });
|
||||||
|
expect(store.hasActiveFilters).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses custom hasActiveFilters function', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
hasActiveFilters: (f) => !!f.search,
|
||||||
|
});
|
||||||
|
store.setFilters({ tagIds: ['t1'] });
|
||||||
|
expect(store.hasActiveFilters).toBe(false);
|
||||||
|
store.setFilters({ search: 'x' });
|
||||||
|
expect(store.hasActiveFilters).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saved filters', () => {
|
||||||
|
it('saves a named filter preset', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
store.setFilters({ search: 'hello' });
|
||||||
|
store.saveFilter('My Filter');
|
||||||
|
expect(store.savedFilters).toHaveLength(1);
|
||||||
|
expect(store.savedFilters[0].name).toBe('My Filter');
|
||||||
|
expect(store.savedFilters[0].criteria).toEqual({ search: 'hello' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('persists saved filters to localStorage', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
store.setFilters({ search: 'hello' });
|
||||||
|
store.saveFilter('My Filter');
|
||||||
|
const stored = JSON.parse(localStorage.getItem('test_saved_filters')!);
|
||||||
|
expect(stored).toHaveLength(1);
|
||||||
|
expect(stored[0].name).toBe('My Filter');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads a saved filter', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
store.setFilters({ search: 'hello' });
|
||||||
|
store.saveFilter('My Filter');
|
||||||
|
store.clearFilters();
|
||||||
|
expect(store.activeFilters).toEqual({});
|
||||||
|
store.loadFilter(store.savedFilters[0].id);
|
||||||
|
expect(store.activeFilters).toEqual({ search: 'hello' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes a saved filter', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
store.setFilters({ search: 'hello' });
|
||||||
|
store.saveFilter('Filter 1');
|
||||||
|
store.saveFilter('Filter 2');
|
||||||
|
expect(store.savedFilters).toHaveLength(2);
|
||||||
|
store.deleteSavedFilter(store.savedFilters[0].id);
|
||||||
|
expect(store.savedFilters).toHaveLength(1);
|
||||||
|
expect(store.savedFilters[0].name).toBe('Filter 2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initialize', () => {
|
||||||
|
it('loads persisted view mode from localStorage', async () => {
|
||||||
|
localStorage.setItem('test_view_mode', JSON.stringify('grid'));
|
||||||
|
localStorage.setItem('test_sort', JSON.stringify({ field: 'date', direction: 'desc' }));
|
||||||
|
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
store.initialize();
|
||||||
|
expect(store.viewMode).toBe('grid');
|
||||||
|
expect(store.sort).toEqual({ field: 'date', direction: 'desc' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only initializes once', async () => {
|
||||||
|
localStorage.setItem('test_view_mode', JSON.stringify('grid'));
|
||||||
|
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
store.initialize();
|
||||||
|
expect(store.viewMode).toBe('grid');
|
||||||
|
|
||||||
|
// Change localStorage directly — second initialize should be no-op
|
||||||
|
localStorage.setItem('test_view_mode', JSON.stringify('table'));
|
||||||
|
store.initialize();
|
||||||
|
expect(store.viewMode).toBe('grid');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads persisted saved filters', async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'test_saved_filters',
|
||||||
|
JSON.stringify([
|
||||||
|
{ id: 'f1', name: 'Preset', criteria: { search: 'test' }, createdAt: '2024-01-01' },
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'test',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
store.initialize();
|
||||||
|
expect(store.savedFilters).toHaveLength(1);
|
||||||
|
expect(store.savedFilters[0].name).toBe('Preset');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('storage prefix isolation', () => {
|
||||||
|
it('uses different localStorage keys per prefix', async () => {
|
||||||
|
const createViewStore = await freshStore();
|
||||||
|
const store1 = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'app1',
|
||||||
|
defaultViewMode: 'list',
|
||||||
|
defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
});
|
||||||
|
const _store2 = createViewStore<TestViewMode, TestFilters>({
|
||||||
|
storagePrefix: 'app2',
|
||||||
|
defaultViewMode: 'grid',
|
||||||
|
defaultSort: { field: 'date', direction: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
store1.setViewMode('table');
|
||||||
|
expect(JSON.parse(localStorage.getItem('app1_view_mode')!)).toBe('table');
|
||||||
|
expect(localStorage.getItem('app2_view_mode')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
176
packages/shared-stores/src/view.svelte.ts
Normal file
176
packages/shared-stores/src/view.svelte.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
/**
|
||||||
|
* View Store Factory
|
||||||
|
*
|
||||||
|
* Creates a type-safe view/filter/sort store with localStorage persistence.
|
||||||
|
* Replaces ~110 LOC boilerplate per module.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { createViewStore } from '@manacore/shared-stores';
|
||||||
|
*
|
||||||
|
* type MyViewMode = 'list' | 'grid' | 'kanban';
|
||||||
|
*
|
||||||
|
* interface MyFilters {
|
||||||
|
* search?: string;
|
||||||
|
* status?: string[];
|
||||||
|
* tagIds?: string[];
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* export const viewStore = createViewStore<MyViewMode, MyFilters>({
|
||||||
|
* storagePrefix: 'inventar',
|
||||||
|
* defaultViewMode: 'list',
|
||||||
|
* defaultSort: { field: 'name', direction: 'asc' },
|
||||||
|
* hasActiveFilters: (f) => !!(f.search || f.status?.length || f.tagIds?.length),
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
export interface SortOption {
|
||||||
|
field: string;
|
||||||
|
direction: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SavedFilter<F> {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
criteria: F;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ViewStoreConfig<V extends string, F extends Record<string, unknown>> {
|
||||||
|
/** Prefix for localStorage keys (e.g. 'inventar' → 'inventar_view_mode') */
|
||||||
|
storagePrefix: string;
|
||||||
|
/** Default view mode */
|
||||||
|
defaultViewMode: V;
|
||||||
|
/** Default sort option */
|
||||||
|
defaultSort: SortOption;
|
||||||
|
/** Returns true if any filters are active (used for UI indicators) */
|
||||||
|
hasActiveFilters?: (filters: F) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ViewStore<V extends string, F extends Record<string, unknown>> {
|
||||||
|
readonly viewMode: V;
|
||||||
|
readonly sort: SortOption;
|
||||||
|
readonly activeFilters: F;
|
||||||
|
readonly savedFilters: SavedFilter<F>[];
|
||||||
|
readonly hasActiveFilters: boolean;
|
||||||
|
|
||||||
|
initialize(): void;
|
||||||
|
setViewMode(mode: V): void;
|
||||||
|
setSort(newSort: SortOption): void;
|
||||||
|
setFilters(filters: F): void;
|
||||||
|
updateFilter<K extends keyof F>(key: K, value: F[K]): void;
|
||||||
|
clearFilters(): void;
|
||||||
|
saveFilter(name: string): void;
|
||||||
|
loadFilter(id: string): void;
|
||||||
|
deleteSavedFilter(id: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function load<T>(key: string, fallback: T): T {
|
||||||
|
if (!browser) return fallback;
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem(key);
|
||||||
|
return data ? JSON.parse(data) : fallback;
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(key: string, value: unknown) {
|
||||||
|
if (!browser) return;
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, JSON.stringify(value));
|
||||||
|
} catch {
|
||||||
|
// Silently fail (quota exceeded, private mode, etc.)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createViewStore<V extends string, F extends Record<string, unknown>>(
|
||||||
|
config: ViewStoreConfig<V, F>
|
||||||
|
): ViewStore<V, F> {
|
||||||
|
const VIEW_KEY = `${config.storagePrefix}_view_mode`;
|
||||||
|
const SORT_KEY = `${config.storagePrefix}_sort`;
|
||||||
|
const FILTERS_KEY = `${config.storagePrefix}_saved_filters`;
|
||||||
|
|
||||||
|
let viewMode = $state<V>(config.defaultViewMode);
|
||||||
|
let sort = $state<SortOption>(config.defaultSort);
|
||||||
|
let activeFilters = $state<F>({} as F);
|
||||||
|
let savedFilters = $state<SavedFilter<F>[]>([]);
|
||||||
|
let initialized = $state(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
get viewMode() {
|
||||||
|
return viewMode;
|
||||||
|
},
|
||||||
|
get sort() {
|
||||||
|
return sort;
|
||||||
|
},
|
||||||
|
get activeFilters() {
|
||||||
|
return activeFilters;
|
||||||
|
},
|
||||||
|
get savedFilters() {
|
||||||
|
return savedFilters;
|
||||||
|
},
|
||||||
|
get hasActiveFilters() {
|
||||||
|
if (config.hasActiveFilters) return config.hasActiveFilters(activeFilters);
|
||||||
|
return Object.values(activeFilters).some(
|
||||||
|
(v) => v !== undefined && v !== null && v !== '' && (!Array.isArray(v) || v.length > 0)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
if (initialized) return;
|
||||||
|
viewMode = load<V>(VIEW_KEY, config.defaultViewMode);
|
||||||
|
sort = load<SortOption>(SORT_KEY, config.defaultSort);
|
||||||
|
savedFilters = load<SavedFilter<F>[]>(FILTERS_KEY, []);
|
||||||
|
initialized = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
setViewMode(mode: V) {
|
||||||
|
viewMode = mode;
|
||||||
|
save(VIEW_KEY, mode);
|
||||||
|
},
|
||||||
|
|
||||||
|
setSort(newSort: SortOption) {
|
||||||
|
sort = newSort;
|
||||||
|
save(SORT_KEY, newSort);
|
||||||
|
},
|
||||||
|
|
||||||
|
setFilters(filters: F) {
|
||||||
|
activeFilters = filters;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateFilter<K extends keyof F>(key: K, value: F[K]) {
|
||||||
|
activeFilters = { ...activeFilters, [key]: value };
|
||||||
|
},
|
||||||
|
|
||||||
|
clearFilters() {
|
||||||
|
activeFilters = {} as F;
|
||||||
|
},
|
||||||
|
|
||||||
|
saveFilter(name: string) {
|
||||||
|
const filter: SavedFilter<F> = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
name,
|
||||||
|
criteria: { ...activeFilters },
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
savedFilters = [...savedFilters, filter];
|
||||||
|
save(FILTERS_KEY, savedFilters);
|
||||||
|
},
|
||||||
|
|
||||||
|
loadFilter(id: string) {
|
||||||
|
const filter = savedFilters.find((f) => f.id === id);
|
||||||
|
if (filter) {
|
||||||
|
activeFilters = { ...filter.criteria };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteSavedFilter(id: string) {
|
||||||
|
savedFilters = savedFilters.filter((f) => f.id !== id);
|
||||||
|
save(FILTERS_KEY, savedFilters);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,6 @@
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src"
|
"rootDir": "./src"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*", "vitest.config.ts"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
packages/shared-stores/vitest.config.ts
Normal file
17
packages/shared-stores/vitest.config.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [svelte()],
|
||||||
|
test: {
|
||||||
|
include: ['src/**/*.{test,spec}.{js,ts}'],
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
conditions: ['browser'],
|
||||||
|
alias: {
|
||||||
|
'$app/environment': new URL('./src/test/mock-environment.ts', import.meta.url).pathname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue