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

@ -39,7 +39,7 @@ export function AutoReloadSettings() {
const pathname = usePathname();
const searchParams = useSearchParams();
const queryClient = useQueryClient();
const searchSpaceId = getWorkspaceIdNumber(params) ?? 0;
const workspaceId = getWorkspaceIdNumber(params) ?? 0;
const [enabled, setEnabled] = useState(false);
const [thresholdInput, setThresholdInput] = useState("");
@ -79,8 +79,7 @@ export function AutoReloadSettings() {
}, [searchParams, router, pathname, queryClient]);
const setupMutation = useMutation({
mutationFn: () =>
stripeApiService.createAutoReloadSetupSession({ workspace_id: searchSpaceId }),
mutationFn: () => stripeApiService.createAutoReloadSetupSession({ workspace_id: workspaceId }),
onSuccess: (response) => {
window.location.assign(response.checkout_url);
},

View file

@ -38,7 +38,7 @@ const formatUsd = (micros: number) => {
export function BuyCreditsContent() {
const params = useParams();
const searchSpaceId = getWorkspaceIdNumber(params) ?? 0;
const workspaceId = getWorkspaceIdNumber(params) ?? 0;
const [quantity, setQuantity] = useState(1);
// Raw text of the amount field so the user can clear it while typing;
// committed back to a clamped integer on blur.
@ -177,7 +177,7 @@ export function BuyCreditsContent() {
<Button
className="w-full"
disabled={purchaseMutation.isPending}
onClick={() => purchaseMutation.mutate({ quantity, workspace_id: searchSpaceId })}
onClick={() => purchaseMutation.mutate({ quantity, workspace_id: workspaceId })}
>
{purchaseMutation.isPending ? (
<>

View file

@ -33,7 +33,7 @@ const formatRewardUsd = (micros: number) => {
export function EarnCreditsContent() {
const params = useParams();
const queryClient = useQueryClient();
const searchSpaceId = getWorkspaceIdParam(params) ?? "";
const workspaceId = getWorkspaceIdParam(params) ?? "";
useEffect(() => {
trackIncentivePageViewed();
@ -163,7 +163,7 @@ export function EarnCreditsContent() {
<p className="text-sm text-muted-foreground">Need more?</p>
{creditBuyingEnabled ? (
<Button asChild variant="link" className="text-emerald-600 dark:text-emerald-400">
<Link href={`/dashboard/${searchSpaceId}/buy-more`}>Buy credits at $1 per $1</Link>
<Link href={`/dashboard/${workspaceId}/buy-more`}>Buy credits at $1 per $1</Link>
</Button>
) : (
<p className="text-xs text-muted-foreground">

View file

@ -6,15 +6,15 @@ import { useTranslations } from "next-intl";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import {
updateSearchSpaceApiAccessMutationAtom,
updateSearchSpaceMutationAtom,
updateWorkspaceApiAccessMutationAtom,
updateWorkspaceMutationAtom,
} from "@/atoms/workspaces/workspace-mutation.atoms";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
import { Switch } from "@/components/ui/switch";
import { searchSpacesApiService } from "@/lib/apis/workspaces-api.service";
import { workspacesApiService } from "@/lib/apis/workspaces-api.service";
import { authenticatedFetch } from "@/lib/auth-fetch";
import { buildBackendUrl } from "@/lib/env-config";
import { cacheKeys } from "@/lib/query-client/cache-keys";
@ -25,23 +25,22 @@ interface GeneralSettingsManagerProps {
}
export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerProps) {
const searchSpaceId = workspaceId;
const t = useTranslations("searchSpaceSettings");
const t = useTranslations("workspaceSettings");
const tCommon = useTranslations("common");
const {
data: searchSpace,
data: workspace,
isLoading: loading,
isError,
refetch: fetchSearchSpace,
refetch: fetchWorkspace,
} = useQuery({
queryKey: cacheKeys.searchSpaces.detail(searchSpaceId.toString()),
queryFn: () => searchSpacesApiService.getSearchSpace({ id: searchSpaceId }),
enabled: !!searchSpaceId,
queryKey: cacheKeys.workspaces.detail(workspaceId.toString()),
queryFn: () => workspacesApiService.getWorkspace({ id: workspaceId }),
enabled: !!workspaceId,
});
const { mutateAsync: updateSearchSpace } = useAtomValue(updateSearchSpaceMutationAtom);
const { mutateAsync: updateSearchSpaceApiAccess } = useAtomValue(
updateSearchSpaceApiAccessMutationAtom
const { mutateAsync: updateWorkspace } = useAtomValue(updateWorkspaceMutationAtom);
const { mutateAsync: updateWorkspaceApiAccess } = useAtomValue(
updateWorkspaceApiAccessMutationAtom
);
const [name, setName] = useState("");
@ -49,16 +48,16 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
const [saving, setSaving] = useState(false);
const [savingApiAccess, setSavingApiAccess] = useState(false);
const [isExporting, setIsExporting] = useState(false);
const hasSearchSpace = !!searchSpace;
const searchSpaceName = searchSpace?.name;
const searchSpaceDescription = searchSpace?.description;
const hasWorkspace = !!workspace;
const workspaceName = workspace?.name;
const workspaceDescription = workspace?.description;
const handleExportKB = useCallback(async () => {
if (isExporting) return;
setIsExporting(true);
try {
const response = await authenticatedFetch(
buildBackendUrl(`/api/v1/workspaces/${searchSpaceId}/export`),
buildBackendUrl(`/api/v1/workspaces/${workspaceId}/export`),
{ method: "GET" }
);
if (!response.ok) {
@ -81,37 +80,37 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
} finally {
setIsExporting(false);
}
}, [searchSpaceId, isExporting]);
}, [workspaceId, isExporting]);
// Initialize state from fetched search space
// Initialize state from fetched workspace
useEffect(() => {
if (hasSearchSpace) {
setName(searchSpaceName || "");
setDescription(searchSpaceDescription || "");
if (hasWorkspace) {
setName(workspaceName || "");
setDescription(workspaceDescription || "");
}
}, [hasSearchSpace, searchSpaceName, searchSpaceDescription]);
}, [hasWorkspace, workspaceName, workspaceDescription]);
// Derive hasChanges during render
const hasChanges =
!!searchSpace &&
((searchSpace.name || "") !== name || (searchSpace.description || "") !== description);
!!workspace &&
((workspace.name || "") !== name || (workspace.description || "") !== description);
const handleSave = async () => {
try {
setSaving(true);
await updateSearchSpace({
id: searchSpaceId,
await updateWorkspace({
id: workspaceId,
data: {
name: name.trim(),
description: description.trim() || undefined,
},
});
await fetchSearchSpace();
await fetchWorkspace();
} catch (error: unknown) {
console.error("Error saving search space details:", error);
toast.error(error instanceof Error ? error.message : "Failed to save search space details");
console.error("Error saving workspace details:", error);
toast.error(error instanceof Error ? error.message : "Failed to save workspace details");
} finally {
setSaving(false);
}
@ -126,11 +125,11 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
async (enabled: boolean) => {
try {
setSavingApiAccess(true);
await updateSearchSpaceApiAccess({
id: searchSpaceId,
await updateWorkspaceApiAccess({
id: workspaceId,
api_access_enabled: enabled,
});
await fetchSearchSpace();
await fetchWorkspace();
} catch (error) {
console.error("Error updating API access:", error);
toast.error(error instanceof Error ? error.message : "Failed to update API access");
@ -138,7 +137,7 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
setSavingApiAccess(false);
}
},
[fetchSearchSpace, searchSpaceId, updateSearchSpaceApiAccess]
[fetchWorkspace, workspaceId, updateWorkspaceApiAccess]
);
if (loading) {
@ -156,7 +155,7 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
return (
<div className="flex flex-col items-center justify-center gap-3 py-8 text-center">
<p className="text-sm text-destructive">Failed to load settings.</p>
<Button variant="outline" size="sm" onClick={() => fetchSearchSpace()}>
<Button variant="outline" size="sm" onClick={() => fetchWorkspace()}>
Retry
</Button>
</div>
@ -168,9 +167,9 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
<form onSubmit={onSubmit} className="space-y-6">
<div className="flex flex-col gap-6">
<div className="space-y-2">
<Label htmlFor="search-space-name">{t("general_name_label")}</Label>
<Label htmlFor="workspace-name">{t("general_name_label")}</Label>
<Input
id="search-space-name"
id="workspace-name"
maxLength={100}
placeholder={t("general_name_placeholder")}
value={name}
@ -180,12 +179,12 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
</div>
<div className="space-y-2">
<Label htmlFor="search-space-description">
<Label htmlFor="workspace-description">
{t("general_description_label")}{" "}
<span className="text-muted-foreground font-normal">({tCommon("optional")})</span>
</Label>
<Input
id="search-space-description"
id="workspace-description"
placeholder={t("general_description_placeholder")}
value={description}
onChange={(e) => setDescription(e.target.value)}
@ -210,13 +209,11 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
<div className="border-t pt-6 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="space-y-1">
<Label htmlFor="api-access-enabled">API key access</Label>
<p className="text-xs text-muted-foreground">
Allow API keys to access this search space.
</p>
<p className="text-xs text-muted-foreground">Allow API keys to access this workspace.</p>
</div>
<Switch
id="api-access-enabled"
checked={!!searchSpace?.api_access_enabled}
checked={!!workspace?.api_access_enabled}
disabled={savingApiAccess}
onCheckedChange={handleApiAccessToggle}
/>
@ -226,7 +223,7 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
<div className="space-y-1">
<Label>Export knowledge base</Label>
<p className="text-xs text-muted-foreground">
Download all documents in this search space as a ZIP of markdown files.
Download all documents in this workspace as a ZIP of markdown files.
</p>
</div>
<Button

View file

@ -51,7 +51,6 @@ function renderAutoModeOption() {
}
export function ModelConnectionsSettings({ workspaceId }: { workspaceId: number }) {
const searchSpaceId = workspaceId;
const [{ data: globalConnections = [] }] = useAtom(globalModelConnectionsAtom);
const [{ data: connections = [] }] = useAtom(modelConnectionsAtom);
const [{ data: roles }] = useAtom(modelRolesAtom);
@ -148,7 +147,7 @@ export function ModelConnectionsSettings({ workspaceId }: { workspaceId: number
<Separator />
<ModelProviderConnectionsPanel searchSpaceId={searchSpaceId} connections={connections} />
<ModelProviderConnectionsPanel workspaceId={workspaceId} connections={connections} />
</div>
);
}

View file

@ -66,7 +66,7 @@ export function ConnectionCard({ connection }: { connection: ConnectionRead }) {
<AlertDialogTitle>Delete this provider?</AlertDialogTitle>
<AlertDialogDescription>
<span className="font-medium text-foreground">{providerLabel}</span> and all of
its models will be removed from this search space. This cannot be undone.
its models will be removed from this workspace. This cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>

View file

@ -23,7 +23,7 @@ import {
} from "./provider-metadata";
interface ModelProviderConnectionsPanelProps {
searchSpaceId: number;
workspaceId: number;
connections: ConnectionRead[];
className?: string;
addProviderTitle?: string;
@ -49,7 +49,7 @@ function toModelSelection(model: SelectableModel): ModelSelection {
}
export function ModelProviderConnectionsPanel({
searchSpaceId,
workspaceId,
connections,
className,
addProviderTitle = "Add Provider",
@ -126,7 +126,7 @@ export function ModelProviderConnectionsPanel({
// Each provider connect form builds its own credential payload; the backend
// resolver (`to_litellm`) forwards `extra.litellm_params` straight to LiteLLM.
function handleCreate(draft: ConnectionDraft) {
if (!Number.isFinite(searchSpaceId) || searchSpaceId <= 0) {
if (!Number.isFinite(workspaceId) || workspaceId <= 0) {
toast.error("Workspace is still loading. Please try again.");
return;
}
@ -143,7 +143,7 @@ export function ModelProviderConnectionsPanel({
base_url: draft.base_url,
api_key: draft.api_key,
scope: "SEARCH_SPACE" as const,
workspace_id: searchSpaceId,
workspace_id: workspaceId,
extra: draft.extra,
enabled: true,
models,
@ -170,7 +170,7 @@ export function ModelProviderConnectionsPanel({
setProvider(providerId);
setIsAddProviderOpen(true);
if (providerId === "vertex_ai") {
if (!Number.isFinite(searchSpaceId) || searchSpaceId <= 0) {
if (!Number.isFinite(workspaceId) || workspaceId <= 0) {
toast.error("Workspace is still loading. Please try again.");
return;
}
@ -181,7 +181,7 @@ export function ModelProviderConnectionsPanel({
base_url: null,
api_key: null,
scope: "SEARCH_SPACE",
workspace_id: searchSpaceId,
workspace_id: workspaceId,
extra: {},
enabled: true,
models: [],
@ -194,7 +194,7 @@ export function ModelProviderConnectionsPanel({
}
function refreshConnectModels(draft: ConnectionDraft) {
if (!Number.isFinite(searchSpaceId) || searchSpaceId <= 0) {
if (!Number.isFinite(workspaceId) || workspaceId <= 0) {
toast.error("Workspace is still loading. Please try again.");
return;
}
@ -205,7 +205,7 @@ export function ModelProviderConnectionsPanel({
base_url: draft.base_url,
api_key: draft.api_key,
scope: "SEARCH_SPACE",
workspace_id: searchSpaceId,
workspace_id: workspaceId,
extra: draft.extra,
enabled: true,
models: [],

View file

@ -5,13 +5,13 @@ import { useAtomValue } from "jotai";
import { AlertTriangle, Info } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { updateSearchSpaceMutationAtom } from "@/atoms/workspaces/workspace-mutation.atoms";
import { updateWorkspaceMutationAtom } from "@/atoms/workspaces/workspace-mutation.atoms";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
import { Textarea } from "@/components/ui/textarea";
import { searchSpacesApiService } from "@/lib/apis/workspaces-api.service";
import { workspacesApiService } from "@/lib/apis/workspaces-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys";
import { Spinner } from "../ui/spinner";
@ -20,36 +20,35 @@ interface PromptConfigManagerProps {
}
export function PromptConfigManager({ workspaceId }: PromptConfigManagerProps) {
const searchSpaceId = workspaceId;
const { data: searchSpace, isLoading: loading } = useQuery({
queryKey: cacheKeys.searchSpaces.detail(searchSpaceId.toString()),
queryFn: () => searchSpacesApiService.getSearchSpace({ id: searchSpaceId }),
enabled: !!searchSpaceId,
const { data: workspace, isLoading: loading } = useQuery({
queryKey: cacheKeys.workspaces.detail(workspaceId.toString()),
queryFn: () => workspacesApiService.getWorkspace({ id: workspaceId }),
enabled: !!workspaceId,
});
const { mutateAsync: updateSearchSpace, isPending: isSaving } = useAtomValue(
updateSearchSpaceMutationAtom
const { mutateAsync: updateWorkspace, isPending: isSaving } = useAtomValue(
updateWorkspaceMutationAtom
);
const [customInstructions, setCustomInstructions] = useState("");
const hasSearchSpace = !!searchSpace;
const searchSpaceInstructions = searchSpace?.qna_custom_instructions;
const hasWorkspace = !!workspace;
const workspaceInstructions = workspace?.qna_custom_instructions;
// Initialize state from fetched search space
// Initialize state from fetched workspace
useEffect(() => {
if (hasSearchSpace) {
setCustomInstructions(searchSpaceInstructions || "");
if (hasWorkspace) {
setCustomInstructions(workspaceInstructions || "");
}
}, [hasSearchSpace, searchSpaceInstructions]);
}, [hasWorkspace, workspaceInstructions]);
// Derive hasChanges during render
const hasChanges =
!!searchSpace && (searchSpace.qna_custom_instructions || "") !== customInstructions;
!!workspace && (workspace.qna_custom_instructions || "") !== customInstructions;
const handleSave = async () => {
try {
await updateSearchSpace({
id: searchSpaceId,
await updateWorkspace({
id: workspaceId,
data: { qna_custom_instructions: customInstructions.trim() || "" },
});
toast.success("System instructions saved successfully");
@ -98,8 +97,8 @@ export function PromptConfigManager({ workspaceId }: PromptConfigManagerProps) {
<Alert>
<Info />
<AlertDescription>
System instructions apply to all AI interactions in this search space. They guide how the
AI responds, its tone, focus areas, and behavior patterns.
System instructions apply to all AI interactions in this workspace. They guide how the AI
responds, its tone, focus areas, and behavior patterns.
</AlertDescription>
</Alert>
@ -112,7 +111,7 @@ export function PromptConfigManager({ workspaceId }: PromptConfigManagerProps) {
</h3>
<p className="text-xs md:text-sm text-muted-foreground">
Provide specific guidelines for how you want the AI to respond. These instructions
will be applied to all answers in this search space.
will be applied to all answers in this workspace.
</p>
</div>
<div className="space-y-3 md:space-y-4">

View file

@ -160,7 +160,7 @@ const CATEGORY_CONFIG: Record<
settings: {
label: "Settings",
icon: Settings,
description: "Manage search space settings",
description: "Manage workspace settings",
order: 10,
},
public_sharing: {
@ -172,7 +172,7 @@ const CATEGORY_CONFIG: Record<
general: {
label: "General",
icon: SlidersHorizontal,
description: "General search space permissions",
description: "General workspace permissions",
order: 12,
},
};
@ -270,7 +270,6 @@ type PermissionWithDescription = PermissionInfo;
// ============ Roles Manager (for Settings page) ============
export function RolesManager({ workspaceId }: { workspaceId: number }) {
const searchSpaceId = workspaceId;
const { data: access = null } = useAtomValue(myAccessAtom);
const hasPermission = useCallback(
@ -279,9 +278,9 @@ export function RolesManager({ workspaceId }: { workspaceId: number }) {
);
const { data: roles = [], isLoading: rolesLoading } = useQuery({
queryKey: cacheKeys.roles.all(searchSpaceId.toString()),
queryFn: () => rolesApiService.getRoles({ workspace_id: searchSpaceId }),
enabled: !!searchSpaceId,
queryKey: cacheKeys.roles.all(workspaceId.toString()),
queryFn: () => rolesApiService.getRoles({ workspace_id: workspaceId }),
enabled: !!workspaceId,
});
const { data: permissionsData } = useAtomValue(permissionsAtom);
@ -312,36 +311,36 @@ export function RolesManager({ workspaceId }: { workspaceId: number }) {
}
): Promise<Role> => {
const request: UpdateRoleRequest = {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
role_id: roleId,
data: data,
};
return await updateRole(request);
},
[updateRole, searchSpaceId]
[updateRole, workspaceId]
);
const handleDeleteRole = useCallback(
async (roleId: number): Promise<boolean> => {
const request: DeleteRoleRequest = {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
role_id: roleId,
};
await deleteRole(request);
return true;
},
[deleteRole, searchSpaceId]
[deleteRole, workspaceId]
);
const handleCreateRole = useCallback(
async (roleData: CreateRoleRequest["data"]): Promise<Role> => {
const request: CreateRoleRequest = {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
data: roleData,
};
return await createRole(request);
},
[createRole, searchSpaceId]
[createRole, workspaceId]
);
return (
@ -885,7 +884,7 @@ function CreateRoleDialog({
<DialogHeader className="px-5 pt-5 pb-4 shrink-0">
<DialogTitle className="text-lg">Create Custom Role</DialogTitle>
<DialogDescription className="text-sm text-muted-foreground">
Define permissions for a new role in this search space
Define permissions for a new role in this workspace
</DialogDescription>
</DialogHeader>
<div className="flex-1 min-h-0 overflow-y-auto">