mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-10 22:32:16 +02:00
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:
parent
2a020629c5
commit
a8c1fb660d
259 changed files with 5480 additions and 2285 deletions
|
|
@ -45,7 +45,7 @@ export function AutomationDetailContent({
|
|||
<ShieldAlert className="mx-auto h-10 w-10 text-muted-foreground" aria-hidden />
|
||||
<h2 className="mt-3 text-base font-semibold text-foreground">Access denied</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground max-w-md mx-auto">
|
||||
You don't have permission to view automations in this search space.
|
||||
You don't have permission to view automations in this workspace.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export function AutomationEditContent({ workspaceId, automationId }: AutomationE
|
|||
<ShieldAlert className="mx-auto h-10 w-10 text-muted-foreground" aria-hidden />
|
||||
<h2 className="mt-3 text-base font-semibold text-foreground">Access denied</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground max-w-md mx-auto">
|
||||
You don't have permission to edit automations in this search space.
|
||||
You don't have permission to edit automations in this workspace.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ interface AutomationsContentProps {
|
|||
|
||||
/**
|
||||
* Client orchestrator for the automations list page. Pulls the active
|
||||
* search space's first page (via ``useAutomations`` → ``automationsListAtom``)
|
||||
* workspace's first page (via ``useAutomations`` → ``automationsListAtom``)
|
||||
* and the user's permissions, then decides between empty / loading / table.
|
||||
*
|
||||
* Read access is mandatory; anything else is hidden behind RBAC. The
|
||||
|
|
@ -46,7 +46,7 @@ export function AutomationsContent({ workspaceId }: AutomationsContentProps) {
|
|||
<ShieldAlert className="mx-auto h-10 w-10 text-muted-foreground" aria-hidden />
|
||||
<h2 className="mt-3 text-base font-semibold text-foreground">Access denied</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground max-w-md mx-auto">
|
||||
You don't have permission to view automations in this search space.
|
||||
You don't have permission to view automations in this workspace.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export function AutomationsEmptyState({ workspaceId, canCreate }: AutomationsEmp
|
|||
</div>
|
||||
) : (
|
||||
<p className="mt-6 text-xs text-muted-foreground">
|
||||
You don't have permission to create automations in this search space.
|
||||
You don't have permission to create automations in this workspace.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ export function AutomationBuilderForm({
|
|||
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// Eligible models + the search-space-seeded defaults. Models are chosen per
|
||||
// Eligible models + the workspace-seeded defaults. Models are chosen per
|
||||
// automation on create; in edit mode the backend preserves the captured
|
||||
// snapshot, so the picker is create-only.
|
||||
const eligibleModels = useAutomationEligibleModels();
|
||||
|
|
|
|||
|
|
@ -235,7 +235,7 @@ export function MentionTaskInput({
|
|||
<ComposerSuggestionPopoverContent side="bottom">
|
||||
<DocumentMentionPicker
|
||||
ref={pickerRef}
|
||||
searchSpaceId={workspaceId}
|
||||
workspaceId={workspaceId}
|
||||
onSelectionChange={handleSelection}
|
||||
onDone={closePopover}
|
||||
initialSelectedDocuments={mentions}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export function DeleteAutomationDialog({
|
|||
async function handleConfirm() {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await deleteAutomation({ automationId, searchSpaceId: workspaceId });
|
||||
await deleteAutomation({ automationId, workspaceId: workspaceId });
|
||||
onDeleted?.();
|
||||
onOpenChange(false);
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export function AutomationNewContent({ workspaceId }: AutomationNewContentProps)
|
|||
<ShieldAlert className="mx-auto h-10 w-10 text-muted-foreground" aria-hidden />
|
||||
<h2 className="mt-3 text-base font-semibold text-foreground">Access denied</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground max-w-md mx-auto">
|
||||
You don't have permission to create automations in this search space.
|
||||
You don't have permission to create automations in this workspace.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export default async function ChatsPage({ params }: { params: Promise<{ workspac
|
|||
|
||||
return (
|
||||
<div className="w-full select-none">
|
||||
<AllChatsWorkspaceContent searchSpaceId={workspace_id} />
|
||||
<AllChatsWorkspaceContent workspaceId={workspace_id} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,12 +52,12 @@ export function DashboardClientLayout({
|
|||
|
||||
const isOnboardingPage = pathname?.includes("/onboard");
|
||||
const isOwner = access?.is_owner ?? false;
|
||||
const isSearchSpaceReady = activeWorkspaceId === workspaceId;
|
||||
const isWorkspaceReady = activeWorkspaceId === workspaceId;
|
||||
|
||||
useEffect(() => {
|
||||
if (isSearchSpaceReady) return;
|
||||
if (isWorkspaceReady) return;
|
||||
setHasCheckedOnboarding(false);
|
||||
}, [isSearchSpaceReady]);
|
||||
}, [isWorkspaceReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOnboardingPage) {
|
||||
|
|
@ -66,7 +66,7 @@ export function DashboardClientLayout({
|
|||
}
|
||||
|
||||
if (
|
||||
isSearchSpaceReady &&
|
||||
isWorkspaceReady &&
|
||||
!loading &&
|
||||
!accessLoading &&
|
||||
!globalConfigsLoading &&
|
||||
|
|
@ -75,7 +75,7 @@ export function DashboardClientLayout({
|
|||
!hasCheckedOnboarding
|
||||
) {
|
||||
// Onboarding is only relevant when no operator-provided
|
||||
// global_llm_config.yaml exists. When it does, search spaces inherit
|
||||
// global_llm_config.yaml exists. When it does, workspaces inherit
|
||||
// the global config and should never be forced into onboarding.
|
||||
if (globalConfigStatus?.exists) {
|
||||
setHasCheckedOnboarding(true);
|
||||
|
|
@ -102,7 +102,7 @@ export function DashboardClientLayout({
|
|||
setHasCheckedOnboarding(true);
|
||||
}
|
||||
}, [
|
||||
isSearchSpaceReady,
|
||||
isWorkspaceReady,
|
||||
loading,
|
||||
accessLoading,
|
||||
globalConfigsLoading,
|
||||
|
|
@ -153,13 +153,13 @@ export function DashboardClientLayout({
|
|||
setActiveWorkspaceIdState(activeSeacrhSpaceId);
|
||||
|
||||
// Sync to Electron store if stored value is null (first navigation)
|
||||
if (electronAPI?.getActiveSearchSpace && electronAPI.setActiveSearchSpace) {
|
||||
const setActiveSearchSpace = electronAPI.setActiveSearchSpace;
|
||||
if (electronAPI?.getActiveWorkspace && electronAPI.setActiveWorkspace) {
|
||||
const setActiveWorkspace = electronAPI.setActiveWorkspace;
|
||||
electronAPI
|
||||
.getActiveSearchSpace()
|
||||
.getActiveWorkspace()
|
||||
.then((stored: string | null) => {
|
||||
if (!stored) {
|
||||
setActiveSearchSpace(activeSeacrhSpaceId);
|
||||
setActiveWorkspace(activeSeacrhSpaceId);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
|
@ -169,7 +169,7 @@ export function DashboardClientLayout({
|
|||
// Determine if we should show loading
|
||||
const shouldShowLoading =
|
||||
!hasCheckedOnboarding &&
|
||||
(!isSearchSpaceReady ||
|
||||
(!isWorkspaceReady ||
|
||||
loading ||
|
||||
accessLoading ||
|
||||
globalConfigsLoading ||
|
||||
|
|
@ -214,7 +214,7 @@ export function DashboardClientLayout({
|
|||
return (
|
||||
<DocumentUploadDialogProvider>
|
||||
<OnboardingTour />
|
||||
<LayoutDataProvider searchSpaceId={workspaceId}>{children}</LayoutDataProvider>
|
||||
<LayoutDataProvider workspaceId={workspaceId}>{children}</LayoutDataProvider>
|
||||
</DocumentUploadDialogProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -579,7 +579,7 @@ export default function NewChatPage() {
|
|||
error,
|
||||
flow,
|
||||
context: {
|
||||
searchSpaceId: workspaceId,
|
||||
workspaceId: workspaceId,
|
||||
threadId,
|
||||
},
|
||||
});
|
||||
|
|
@ -717,7 +717,7 @@ export default function NewChatPage() {
|
|||
data: typeof threadMessagesQuery.data;
|
||||
}>({ threadId: null, data: undefined });
|
||||
|
||||
// Reset thread-local runtime state on route/search-space changes. Data fetching
|
||||
// Reset thread-local runtime state on route/workspace changes. Data fetching
|
||||
// is handled by React Query below so the chat shell can render immediately.
|
||||
useEffect(() => {
|
||||
const nextThreadId = urlChatId > 0 ? urlChatId : null;
|
||||
|
|
@ -755,7 +755,7 @@ export default function NewChatPage() {
|
|||
chatId: thread.id,
|
||||
title: thread.title,
|
||||
chatUrl: `/dashboard/${thread.workspace_id ?? workspaceId}/new-chat/${thread.id}`,
|
||||
searchSpaceId: thread.workspace_id ?? workspaceId,
|
||||
workspaceId: thread.workspace_id ?? workspaceId,
|
||||
visibility: thread.visibility,
|
||||
hasComments: thread.has_comments ?? false,
|
||||
});
|
||||
|
|
@ -892,7 +892,7 @@ export default function NewChatPage() {
|
|||
}
|
||||
setCurrentThreadMetadata({
|
||||
id: null,
|
||||
searchSpaceId: null,
|
||||
workspaceId: null,
|
||||
visibility: null,
|
||||
hasComments: false,
|
||||
});
|
||||
|
|
@ -906,7 +906,7 @@ export default function NewChatPage() {
|
|||
|
||||
setCurrentThreadMetadata({
|
||||
id: currentThread.id,
|
||||
searchSpaceId: currentThread.workspace_id ?? workspaceId,
|
||||
workspaceId: currentThread.workspace_id ?? workspaceId,
|
||||
visibility,
|
||||
hasComments: currentThread.has_comments ?? false,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export default function OnboardPage() {
|
|||
</p>
|
||||
</div>
|
||||
<ModelProviderConnectionsPanel
|
||||
searchSpaceId={workspaceId}
|
||||
workspaceId={workspaceId}
|
||||
connections={connections}
|
||||
className="flex flex-col gap-6 text-left"
|
||||
footerAction={
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function SearchSpaceDashboardPage({
|
||||
export default async function WorkspaceDashboardPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ workspace_id: string }>;
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ import type { Role } from "@/contracts/types/roles.types";
|
|||
import { invitesApiService } from "@/lib/apis/invites-api.service";
|
||||
import { rolesApiService } from "@/lib/apis/roles-api.service";
|
||||
import { formatRelativeDate } from "@/lib/format-date";
|
||||
import { trackSearchSpaceInviteSent, trackSearchSpaceUsersViewed } from "@/lib/posthog/events";
|
||||
import { trackWorkspaceInviteSent, trackWorkspaceUsersViewed } from "@/lib/posthog/events";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
@ -229,7 +229,7 @@ export function TeamContent({ workspaceId }: TeamContentProps) {
|
|||
useEffect(() => {
|
||||
if (members.length > 0 && !membersLoading) {
|
||||
const ownerCount = members.filter((m) => m.is_owner).length;
|
||||
trackSearchSpaceUsersViewed(workspaceId, members.length, ownerCount);
|
||||
trackWorkspaceUsersViewed(workspaceId, members.length, ownerCount);
|
||||
}
|
||||
}, [members, membersLoading, workspaceId]);
|
||||
|
||||
|
|
@ -654,7 +654,7 @@ function CreateInviteDialog({
|
|||
setCreatedInvite(invite);
|
||||
|
||||
const roleName = roleId ? roles.find((r) => r.id.toString() === roleId)?.name : undefined;
|
||||
trackSearchSpaceInviteSent(workspaceId, {
|
||||
trackWorkspaceInviteSent(workspaceId, {
|
||||
roleName,
|
||||
hasExpiry: !!expiresAt,
|
||||
hasMaxUses: !!maxUses,
|
||||
|
|
|
|||
|
|
@ -226,9 +226,7 @@ export function AgentPermissionsContent() {
|
|||
}
|
||||
|
||||
if (!workspaceId) {
|
||||
return (
|
||||
<p className="text-sm text-muted-foreground">Open a search space to manage agent rules.</p>
|
||||
);
|
||||
return <p className="text-sm text-muted-foreground">Open a workspace to manage agent rules.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -13,15 +13,15 @@ import {
|
|||
import { Separator } from "@/components/ui/separator";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { SearchSpace } from "@/contracts/types/workspace.types";
|
||||
import type { Workspace } from "@/contracts/types/workspace.types";
|
||||
import { useElectronAPI } from "@/hooks/use-platform";
|
||||
import { searchSpacesApiService } from "@/lib/apis/workspaces-api.service";
|
||||
import { workspacesApiService } from "@/lib/apis/workspaces-api.service";
|
||||
|
||||
export function DesktopContent() {
|
||||
const api = useElectronAPI();
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [searchSpaces, setSearchSpaces] = useState<SearchSpace[]>([]);
|
||||
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
|
||||
const [activeSpaceId, setActiveSpaceId] = useState<string | null>(null);
|
||||
|
||||
const [autoLaunchEnabled, setAutoLaunchEnabled] = useState(false);
|
||||
|
|
@ -40,14 +40,14 @@ export function DesktopContent() {
|
|||
setAutoLaunchSupported(hasAutoLaunchApi);
|
||||
|
||||
Promise.all([
|
||||
api.getActiveSearchSpace?.() ?? Promise.resolve(null),
|
||||
searchSpacesApiService.getSearchSpaces(),
|
||||
api.getActiveWorkspace?.() ?? Promise.resolve(null),
|
||||
workspacesApiService.getWorkspaces(),
|
||||
hasAutoLaunchApi ? api.getAutoLaunch() : Promise.resolve(null),
|
||||
])
|
||||
.then(([spaceId, spaces, autoLaunch]) => {
|
||||
if (!mounted) return;
|
||||
setActiveSpaceId(spaceId);
|
||||
if (spaces) setSearchSpaces(spaces);
|
||||
if (spaces) setWorkspaces(spaces);
|
||||
if (autoLaunch) {
|
||||
setAutoLaunchEnabled(autoLaunch.enabled);
|
||||
setAutoLaunchHidden(autoLaunch.openAsHidden);
|
||||
|
|
@ -136,30 +136,30 @@ export function DesktopContent() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleSearchSpaceChange = (value: string) => {
|
||||
const handleWorkspaceChange = (value: string) => {
|
||||
setActiveSpaceId(value);
|
||||
api.setActiveSearchSpace?.(value);
|
||||
toast.success("Default search space updated");
|
||||
api.setActiveWorkspace?.(value);
|
||||
toast.success("Default workspace updated");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 md:gap-6">
|
||||
<section>
|
||||
<div className="pb-2 md:pb-3">
|
||||
<h2 className="text-base md:text-lg font-semibold">Default Search Space</h2>
|
||||
<h2 className="text-base md:text-lg font-semibold">Default Workspace</h2>
|
||||
<p className="text-xs md:text-sm text-muted-foreground">
|
||||
Choose which search space General Assist, Screenshot Assist, and Quick Assist use by
|
||||
Choose which workspace General Assist, Screenshot Assist, and Quick Assist use by
|
||||
default.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
{searchSpaces.length > 0 ? (
|
||||
<Select value={activeSpaceId ?? undefined} onValueChange={handleSearchSpaceChange}>
|
||||
{workspaces.length > 0 ? (
|
||||
<Select value={activeSpaceId ?? undefined} onValueChange={handleWorkspaceChange}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select a search space" />
|
||||
<SelectValue placeholder="Select a workspace" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{searchSpaces.map((space) => (
|
||||
{workspaces.map((space) => (
|
||||
<SelectItem key={space.id} value={String(space.id)}>
|
||||
{space.name}
|
||||
</SelectItem>
|
||||
|
|
@ -167,9 +167,7 @@ export function DesktopContent() {
|
|||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No search spaces found. Create one first.
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">No workspaces found. Create one first.</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ import {
|
|||
} from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import type { SearchSpace } from "@/contracts/types/workspace.types";
|
||||
import { searchSpacesApiService } from "@/lib/apis/workspaces-api.service";
|
||||
import type { Workspace } from "@/contracts/types/workspace.types";
|
||||
import { workspacesApiService } from "@/lib/apis/workspaces-api.service";
|
||||
import { authenticatedFetch } from "@/lib/auth-fetch";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -79,7 +79,7 @@ export function MessagingChannelsContent() {
|
|||
const workspaceId = Number(params.workspace_id);
|
||||
const [gatewayConfig, setGatewayConfig] = useState<GatewayConfigState>(null);
|
||||
const [connections, setConnections] = useState<GatewayConnection[]>([]);
|
||||
const [searchSpaces, setSearchSpaces] = useState<SearchSpace[]>([]);
|
||||
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
|
||||
const [pairing, setPairing] = useState<Pairing | null>(null);
|
||||
const [pairingPlatform, setPairingPlatform] = useState<PairingPlatform | null>(null);
|
||||
const [baileysHealth, setBaileysHealth] = useState<BaileysHealth | null>(null);
|
||||
|
|
@ -114,11 +114,11 @@ export function MessagingChannelsContent() {
|
|||
const refresh = useCallback(async () => {
|
||||
const [nextConnections, spaces, nextGatewayConfig] = await Promise.all([
|
||||
fetchConnections(),
|
||||
searchSpacesApiService.getSearchSpaces(),
|
||||
workspacesApiService.getWorkspaces(),
|
||||
fetchGatewayConfig(),
|
||||
]);
|
||||
setConnections(nextConnections);
|
||||
setSearchSpaces(spaces);
|
||||
setWorkspaces(spaces);
|
||||
setGatewayConfig(nextGatewayConfig);
|
||||
}, [fetchConnections, fetchGatewayConfig]);
|
||||
|
||||
|
|
@ -210,10 +210,7 @@ export function MessagingChannelsContent() {
|
|||
await refreshPlatform(connection.platform as GatewayPlatform);
|
||||
}
|
||||
|
||||
async function updateConnectionSearchSpace(
|
||||
connection: GatewayConnection,
|
||||
nextWorkspaceId: string
|
||||
) {
|
||||
async function updateConnectionWorkspace(connection: GatewayConnection, nextWorkspaceId: string) {
|
||||
const previousConnections = connections;
|
||||
const parsedWorkspaceId = Number(nextWorkspaceId);
|
||||
const targetKey = connectionKey(connection);
|
||||
|
|
@ -324,14 +321,14 @@ export function MessagingChannelsContent() {
|
|||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Select
|
||||
value={String(connection.workspace_id)}
|
||||
onValueChange={(value) => updateConnectionSearchSpace(connection, value)}
|
||||
disabled={searchSpaces.length === 0}
|
||||
onValueChange={(value) => updateConnectionWorkspace(connection, value)}
|
||||
disabled={workspaces.length === 0}
|
||||
>
|
||||
<SelectTrigger className="h-8 min-w-[180px] flex-1 text-xs">
|
||||
<SelectValue placeholder="Select search space" />
|
||||
<SelectValue placeholder="Select workspace" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{searchSpaces.map((space) => (
|
||||
{workspaces.map((space) => (
|
||||
<SelectItem key={space.id} value={String(space.id)}>
|
||||
{space.name}
|
||||
</SelectItem>
|
||||
|
|
@ -409,7 +406,7 @@ export function MessagingChannelsContent() {
|
|||
<AlertDescription>
|
||||
<p>
|
||||
Soon you'll be able to connect WhatsApp, Telegram, Slack, and Discord to your
|
||||
SurfSense agent so you can ask questions, route messages to search spaces, and get
|
||||
SurfSense agent so you can ask questions, route messages to workspaces, and get
|
||||
answers from your knowledge base without leaving your chat app.
|
||||
</p>
|
||||
</AlertDescription>
|
||||
|
|
|
|||
|
|
@ -9,25 +9,20 @@ import { useCallback, useMemo, useState } from "react";
|
|||
import { Separator } from "@/components/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type SearchSpaceSettingsTab =
|
||||
| "general"
|
||||
| "models"
|
||||
| "team-roles"
|
||||
| "prompts"
|
||||
| "public-links";
|
||||
export type WorkspaceSettingsTab = "general" | "models" | "team-roles" | "prompts" | "public-links";
|
||||
|
||||
const DEFAULT_TAB: SearchSpaceSettingsTab = "general";
|
||||
const DEFAULT_TAB: WorkspaceSettingsTab = "general";
|
||||
|
||||
interface SearchSpaceSettingsLayoutShellProps {
|
||||
interface WorkspaceSettingsLayoutShellProps {
|
||||
workspaceId: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function SearchSpaceSettingsLayoutShell({
|
||||
export function WorkspaceSettingsLayoutShell({
|
||||
workspaceId,
|
||||
children,
|
||||
}: SearchSpaceSettingsLayoutShellProps) {
|
||||
const t = useTranslations("searchSpaceSettings");
|
||||
}: WorkspaceSettingsLayoutShellProps) {
|
||||
const t = useTranslations("workspaceSettings");
|
||||
const segment = useSelectedLayoutSegment();
|
||||
const [tabScrollPos, setTabScrollPos] = useState<"start" | "middle" | "end">("start");
|
||||
|
||||
|
|
@ -69,13 +64,13 @@ export function SearchSpaceSettingsLayoutShell({
|
|||
[t]
|
||||
);
|
||||
|
||||
const activeTab: SearchSpaceSettingsTab =
|
||||
const activeTab: WorkspaceSettingsTab =
|
||||
segment && navItems.some((item) => item.value === segment)
|
||||
? (segment as SearchSpaceSettingsTab)
|
||||
? (segment as WorkspaceSettingsTab)
|
||||
: DEFAULT_TAB;
|
||||
const selectedLabel = navItems.find((item) => item.value === activeTab)?.label ?? t("title");
|
||||
|
||||
const hrefFor = (tab: SearchSpaceSettingsTab) =>
|
||||
const hrefFor = (tab: WorkspaceSettingsTab) =>
|
||||
`/dashboard/${workspaceId}/workspace-settings/${tab}`;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type React from "react";
|
||||
import { use } from "react";
|
||||
import { SearchSpaceSettingsLayoutShell } from "./layout-shell";
|
||||
import { WorkspaceSettingsLayoutShell } from "./layout-shell";
|
||||
|
||||
export default function SearchSpaceSettingsLayout({
|
||||
export default function WorkspaceSettingsLayout({
|
||||
params,
|
||||
children,
|
||||
}: {
|
||||
|
|
@ -12,8 +12,8 @@ export default function SearchSpaceSettingsLayout({
|
|||
const { workspace_id } = use(params);
|
||||
|
||||
return (
|
||||
<SearchSpaceSettingsLayoutShell workspaceId={workspace_id}>
|
||||
<WorkspaceSettingsLayoutShell workspaceId={workspace_id}>
|
||||
{children}
|
||||
</SearchSpaceSettingsLayoutShell>
|
||||
</WorkspaceSettingsLayoutShell>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function SearchSpaceSettingsPage({
|
||||
export default async function WorkspaceSettingsPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ workspace_id: string }>;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import { motion } from "motion/react";
|
|||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect, useState } from "react";
|
||||
import { searchSpacesAtom } from "@/atoms/workspaces/workspace-query.atoms";
|
||||
import { CreateSearchSpaceDialog } from "@/components/layout";
|
||||
import { workspacesAtom } from "@/atoms/workspaces/workspace-query.atoms";
|
||||
import { CreateWorkspaceDialog } from "@/components/layout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
|
||||
|
|
@ -42,7 +42,7 @@ function ErrorScreen({ message }: { message: string }) {
|
|||
}
|
||||
|
||||
function EmptyState({ onCreateClick }: { onCreateClick: () => void }) {
|
||||
const t = useTranslations("searchSpace");
|
||||
const t = useTranslations("workspace");
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center gap-4 p-4">
|
||||
|
|
@ -74,26 +74,26 @@ export default function DashboardPage() {
|
|||
const router = useRouter();
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
|
||||
const { data: searchSpaces = [], isLoading, error } = useAtomValue(searchSpacesAtom);
|
||||
const { data: workspaces = [], isLoading, error } = useAtomValue(workspacesAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
|
||||
if (searchSpaces.length > 0) {
|
||||
if (workspaces.length > 0) {
|
||||
// Read the query string at the time of redirect — no subscription needed.
|
||||
// (Vercel Best Practice: rerender-defer-reads 5.2)
|
||||
const query = window.location.search;
|
||||
router.replace(`/dashboard/${searchSpaces[0].id}/new-chat${query}`);
|
||||
router.replace(`/dashboard/${workspaces[0].id}/new-chat${query}`);
|
||||
}
|
||||
}, [isLoading, searchSpaces, router]);
|
||||
}, [isLoading, workspaces, router]);
|
||||
|
||||
// Show loading while fetching or while we have spaces and are about to redirect
|
||||
const shouldShowLoading = isLoading || searchSpaces.length > 0;
|
||||
const shouldShowLoading = isLoading || workspaces.length > 0;
|
||||
|
||||
// Use global loading screen - spinner animation won't reset
|
||||
useGlobalLoadingEffect(shouldShowLoading);
|
||||
|
||||
if (error) return <ErrorScreen message={error?.message || "Failed to load search spaces"} />;
|
||||
if (error) return <ErrorScreen message={error?.message || "Failed to load workspaces"} />;
|
||||
|
||||
if (shouldShowLoading) {
|
||||
return null;
|
||||
|
|
@ -102,7 +102,7 @@ export default function DashboardPage() {
|
|||
return (
|
||||
<>
|
||||
<EmptyState onCreateClick={() => setShowCreateDialog(true)} />
|
||||
<CreateSearchSpaceDialog open={showCreateDialog} onOpenChange={setShowCreateDialog} />
|
||||
<CreateWorkspaceDialog open={showCreateDialog} onOpenChange={setShowCreateDialog} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { Separator } from "@/components/ui/separator";
|
|||
import { ShortcutKbd } from "@/components/ui/shortcut-kbd";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { useElectronAPI } from "@/hooks/use-platform";
|
||||
import { searchSpacesApiService } from "@/lib/apis/workspaces-api.service";
|
||||
import { workspacesApiService } from "@/lib/apis/workspaces-api.service";
|
||||
import { getPostLoginRedirectPath } from "@/lib/auth-utils";
|
||||
|
||||
type ShortcutKey = "generalAssist" | "quickAsk" | "screenshotAssist";
|
||||
|
|
@ -239,7 +239,7 @@ export default function DesktopLoginPage() {
|
|||
setIsGoogleRedirecting(true);
|
||||
try {
|
||||
await api?.startGoogleOAuth?.();
|
||||
await autoSetSearchSpace();
|
||||
await autoSetWorkspace();
|
||||
router.push(getPostLoginRedirectPath());
|
||||
} catch (error) {
|
||||
setIsGoogleRedirecting(false);
|
||||
|
|
@ -247,13 +247,13 @@ export default function DesktopLoginPage() {
|
|||
}
|
||||
};
|
||||
|
||||
const autoSetSearchSpace = async () => {
|
||||
const autoSetWorkspace = async () => {
|
||||
try {
|
||||
const stored = await api?.getActiveSearchSpace?.();
|
||||
const stored = await api?.getActiveWorkspace?.();
|
||||
if (stored) return;
|
||||
const spaces = await searchSpacesApiService.getSearchSpaces();
|
||||
const spaces = await workspacesApiService.getWorkspaces();
|
||||
if (spaces?.length) {
|
||||
await api?.setActiveSearchSpace?.(String(spaces[0].id));
|
||||
await api?.setActiveWorkspace?.(String(spaces[0].id));
|
||||
}
|
||||
} catch {
|
||||
// non-critical — dashboard-sync will catch it later
|
||||
|
|
@ -272,7 +272,7 @@ export default function DesktopLoginPage() {
|
|||
}
|
||||
await api.loginPassword(email, password);
|
||||
|
||||
await autoSetSearchSpace();
|
||||
await autoSetWorkspace();
|
||||
|
||||
setTimeout(() => {
|
||||
router.push(getPostLoginRedirectPath());
|
||||
|
|
|
|||
|
|
@ -34,9 +34,9 @@ import { useSession } from "@/hooks/use-session";
|
|||
import { invitesApiService } from "@/lib/apis/invites-api.service";
|
||||
import { setRedirectPath } from "@/lib/auth-utils";
|
||||
import {
|
||||
trackSearchSpaceInviteAccepted,
|
||||
trackSearchSpaceInviteDeclined,
|
||||
trackSearchSpaceUserAdded,
|
||||
trackWorkspaceInviteAccepted,
|
||||
trackWorkspaceInviteDeclined,
|
||||
trackWorkspaceUserAdded,
|
||||
} from "@/lib/posthog/events";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
|
||||
|
|
@ -97,12 +97,8 @@ export default function InviteAcceptPage() {
|
|||
setAcceptedData(result);
|
||||
|
||||
// Track invite accepted and user added events
|
||||
trackSearchSpaceInviteAccepted(
|
||||
result.workspace_id,
|
||||
result.workspace_name,
|
||||
result.role_name
|
||||
);
|
||||
trackSearchSpaceUserAdded(result.workspace_id, result.workspace_name, result.role_name);
|
||||
trackWorkspaceInviteAccepted(result.workspace_id, result.workspace_name, result.role_name);
|
||||
trackWorkspaceUserAdded(result.workspace_id, result.workspace_name, result.role_name);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Failed to accept invite");
|
||||
|
|
@ -113,7 +109,7 @@ export default function InviteAcceptPage() {
|
|||
|
||||
const handleDecline = () => {
|
||||
// Track invite declined event
|
||||
trackSearchSpaceInviteDeclined(inviteInfo?.workspace_name);
|
||||
trackWorkspaceInviteDeclined(inviteInfo?.workspace_name);
|
||||
router.push("/dashboard");
|
||||
};
|
||||
|
||||
|
|
@ -187,7 +183,7 @@ export default function InviteAcceptPage() {
|
|||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{acceptedData.workspace_name}</p>
|
||||
<p className="text-sm text-muted-foreground">Search Space</p>
|
||||
<p className="text-sm text-muted-foreground">Workspace</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -206,7 +202,7 @@ export default function InviteAcceptPage() {
|
|||
className="w-full gap-2"
|
||||
onClick={() => router.push(`/dashboard/${acceptedData.workspace_id}`)}
|
||||
>
|
||||
Go to Search Space
|
||||
Go to Workspace
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</CardFooter>
|
||||
|
|
@ -256,7 +252,7 @@ export default function InviteAcceptPage() {
|
|||
</motion.div>
|
||||
<CardTitle className="text-2xl">You're Invited!</CardTitle>
|
||||
<CardDescription>
|
||||
Sign in to join {inviteInfo?.workspace_name || "this search space"}
|
||||
Sign in to join {inviteInfo?.workspace_name || "this workspace"}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
|
|
@ -267,7 +263,7 @@ export default function InviteAcceptPage() {
|
|||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{inviteInfo?.workspace_name}</p>
|
||||
<p className="text-sm text-muted-foreground">Search Space</p>
|
||||
<p className="text-sm text-muted-foreground">Workspace</p>
|
||||
</div>
|
||||
</div>
|
||||
{inviteInfo?.role_name && (
|
||||
|
|
@ -303,7 +299,7 @@ export default function InviteAcceptPage() {
|
|||
</motion.div>
|
||||
<CardTitle className="text-2xl">You're Invited!</CardTitle>
|
||||
<CardDescription>
|
||||
Accept this invite to join {inviteInfo?.workspace_name || "this search space"}
|
||||
Accept this invite to join {inviteInfo?.workspace_name || "this workspace"}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
|
|
@ -314,7 +310,7 @@ export default function InviteAcceptPage() {
|
|||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{inviteInfo?.workspace_name}</p>
|
||||
<p className="text-sm text-muted-foreground">Search Space</p>
|
||||
<p className="text-sm text-muted-foreground">Workspace</p>
|
||||
</div>
|
||||
</div>
|
||||
{inviteInfo?.role_name && (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue