From 752c1daf8a1c6c5c49b1f98dbe06f72927da3c9d Mon Sep 17 00:00:00 2001 From: dudina-ma Date: Wed, 8 Jul 2026 10:05:26 +0200 Subject: [PATCH 1/3] [tasks] fix task pagination race in project view --- frontend/src/lib/pages/project-tasks.svelte | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/pages/project-tasks.svelte b/frontend/src/lib/pages/project-tasks.svelte index 7bef64d..e046ad0 100644 --- a/frontend/src/lib/pages/project-tasks.svelte +++ b/frontend/src/lib/pages/project-tasks.svelte @@ -26,6 +26,7 @@ let sort = $state("-created_at"); let offset = $state(0); const limit = 50; + let requestVersion = 0; let filterStatuses = $state([...ACTIVE_STATUSES]); let filterPriorities = $state([]); @@ -58,6 +59,7 @@ function load() { if (!slug) return; + const version = ++requestVersion; loading = true; error = ""; @@ -69,15 +71,18 @@ Promise.all([getProject(slug), listTasks(filters)]) .then(([proj, res]) => { + if (version !== requestVersion) return; project = proj; tasks = res.items; total = res.total; touchProject({ id: proj.id, name: proj.name }); }) .catch((e) => { + if (version !== requestVersion) return; error = e.message || "Failed to load data"; }) .finally(() => { + if (version !== requestVersion) return; loading = false; }); } @@ -138,7 +143,6 @@ disabled={offset === 0} onclick={() => { offset = Math.max(0, offset - limit); - load(); }} > Previous @@ -154,7 +158,6 @@ disabled={offset + limit >= total} onclick={() => { offset = offset + limit; - load(); }} > Next From 3b912ab46bb3e9b6574ad84dc81dfb4383a0c707 Mon Sep 17 00:00:00 2001 From: dudina-ma Date: Fri, 10 Jul 2026 12:01:11 +0200 Subject: [PATCH 2/3] [frontend] fix race conditions --- .../lib/components/AssigneeCombobox.svelte | 27 ++++++----- .../src/lib/components/TaskFilters.svelte | 6 ++- frontend/src/lib/latest-request.ts | 44 ++++++++++++++++++ frontend/src/lib/pages/admin-projects.svelte | 28 ++++++++---- frontend/src/lib/pages/admin-users.svelte | 43 +++++++++++------- .../src/lib/pages/dashboard-personal.svelte | 45 +++++++++++-------- frontend/src/lib/pages/dashboard-tasks.svelte | 39 +++++++++------- frontend/src/lib/pages/project-tasks.svelte | 33 +++++++------- frontend/src/lib/pages/projects.svelte | 36 ++++++++------- 9 files changed, 196 insertions(+), 105 deletions(-) create mode 100644 frontend/src/lib/latest-request.ts 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 e046ad0..4a9d020 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,7 +27,7 @@ let sort = $state("-created_at"); let offset = $state(0); const limit = 50; - let requestVersion = 0; + const requestGuard = createLatestRequestGuard(); let filterStatuses = $state([...ACTIVE_STATUSES]); let filterPriorities = $state([]); @@ -59,7 +60,6 @@ function load() { if (!slug) return; - const version = ++requestVersion; loading = true; error = ""; @@ -69,22 +69,20 @@ filters.priorities = filterPriorities.join(","); if (searchQuery) filters.search = searchQuery; - Promise.all([getProject(slug), listTasks(filters)]) - .then(([proj, res]) => { - if (version !== requestVersion) return; + 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) => { - if (version !== requestVersion) return; - error = e.message || "Failed to load data"; - }) - .finally(() => { - if (version !== requestVersion) return; + }, + onError: (e) => { + error = (e as Error).message || "Failed to load data"; + }, + onFinally: () => { loading = false; - }); + }, + }); } $effect(load); @@ -97,12 +95,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}
@@ -124,6 +124,9 @@
+ {#if loading} +

Loading...

+ {/if} } = $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); From 3ee80b01d009cf8e527fdab35886ee1e971b6ac7 Mon Sep 17 00:00:00 2001 From: dudina-ma Date: Sat, 11 Jul 2026 22:08:55 +0200 Subject: [PATCH 3/3] [frontend] address review comment --- frontend/src/lib/pages/project-tasks.svelte | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/lib/pages/project-tasks.svelte b/frontend/src/lib/pages/project-tasks.svelte index 4a9d020..4b666c1 100644 --- a/frontend/src/lib/pages/project-tasks.svelte +++ b/frontend/src/lib/pages/project-tasks.svelte @@ -32,6 +32,7 @@ let filterStatuses = $state([...ACTIVE_STATUSES]); let filterPriorities = $state([]); let searchQuery = $state(""); + let lastSlug = ""; function toggleStatus(s: string) { if (filterStatuses.includes(s)) { @@ -60,6 +61,10 @@ function load() { if (!slug) return; + if (slug !== lastSlug) { + lastSlug = slug; + project = null; + } loading = true; error = "";