diff --git a/frontend/src/lib/components/AssigneeCombobox.svelte b/frontend/src/lib/components/AssigneeCombobox.svelte index c3f1987..ab2498e 100644 --- a/frontend/src/lib/components/AssigneeCombobox.svelte +++ b/frontend/src/lib/components/AssigneeCombobox.svelte @@ -1,6 +1,7 @@ diff --git a/frontend/src/lib/pages/dashboard-tasks.svelte b/frontend/src/lib/pages/dashboard-tasks.svelte index 742dc73..f0e57c8 100644 --- a/frontend/src/lib/pages/dashboard-tasks.svelte +++ b/frontend/src/lib/pages/dashboard-tasks.svelte @@ -11,6 +11,7 @@ import { Button } from "$lib/components/ui/button"; import type { Project, Task } from "$lib/types/api"; import { ACTIVE_STATUSES } from "$lib/types/api"; + import { createLatestRequestGuard, runLatest } from "$lib/latest-request"; let { params = {} }: { params?: Record } = $props(); @@ -28,6 +29,8 @@ let offset = $state(0); const limit = 50; + const requestGuard = createLatestRequestGuard(); + function toggleStatus(s: string) { if (filterStatuses.includes(s)) { filterStatuses = filterStatuses.filter((x) => x !== s); @@ -71,21 +74,27 @@ filters.priorities = filterPriorities.join(","); if (searchQuery) filters.search = searchQuery; - Promise.all([ - listProjects(100, 0).then((r) => { - projects = r.items; - }), - listTasks(filters).then((r) => { - tasks = r.items; - total = r.total; - }), - ]) - .catch((e) => { - error = e.message || "Failed to load tasks"; - }) - .finally(() => { - loading = false; - }); + runLatest( + requestGuard, + () => + Promise.all([ + listProjects(100, 0).then((r) => r.items), + listTasks(filters).then((r) => ({ items: r.items, total: r.total })), + ]), + { + onSuccess: ([projectItems, taskRes]) => { + projects = projectItems; + tasks = taskRes.items; + total = taskRes.total; + }, + onError: (e) => { + error = (e as Error).message || "Failed to load tasks"; + }, + onFinally: () => { + loading = false; + }, + }, + ); } function resetFilters() { diff --git a/frontend/src/lib/pages/project-tasks.svelte b/frontend/src/lib/pages/project-tasks.svelte index 7bef64d..4b666c1 100644 --- a/frontend/src/lib/pages/project-tasks.svelte +++ b/frontend/src/lib/pages/project-tasks.svelte @@ -12,6 +12,7 @@ import { Button } from "$lib/components/ui/button"; import type { Project, Task } from "$lib/types/api"; import { ACTIVE_STATUSES } from "$lib/types/api"; + import { createLatestRequestGuard, runLatest } from "$lib/latest-request"; // svelte-ignore a11y_click_events_have_key_events @@ -26,10 +27,12 @@ let sort = $state("-created_at"); let offset = $state(0); const limit = 50; + const requestGuard = createLatestRequestGuard(); let filterStatuses = $state([...ACTIVE_STATUSES]); let filterPriorities = $state([]); let searchQuery = $state(""); + let lastSlug = ""; function toggleStatus(s: string) { if (filterStatuses.includes(s)) { @@ -58,6 +61,10 @@ function load() { if (!slug) return; + if (slug !== lastSlug) { + lastSlug = slug; + project = null; + } loading = true; error = ""; @@ -67,19 +74,20 @@ filters.priorities = filterPriorities.join(","); if (searchQuery) filters.search = searchQuery; - Promise.all([getProject(slug), listTasks(filters)]) - .then(([proj, res]) => { + runLatest(requestGuard, () => Promise.all([getProject(slug), listTasks(filters)]), { + onSuccess: ([proj, res]) => { project = proj; tasks = res.items; total = res.total; touchProject({ id: proj.id, name: proj.name }); - }) - .catch((e) => { - error = e.message || "Failed to load data"; - }) - .finally(() => { + }, + onError: (e) => { + error = (e as Error).message || "Failed to load data"; + }, + onFinally: () => { loading = false; - }); + }, + }); } $effect(load); @@ -92,12 +100,14 @@ function handleTaskClick(task: Task) { navigate(`/tasks/${task.id}`); } + + let initialLoading = $derived(loading && project === null);
- {#if loading} + {#if initialLoading}

Loading...

- {:else if error} + {:else if error && !project}

{error}

{:else if project}
@@ -119,6 +129,9 @@
+ {#if loading} +

Loading...

+ {/if} { offset = Math.max(0, offset - limit); - load(); }} > Previous @@ -154,7 +166,6 @@ disabled={offset + limit >= total} onclick={() => { offset = offset + limit; - load(); }} > Next diff --git a/frontend/src/lib/pages/projects.svelte b/frontend/src/lib/pages/projects.svelte index 8a43987..6694b84 100644 --- a/frontend/src/lib/pages/projects.svelte +++ b/frontend/src/lib/pages/projects.svelte @@ -13,6 +13,7 @@ import SearchIcon from "@lucide/svelte/icons/search"; import XIcon from "@lucide/svelte/icons/x"; import type { Project } from "$lib/types/api"; + import { createLatestRequestGuard, runLatest } from "$lib/latest-request"; let { params = {} }: { params?: Record } = $props(); @@ -25,7 +26,7 @@ let searchTerm = $state(""); let debouncedSearch = $state(""); let searchTimeout: ReturnType | null = null; - let requestVersion = 0; + const requestGuard = createLatestRequestGuard(); function handleSearchInput(value: string) { searchTerm = value; @@ -45,24 +46,25 @@ } function load() { - const version = ++requestVersion; loading = true; error = ""; - listProjects(limit, offset, debouncedSearch || undefined) - .then((res) => { - if (version !== requestVersion) return; - projects = res.items; - total = res.total; - enrichRecentFromProjects(res.items); - }) - .catch((e) => { - if (version !== requestVersion) return; - error = e.message || "Failed to load projects"; - }) - .finally(() => { - if (version !== requestVersion) return; - loading = false; - }); + runLatest( + requestGuard, + () => listProjects(limit, offset, debouncedSearch || undefined), + { + onSuccess: (res) => { + projects = res.items; + total = res.total; + enrichRecentFromProjects(res.items); + }, + onError: (e) => { + error = (e as Error).message || "Failed to load projects"; + }, + onFinally: () => { + loading = false; + }, + }, + ); } $effect(load);