From 4417fc9828752f39f9132531b812e38222c05707 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 17 Mar 2026 11:43:26 +0100 Subject: [PATCH] refactor: migrate all Svelte components from Svelte 4 to Svelte 5 runes - Replace `export let` with `$props()` and `$bindable()` across all components - Replace `$:` reactive statements with `$derived()` and `$effect()` - Replace `createEventDispatcher` with callback props (e.g. `onchange`) - Replace `on:event` directives with inline event handlers (`onclick`, `oninput`, etc.) - Replace `` with `{@render children()}` in layout - Use `untrack()` for SSR-safe $state initialization from reactive props - Replace `blur` + `setTimeout` anti-pattern in TagInput with `clickOutside` action - Fix `page` store usage in layout to use `$app/state` directly - 0 errors, 0 warnings after svelte-check Co-Authored-By: Claude Sonnet 4.6 --- .../lib/components/PersonMultiSelect.svelte | 33 +-- .../src/lib/components/PersonTypeahead.svelte | 71 +++---- frontend/src/lib/components/TagInput.svelte | 173 +++++++-------- frontend/src/routes/+layout.svelte | 198 +++++++++--------- frontend/src/routes/+page.svelte | 66 +++--- frontend/src/routes/admin/+page.svelte | 61 ++---- .../src/routes/conversations/+page.svelte | 81 +++---- .../src/routes/documents/[id]/+page.svelte | 47 ++--- .../routes/documents/[id]/edit/+page.svelte | 24 +-- .../src/routes/documents/new/+page.svelte | 20 +- frontend/src/routes/login/+page.svelte | 9 +- frontend/src/routes/persons/+page.svelte | 8 +- frontend/src/routes/persons/[id]/+page.svelte | 36 ++-- frontend/src/routes/persons/new/+page.svelte | 2 +- 14 files changed, 388 insertions(+), 441 deletions(-) diff --git a/frontend/src/lib/components/PersonMultiSelect.svelte b/frontend/src/lib/components/PersonMultiSelect.svelte index cffbf38f..c98ba5ab 100644 --- a/frontend/src/lib/components/PersonMultiSelect.svelte +++ b/frontend/src/lib/components/PersonMultiSelect.svelte @@ -1,15 +1,20 @@ - + {#each selectedPersons as person} @@ -69,7 +74,7 @@ {person.firstName} {person.lastName} - - {/each} +
+ +
+ + {#each tags as tag, i} + + {tag} + + + {/each} - -
- handleInput()} - on:blur={() => setTimeout(() => (showSuggestions = false), 200)} - placeholder={tags.length === 0 - ? allowCreation - ? 'Schlagworte hinzufügen...' - : 'Nach Schlagworten filtern...' - : ''} - class="w-full h-full border-none p-1 focus:ring-0 text-sm bg-transparent outline-none" - /> + +
+ fetchSuggestions(inputVal)} + onkeydown={handleKeydown} + onfocus={() => fetchSuggestions(inputVal)} + placeholder={tags.length === 0 + ? allowCreation + ? 'Schlagworte hinzufügen...' + : 'Nach Schlagworten filtern...' + : ''} + class="w-full h-full border-none p-1 focus:ring-0 text-sm bg-transparent outline-none" + /> - - {#if showSuggestions && suggestions.length > 0} -
    - {#each suggestions as suggestion, i} -
  • addTag(suggestion)} - on:keydown={(e) => e.key === 'Enter' && addTag(suggestion)} - > - {suggestion} -
  • - {/each} -
- {/if} -
-
- {#if allowCreation} -

Enter drücken um Schlagwort zu erstellen.

- {/if} + + {#if showSuggestions && suggestions.length > 0} +
    + {#each suggestions as suggestion, i} +
  • addTag(suggestion)} + onkeydown={(e) => e.key === 'Enter' && addTag(suggestion)} + > + {suggestion} +
  • + {/each} +
+ {/if} +
+
+ {#if allowCreation} +

Enter drücken um Schlagwort zu erstellen.

+ {/if} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 50dc8338..d019a23d 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -2,116 +2,112 @@ import './layout.css'; import { enhance } from '$app/forms'; import { page } from '$app/state'; - $: user = page.data.user; - $: isAdmin = user?.groups.some(g => g.permissions.includes("ADMIN")) + + let { children } = $props(); + + const isAdmin = $derived(page.data.user?.groups.some((g: { permissions: string[] }) => g.permissions.includes('ADMIN')));
- - - {#if !page.url.pathname.startsWith('/login')} -
-
-
- + {#if !page.url.pathname.startsWith('/login')} +
+
+
- - - -
-
- -
-
-
-
-
- {/if} + +
+
+ +
+
+
+
+
+ {/if} -
- -
+
+ {@render children()} +
diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index ceae8171..f1ea2f67 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -3,21 +3,19 @@ import PersonTypeahead from '$lib/components/PersonTypeahead.svelte'; import { goto } from '$app/navigation'; import TagInput from '$lib/components/TagInput.svelte'; import { slide } from 'svelte/transition'; +import { untrack } from 'svelte'; -export let data; +let { data } = $props(); -// Local state variables -let q = data.filters?.q || ''; -let from = data.filters?.from || ''; -let to = data.filters?.to || ''; -let senderId = data.filters?.senderId || ''; -let receiverId = data.filters?.receiverId || ''; -let tagNames = data.filters?.tags || []; +let q = $state(untrack(() => data.filters?.q || '')); +let from = $state(untrack(() => data.filters?.from || '')); +let to = $state(untrack(() => data.filters?.to || '')); +let senderId = $state(untrack(() => data.filters?.senderId || '')); +let receiverId = $state(untrack(() => data.filters?.receiverId || '')); +let tagNames = $state(untrack(() => data.filters?.tags || [])); -// Debounce Timer -let searchTimer: any; - -let showAdvanced = false; +let searchTimer: ReturnType; +let showAdvanced = $state(false); function triggerSearch() { const params = new URLSearchParams(); @@ -42,27 +40,24 @@ function handleTextSearch() { }, 500); } -let previousTags = tagNames.join(','); -$: { - const currentTags = tagNames.join(','); - if (currentTags !== previousTags) { - previousTags = currentTags; +// Trigger search when tags change +let prevTagStr = untrack(() => tagNames.join(',')); +$effect(() => { + const cur = tagNames.join(','); + if (cur !== prevTagStr) { + prevTagStr = cur; triggerSearch(); } -} +}); -function toggleAdvanced() { - showAdvanced = !showAdvanced; -} - -// Sync with server data (e.g. after reset) -$: { +// Sync local state with server data after navigation +$effect(() => { q = data.filters?.q || ''; from = data.filters?.from || ''; to = data.filters?.to || ''; senderId = data.filters?.senderId || ''; receiverId = data.filters?.receiverId || ''; -} +}); @@ -76,7 +71,7 @@ $: { @@ -94,7 +89,7 @@ $: { @@ -384,7 +378,7 @@ $: { Versuchen Sie, die Filter anzupassen oder den Suchbegriff zu ändern.

(activeTab = 'users')}>Benutzer (activeTab = 'groups')}>Gruppen (activeTab = 'tags')}>Schlagworte @@ -106,7 +105,6 @@ {user.username} - - {:else if activeTab === 'tags'} -

Schlagworte

@@ -332,9 +324,9 @@ >
@@ -106,7 +87,7 @@ id="dateFrom" type="date" bind:value={fromDate} - on:change={() => applyFilters()} + onchange={() => applyFilters()} class="block w-full border-gray-300 shadow-sm focus:border-brand-navy focus:ring-brand-navy text-sm py-2.5" /> @@ -122,7 +103,7 @@ id="dateTo" type="date" bind:value={toDate} - on:change={() => applyFilters()} + onchange={() => applyFilters()} class="block w-full border-gray-300 shadow-sm focus:border-brand-navy focus:ring-brand-navy text-sm py-2.5" /> @@ -130,7 +111,7 @@
- {:else if documents.length === 0} + {:else if data.documents.length === 0}
@@ -178,18 +159,15 @@
{:else} -
-
-
- {#each documents as doc} + {#each data.documents as doc} {@const isRight = doc.sender?.id === senderId} @@ -200,7 +178,7 @@ ? 'flex-row-reverse' : 'flex-row'}" > - +