From bdbaab25a41e9bf72f45e22309482a7649ef25f7 Mon Sep 17 00:00:00 2001 From: Max Richter Date: Wed, 21 Jan 2026 15:02:34 +0100 Subject: [PATCH] feat: initial working version of project manager --- .../graph-interface/graph-manager.svelte.ts | 3 +- .../lib/graph-interface/graph/Wrapper.svelte | 8 +- .../lib/project-manager/ProjectManager.svelte | 85 +++++++++++++++++ .../project-database.svelte.ts | 54 +++++++++++ .../project-manager/project-manager.svelte.ts | 93 +++++++++++++++++++ app/src/lib/sidebar/Panel.svelte | 3 - app/src/lib/sidebar/panels/GraphSource.svelte | 4 +- app/src/routes/+page.svelte | 49 +++++----- packages/types/src/types.ts | 2 +- packages/ui/src/lib/inputs/Search.svelte | 0 10 files changed, 267 insertions(+), 34 deletions(-) create mode 100644 app/src/lib/project-manager/ProjectManager.svelte create mode 100644 app/src/lib/project-manager/project-database.svelte.ts create mode 100644 app/src/lib/project-manager/project-manager.svelte.ts create mode 100644 packages/ui/src/lib/inputs/Search.svelte diff --git a/app/src/lib/graph-interface/graph-manager.svelte.ts b/app/src/lib/graph-interface/graph-manager.svelte.ts index 0f29368..2cff200 100644 --- a/app/src/lib/graph-interface/graph-manager.svelte.ts +++ b/app/src/lib/graph-interface/graph-manager.svelte.ts @@ -109,6 +109,7 @@ export class GraphManager extends EventEmitter<{ const serialized = { id: this.graph.id, settings: $state.snapshot(this.settings), + meta: this.graph.meta, nodes, edges }; @@ -304,7 +305,7 @@ export class GraphManager extends EventEmitter<{ this.status = 'loading'; this.id = graph.id; - logger.info('loading graph', $state.snapshot(graph)); + logger.info('loading graph', graph); const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)])); await this.registry.load(nodeIds); diff --git a/app/src/lib/graph-interface/graph/Wrapper.svelte b/app/src/lib/graph-interface/graph/Wrapper.svelte index 783b2cf..39af05e 100644 --- a/app/src/lib/graph-interface/graph/Wrapper.svelte +++ b/app/src/lib/graph-interface/graph/Wrapper.svelte @@ -11,7 +11,7 @@ import { setupKeymaps } from "../keymaps"; type Props = { - graph: Graph; + graph?: Graph; registry: NodeRegistry; settings?: Record; @@ -85,7 +85,11 @@ manager.on("save", (save) => onsave?.(save)); - manager.load(graph); + $effect(() => { + if (graph) { + manager.load(graph); + } + }); diff --git a/app/src/lib/project-manager/ProjectManager.svelte b/app/src/lib/project-manager/ProjectManager.svelte new file mode 100644 index 0000000..df09a0c --- /dev/null +++ b/app/src/lib/project-manager/ProjectManager.svelte @@ -0,0 +1,85 @@ + + +
+

Project

+ +
+ +{#if showNewProject} +
+ e.key === "Enter" && handleCreate()} + /> + +
+{/if} + +
+ {#if projectManager.loading} +

Loading...

+ {/if} + +
    + {#each projectManager.projects as project (project.id)} +
  • +
    projectManager.handleSelectProject(project.id!)} + role="button" + tabindex="0" + onkeydown={(e) => + e.key === "Enter" && + projectManager.handleSelectProject(project.id!)} + > +
    + {project.meta?.title || "Untitled"} + +
    +
    +
  • + {/each} +
+
diff --git a/app/src/lib/project-manager/project-database.svelte.ts b/app/src/lib/project-manager/project-database.svelte.ts new file mode 100644 index 0000000..4e852ef --- /dev/null +++ b/app/src/lib/project-manager/project-database.svelte.ts @@ -0,0 +1,54 @@ +import type { Graph } from '@nodarium/types'; +import { type IDBPDatabase, openDB } from 'idb'; + +export interface GraphDatabase { + projects: Graph; +} + +const DB_NAME = 'nodarium-graphs'; +const DB_VERSION = 1; +const STORE_NAME = 'graphs'; + +let dbPromise: Promise> | null = null; + +export function getDB() { + if (!dbPromise) { + dbPromise = openDB(DB_NAME, DB_VERSION, { + upgrade(db) { + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME, { keyPath: 'id' }); + } + } + }); + } + return dbPromise; +} + +export async function getGraph(id: number): Promise { + const db = await getDB(); + return db.get(STORE_NAME, id); +} + +export async function saveGraph(graph: Graph): Promise { + const db = await getDB(); + graph.meta = { ...graph.meta, lastModified: new Date().toISOString() }; + console.log('SAVING GRAPH', { graph }); + await db.put(STORE_NAME, graph); + return graph; +} + +export async function deleteGraph(id: number): Promise { + const db = await getDB(); + await db.delete(STORE_NAME, id); + console.log('DELETE GRAPH', { id }); +} + +export async function getGraphs(): Promise { + const db = await getDB(); + return db.getAll(STORE_NAME); +} + +export async function clear(): Promise { + const db = await getDB(); + return db.clear(STORE_NAME); +} diff --git a/app/src/lib/project-manager/project-manager.svelte.ts b/app/src/lib/project-manager/project-manager.svelte.ts new file mode 100644 index 0000000..8af41d3 --- /dev/null +++ b/app/src/lib/project-manager/project-manager.svelte.ts @@ -0,0 +1,93 @@ +import * as templates from '$lib/graph-templates'; +import { localState } from '$lib/helpers/localState.svelte'; +import type { Graph } from '@nodarium/types'; +import * as db from './project-database.svelte'; + +export class ProjectManager { + public graph = $state(); + private projects = $state([]); + private activeProjectId = localState( + 'node.activeProjectId', + undefined + ); + public readonly loading = $derived(this.graph?.id !== this.activeProjectId.value); + + constructor() { + this.init(); + } + + async saveGraph(g: Graph) { + db.saveGraph(g); + } + + private async init() { + await db.getDB(); + this.projects = await db.getGraphs(); + + console.log('PM: INIT', { + projects: this.projects, + activeProjectId: this.activeProjectId.value + }); + + if (this.activeProjectId.value !== undefined) { + let loadedGraph = await db.getGraph(this.activeProjectId.value); + console.log('PM: LOAD ACTIVE PROJECT', { loadedGraph }); + if (loadedGraph) { + console.log('Load active project'); + this.graph = loadedGraph; + } + } + + if (!this.graph) { + console.log('Load first active project', { projectsAmount: this.projects.length }); + if (this.projects?.length && this.projects[0]?.id !== undefined) { + this.graph = this.projects[0]; + this.activeProjectId.value = this.graph.id; + } + } + + if (!this.graph) { + console.log('Create default project'); + this.handleCreateProject(); + } + } + + public handleCreateProject( + g: Graph = templates.defaultPlant as unknown as Graph, + title: string = 'New Project' + ) { + let id = g?.id || 0; + while (this.projects.find((p) => p.id === id)) { + id++; + } + + console.log('CREATE PROJECT', { id, title }); + + g.id = id; + if (!g.meta) g.meta = {}; + if (!g.meta.title) g.meta.title = title; + + db.saveGraph(g); + this.projects = [...this.projects, g]; + this.handleSelectProject(id); + } + + public async handleDeleteProject(projectId: number) { + await db.deleteGraph(projectId); + if (this.projects.length === 1) { + this.graph = undefined; + this.projects = []; + } else { + this.projects = this.projects.filter((p) => p.id !== projectId); + this.handleSelectProject(this.projects[0].id); + } + } + + public async handleSelectProject(id: number) { + if (this.activeProjectId.value !== id) { + const project = await db.getGraph(id); + this.graph = project; + this.activeProjectId.value = id; + } + } +} diff --git a/app/src/lib/sidebar/Panel.svelte b/app/src/lib/sidebar/Panel.svelte index 19d90c0..e079e9c 100644 --- a/app/src/lib/sidebar/Panel.svelte +++ b/app/src/lib/sidebar/Panel.svelte @@ -43,7 +43,4 @@ align-items: center; padding-left: 1em; } - h3 { - margin: 0px; - } diff --git a/app/src/lib/sidebar/panels/GraphSource.svelte b/app/src/lib/sidebar/panels/GraphSource.svelte index 37b3552..0866b3e 100644 --- a/app/src/lib/sidebar/panels/GraphSource.svelte +++ b/app/src/lib/sidebar/panels/GraphSource.svelte @@ -1,7 +1,7 @@
-  {convert(graph)}
+  {graph ? convert(graph) : 'No graph loaded'}
 
diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte index 26a98cd..ea996ce 100644 --- a/app/src/routes/+page.svelte +++ b/app/src/routes/+page.svelte @@ -28,6 +28,8 @@ import BenchmarkPanel from "$lib/sidebar/panels/BenchmarkPanel.svelte"; import { debounceAsyncFunction } from "$lib/helpers"; import GraphSource from "$lib/sidebar/panels/GraphSource.svelte"; + import { ProjectManager } from "$lib/project-manager/project-manager.svelte"; + import ProjectManagerEl from "$lib/project-manager/ProjectManager.svelte"; let performanceStore = createPerformanceStore(); @@ -37,6 +39,7 @@ const runtimeCache = new MemoryRuntimeCache(); const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache); memoryRuntime.perf = performanceStore; + const pm = new ProjectManager(); const runtime = $derived( appSettings.value.debug.useWorker ? workerRuntime : memoryRuntime, @@ -64,15 +67,6 @@ let activeNode = $state(undefined); let scene = $state(null!); - let graph = $state( - localStorage.getItem("graph") - ? JSON.parse(localStorage.getItem("graph")!) - : templates.defaultPlant, - ); - function handleSave(graph: Graph) { - localStorage.setItem("graph", JSON.stringify(graph)); - } - let graphInterface = $state>(null!); let viewerComponent = $state>(); const manager = $derived(graphInterface?.manager); @@ -172,7 +166,7 @@
-
+
- handleUpdate(result)} - onsave={(graph) => handleSave(graph)} - /> + {#if pm.graph} + pm.saveGraph(g)} + onresult={(result) => handleUpdate(result)} + /> + {/if} {/if} + + +