Skip to content

Commit 7e419c3

Browse files
myieyeclaude
andauthored
Replace URL params with project-scoped storage for task selection (#2142)
* Make task selection project-specific instead of global Previously, the selected task was stored globally in localStorage, causing the same task to be selected across all projects. This fix makes task selection project-specific by: 1. Creating a new project-storage utility that scopes storage keys by projectCode (e.g., "project:myproject:selectedTaskId") 2. Updating TasksView to use project-specific storage instead of global localStorage This ensures each project maintains its own task selection state. Future enhancement: The storage utility includes a TODO to use MAUI Preferences when running in the MAUI app, instead of localStorage. This will provide better native integration. https://claude.ai/code/session_01GXJiKFBAUAGV6nfG88wDAw * Refactor project storage to use service pattern with reactive properties Refactored the project storage utility to follow the codebase's established patterns: 1. Converted project-storage to a class-based service with reactive properties using Svelte 5's $state 2. Added useProjectStorage() hook pattern for easy access in components 3. Properties like selectedTaskId now automatically sync to localStorage using watch() 4. Initialized ProjectStorage service in all project contexts: - ShadcnProjectView.svelte (main project view) - WebComponent.svelte (web component integration) - in-memory-demo-api.ts (demo mode) 5. Simplified TasksView.svelte to use the new service pattern Benefits: - Cleaner API: `projectStorage.selectedTaskId` instead of function calls - Automatic persistence: Changes to properties automatically sync - Consistent with other services in the codebase - Easier to extend with new project-specific settings https://claude.ai/code/session_01GXJiKFBAUAGV6nfG88wDAw * Refactor to use StorageProp pattern and auto-initialization Improvements based on code review feedback: 1. Created StorageProp class to eliminate string repetition - Properties defined as: readonly selectedTaskId = new StorageProp(...) - Accessed via .current property (familiar pattern) - Each property encapsulates its own watch() and storage sync 2. Removed QueryParamState from TasksView - Task selection is now pure app state, not URL-based - Simplifies logic and prevents unintended clearing - Uses projectStorage.selectedTaskId.current directly 3. Auto-initialization in useProjectStorage() - Removed explicit initProjectStorage() calls - Service lazily initializes on first use - Safer: throws if project context not set up 4. Removed unnecessary clear() method Benefits: - No string repetition (key appears once in StorageProp constructor) - Cleaner component code (no QueryParamState complexity) - One less thing to remember to initialize - Familiar .current pattern from other reactive utilities https://claude.ai/code/session_01GXJiKFBAUAGV6nfG88wDAw * Simplify StorageProp to be self-contained and remove watch Improvements based on code review: 1. StorageProp is now self-contained - Not exported (internal implementation detail) - Doesn't depend on ProjectStorage instance - Receives projectCode directly, builds its own keys - Handles all persistence logic internally 2. Removed watch() in favor of direct persistence - Setter now calls persist() directly - Cleaner, more predictable behavior - Less overhead 3. Consolidated set/remove logic - persist() handles both set and remove in one place - Empty strings automatically trigger removal 4. Removed unused ProjectStorage methods - Removed get(), set(), remove(), getStorageKey() - ProjectStorage is now minimal (just holds properties) - Can add them back later if needed for testing 5. Simplified TasksView - Extract just selectedTaskId property - Cleaner: const selectedTaskId = useProjectStorage().selectedTaskId Benefits: - Simpler architecture (StorageProp doesn't need ProjectStorage) - More direct (no watch indirection) - Easier to test (StorageProp is self-contained) - Minimal API surface (ProjectStorage only exposes properties) https://claude.ai/code/session_01GXJiKFBAUAGV6nfG88wDAw * Move public API after constructor for better readability Reordered StorageProp class members to: - Fields - Constructor - Public API (current getter/setter) - Private implementation methods This makes the public interface immediately visible after understanding the basic structure, improving code readability. https://claude.ai/code/session_01GXJiKFBAUAGV6nfG88wDAw * Remove unused #projectCode field from ProjectStorage The field was stored but never used - projectCode is only needed as a constructor parameter to pass to StorageProp instances. Fixes linter error: no-unused-private-class-members https://claude.ai/code/session_01GXJiKFBAUAGV6nfG88wDAw --------- Co-authored-by: Claude <[email protected]>
1 parent 17375ce commit 7e419c3

File tree

2 files changed

+78
-9
lines changed

2 files changed

+78
-9
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { getContext, setContext } from 'svelte';
2+
import { useProjectContext } from '$project/project-context.svelte';
3+
4+
/**
5+
* Project-specific storage service
6+
*
7+
* This service provides project-scoped storage for user preferences.
8+
* Currently uses localStorage with project-prefixed keys.
9+
*
10+
* TODO: Enhance to use MAUI Preferences when running in MAUI app
11+
* (detect platform via useFwLiteConfig().os and use a preferences service)
12+
*/
13+
14+
const projectStorageContextKey = 'project-storage';
15+
16+
/**
17+
* Reactive storage property that automatically syncs to localStorage
18+
*/
19+
class StorageProp {
20+
#projectCode: string;
21+
#key: string;
22+
#value = $state<string>('');
23+
24+
constructor(projectCode: string, key: string) {
25+
this.#projectCode = projectCode;
26+
this.#key = key;
27+
// Load initial value
28+
this.#value = this.load();
29+
}
30+
31+
get current(): string {
32+
return this.#value;
33+
}
34+
35+
set current(value: string) {
36+
this.#value = value;
37+
this.persist(value);
38+
}
39+
40+
private getStorageKey(): string {
41+
return `project:${this.#projectCode}:${this.#key}`;
42+
}
43+
44+
private load(): string {
45+
return localStorage.getItem(this.getStorageKey()) ?? '';
46+
}
47+
48+
private persist(value: string): void {
49+
const storageKey = this.getStorageKey();
50+
if (value) {
51+
localStorage.setItem(storageKey, value);
52+
} else {
53+
localStorage.removeItem(storageKey);
54+
}
55+
}
56+
}
57+
58+
export class ProjectStorage {
59+
readonly selectedTaskId: StorageProp;
60+
61+
constructor(projectCode: string) {
62+
this.selectedTaskId = new StorageProp(projectCode, 'selectedTaskId');
63+
}
64+
}
65+
66+
export function useProjectStorage(): ProjectStorage {
67+
let storage = getContext<ProjectStorage>(projectStorageContextKey);
68+
if (!storage) {
69+
const projectContext = useProjectContext();
70+
storage = new ProjectStorage(projectContext.projectCode);
71+
setContext(projectStorageContextKey, storage);
72+
}
73+
return storage;
74+
}

frontend/viewer/src/project/tasks/TasksView.svelte

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,16 @@
22
import * as Select from '$lib/components/ui/select';
33
import {useTasksService} from './tasks-service';
44
import {t} from 'svelte-i18n-lingui';
5-
import {QueryParamState} from '$lib/utils/url.svelte';
5+
import {useProjectStorage} from '$lib/utils/project-storage.svelte';
66
import TaskView from './TaskView.svelte';
7-
import {watch} from 'runed';
87
import {onMount} from 'svelte';
98
import {SidebarTrigger} from '$lib/components/ui/sidebar';
10-
const selectedTaskStorageKey = 'selectedTaskId';
9+
10+
const selectedTaskId = useProjectStorage().selectedTaskId;
1111
const tasksService = useTasksService();
1212
const tasks = $derived(tasksService.listTasks());
13-
const selectedTaskId = new QueryParamState({key: 'taskId', allowBack: true, replaceOnDefaultValue: true}, localStorage.getItem(selectedTaskStorageKey) ?? '');
14-
watch(() => selectedTaskId.current, (selectedTaskId) => {
15-
if (selectedTaskId) {
16-
localStorage.setItem(selectedTaskStorageKey, selectedTaskId);
17-
}
18-
});
1913
const selectedTask = $derived(tasks.find(task => task.id === selectedTaskId.current));
14+
2015
onMount(() => {
2116
if (!selectedTaskId.current) {
2217
open = true;

0 commit comments

Comments
 (0)