From 391eb7f411a1bea59831580e5caeecdd52d7201c Mon Sep 17 00:00:00 2001 From: Giancarmine Salucci Date: Mon, 23 Mar 2026 09:17:44 +0100 Subject: [PATCH] fix(ui): resolve state_referenced_locally warnings and add folder picker - Fix Svelte state_referenced_locally warning in +page.svelte and repos/[id]/+page.svelte by initializing $state with empty defaults and syncing via $effect - Add FolderPicker component with server-side filesystem browser (single-click to navigate, double-click or "Select This Folder" to confirm) - Git repos highlighted with orange folder icon and "git" badge - Add GET /api/v1/fs/browse endpoint listing subdirectories - Wire FolderPicker into AddRepositoryModal for local source type - Auto-fills title from the selected folder name Co-Authored-By: Claude Sonnet 4.6 --- src/lib/components/AddRepositoryModal.svelte | 28 ++- src/lib/components/FolderPicker.svelte | 210 +++++++++++++++++++ src/routes/+page.svelte | 8 +- src/routes/api/v1/fs/browse/+server.ts | 36 ++++ src/routes/repos/[id]/+page.svelte | 10 +- 5 files changed, 275 insertions(+), 17 deletions(-) create mode 100644 src/lib/components/FolderPicker.svelte create mode 100644 src/routes/api/v1/fs/browse/+server.ts diff --git a/src/lib/components/AddRepositoryModal.svelte b/src/lib/components/AddRepositoryModal.svelte index 51a18ac..d620cd1 100644 --- a/src/lib/components/AddRepositoryModal.svelte +++ b/src/lib/components/AddRepositoryModal.svelte @@ -1,4 +1,6 @@ + + + +
+
+ + +
+
+ +{#if open} + + +{/if} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index e3151b0..d17cfce 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -8,9 +8,11 @@ let { data }: { data: PageData } = $props(); - // Local mutable copy; refreshRepositories() keeps it up to date after mutations. - // Intentionally captures initial value from server load — mutations happen via fetch. - let repositories = $state(data.repositories ?? []); // svelte-disable state_referenced_locally + // Initialized empty; $effect syncs from data prop on every navigation/reload. + let repositories = $state([]); + $effect(() => { + repositories = data.repositories ?? []; + }); let showAddModal = $state(false); let confirmDeleteId = $state(null); let activeJobIds = $state>({}); diff --git a/src/routes/api/v1/fs/browse/+server.ts b/src/routes/api/v1/fs/browse/+server.ts new file mode 100644 index 0000000..2da2ee5 --- /dev/null +++ b/src/routes/api/v1/fs/browse/+server.ts @@ -0,0 +1,36 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; + +export const GET: RequestHandler = ({ url }) => { + const rawPath = url.searchParams.get('path') ?? os.homedir(); + const target = path.resolve(rawPath); + + let entries: { name: string; path: string; isGitRepo: boolean }[] = []; + let error: string | null = null; + let resolved = target; + + try { + const items = fs.readdirSync(target, { withFileTypes: true }); + entries = items + .filter((d) => d.isDirectory() && !d.name.startsWith('.')) + .map((d) => { + const full = path.join(target, d.name); + const isGitRepo = fs.existsSync(path.join(full, '.git')); + return { name: d.name, path: full, isGitRepo }; + }) + .sort((a, b) => { + // git repos first, then alphabetical + if (a.isGitRepo !== b.isGitRepo) return a.isGitRepo ? -1 : 1; + return a.name.localeCompare(b.name); + }); + } catch (e) { + error = (e as NodeJS.ErrnoException).code === 'EACCES' ? 'Permission denied' : 'Path not found'; + } + + const parent = target !== path.parse(target).root ? path.dirname(target) : null; + + return json({ path: resolved, parent, entries, error }); +}; diff --git a/src/routes/repos/[id]/+page.svelte b/src/routes/repos/[id]/+page.svelte index 86e6891..6da717a 100644 --- a/src/routes/repos/[id]/+page.svelte +++ b/src/routes/repos/[id]/+page.svelte @@ -7,9 +7,13 @@ let { data }: { data: PageData } = $props(); - // Local mutable copies updated via fetch — intentionally capturing initial server values. - let repo = $state(data.repo); // svelte-disable state_referenced_locally - let recentJobs = $state(data.recentJobs ?? []); // svelte-disable state_referenced_locally + // Initialized empty; $effect syncs from data prop on every navigation/reload. + let repo = $state({} as Repository & { versions?: RepositoryVersion[] }); + let recentJobs = $state([]); + $effect(() => { + if (data.repo) repo = data.repo; + recentJobs = data.recentJobs ?? []; + }); let showDeleteConfirm = $state(false); let activeJobId = $state(null); let errorMessage = $state(null);