Skip to content

Commit 2474835

Browse files
committed
fix: improve optimistic task creation and merge logic
- Ensure optimistic tasks have a stable temporary ID for reliable identification - Enhance task merge logic to properly handle clientId mapping during server sync - Preserve selected task during creation by checking local state - Fix date serialization formatting consistency
1 parent 98f6d88 commit 2474835

2 files changed

Lines changed: 52 additions & 17 deletions

File tree

frontend/src/components/ProjectManagement.tsx

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,14 @@ export default function ProjectManagement({
120120
useStoreWorkspace();
121121

122122
const [activeProjectId, setActiveProjectId] = useState<string>(
123-
initialProjectId ? initialProjectId : (projects[0]?.id ?? ""),
123+
initialProjectId ? initialProjectId : projects[0]?.id ?? "",
124124
);
125125

126126
const [activeWorkspaceId, setActiveWorkspaceId] = useState<string>(
127-
initialWorkspaceId ? initialWorkspaceId : (workspaces[0]?.id ?? ""),
127+
initialWorkspaceId ? initialWorkspaceId : workspaces[0]?.id ?? "",
128128
);
129129
const [lastActiveWorkspaceId, setLastActiveWorkspaceId] = useState<string>(
130-
initialWorkspaceId ? initialWorkspaceId : (workspaces[0]?.id ?? ""),
130+
initialWorkspaceId ? initialWorkspaceId : workspaces[0]?.id ?? "",
131131
);
132132

133133
const rafRef = useRef<number | null>(null);
@@ -1412,12 +1412,21 @@ export default function ProjectManagement({
14121412

14131413
// 🧠 NORMAL MERGE (REALTIME / OPTIMISTIC SAFE)
14141414
setTasks((localTasks) => {
1415-
const tmp = localTasks.filter((t) => nid(t.id).startsWith("tmp_"));
1416-
1415+
// 1) Group local tasks by ID (real and tmp) and clientId (if available)
14171416
const localById = new Map(localTasks.map((t) => [nid(t.id), t]));
1417+
const localByClientId = new Map();
1418+
localTasks.forEach((t) => {
1419+
const cid = (t as any).clientId;
1420+
if (cid) localByClientId.set(nid(cid), t);
1421+
});
14181422

1423+
// 2) Merge server tasks with local versions (preserving local changes like comments)
14191424
const mergedServerTasks: Task[] = serverTasks.map((st) => {
1420-
const lt = localById.get(nid(st.id));
1425+
// Try to find local version by id or by clientId (useful if server returned real id but cache still has tmp id)
1426+
const lt =
1427+
localById.get(nid(st.id)) ||
1428+
localByClientId.get(nid(st.id)) ||
1429+
localByClientId.get(nid((st as any).clientId));
14211430

14221431
// merge comments safely
14231432
const serverComments = Array.isArray((st as any).comments)
@@ -1461,16 +1470,35 @@ export default function ProjectManagement({
14611470
return { ...st, comments: chosenComments };
14621471
});
14631472

1464-
const merged = [...mergedServerTasks, ...tmp];
1473+
// 3) Keep tasks that are in local state but not yet in server tasks
1474+
// (This covers the gap between mutateAsync return and refetch completion)
1475+
const serverIds = new Set(serverTasks.map((st) => nid(st.id)));
1476+
const serverClientIds = new Set(
1477+
serverTasks.map((st) => nid((st as any).clientId)).filter((id) => id),
1478+
);
1479+
1480+
const localOnly = localTasks.filter((lt) => {
1481+
const id = nid(lt.id);
1482+
const cid = nid((lt as any).clientId);
1483+
return !serverIds.has(id) && (!cid || !serverClientIds.has(cid));
1484+
});
1485+
1486+
const merged = [...mergedServerTasks, ...localOnly];
14651487

14661488
// sync selectedTask ref
14671489
const mergedMap = new Map(merged.map((t) => [nid(t.id), t]));
14681490
setSelectedTask((cur) => {
14691491
if (!cur) return null;
14701492
const found = mergedMap.get(nid(cur.id));
14711493
if (found) return found;
1472-
// keep optimistic task if it's currently selected (prevents auto-close during creation)
1494+
1495+
// keep optimistic task if it's currently selected
14731496
if (nid(cur.id).startsWith("tmp_")) return cur;
1497+
1498+
// also keep if it's in localTasks (prevents auto-close during creation)
1499+
const inLocal = localTasks.find((t) => nid(t.id) === nid(cur.id));
1500+
if (inLocal) return inLocal;
1501+
14741502
return null;
14751503
});
14761504

@@ -1512,15 +1540,19 @@ export default function ProjectManagement({
15121540

15131541
setTasks((s) => {
15141542
const replaced = s.map((t) =>
1515-
nid(t.id) === nid(optimistic.id) ? created : t,
1543+
nid(t.id) === nid(optimistic.id)
1544+
? { ...created, clientId: optimistic.id }
1545+
: t,
15161546
);
15171547
const map = new Map<string, Task>();
15181548
for (const t of replaced) map.set(nid(t.id), t);
15191549
return Array.from(map.values());
15201550
});
15211551

15221552
setSelectedTask((cur) =>
1523-
cur && nid(cur.id) === nid(optimistic.id) ? created : cur,
1553+
cur && nid(cur.id) === nid(optimistic.id)
1554+
? { ...created, clientId: optimistic.id }
1555+
: cur,
15241556
);
15251557

15261558
qcRef.current.invalidateQueries(["tasks", activeProjectId]);
@@ -1623,17 +1655,17 @@ export default function ProjectManagement({
16231655
(updated as any).startDate === null
16241656
? null
16251657
: (updated as any).startDate instanceof Date
1626-
? (updated as any).startDate.toISOString()
1627-
: String((updated as any).startDate);
1658+
? (updated as any).startDate.toISOString()
1659+
: String((updated as any).startDate);
16281660
}
16291661

16301662
if (typeof (updated as any).dueDate !== "undefined") {
16311663
patch.dueDate =
16321664
(updated as any).dueDate === null
16331665
? null
16341666
: (updated as any).dueDate instanceof Date
1635-
? (updated as any).dueDate.toISOString()
1636-
: String((updated as any).dueDate);
1667+
? (updated as any).dueDate.toISOString()
1668+
: String((updated as any).dueDate);
16371669
}
16381670

16391671
// include comments when provided (array|null)
@@ -2543,8 +2575,8 @@ export default function ProjectManagement({
25432575
? projects.find((x) => x.id === activeProjectId)?.name ||
25442576
"—"
25452577
: viewMode === "MY_TASKS"
2546-
? "My Tasks"
2547-
: "Report"}
2578+
? "My Tasks"
2579+
: "Report"}
25482580
</h2>
25492581
)}
25502582
{viewMode === "PROJECT" && (

frontend/src/hooks/useTasks.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ export function useCreateTask() {
5656
}
5757

5858
// apply optimistic insertion to every tasks* query
59-
const optimisticTask = payload; // payload usually contains clientId as id
59+
const optimisticTask = {
60+
...payload,
61+
id: payload.id || payload.clientId || `tmp_${Math.random().toString(36).slice(2, 9)}`,
62+
};
6063
for (const [qk, data] of prevEntries) {
6164
try {
6265
qc.setQueryData(qk, (old: any) =>

0 commit comments

Comments
 (0)