mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 01:19:40 +02:00
The workbench-registry app id 'inventar' did not match its @mana/shared-branding MANA_APPS counterpart 'inventory', so the tier- gating join in apps/web/src/lib/app-registry/registry.ts silently failed for the inventory module — it fell into the "no MANA_APPS entry, default visible" fallback and was effectively un-gated. The codebase had also voted overwhelmingly for 'inventar' (53 files) vs 'inventory' (3 files in shared-branding), so the long-standing mismatch was just bookkeeping debt waiting to bite. Pre-release, no live data, so the cleanest fix is to align everything on the English 'inventory': - Workbench-registry id, module.config.ts appId, module folder, route folder and i18n locale folder all renamed via git mv - Standalone apps/inventar/ workspace package renamed - All imports, store identifiers (InventarEvents → InventoryEvents, INVENTAR_GUEST_SEED, inventarModuleConfig), i18n keys and href/goto paths follow the rename - The German display label "Inventar" is preserved everywhere it is a user-visible string (page titles, i18n values, toast labels) - Dexie table prefixes (invCollections, invItems, …) are unchanged - Drive-by fix: ListView.svelte was querying non-existent inventarCollections/inventarItems tables — corrected to the actual invCollections/invItems names from module.config - The "inventar ↔ inventory id mismatch" workaround comment in registry.ts is removed since the mismatch no longer exists module-registry.ts also picks up the user's parallel newsModuleConfig addition because both edits land in the same import block — keeping them split would have left the build in an inconsistent state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
176 lines
4.3 KiB
TypeScript
176 lines
4.3 KiB
TypeScript
/**
|
|
* 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 '@mana/shared-stores';
|
|
*
|
|
* type MyViewMode = 'list' | 'grid' | 'kanban';
|
|
*
|
|
* interface MyFilters {
|
|
* search?: string;
|
|
* status?: string[];
|
|
* tagIds?: string[];
|
|
* }
|
|
*
|
|
* export const viewStore = createViewStore<MyViewMode, MyFilters>({
|
|
* storagePrefix: 'inventory',
|
|
* 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 object> {
|
|
/** Prefix for localStorage keys (e.g. 'inventory' → 'inventory_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 object> {
|
|
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 object>(
|
|
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);
|
|
},
|
|
};
|
|
}
|