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 84e6489..2d233c6 100644 --- a/frontend/src/lib/pages/dashboard-tasks.svelte +++ b/frontend/src/lib/pages/dashboard-tasks.svelte @@ -12,6 +12,7 @@ import type { Project, Task } from "$lib/types/api"; import { ACTIVE_STATUSES } from "$lib/types/api"; import { getSnapshot, saveSnapshot } from "$lib/stores/task-filters.svelte"; + import { createLatestRequestGuard, runLatest } from "$lib/latest-request"; let { params = {} }: { params?: Record } = $props(); @@ -40,6 +41,8 @@ offset = saved.offset; } + const requestGuard = createLatestRequestGuard(); + function toggleStatus(s: string) { if (filterStatuses.includes(s)) { filterStatuses = filterStatuses.filter((x) => x !== s); @@ -83,21 +86,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 f4d1ccb..94330b1 100644 --- a/frontend/src/lib/pages/project-tasks.svelte +++ b/frontend/src/lib/pages/project-tasks.svelte @@ -14,6 +14,7 @@ import type { Project, Task } from "$lib/types/api"; import { ACTIVE_STATUSES } from "$lib/types/api"; import { getSnapshot, saveSnapshot } from "$lib/stores/task-filters.svelte"; + import { createLatestRequestGuard, runLatest } from "$lib/latest-request"; // svelte-ignore a11y_click_events_have_key_events @@ -30,6 +31,7 @@ let sort = $state("-created_at"); let offset = $state(0); const limit = 50; + const requestGuard = createLatestRequestGuard(); let filterStatuses = $state([...ACTIVE_STATUSES]); let filterPriorities = $state([]); @@ -48,6 +50,7 @@ sort = "-created_at"; } }); + let lastSlug = ""; function toggleStatus(s: string) { if (filterStatuses.includes(s)) { @@ -76,6 +79,10 @@ function load() { if (!slug) return; + if (slug !== lastSlug) { + lastSlug = slug; + project = null; + } loading = true; error = ""; @@ -85,20 +92,25 @@ filters.priorities = filterPriorities.join(","); if (searchQuery) filters.search = searchQuery; - Promise.all([getProject(slug), listTasks(filters)]) - .then(([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(() => { - currentRouteKey = routeKey; - loading = false; - }); + 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 }); + }, + onError: (e) => { + error = (e as Error).message || "Failed to load data"; + }, + onFinally: () => { + currentRouteKey = routeKey; + loading = false; + }, + }, + ); } $effect(load); @@ -121,12 +133,14 @@ function handleTaskClick(task: Task) { navigate(`/tasks/${task.project_slug}/${task.number}`); } + + let initialLoading = $derived(loading && project === null);
- {#if loading} + {#if initialLoading}

Loading...

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

{error}

{:else if project}
@@ -148,6 +162,11 @@
+ {#if loading} +

+ Loading... +

+ {/if} { offset = Math.max(0, offset - limit); - load(); }} > Previous @@ -183,7 +201,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);