use-global-canvas-presets.ts6.0 KBView on GitHub /**
* useGlobalCanvasPresets Hook
*
* Manages global canvas column presets stored in userSettings.canvasPresets.
* All canvas column presets are stored here at the user level and shared
* across all conversation canvases. Individual canvases only store their
* current column state (presetId + columnConfiguration).
*
* - Save current canvas presets as global defaults
* - Load global presets for new canvases
* - CRUD operations on global presets
*/
import { useSettings } from '@/modules/userSettings/hooks/use-settings';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import type { CanvasPreset } from '@/modules/canvas/types/canvas-types';
import type { CanvasPresetSettings, UserSettings } from '@zero/server/schemas';
import { useTRPC } from '@/providers/query-provider';
import { useCallback, useMemo } from 'react';
import { toast } from 'sonner';
/**
* Hook to manage global canvas column presets (stored in userSettings).
*/
export function useGlobalCanvasPresets() {
const trpc = useTRPC();
const queryClient = useQueryClient();
const { data: settingsData } = useSettings();
const { mutateAsync: saveUserSettings, isPending: isSaving } = useMutation({
...trpc.settings.save.mutationOptions(),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey=[redacted],
});
},
});
/** Current global presets from userSettings (empty array if none set). */
const globalPresets = useMemo((): CanvasPreset[] => {
const raw = settingsData?.settings?.canvasPresets ?? [];
return raw.map((p: CanvasPresetSettings) => ({
id: p.id,
name: p.name,
columnIds: p.columnIds,
expanded: p.expanded,
}));
}, [settingsData?.settings?.canvasPresets]);
/** Whether the user has any global presets configured. */
const hasGlobalPresets = globalPresets.length > 0;
/** Save an array of presets as the user's global canvas presets. */
const saveGlobalPresets = useCallback(
async (presets: CanvasPreset[], options?: { silent?: boolean }) => {
const serialized: CanvasPresetSettings[] = presets.map((p) => ({
id: p.id,
name: p.name,
columnIds: p.columnIds,
expanded: p.expanded,
}));
// Optimistically update the cache so the UI reflects the change immediately.
// Spread `previous` rather than rebuilding the envelope: the response carries
// siblings of `settings` (`authenticated`, which gates the chat composer's
// access notice) and dropping them here downgrades those readers to "unknown"
// until the next refetch.
const queryKey=[redacted];
type SettingsQueryData = { settings: UserSettings; authenticated?: boolean };
const previous = queryClient.getQueryData<SettingsQueryData>(queryKey);
queryClient.setQueryData<SettingsQueryData>(queryKey, {
...previous,
settings: { ...(previous?.settings ?? settingsData?.settings ?? {} as UserSettings), canvasPresets: serialized },
});
try {
await saveUserSettings({
canvasPresets: serialized,
});
} catch (error) {
// Rollback optimistic update on failure
queryClient.setQueryData<SettingsQueryData>(queryKey, previous);
console.error('Failed to save global presets:', error);
if (!options?.silent) {
toast.error('Failed to save global presets');
}
throw error;
}
},
[settingsData?.settings, saveUserSettings, queryClient, trpc.settings.get],
);
/** Upsert a single preset in the global list. */
const upsertGlobalPreset = useCallback(
async (preset: CanvasPreset, options?: { silent?: boolean }) => {
const existing = [...globalPresets];
const idx = existing.findIndex((p) => p.id === preset.id);
if (idx >= 0) {
existing[idx] = preset;
} else {
existing.push(preset);
}
await saveGlobalPresets(existing, options);
},
[globalPresets, saveGlobalPresets],
);
/** Remove a single preset from the global list by id. */
const removeGlobalPreset = useCallback(
async (presetId: string) => {
const updated = globalPresets.filter((p) => p.id !== presetId);
if (updated.length === globalPresets.length) return;
await saveGlobalPresets(updated);
},
[globalPresets, saveGlobalPresets],
);
/** Save all presets from a specific canvas as the global defaults. */
const saveCanvasPresetsAsGlobal = useCallback(
async (canvasPresets: CanvasPreset[]) => {
await saveGlobalPresets(canvasPresets);
},
[saveGlobalPresets],
);
/**
* Silently sync column order/visibility to the active preset.
* Called after column reorder or visibility toggle to keep the preset in sync.
*/
const syncActivePresetColumns = useCallback(
async (presetId: string | undefined, orderedColumnIds: string[]) => {
if (!presetId) return;
const preset = globalPresets.find((p) => p.id === presetId);
if (!preset) return;
// Skip if columnIds are already identical (same items in same order)
if (
preset.columnIds.length === orderedColumnIds.length &&
preset.columnIds.every((id, i) => id === orderedColumnIds[i])
) {
return;
}
await upsertGlobalPreset({ ...preset, columnIds: orderedColumnIds }, { silent: true });
},
[globalPresets, upsertGlobalPreset],
);
/** Clear all global presets (revert to built-in defaults). */
const clearGlobalPresets = useCallback(async () => {
try {
await saveUserSettings({
canvasPresets: [],
});
} catch (error) {
console.error('Failed to clear global presets:', error);
toast.error('Failed to clear global presets');
throw error;
}
}, [settingsData?.settings, saveUserSettings]);
return {
globalPresets,
hasGlobalPresets,
isSaving,
saveGlobalPresets,
upsertGlobalPreset,
removeGlobalPreset,
saveCanvasPresetsAsGlobal,
clearGlobalPresets,
syncActivePresetColumns,
};
}