Workspaces
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "state",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"hooks": "workspace:*",
|
||||
"officerdev": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export { useSettings, DEFAULT_SETTINGS } from './useSettings';
|
||||
export type { UseSettingsType, UserSettings, UserState } from './useSettings';
|
||||
export { useUserState } from './useUserState';
|
||||
export { useWorkspacesState } from './useWorkspacesState';
|
||||
export { useProjectsState } from './useProjectsState';
|
||||
export { usePiModels, useVisiblePiModels, modelKey } from './useModels';
|
||||
export type { ModelOption } from './useModels';
|
||||
export { useRecentModels } from './useRecentModels';
|
||||
export { usePlans } from './usePlans';
|
||||
export { useLandingPage } from './useLandingPage';
|
||||
export { useServerSettings } from './useServerSettings';
|
||||
export { useResources, getResourceCategory } from './useResources';
|
||||
export type { Resource, ResourceCredentials, ResourceConnectionConfig, PingResult, ResourceCategory } from './useResources';
|
||||
export { useChatSessions } from './useChatSessions';
|
||||
export type { UseChatSessionsType } from './useChatSessions';
|
||||
export { useChatGroups } from './useChatGroups';
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { GroupEntry } from 'officerdev';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
export function useChatGroups() {
|
||||
const client = useClient();
|
||||
const queryClient = useQueryClient();
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
const { data: groups = [] } = useQuery<GroupEntry[]>({
|
||||
queryKey: ['PI_GROUPS'],
|
||||
enabled: isAuthenticated,
|
||||
queryFn: () => client.get<{ groups: GroupEntry[] }>('/pi/groups').then((r) => r.groups),
|
||||
});
|
||||
|
||||
async function createGroup(name: string, slug: string, description?: string, sessionIds?: string[]) {
|
||||
const result = await client.post<{ group: GroupEntry }>('/pi/groups', {
|
||||
name,
|
||||
slug,
|
||||
description,
|
||||
sessionIds,
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['PI_GROUPS'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['PI_SESSIONS'] });
|
||||
return result.group;
|
||||
}
|
||||
|
||||
async function updateGroup(slug: string, updates: { name?: string; description?: string }) {
|
||||
await client.patch(`/pi/groups/${slug}`, updates);
|
||||
queryClient.invalidateQueries({ queryKey: ['PI_GROUPS'] });
|
||||
}
|
||||
|
||||
async function deleteGroup(slug: string) {
|
||||
await client.delete(`/pi/groups/${slug}`);
|
||||
queryClient.invalidateQueries({ queryKey: ['PI_GROUPS'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['PI_SESSIONS'] });
|
||||
}
|
||||
|
||||
async function moveSession(sessionId: string, groupSlug: string | null) {
|
||||
await client.post(`/pi/sessions/${sessionId}/move`, { groupSlug });
|
||||
queryClient.invalidateQueries({ queryKey: ['PI_GROUPS'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['PI_SESSIONS'] });
|
||||
}
|
||||
|
||||
return {
|
||||
groups,
|
||||
createGroup,
|
||||
updateGroup,
|
||||
deleteGroup,
|
||||
moveSession,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { SessionEntry, ChatMessage, Message } from 'officerdev';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
export function useChatSessions() {
|
||||
const client = useClient();
|
||||
const queryClient = useQueryClient();
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
const { data: sessions = [] } = useQuery<SessionEntry[]>({
|
||||
queryKey: ['PI_SESSIONS'],
|
||||
enabled: isAuthenticated,
|
||||
queryFn: () => client.post<{ sessions: SessionEntry[] }>('/pi/sessions').then((r) => r.sessions),
|
||||
});
|
||||
|
||||
function getSession(sessionId: string) {
|
||||
return client.get<{ session: SessionWithMessages }>(`/pi/sessions/${sessionId}`);
|
||||
}
|
||||
|
||||
function saveMessages(sessionId: string, messages: ChatMessage[]) {
|
||||
return client.put(`/pi/sessions/${sessionId}/messages`, messages);
|
||||
}
|
||||
|
||||
async function renameSession(sessionId: string, title: string) {
|
||||
await client.patch(`/pi/sessions/${sessionId}`, { title });
|
||||
queryClient.invalidateQueries({ queryKey: ['PI_SESSIONS'] });
|
||||
}
|
||||
|
||||
async function deleteSession(sessionId: string) {
|
||||
await client.delete(`/pi/sessions/${sessionId}`);
|
||||
queryClient.setQueryData<SessionEntry[]>(
|
||||
['PI_SESSIONS'],
|
||||
(prev) => prev?.filter((s) => s.id !== sessionId) ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
function searchSessions(query: string) {
|
||||
return client.get<{ results: SessionEntry[] }>(`/pi/sessions/search?q=${encodeURIComponent(query)}`);
|
||||
}
|
||||
|
||||
return {
|
||||
sessions,
|
||||
getSession,
|
||||
saveMessages,
|
||||
renameSession,
|
||||
deleteSession,
|
||||
searchSessions,
|
||||
};
|
||||
}
|
||||
|
||||
export type UseChatSessionsType = ReturnType<typeof useChatSessions>;
|
||||
|
||||
type SessionWithMessages = {
|
||||
id: string;
|
||||
title: string;
|
||||
model: string;
|
||||
cwd: string;
|
||||
groupSlug?: string | null;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
messageCount: number;
|
||||
cost: {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
totalUSD: number;
|
||||
};
|
||||
messages: Message[];
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
export const useLandingPage = () => {
|
||||
const apiClient = useClient();
|
||||
|
||||
const { data: setupData, isLoading } = useQuery({
|
||||
queryKey: ['LANDING_PAGE_DATA'],
|
||||
queryFn: () => apiClient.get<{ registrationOpen: boolean }>('/landing-page-data'),
|
||||
});
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
registrationOpen: setupData?.registrationOpen,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import { useSettings } from './useSettings';
|
||||
import type { ModelOption } from 'officerdev';
|
||||
|
||||
export type { ModelOption };
|
||||
|
||||
export function modelKey(m: ModelOption): string {
|
||||
return `${m.provider}:${m.id}`;
|
||||
}
|
||||
|
||||
export function usePiModels() {
|
||||
const client = useClient();
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
const { data: models = [] } = useQuery<ModelOption[]>({
|
||||
queryKey: ['PI_MODELS'],
|
||||
enabled: isAuthenticated,
|
||||
queryFn: async () => {
|
||||
const data = await client.get<{ models: ModelOption[] }>('/pi/models');
|
||||
return data.models;
|
||||
},
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
export function useVisiblePiModels() {
|
||||
const models = usePiModels();
|
||||
const { settings } = useSettings();
|
||||
const enabled = settings.ai?.enabledModels ?? [];
|
||||
|
||||
const filtered = models.filter((m) => enabled.includes(modelKey(m)));
|
||||
// If no models match the visibility filter, show all
|
||||
// The provider list changes dynamically based on API keys so the filter may be stale
|
||||
return filtered.length > 0 ? filtered : models;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
export const usePlans = () => {
|
||||
const client = useClient();
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
const { data: plans = [] } = useQuery<string[]>({
|
||||
queryKey: ['PLANS'],
|
||||
enabled: isAuthenticated,
|
||||
queryFn: () => client.get<string[]>('/plans'),
|
||||
});
|
||||
|
||||
const getPlan = (name: string) => client.getText(`/plans/${name}`);
|
||||
|
||||
return { plans, getPlan };
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import type { UserState } from './useSettings';
|
||||
|
||||
const QUERY_KEY = ['PROJECTS_STATE'];
|
||||
|
||||
export function useProjectsState<T>(key: string, defaultValue: T): [T, (value: T | ((prev: T) => T)) => void, boolean] {
|
||||
const client = useClient();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const clientRef = useRef(client);
|
||||
clientRef.current = client;
|
||||
|
||||
const { data: state = {}, isSuccess } = useQuery<UserState>({
|
||||
queryKey: QUERY_KEY,
|
||||
enabled: isAuthenticated,
|
||||
queryFn: () => client.get<UserState>('/user/projects-state'),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
const value = key in state ? (state[key] as T) : defaultValue;
|
||||
|
||||
const setValue = useCallback(
|
||||
(update: T | ((prev: T) => T)) => {
|
||||
const currentState = queryClient.getQueryData<UserState>(QUERY_KEY) ?? {};
|
||||
const currentValue = key in currentState ? (currentState[key] as T) : defaultValue;
|
||||
const newValue = typeof update === 'function' ? (update as (prev: T) => T)(currentValue) : update;
|
||||
|
||||
queryClient.setQueryData(QUERY_KEY, { ...currentState, [key]: newValue });
|
||||
|
||||
clientRef.current
|
||||
.patch<UserState>('/user/projects-state', { [key]: newValue })
|
||||
.then((serverState) => {
|
||||
if (serverState) queryClient.setQueryData(QUERY_KEY, serverState);
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
[key, defaultValue, queryClient],
|
||||
);
|
||||
|
||||
return [value, setValue, isSuccess];
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useUserState } from './useUserState';
|
||||
import type { ModelOption } from './useModels';
|
||||
|
||||
const MAX_RECENTS = 5;
|
||||
|
||||
export const useRecentModels = () => {
|
||||
const [recents, setRecents] = useUserState<ModelOption[]>('recentModels', []);
|
||||
const migrated = useRef(false);
|
||||
|
||||
// One-time migration from localStorage
|
||||
useEffect(() => {
|
||||
if (migrated.current) return;
|
||||
migrated.current = true;
|
||||
|
||||
const raw = localStorage.getItem('OC_RECENT_MODELS');
|
||||
if (!raw) return;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as ModelOption[];
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
setRecents(parsed.slice(0, MAX_RECENTS));
|
||||
localStorage.removeItem('OC_RECENT_MODELS');
|
||||
}
|
||||
} catch {
|
||||
localStorage.removeItem('OC_RECENT_MODELS');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const addRecent = useCallback(
|
||||
(model: ModelOption) => {
|
||||
setRecents((prev) => {
|
||||
const filtered = prev.filter((m) => m.id !== model.id);
|
||||
return [model, ...filtered].slice(0, MAX_RECENTS);
|
||||
});
|
||||
},
|
||||
[setRecents],
|
||||
);
|
||||
|
||||
return { recents, addRecent };
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
|
||||
export type ResourceCredentials = {
|
||||
apiKey?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
export type ResourceConnectionConfig = {
|
||||
url: string;
|
||||
credentials?: ResourceCredentials;
|
||||
};
|
||||
|
||||
export type Resource = {
|
||||
id: string;
|
||||
name: string;
|
||||
subtitle: string;
|
||||
type: string;
|
||||
port: string | null;
|
||||
description: string;
|
||||
installCommand: string | null;
|
||||
uninstallCommand: string | null;
|
||||
manageCommand: string | null;
|
||||
verifyCommand: string | null;
|
||||
updateCommand: string | null;
|
||||
installed: boolean;
|
||||
version: string | null;
|
||||
connectionConfig: ResourceConnectionConfig | null;
|
||||
};
|
||||
|
||||
export type PingResult = {
|
||||
reachable: boolean;
|
||||
latencyMs: number | null;
|
||||
};
|
||||
|
||||
export type ResourceCategory = 'api-based' | 'local-cli';
|
||||
|
||||
export const getResourceCategory = (r: Resource): ResourceCategory => (r.port ? 'api-based' : 'local-cli');
|
||||
|
||||
const RESOURCES_KEY = ['RESOURCES'];
|
||||
|
||||
export const useResources = () => {
|
||||
const client = useClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: resources, isLoading } = useQuery({
|
||||
queryKey: RESOURCES_KEY,
|
||||
queryFn: () => client.get<Resource[]>('/server-settings/resources'),
|
||||
});
|
||||
|
||||
const saveConnectionConfig = async (id: string, config: Partial<ResourceConnectionConfig>) => {
|
||||
await client.patch(`/server-settings/resources/config/${id}`, config);
|
||||
queryClient.invalidateQueries({ queryKey: RESOURCES_KEY });
|
||||
};
|
||||
|
||||
const pingResource = async (id: string, url?: string) => {
|
||||
return client.post<PingResult>(`/server-settings/resources/${id}/ping`, { url });
|
||||
};
|
||||
|
||||
const runCommand = async (id: string, action: string) => {
|
||||
const result = await client.post<{ exitCode: number; output: string }>(`/server-settings/resources/${id}/run`, { action });
|
||||
queryClient.invalidateQueries({ queryKey: RESOURCES_KEY });
|
||||
return result;
|
||||
};
|
||||
|
||||
return { resources, isLoading, saveConnectionConfig, pingResource, runCommand };
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
|
||||
type AIHarnesses = {
|
||||
claudeCode: boolean;
|
||||
opencode: boolean;
|
||||
piMono: boolean;
|
||||
};
|
||||
|
||||
type ServerSettings = {
|
||||
onboardingComplete?: boolean;
|
||||
accountMode?: 'organization' | 'single';
|
||||
aiHarnesses?: AIHarnesses;
|
||||
plugins?: Record<string, boolean>;
|
||||
};
|
||||
|
||||
const SETTINGS_KEY = ['SERVER_SETTINGS'];
|
||||
|
||||
export const useServerSettings = () => {
|
||||
const client = useClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: settings, isLoading } = useQuery({
|
||||
queryKey: SETTINGS_KEY,
|
||||
queryFn: () => client.get<ServerSettings>('/server-settings/settings'),
|
||||
});
|
||||
|
||||
const onboardingComplete = settings?.onboardingComplete ?? true;
|
||||
const accountMode = settings?.accountMode;
|
||||
const aiHarnesses = settings?.aiHarnesses;
|
||||
const plugins = settings?.plugins;
|
||||
const saveSettings = useCallback(
|
||||
async (update: Partial<ServerSettings>) => {
|
||||
const result = await client.put<ServerSettings>('/server-settings', update);
|
||||
queryClient.setQueryData(SETTINGS_KEY, result);
|
||||
},
|
||||
[client, queryClient],
|
||||
);
|
||||
|
||||
return { onboardingComplete, accountMode, aiHarnesses, plugins, isLoading, saveSettings };
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
|
||||
const QUERY_KEY = ['USER_SETTINGS'];
|
||||
|
||||
const mergeWithDefaults = (saved: Partial<UserSettings>): UserSettings => ({
|
||||
chat: { ...DEFAULT_SETTINGS.chat, ...saved.chat },
|
||||
ai: {
|
||||
enabledModels: saved.ai?.enabledModels?.length ? saved.ai.enabledModels : DEFAULT_SETTINGS.ai.enabledModels,
|
||||
enabledProviders: saved.ai?.enabledProviders ?? DEFAULT_SETTINGS.ai.enabledProviders,
|
||||
},
|
||||
tasks: { ...DEFAULT_SETTINGS.tasks, ...saved.tasks },
|
||||
appearance: { ...DEFAULT_SETTINGS.appearance, ...saved.appearance },
|
||||
languages: { ...DEFAULT_SETTINGS.languages, ...saved.languages },
|
||||
});
|
||||
|
||||
export const useSettings = () => {
|
||||
const client = useClient();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: settings = DEFAULT_SETTINGS } = useQuery<UserSettings>({
|
||||
queryKey: QUERY_KEY,
|
||||
enabled: isAuthenticated,
|
||||
queryFn: async () => {
|
||||
const saved = await client.get<Partial<UserSettings>>('/user/settings');
|
||||
return mergeWithDefaults(saved);
|
||||
},
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
const saveSettings = useCallback(
|
||||
async (newSettings: UserSettings) => {
|
||||
queryClient.setQueryData(QUERY_KEY, newSettings);
|
||||
await client.put<UserSettings>('/user/settings', newSettings);
|
||||
},
|
||||
[client, queryClient],
|
||||
);
|
||||
|
||||
return { settings, saveSettings };
|
||||
};
|
||||
|
||||
export type UseSettingsType = ReturnType<typeof useSettings>;
|
||||
|
||||
export type UserSettings = {
|
||||
chat: {
|
||||
defaultProvider: 'pi';
|
||||
defaultModel: string | null;
|
||||
systemPrompt: string;
|
||||
temperature: number;
|
||||
defaultPwd: string;
|
||||
};
|
||||
ai: {
|
||||
enabledModels: string[];
|
||||
enabledProviders: string[];
|
||||
};
|
||||
tasks: {
|
||||
defaultProvider: 'pi';
|
||||
defaultModel: string | null;
|
||||
};
|
||||
appearance: {
|
||||
colorMode: 'light' | 'dark';
|
||||
colorTheme: string;
|
||||
};
|
||||
languages: {
|
||||
spoken: string[];
|
||||
default: string;
|
||||
translateTo: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type UserState = Record<string, unknown>;
|
||||
|
||||
export const DEFAULT_SETTINGS: UserSettings = {
|
||||
chat: {
|
||||
defaultProvider: 'pi',
|
||||
defaultModel: null,
|
||||
systemPrompt: '',
|
||||
temperature: 1,
|
||||
defaultPwd: '~',
|
||||
},
|
||||
ai: {
|
||||
enabledModels: [],
|
||||
enabledProviders: [],
|
||||
},
|
||||
tasks: {
|
||||
defaultProvider: 'pi',
|
||||
defaultModel: null,
|
||||
},
|
||||
appearance: {
|
||||
colorMode: 'light',
|
||||
colorTheme: 'DuckPond',
|
||||
},
|
||||
languages: {
|
||||
spoken: ['en'],
|
||||
default: 'en',
|
||||
translateTo: 'en',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import type { UserState } from './useSettings';
|
||||
|
||||
const QUERY_KEY = ['USER_STATE'];
|
||||
|
||||
export function useUserState<T>(key: string, defaultValue: T): [T, (value: T | ((prev: T) => T)) => void, boolean] {
|
||||
const client = useClient();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const clientRef = useRef(client);
|
||||
clientRef.current = client;
|
||||
|
||||
const { data: state = {}, isSuccess } = useQuery<UserState>({
|
||||
queryKey: QUERY_KEY,
|
||||
enabled: isAuthenticated,
|
||||
queryFn: () => client.get<UserState>('/user/state'),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
const value = key in state ? (state[key] as T) : defaultValue;
|
||||
|
||||
const setValue = useCallback(
|
||||
(update: T | ((prev: T) => T)) => {
|
||||
const currentState = queryClient.getQueryData<UserState>(QUERY_KEY) ?? {};
|
||||
const currentValue = key in currentState ? (currentState[key] as T) : defaultValue;
|
||||
const newValue = typeof update === 'function' ? (update as (prev: T) => T)(currentValue) : update;
|
||||
|
||||
queryClient.setQueryData(QUERY_KEY, { ...currentState, [key]: newValue });
|
||||
|
||||
// Immediate fire-and-forget PATCH
|
||||
clientRef.current.patch('/user/state', { [key]: newValue }).catch(() => {});
|
||||
},
|
||||
[key, defaultValue, queryClient],
|
||||
);
|
||||
|
||||
return [value, setValue, isSuccess];
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import type { UserState } from './useSettings';
|
||||
|
||||
const QUERY_KEY = ['WORKSPACES_STATE'];
|
||||
|
||||
export function useWorkspacesState<T>(key: string, defaultValue: T) {
|
||||
const client = useClient();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const clientRef = useRef(client);
|
||||
clientRef.current = client;
|
||||
|
||||
const { data: state = {}, isSuccess } = useQuery<UserState>({
|
||||
queryKey: QUERY_KEY,
|
||||
enabled: isAuthenticated,
|
||||
queryFn: () => client.get<UserState>('/workspaces'),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
// Seed default to backend when key is missing after initial fetch
|
||||
const seededKeyRef = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (!isSuccess || seededKeyRef.current === key) return;
|
||||
seededKeyRef.current = key;
|
||||
|
||||
const currentState = queryClient.getQueryData<UserState>(QUERY_KEY) ?? {};
|
||||
if (!(key in currentState)) {
|
||||
queryClient.setQueryData(QUERY_KEY, { ...currentState, [key]: defaultValue });
|
||||
clientRef.current.patch('/workspaces', { [key]: defaultValue }).catch(() => { });
|
||||
}
|
||||
}, [isSuccess, key, defaultValue, queryClient]);
|
||||
|
||||
const value = key in state ? (state[key] as T) : defaultValue;
|
||||
|
||||
const setValue = useCallback(
|
||||
(update: T | ((prev: T) => T)) => {
|
||||
const currentState = queryClient.getQueryData<UserState>(QUERY_KEY) ?? {};
|
||||
const currentValue = key in currentState ? (currentState[key] as T) : defaultValue;
|
||||
const newValue = typeof update === 'function' ? (update as (prev: T) => T)(currentValue) : update;
|
||||
|
||||
queryClient.setQueryData(QUERY_KEY, { ...currentState, [key]: newValue });
|
||||
|
||||
clientRef.current.patch('/workspaces', { [key]: newValue }).catch(() => { });
|
||||
},
|
||||
[key, defaultValue, queryClient],
|
||||
);
|
||||
|
||||
return { key, value, setValue, isLoaded: isSuccess };
|
||||
}
|
||||
Reference in New Issue
Block a user