Skip to content
This repository was archived by the owner on Sep 21, 2026. It is now read-only.

Commit f113c4b

Browse files
committed
fix: guard async race and pre-load options for existing project rows
Two bugs in the project access selector: 1. Race condition: loadProjects had no cancellation guard. Two in-flight requests for the same row (e.g. rapid typing) could resolve out of order, leaving stale results visible. Added a per-row generation counter — responses are discarded if a newer request has been dispatched since they were started. 2. Edit-mode UUIDs: when the edit modal opens with a member's existing project-specific roles, rowOptions starts empty so Input.ComboBox falls back to displaying the raw projectId. A $effect now eagerly calls loadProjects for any row that has a projectId but no loaded options, resolving the label as soon as the dropdown mounts.
1 parent 99b16bc commit f113c4b

1 file changed

Lines changed: 22 additions & 1 deletion

File tree

‎src/routes/(console)/organization-[organization]/projectAccessSelector.svelte‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@
1818
projectAccess: ProjectAccess[];
1919
} = $props();
2020
21-
// Per-row options and loading state
21+
// Per-row options, loading state, and request-generation counters
2222
let rowOptions = $state<Option[][]>([]);
2323
let rowSearching = $state<boolean[]>([]);
24+
// Incremented on every loadProjects call for a row; the response is
25+
// discarded if a newer request has already been dispatched.
26+
let rowGeneration = $state<number[]>([]);
2427
2528
function takenIds(excludeIndex: number): Set<string> {
2629
return new Set(
@@ -32,7 +35,10 @@
3235
}
3336
3437
async function loadProjects(index: number, search = '') {
38+
const gen = (rowGeneration[index] ?? 0) + 1;
39+
rowGeneration[index] = gen;
3540
rowSearching[index] = true;
41+
3642
const queries: string[] = [
3743
Query.limit(25),
3844
Query.orderDesc(''),
@@ -47,13 +53,27 @@
4753
.listProjects({ queries })
4854
.catch(() => null);
4955
56+
// Discard stale responses — a newer request has already been dispatched
57+
if (rowGeneration[index] !== gen) return;
58+
5059
const taken = takenIds(index);
5160
rowOptions[index] = (result?.projects ?? [])
5261
.filter((p) => !taken.has(p.$id))
5362
.map((p) => ({ value: p.$id, label: p.name }));
5463
rowSearching[index] = false;
5564
}
5665
66+
// When the component is initialised with pre-existing rows (edit mode),
67+
// options must be loaded eagerly so the ComboBox can resolve the project
68+
// name from the stored projectId instead of showing the raw UUID.
69+
$effect(() => {
70+
projectAccess.forEach((access, i) => {
71+
if (access.projectId && !rowOptions[i]?.length && !rowSearching[i]) {
72+
loadProjects(i);
73+
}
74+
});
75+
});
76+
5777
// One debounced searcher per row — created on demand
5878
const debouncedSearchers = new Map<number, (search: string) => void>();
5979
function getDebouncedSearch(index: number) {
@@ -76,6 +96,7 @@
7696
projectAccess = projectAccess.filter((_, idx) => idx !== i);
7797
rowOptions = rowOptions.filter((_, idx) => idx !== i);
7898
rowSearching = rowSearching.filter((_, idx) => idx !== i);
99+
rowGeneration = rowGeneration.filter((_, idx) => idx !== i);
79100
debouncedSearchers.delete(i);
80101
// The removed row's project is no longer taken — invalidate sibling caches
81102
// so they reload with the freed-up project available again.

0 commit comments

Comments
 (0)