feat: add threaded comments, activity feeds, and task dependencies
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
interface PatchVariables {
|
||||
id: string;
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface UseOptimisticPatchOptions<T extends { id: string }> {
|
||||
/** Detail query key, e.g. ["task", id]. */
|
||||
entityKey: string[];
|
||||
/** List query keys to prefix-invalidate after settle, e.g. [["tasks"]]. */
|
||||
listKeys?: string[][];
|
||||
/** Builds the PATCH url from the entity id, e.g. (id) => `/tasks/${id}`. */
|
||||
patchUrl: (id: string) => string;
|
||||
/** Merges the partial update into the cached entity for the optimistic UI. */
|
||||
applyPatch: (current: T, data: Record<string, unknown>) => T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimistic PATCH with rollback, shared by all entity detail pages.
|
||||
*
|
||||
* Returns a `patch({ id, data })` callable: the mutation updates the detail
|
||||
* query cache immediately via `applyPatch`, restores the previous snapshot on
|
||||
* error (with an error toast), and prefix-invalidates the entity key plus all
|
||||
* list keys once the request settles.
|
||||
*/
|
||||
export function useOptimisticPatch<T extends { id: string }>(
|
||||
opts: UseOptimisticPatchOptions<T>
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
const { entityKey, listKeys = [], patchUrl, applyPatch } = opts;
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: ({ id, data }: PatchVariables) =>
|
||||
api.patch<unknown>(patchUrl(id), data),
|
||||
onMutate: async ({ id, data }) => {
|
||||
await queryClient.cancelQueries({ queryKey: entityKey });
|
||||
const previous = queryClient.getQueryData<T>(entityKey);
|
||||
queryClient.setQueryData<T>(entityKey, (old) =>
|
||||
old ? applyPatch(old, data) : old
|
||||
);
|
||||
return { previous };
|
||||
},
|
||||
onError: (err, _vars, context) => {
|
||||
if (context?.previous !== undefined) {
|
||||
queryClient.setQueryData<T>(entityKey, context.previous);
|
||||
}
|
||||
const message = err instanceof Error ? err.message : "Failed to save";
|
||||
toast.error(message || "Failed to save");
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: entityKey });
|
||||
for (const key of listKeys) {
|
||||
queryClient.invalidateQueries({ queryKey: key });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...mutation,
|
||||
patch: mutation.mutate,
|
||||
};
|
||||
}
|
||||
@@ -28,16 +28,17 @@ export function useRealtime(options: UseRealtimeOptions = {}) {
|
||||
|
||||
switch (entityType) {
|
||||
case "task":
|
||||
queryKeys.push(["tasks"], ["tasks-due"], ["stats"], ["productivity-chart"], ["analytics-daily"], ["analytics-projects"]);
|
||||
// Singular keys (e.g. ["task", id]) refresh open detail pages live.
|
||||
queryKeys.push(["tasks"], ["task"], ["tasks-due"], ["stats"], ["productivity-chart"], ["analytics-daily"], ["analytics-projects"]);
|
||||
break;
|
||||
case "habit":
|
||||
queryKeys.push(["habits"], ["habits-today"], ["streaks"], ["analytics-habits"]);
|
||||
queryKeys.push(["habits"], ["habit"], ["habits-today"], ["streaks"], ["analytics-habits"]);
|
||||
break;
|
||||
case "project":
|
||||
queryKeys.push(["projects"], ["active-projects"], ["analytics-projects"]);
|
||||
queryKeys.push(["projects"], ["project"], ["active-projects"], ["analytics-projects"]);
|
||||
break;
|
||||
case "note":
|
||||
queryKeys.push(["notes"], ["recent-notes"]);
|
||||
queryKeys.push(["notes"], ["note"], ["recent-notes"]);
|
||||
break;
|
||||
case "calendar_event":
|
||||
queryKeys.push(["calendar-events"], ["upcoming-events"]);
|
||||
|
||||
Reference in New Issue
Block a user