feat(rename): complete searchSpace to workspace transition across frontend and backend

- Introduced a comprehensive specification for renaming `searchSpace` to `workspace` in `surfsense_web` and `surfsense_desktop`, ensuring all TypeScript identifiers, React props, and local data structures are updated.
- Implemented migration shims for persisted local state to prevent data loss during the transition.
- Updated observability metrics and IPC channels to reflect the new naming convention.
- Removed legacy `active-search-space` module and replaced it with `active-workspace` to maintain consistency.
- Ensured no behavioral changes or data loss for users during the renaming process.
This commit is contained in:
Anish Sarkar 2026-07-06 15:12:40 +05:30
parent 2a020629c5
commit a8c1fb660d
259 changed files with 5480 additions and 2285 deletions

View file

@ -13,21 +13,20 @@ interface ActivateChatThreadInput {
id: number | null;
title?: string;
url?: string;
searchSpaceId: number | string;
workspaceId: number | string;
visibility?: ChatVisibility;
hasComments?: boolean;
}
function getSearchSpaceId(searchSpaceId: number | string): number {
const parsed =
typeof searchSpaceId === "number" ? searchSpaceId : Number.parseInt(searchSpaceId, 10);
function getWorkspaceId(workspaceId: number | string): number {
const parsed = typeof workspaceId === "number" ? workspaceId : Number.parseInt(workspaceId, 10);
return Number.isNaN(parsed) ? 0 : parsed;
}
function getChatUrl(searchSpaceId: number | string, threadId: number | null): string {
function getChatUrl(workspaceId: number | string, threadId: number | null): string {
return threadId
? `/dashboard/${searchSpaceId}/new-chat/${threadId}`
: `/dashboard/${searchSpaceId}/new-chat`;
? `/dashboard/${workspaceId}/new-chat/${threadId}`
: `/dashboard/${workspaceId}/new-chat`;
}
export function useActivateChatThread() {
@ -46,22 +45,22 @@ export function useActivateChatThread() {
);
const activateChatThread = useCallback(
({ id, title, url, searchSpaceId, visibility, hasComments }: ActivateChatThreadInput) => {
const numericSearchSpaceId = getSearchSpaceId(searchSpaceId);
const chatUrl = url ?? getChatUrl(searchSpaceId, id);
({ id, title, url, workspaceId, visibility, hasComments }: ActivateChatThreadInput) => {
const numericWorkspaceId = getWorkspaceId(workspaceId);
const chatUrl = url ?? getChatUrl(workspaceId, id);
syncChatTab({
chatId: id,
title: id ? title : (title ?? "New Chat"),
chatUrl,
searchSpaceId: numericSearchSpaceId,
workspaceId: numericWorkspaceId,
...(visibility !== undefined ? { visibility } : {}),
...(hasComments !== undefined ? { hasComments } : {}),
});
setCurrentThreadMetadata({
id,
searchSpaceId: numericSearchSpaceId,
workspaceId: numericWorkspaceId,
...(visibility !== undefined ? { visibility } : {}),
...(hasComments !== undefined ? { hasComments } : {}),
});

View file

@ -80,12 +80,12 @@ export interface ActionLogSseEvent {
export function applyActionLogSse(
queryClient: QueryClient,
threadId: number,
searchSpaceId: number,
workspaceId: number,
event: ActionLogSseEvent
): void {
dbg("applyActionLogSse: incoming SSE event", {
threadId,
searchSpaceId,
workspaceId,
event,
});
queryClient.setQueryData<AgentActionListResponse>(agentActionsQueryKey(threadId), (prev) => {
@ -93,7 +93,7 @@ export function applyActionLogSse(
id: event.id,
thread_id: threadId,
user_id: null,
workspace_id: searchSpaceId,
workspace_id: workspaceId,
tool_name: event.tool_name,
args: null,
result_id: null,

View file

@ -24,7 +24,7 @@ export interface EligibleModelOption {
export interface EligibleModelKind {
options: EligibleModelOption[];
/** Default selection: the search-space pref when eligible, else first option. */
/** Default selection: the workspace pref when eligible, else first option. */
defaultId: number | null;
/** O(1) id → option lookup for trigger labels (avoids per-render `.find()`). */
byId: Map<number, EligibleModelOption>;
@ -39,7 +39,7 @@ export interface AutomationEligibleModels {
/**
* Build the eligible option list for one model kind: premium globals
* followed by all BYOK/search-space models.
* followed by all BYOK/workspace models.
*/
function buildKind(
globals: ConnectionRead[] | undefined,
@ -93,7 +93,7 @@ function buildKind(
/**
* Lists the LLM / image / vision models that are eligible for automations
* (premium globals + user BYOK never free globals or Auto mode), with a
* default selection seeded from the search space's role preferences.
* default selection seeded from the workspace's role preferences.
*
* Everything is derived during render from the connection/model query atoms;
* there are no effects, so option lists/maps keep stable references.

View file

@ -5,21 +5,21 @@ import { automationsApiService } from "@/lib/apis/automations-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys";
/**
* Whether the search space's configured models are billable for automations.
* Whether the workspace's configured models are billable for automations.
*
* Automations may only run on premium global models or user-provided (BYOK)
* models; free global models and Auto mode are blocked so every run is metered
* in premium credits. Creation surfaces use this to gate their CTAs before the
* user invests effort drafting an automation that can't be saved.
*
* Keyed by search space id (not the jotai "current scope" atom) so it can be
* Keyed by workspace id (not the jotai "current scope" atom) so it can be
* used on the create route as well as the list page.
*/
export function useAutomationModelEligibility(searchSpaceId: number | undefined) {
export function useAutomationModelEligibility(workspaceId: number | undefined) {
return useQuery<ModelEligibility, Error>({
queryKey: cacheKeys.automations.modelEligibility(searchSpaceId ?? 0),
queryFn: () => automationsApiService.getModelEligibility(searchSpaceId as number),
enabled: !!searchSpaceId,
queryKey: cacheKeys.automations.modelEligibility(workspaceId ?? 0),
queryFn: () => automationsApiService.getModelEligibility(workspaceId as number),
enabled: !!workspaceId,
staleTime: 60_000,
});
}

View file

@ -3,7 +3,7 @@ import { useAtomValue } from "jotai";
import { automationsListAtom } from "@/atoms/automations/automations-query.atoms";
/**
* List automations in the active search space (first page).
* List automations in the active workspace (first page).
* Pagination knobs live in detail/list hooks below; v1 surfaces only the
* first page since automation counts are expected to be small.
*/

View file

@ -6,16 +6,16 @@ import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { queries } from "@/zero/queries";
/**
* Syncs connectors for a search space via Zero.
* Syncs connectors for a workspace via Zero.
* Returns connectors, loading state, error, and a refresh function.
*/
export function useConnectorsSync(searchSpaceId: number | string | null) {
const spaceId = searchSpaceId ? Number(searchSpaceId) : -1;
export function useConnectorsSync(workspaceId: number | string | null) {
const spaceId = workspaceId ? Number(workspaceId) : -1;
const [data, result] = useQuery(queries.connectors.bySpace({ searchSpaceId: spaceId }));
const [data, result] = useQuery(queries.connectors.bySpace({ workspaceId: spaceId }));
const connectors: SearchSourceConnector[] = useMemo(() => {
if (!searchSpaceId || !data) return [];
if (!workspaceId || !data) return [];
return data.map((c) => ({
id: c.id,
name: c.name,
@ -27,14 +27,14 @@ export function useConnectorsSync(searchSpaceId: number | string | null) {
periodic_indexing_enabled: c.periodicIndexingEnabled,
indexing_frequency_minutes: c.indexingFrequencyMinutes ?? null,
next_scheduled_at: c.nextScheduledAt ? new Date(c.nextScheduledAt).toISOString() : null,
workspace_id: c.searchSpaceId,
workspace_id: c.workspaceId,
user_id: c.userId,
created_at: c.createdAt ? new Date(c.createdAt).toISOString() : new Date().toISOString(),
}));
}, [searchSpaceId, data]);
}, [workspaceId, data]);
const loading = !searchSpaceId ? false : result.type !== "complete";
const error = !searchSpaceId ? null : null;
const loading = !workspaceId ? false : result.type !== "complete";
const error = !workspaceId ? null : null;
const refreshConnectors = async () => {};

View file

@ -15,13 +15,13 @@ const SEARCH_SCROLL_SIZE = 5;
* pagination via skip/page_size, and staleness detection
* so fast typing never renders stale results.
*
* @param searchSpaceId - The search space to search within
* @param workspaceId - The workspace to search within
* @param query - The debounced search query
* @param activeTypes - Document types to filter by
* @param enabled - When false the hook resets and stops fetching
*/
export function useDocumentSearch(
searchSpaceId: number,
workspaceId: number,
query: string,
activeTypes: DocumentTypeEnum[],
enabled: boolean
@ -40,7 +40,7 @@ export function useDocumentSearch(
// biome-ignore lint/correctness/useExhaustiveDependencies: activeTypesKey serializes activeTypes
useEffect(() => {
if (!isActive || !searchSpaceId) {
if (!isActive || !workspaceId) {
setDocuments([]);
setHasMore(false);
setError(false);
@ -56,7 +56,7 @@ export function useDocumentSearch(
documentsApiService
.searchDocuments({
queryParams: {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
page: 0,
page_size: SEARCH_INITIAL_SIZE,
title: query.trim(),
@ -81,7 +81,7 @@ export function useDocumentSearch(
return () => {
cancelled = true;
};
}, [query, searchSpaceId, isActive, activeTypesKey]);
}, [query, workspaceId, isActive, activeTypesKey]);
// biome-ignore lint/correctness/useExhaustiveDependencies: activeTypesKey serializes activeTypes
const loadMore = useCallback(async () => {
@ -91,7 +91,7 @@ export function useDocumentSearch(
try {
const response = await documentsApiService.searchDocuments({
queryParams: {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
skip: apiLoadedRef.current,
page_size: SEARCH_SCROLL_SIZE,
title: query.trim(),
@ -108,7 +108,7 @@ export function useDocumentSearch(
} finally {
setLoadingMore(false);
}
}, [loadingMore, isActive, hasMore, searchSpaceId, query, activeTypesKey]);
}, [loadingMore, isActive, hasMore, workspaceId, query, activeTypesKey]);
const removeItems = useCallback((ids: number[]) => {
const idSet = new Set(ids);

View file

@ -18,7 +18,7 @@ interface UseDocumentsProcessingOptions {
}
/**
* Returns the processing status of documents in the search space:
* Returns the processing status of documents in the workspace:
* - "processing" docs are queued or actively being prepared for search
* - "background_sync" existing docs are being refreshed in the background
* - "error" nothing processing, but failed docs exist (show red icon)
@ -26,17 +26,17 @@ interface UseDocumentsProcessingOptions {
* - "idle" nothing noteworthy (show normal icon)
*/
export function useDocumentsProcessing(
searchSpaceId: number | null,
workspaceId: number | null,
{ hasPeriodicSyncEnabled = false }: UseDocumentsProcessingOptions = {}
): DocumentsProcessingStatus {
const [status, setStatus] = useState<DocumentsProcessingStatus>("idle");
const wasProcessingRef = useRef(false);
const successTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [documents] = useQuery(queries.documents.bySpace({ searchSpaceId: searchSpaceId ?? -1 }));
const [documents] = useQuery(queries.documents.bySpace({ workspaceId: workspaceId ?? -1 }));
useEffect(() => {
if (!searchSpaceId || !documents) return;
if (!workspaceId || !documents) return;
const clearSuccessTimer = () => {
if (successTimerRef.current) {
@ -95,7 +95,7 @@ export function useDocumentsProcessing(
} else {
setStatus("idle");
}
}, [searchSpaceId, documents, hasPeriodicSyncEnabled]);
}, [workspaceId, documents, hasPeriodicSyncEnabled]);
useEffect(() => {
return () => {

View file

@ -63,7 +63,7 @@ const SCROLL_PAGE_SIZE = 5;
* 4. Server-side sorting via sort_by + sort_order params
*/
export function useDocuments(
searchSpaceId: number | null,
workspaceId: number | null,
typeFilter: DocumentTypeEnum[] = EMPTY_TYPE_FILTER,
sortBy: DocumentSortBy = "created_at",
sortOrder: SortOrder = "desc"
@ -116,7 +116,7 @@ export function useDocuments(
// EFFECT 1: Fetch first page + type counts when params change
// biome-ignore lint/correctness/useExhaustiveDependencies: typeFilterKey serializes typeFilter
useEffect(() => {
if (!searchSpaceId) return;
if (!workspaceId) return;
let cancelled = false;
@ -142,7 +142,7 @@ export function useDocuments(
const [docsResponse, countsResponse] = await Promise.all([
documentsApiService.getDocuments({
queryParams: {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
page: 0,
page_size: INITIAL_PAGE_SIZE,
...(typeFilter.length > 0 && { document_types: typeFilter }),
@ -151,7 +151,7 @@ export function useDocuments(
},
}),
documentsApiService.getDocumentTypeCounts({
queryParams: { workspace_id: searchSpaceId },
queryParams: { workspace_id: workspaceId },
}),
]);
@ -179,15 +179,13 @@ export function useDocuments(
return () => {
cancelled = true;
};
}, [searchSpaceId, typeFilterKey, sortBy, sortOrder, populateUserCache, apiToDisplayDoc]);
}, [workspaceId, typeFilterKey, sortBy, sortOrder, populateUserCache, apiToDisplayDoc]);
// EFFECT 2: Zero real-time sync for document updates
const [zeroDocuments] = useQuery(
queries.documents.bySpace({ searchSpaceId: searchSpaceId ?? -1 })
);
const [zeroDocuments] = useQuery(queries.documents.bySpace({ workspaceId: workspaceId ?? -1 }));
useEffect(() => {
if (!searchSpaceId || !zeroDocuments || !initialLoadDoneRef.current) return;
if (!workspaceId || !zeroDocuments || !initialLoadDoneRef.current) return;
const validItems = zeroDocuments.filter(
(doc) => doc.id != null && doc.title != null && doc.title !== ""
@ -201,7 +199,7 @@ export function useDocuments(
documentsApiService
.getDocuments({
queryParams: {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
page: 0,
page_size: 20,
},
@ -232,7 +230,7 @@ export function useDocuments(
.filter((d) => !prevIds.has(d.id))
.map((doc) => ({
id: doc.id,
workspace_id: doc.searchSpaceId,
workspace_id: doc.workspaceId,
document_type: doc.documentType,
title: doc.title,
created_by_id: doc.createdById ?? null,
@ -280,13 +278,13 @@ export function useDocuments(
}
setTypeCounts(counts);
setTotal(validItems.length);
}, [searchSpaceId, zeroDocuments, populateUserCache]);
}, [workspaceId, zeroDocuments, populateUserCache]);
// EFFECT 3: Reset on search space change
const prevSearchSpaceIdRef = useRef<number | null>(null);
// EFFECT 3: Reset on workspace change
const prevWorkspaceIdRef = useRef<number | null>(null);
useEffect(() => {
if (prevSearchSpaceIdRef.current !== null && prevSearchSpaceIdRef.current !== searchSpaceId) {
if (prevWorkspaceIdRef.current !== null && prevWorkspaceIdRef.current !== workspaceId) {
setDocuments([]);
setTypeCounts({});
setTotal(0);
@ -296,19 +294,19 @@ export function useDocuments(
userCacheRef.current.clear();
emailCacheRef.current.clear();
}
prevSearchSpaceIdRef.current = searchSpaceId;
}, [searchSpaceId]);
prevWorkspaceIdRef.current = workspaceId;
}, [workspaceId]);
// Load more pages via API
// biome-ignore lint/correctness/useExhaustiveDependencies: typeFilterKey serializes typeFilter
const loadMore = useCallback(async () => {
if (loadingMore || !hasMore || !searchSpaceId) return;
if (loadingMore || !hasMore || !workspaceId) return;
setLoadingMore(true);
try {
const response = await documentsApiService.getDocuments({
queryParams: {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
skip: apiLoadedCountRef.current,
page_size: SCROLL_PAGE_SIZE,
...(typeFilter.length > 0 && { document_types: typeFilter }),
@ -336,7 +334,7 @@ export function useDocuments(
}, [
loadingMore,
hasMore,
searchSpaceId,
workspaceId,
typeFilterKey,
sortBy,
sortOrder,

View file

@ -7,7 +7,7 @@ import { documentsApiService } from "@/lib/apis/documents-api.service";
interface FileChangedEvent {
id: string;
rootFolderId: number | null;
searchSpaceId: number;
workspaceId: number;
folderPath: string;
folderName: string;
relativePath: string;
@ -29,7 +29,7 @@ interface FileEntry {
interface BatchItem {
folderPath: string;
folderName: string;
searchSpaceId: number;
workspaceId: number;
rootFolderId: number | null;
files: FileEntry[];
ackIds: string[];
@ -66,7 +66,7 @@ export function useFolderSync() {
await documentsApiService.folderUploadFiles(files, {
folder_name: batch.folderName,
workspace_id: batch.searchSpaceId,
workspace_id: batch.workspaceId,
relative_paths: addChangeFiles.map((f) => f.relativePath),
root_folder_id: batch.rootFolderId,
});
@ -75,7 +75,7 @@ export function useFolderSync() {
if (unlinkFiles.length > 0) {
await documentsApiService.folderNotifyUnlinked({
folder_name: batch.folderName,
workspace_id: batch.searchSpaceId,
workspace_id: batch.workspaceId,
root_folder_id: batch.rootFolderId,
relative_paths: unlinkFiles.map((f) => f.relativePath),
});
@ -128,7 +128,7 @@ export function useFolderSync() {
pendingByFolder.current.set(folderKey, {
folderPath: event.folderPath,
folderName: event.folderName,
searchSpaceId: event.searchSpaceId,
workspaceId: event.workspaceId,
rootFolderId: event.rootFolderId,
files: [
{

View file

@ -43,7 +43,7 @@ function getSyncCutoffDate(): string {
*/
export function useInbox(
userId: string | null,
searchSpaceId: number | null,
workspaceId: number | null,
category: NotificationCategory,
prefetchedUnread?: { total_unread: number; recent_unread: number } | null,
prefetchedUnreadReady = true
@ -63,7 +63,7 @@ export function useInbox(
// EFFECT 1: Fetch first page + unread count from API with category filter
useEffect(() => {
if (!userId || !searchSpaceId) return;
if (!userId || !workspaceId) return;
if (!prefetchedUnreadReady) return;
let cancelled = false;
@ -79,7 +79,7 @@ export function useInbox(
try {
const notificationsPromise = notificationsApiService.getNotifications({
queryParams: {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
category,
limit: INITIAL_PAGE_SIZE,
},
@ -87,7 +87,7 @@ export function useInbox(
const unreadPromise = prefetchedUnread
? Promise.resolve(prefetchedUnread)
: notificationsApiService.getUnreadCount(searchSpaceId, undefined, category);
: notificationsApiService.getUnreadCount(workspaceId, undefined, category);
const [notificationsResponse, unreadResponse] = await Promise.all([
notificationsPromise,
@ -115,20 +115,20 @@ export function useInbox(
return () => {
cancelled = true;
};
}, [userId, searchSpaceId, category, prefetchedUnread, prefetchedUnreadReady]);
}, [userId, workspaceId, category, prefetchedUnread, prefetchedUnreadReady]);
// EFFECT 2: Zero real-time sync for notification updates
const [zeroNotifications] = useQuery(queries.notifications.byUser({ userId: userId ?? "" }));
useEffect(() => {
if (!userId || !searchSpaceId || !zeroNotifications || !initialLoadDoneRef.current) return;
if (!userId || !workspaceId || !zeroNotifications || !initialLoadDoneRef.current) return;
const cutoff = new Date(getSyncCutoffDate());
const validItems = zeroNotifications.filter((item) => {
if (item.id == null) return false;
if (!categoryTypes.includes(item.type)) return false;
if (item.searchSpaceId !== null && item.searchSpaceId !== searchSpaceId) return false;
if (item.workspaceId !== null && item.workspaceId !== workspaceId) return false;
return true;
});
@ -146,7 +146,7 @@ export function useInbox(
({
id: item.id,
user_id: item.userId,
workspace_id: item.searchSpaceId ?? undefined,
workspace_id: item.workspaceId ?? undefined,
type: item.type,
title: item.title,
message: item.message,
@ -195,11 +195,11 @@ export function useInbox(
const recentUnreadCount = recentItems.filter((item) => !item.read).length;
setUnreadCount(olderUnreadOffsetRef.current + recentUnreadCount);
}
}, [userId, searchSpaceId, zeroNotifications, categoryTypes]);
}, [userId, workspaceId, zeroNotifications, categoryTypes]);
// Load more pages via API (cursor-based using before_date)
const loadMore = useCallback(async () => {
if (loadingMore || !hasMore || !userId || !searchSpaceId) return;
if (loadingMore || !hasMore || !userId || !workspaceId) return;
setLoadingMore(true);
try {
@ -208,7 +208,7 @@ export function useInbox(
const response = await notificationsApiService.getNotifications({
queryParams: {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
category,
before_date: beforeDate,
limit: SCROLL_PAGE_SIZE,
@ -228,7 +228,7 @@ export function useInbox(
} finally {
setLoadingMore(false);
}
}, [loadingMore, hasMore, userId, searchSpaceId, inboxItems, category]);
}, [loadingMore, hasMore, userId, workspaceId, inboxItems, category]);
// Mark single item as read with optimistic update
const markAsRead = useCallback(

View file

@ -13,7 +13,7 @@ export type {
LogSummary,
} from "@/contracts/types/log.types";
export function useLogs(searchSpaceId?: number, filters: LogFilters = {}) {
export function useLogs(workspaceId?: number, filters: LogFilters = {}) {
const filtersKey = JSON.stringify(filters);
// biome-ignore lint/correctness/useExhaustiveDependencies: stable serialized key used intentionally
const memoizedFilters = useMemo(() => filters, [filtersKey]);
@ -55,17 +55,17 @@ export function useLogs(searchSpaceId?: number, filters: LogFilters = {}) {
refetch,
} = useQuery({
queryKey: cacheKeys.logs.withQueryParams({
workspace_id: searchSpaceId,
workspace_id: workspaceId,
...buildQueryParams(filters ?? {}),
}),
queryFn: () =>
logsApiService.getLogs({
queryParams: {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
...buildQueryParams(filters ?? {}),
},
}),
enabled: !!searchSpaceId,
enabled: !!workspaceId,
staleTime: 3 * 60 * 1000,
});
@ -80,7 +80,7 @@ export function useLogs(searchSpaceId?: number, filters: LogFilters = {}) {
// Separate hook for log summary with smart polling support for document processing indicator UI
// Polling only happens when there are active tasks, otherwise it stops to save resources
export function useLogsSummary(
searchSpaceId: number,
workspaceId: number,
hours: number = 24,
options: { refetchInterval?: number; enablePolling?: boolean } = {}
) {
@ -92,13 +92,13 @@ export function useLogsSummary(
error,
refetch,
} = useQuery({
queryKey: cacheKeys.logs.summary(searchSpaceId),
queryKey: cacheKeys.logs.summary(workspaceId),
queryFn: () =>
logsApiService.getLogSummary({
workspace_id: searchSpaceId,
workspace_id: workspaceId,
hours: hours,
}),
enabled: !!searchSpaceId,
enabled: !!workspaceId,
staleTime: 3 * 60 * 1000,
// Always refetch on mount to show fresh processing tasks when navigating to the page
refetchOnMount: "always",

View file

@ -18,7 +18,7 @@ export interface LivePodcast {
specVersion: number;
durationSeconds: number | null;
error: string | null;
searchSpaceId: number;
workspaceId: number;
threadId: number | null;
}
@ -40,7 +40,7 @@ export function usePodcastLive(podcastId: number | undefined): UsePodcastLiveRes
specVersion: row.specVersion,
durationSeconds: row.durationSeconds ?? null,
error: row.error ?? null,
searchSpaceId: row.searchSpaceId,
workspaceId: row.workspaceId,
threadId: row.threadId ?? null,
};
}, [podcastId, row]);

View file

@ -26,9 +26,9 @@ export interface ConnectorSourceItem {
/**
* Hook to fetch search source connectors from the API
* @param lazy - If true, connectors won't be fetched on mount
* @param searchSpaceId - Optional search space ID to filter connectors
* @param workspaceId - Optional workspace ID to filter connectors
*/
export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?: number) => {
export const useSearchSourceConnectors = (lazy: boolean = false, workspaceId?: number) => {
const [connectors, setConnectors] = useState<SearchSourceConnector[]>([]);
const [isLoading, setIsLoading] = useState(!lazy); // Don't show loading initially for lazy mode
const [isLoaded, setIsLoaded] = useState(false); // Memoization flag
@ -139,26 +139,26 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
);
useEffect(() => {
// Only auto-fetch if lazy is false AND searchSpaceId is provided
// This prevents 400 errors when the hook is used without a searchSpaceId
if (!lazy && searchSpaceId !== undefined) {
fetchConnectors(searchSpaceId);
// Only auto-fetch if lazy is false AND workspaceId is provided
// This prevents 400 errors when the hook is used without a workspaceId
if (!lazy && workspaceId !== undefined) {
fetchConnectors(workspaceId);
}
}, [lazy, fetchConnectors, searchSpaceId]);
}, [lazy, fetchConnectors, workspaceId]);
// Function to refresh the connectors list
const refreshConnectors = useCallback(
async (spaceId?: number) => {
setIsLoaded(false); // Reset memoization flag to allow refetch
await fetchConnectors(spaceId !== undefined ? spaceId : searchSpaceId);
await fetchConnectors(spaceId !== undefined ? spaceId : workspaceId);
},
[fetchConnectors, searchSpaceId]
[fetchConnectors, workspaceId]
);
/**
* Create a new search source connector
* @param connectorData - The connector data (excluding workspace_id)
* @param spaceId - The search space ID to associate the connector with
* @param spaceId - The workspace ID to associate the connector with
*/
const createConnector = async (
connectorData: Omit<SearchSourceConnector, "id" | "user_id" | "created_at" | "workspace_id">,
@ -258,18 +258,18 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
};
/**
* Index content from a connector to a search space
* Index content from a connector to a workspace
*/
const indexConnector = async (
connectorId: number,
searchSpaceId: string | number,
workspaceId: string | number,
startDate?: string,
endDate?: string
) => {
try {
const response = await authenticatedFetch(
buildBackendUrl(`/api/v1/search-source-connectors/${connectorId}/index`, {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
start_date: startDate,
end_date: endDate,
}),

View file

@ -21,7 +21,7 @@ import {
updateThreadVisibility,
} from "@/lib/chat/thread-persistence";
type SearchSpaceKey = number | string;
type WorkspaceKey = number | string;
interface VisibilityVariables {
thread: ThreadRecord;
@ -58,7 +58,7 @@ interface ArchiveRollback {
archived: boolean;
}
export function useUpdateThreadVisibility(searchSpaceId: SearchSpaceKey) {
export function useUpdateThreadVisibility(workspaceId: WorkspaceKey) {
const queryClient = useQueryClient();
const currentThread = useAtomValue(currentThreadAtom);
const patchCurrentThreadMetadata = useSetAtom(patchCurrentThreadMetadataAtom);
@ -68,7 +68,7 @@ export function useUpdateThreadVisibility(searchSpaceId: SearchSpaceKey) {
onMutate: ({ thread, visibility }) => {
const previousVisibility = thread.visibility ?? "PRIVATE";
patchThreadEverywhere(queryClient, searchSpaceId, thread.id, { visibility });
patchThreadEverywhere(queryClient, workspaceId, thread.id, { visibility });
if (currentThread.id === thread.id) {
patchCurrentThreadMetadata({ id: thread.id, visibility });
}
@ -77,7 +77,7 @@ export function useUpdateThreadVisibility(searchSpaceId: SearchSpaceKey) {
},
onError: (_error, _variables, rollback) => {
if (!rollback) return;
patchThreadEverywhere(queryClient, searchSpaceId, rollback.threadId, {
patchThreadEverywhere(queryClient, workspaceId, rollback.threadId, {
visibility: rollback.visibility,
});
if (currentThread.id === rollback.threadId) {
@ -88,7 +88,7 @@ export function useUpdateThreadVisibility(searchSpaceId: SearchSpaceKey) {
}
},
onSuccess: (thread) => {
replaceThreadEverywhere(queryClient, searchSpaceId, thread);
replaceThreadEverywhere(queryClient, workspaceId, thread);
if (currentThread.id === thread.id) {
patchCurrentThreadMetadata({
id: thread.id,
@ -100,48 +100,48 @@ export function useUpdateThreadVisibility(searchSpaceId: SearchSpaceKey) {
});
}
export function useRenameThread(searchSpaceId: SearchSpaceKey) {
export function useRenameThread(workspaceId: WorkspaceKey) {
const queryClient = useQueryClient();
return useMutation<ThreadRecord, Error, RenameVariables, RenameRollback>({
mutationFn: ({ threadId, title }) => updateThread(threadId, { title }),
onMutate: ({ threadId, title, previousTitle }) => {
patchThreadEverywhere(queryClient, searchSpaceId, threadId, { title });
patchThreadEverywhere(queryClient, workspaceId, threadId, { title });
return { threadId, title: previousTitle };
},
onError: (_error, _variables, rollback) => {
if (!rollback || rollback.title === undefined) return;
patchThreadEverywhere(queryClient, searchSpaceId, rollback.threadId, {
patchThreadEverywhere(queryClient, workspaceId, rollback.threadId, {
title: rollback.title,
});
},
onSuccess: (thread) => {
replaceThreadEverywhere(queryClient, searchSpaceId, thread);
replaceThreadEverywhere(queryClient, workspaceId, thread);
},
});
}
export function useArchiveThread(searchSpaceId: SearchSpaceKey) {
export function useArchiveThread(workspaceId: WorkspaceKey) {
const queryClient = useQueryClient();
return useMutation<ThreadRecord, Error, ArchiveVariables, ArchiveRollback>({
mutationFn: ({ threadId, archived }) => updateThread(threadId, { archived }),
onMutate: ({ threadId, archived }) => {
moveThreadArchiveState(queryClient, searchSpaceId, threadId, archived);
moveThreadArchiveState(queryClient, workspaceId, threadId, archived);
return { threadId, archived: !archived };
},
onError: (_error, _variables, rollback) => {
if (!rollback) return;
moveThreadArchiveState(queryClient, searchSpaceId, rollback.threadId, rollback.archived);
moveThreadArchiveState(queryClient, workspaceId, rollback.threadId, rollback.archived);
},
onSuccess: (thread) => {
replaceThreadEverywhere(queryClient, searchSpaceId, thread);
moveThreadArchiveState(queryClient, searchSpaceId, thread.id, thread.archived);
replaceThreadEverywhere(queryClient, workspaceId, thread);
moveThreadArchiveState(queryClient, workspaceId, thread.id, thread.archived);
},
});
}
export function useDeleteThread(searchSpaceId: SearchSpaceKey) {
export function useDeleteThread(workspaceId: WorkspaceKey) {
const queryClient = useQueryClient();
const currentThread = useAtomValue(currentThreadAtom);
const resetCurrentThread = useSetAtom(resetCurrentThreadAtom);
@ -149,7 +149,7 @@ export function useDeleteThread(searchSpaceId: SearchSpaceKey) {
return useMutation<void, Error, DeleteVariables>({
mutationFn: ({ threadId }) => deleteThread(threadId),
onSuccess: (_data, { threadId }) => {
removeThreadEverywhere(queryClient, searchSpaceId, threadId);
removeThreadEverywhere(queryClient, workspaceId, threadId);
if (currentThread.id === threadId) {
resetCurrentThread();
}

View file

@ -9,11 +9,11 @@ import { queries } from "@/zero/queries";
* Updates instantly as documents are created, deleted, or change type.
*/
export function useZeroDocumentTypeCounts(
searchSpaceId: number | string | null
workspaceId: number | string | null
): Record<string, number> | undefined {
const numericId = searchSpaceId != null ? Number(searchSpaceId) : null;
const numericId = workspaceId != null ? Number(workspaceId) : null;
const [zeroDocuments] = useQuery(queries.documents.bySpace({ searchSpaceId: numericId ?? -1 }));
const [zeroDocuments] = useQuery(queries.documents.bySpace({ workspaceId: numericId ?? -1 }));
return useMemo(() => {
if (!zeroDocuments || numericId == null) return undefined;