mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-06 22:12:12 +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
|
|
@ -543,7 +543,7 @@ def record_kb_search_duration(
|
|||
_record(
|
||||
_kb_search_duration(),
|
||||
duration_ms,
|
||||
{"search_space.id": workspace_id, "search.surface": surface},
|
||||
{"workspace.id": workspace_id, "search.surface": surface},
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ def kb_search_span(
|
|||
"""Span around knowledge-base search routines."""
|
||||
attrs: dict[str, Any] = {}
|
||||
if workspace_id is not None:
|
||||
attrs["search_space.id"] = int(workspace_id)
|
||||
attrs["workspace.id"] = int(workspace_id)
|
||||
if query_chars is not None:
|
||||
attrs["query.chars"] = int(query_chars)
|
||||
if extra:
|
||||
|
|
@ -303,7 +303,7 @@ def chat_request_span(
|
|||
if chat_id is not None:
|
||||
attrs["chat.id"] = int(chat_id)
|
||||
if workspace_id is not None:
|
||||
attrs["search_space.id"] = int(workspace_id)
|
||||
attrs["workspace.id"] = int(workspace_id)
|
||||
if flow:
|
||||
attrs["chat.flow"] = flow
|
||||
if request_id:
|
||||
|
|
|
|||
|
|
@ -112,12 +112,12 @@ const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
|
|||
}));
|
||||
|
||||
const token = await storage.get("token");
|
||||
const search_space_id = parseInt(await storage.get("search_space_id"), 10);
|
||||
const workspace_id = parseInt(await storage.get("workspace_id"), 10);
|
||||
|
||||
const toSend = {
|
||||
document_type: "EXTENSION",
|
||||
content: content,
|
||||
search_space_id: search_space_id,
|
||||
workspace_id: workspace_id,
|
||||
};
|
||||
|
||||
console.log("toSend", toSend);
|
||||
|
|
|
|||
|
|
@ -106,12 +106,12 @@ const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
|
|||
}));
|
||||
|
||||
const token = await storage.get("token");
|
||||
const search_space_id = parseInt(await storage.get("search_space_id"), 10);
|
||||
const workspace_id = parseInt(await storage.get("workspace_id"), 10);
|
||||
|
||||
const toSend = {
|
||||
document_type: "EXTENSION",
|
||||
content: content,
|
||||
search_space_id: search_space_id,
|
||||
workspace_id: workspace_id,
|
||||
};
|
||||
|
||||
const requestOptions = {
|
||||
|
|
|
|||
7
surfsense_browser_extension/pnpm-workspace.yaml
Normal file
7
surfsense_browser_extension/pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
allowBuilds:
|
||||
'@parcel/watcher': set this to true or false
|
||||
'@swc/core': set this to true or false
|
||||
esbuild: set this to true or false
|
||||
lmdb: set this to true or false
|
||||
msgpackr-extract: set this to true or false
|
||||
sharp: set this to true or false
|
||||
|
|
@ -33,6 +33,20 @@ import { getRenderedHtml } from "~utils/commons";
|
|||
import type { WebHistory } from "~utils/interfaces";
|
||||
import Loading from "./Loading";
|
||||
|
||||
// One-time migration: legacy persisted keys were `search_space` / `search_space_id`.
|
||||
// Copy them to the new `workspace` / `workspace_id` keys on first read so the
|
||||
// user's selected workspace survives the rename.
|
||||
async function migrateLegacyWorkspaceKeys(storage: Storage): Promise<void> {
|
||||
const legacyName = await storage.get("search_space");
|
||||
if (legacyName !== undefined && (await storage.get("workspace")) === undefined) {
|
||||
await storage.set("workspace", legacyName);
|
||||
}
|
||||
const legacyId = await storage.get("search_space_id");
|
||||
if (legacyId !== undefined && (await storage.get("workspace_id")) === undefined) {
|
||||
await storage.set("workspace_id", legacyId);
|
||||
}
|
||||
}
|
||||
|
||||
const HomePage = () => {
|
||||
const { toast } = useToast();
|
||||
const navigation = useNavigate();
|
||||
|
|
@ -40,11 +54,11 @@ const HomePage = () => {
|
|||
const [loading, setLoading] = useState(true);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [value, setValue] = React.useState<string>("");
|
||||
const [searchspaces, setSearchSpaces] = useState([]);
|
||||
const [workspaces, setWorkspaces] = useState([]);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkSearchSpaces = async () => {
|
||||
const checkWorkspaces = async () => {
|
||||
const storage = new Storage({ area: "local" });
|
||||
const token = await storage.get("token");
|
||||
|
||||
|
|
@ -55,7 +69,7 @@ const HomePage = () => {
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(await buildBackendUrl("/api/v1/searchspaces"), {
|
||||
const response = await fetch(await buildBackendUrl("/api/v1/workspaces"), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
}
|
||||
|
|
@ -66,7 +80,7 @@ const HomePage = () => {
|
|||
} else {
|
||||
const res = await response.json();
|
||||
console.log(res);
|
||||
setSearchSpaces(res);
|
||||
setWorkspaces(res);
|
||||
}
|
||||
} catch (error) {
|
||||
await storage.remove("token");
|
||||
|
|
@ -77,7 +91,7 @@ const HomePage = () => {
|
|||
}
|
||||
};
|
||||
|
||||
checkSearchSpaces();
|
||||
checkWorkspaces();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -98,10 +112,11 @@ const HomePage = () => {
|
|||
});
|
||||
|
||||
const storage = new Storage({ area: "local" });
|
||||
const searchspace = await storage.get("search_space");
|
||||
await migrateLegacyWorkspaceKeys(storage);
|
||||
const workspace = await storage.get("workspace");
|
||||
|
||||
if (searchspace) {
|
||||
setValue(searchspace);
|
||||
if (workspace) {
|
||||
setValue(workspace);
|
||||
}
|
||||
|
||||
await storage.set("showShadowDom", true);
|
||||
|
|
@ -262,17 +277,17 @@ const HomePage = () => {
|
|||
const saveDatamessage = async () => {
|
||||
if (value === "") {
|
||||
toast({
|
||||
title: "Select a SearchSpace !",
|
||||
title: "Select a Workspace !",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const storage = new Storage({ area: "local" });
|
||||
const search_space_id = await storage.get("search_space_id");
|
||||
const workspace_id = await storage.get("workspace_id");
|
||||
|
||||
if (!search_space_id) {
|
||||
if (!workspace_id) {
|
||||
toast({
|
||||
title: "Invalid SearchSpace selected!",
|
||||
title: "Invalid Workspace selected!",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
|
|
@ -319,15 +334,15 @@ const HomePage = () => {
|
|||
const storage = new Storage({ area: "local" });
|
||||
await storage.remove("token");
|
||||
await storage.remove("showShadowDom");
|
||||
await storage.remove("search_space");
|
||||
await storage.remove("search_space_id");
|
||||
await storage.remove("workspace");
|
||||
await storage.remove("workspace_id");
|
||||
navigation("/login");
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
} else {
|
||||
return searchspaces.length === 0 ? (
|
||||
return workspaces.length === 0 ? (
|
||||
<div className="flex min-h-screen flex-col bg-gradient-to-br from-gray-900 to-gray-800">
|
||||
<div className="flex flex-1 items-center justify-center p-4">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
|
|
@ -337,7 +352,7 @@ const HomePage = () => {
|
|||
</div>
|
||||
<h1 className="mt-4 text-3xl font-semibold tracking-tight text-white">SurfSense</h1>
|
||||
<div className="mt-4 rounded-lg border border-yellow-500/20 bg-yellow-500/10 p-4 text-yellow-300">
|
||||
<p className="text-sm">Please create a Search Space to continue</p>
|
||||
<p className="text-sm">Please create a Workspace to continue</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -390,7 +405,7 @@ const HomePage = () => {
|
|||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-700 bg-gray-800/50 p-4 backdrop-blur-sm">
|
||||
<Label className="mb-2 block text-sm font-medium text-gray-300">Search Space</Label>
|
||||
<Label className="mb-2 block text-sm font-medium text-gray-300">Workspace</Label>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
|
|
@ -399,8 +414,8 @@ const HomePage = () => {
|
|||
className="w-full justify-between border-gray-700 bg-gray-900 text-white hover:bg-gray-700"
|
||||
>
|
||||
{value
|
||||
? searchspaces.find((space) => space.name === value)?.name
|
||||
: "Select Search Space..."}
|
||||
? workspaces.find((space) => space.name === value)?.name
|
||||
: "Select Workspace..."}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
|
@ -411,23 +426,23 @@ const HomePage = () => {
|
|||
className="border-gray-700 bg-gray-900 text-gray-200"
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No search spaces found.</CommandEmpty>
|
||||
<CommandEmpty>No workspaces found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{searchspaces.map((space) => (
|
||||
{workspaces.map((space) => (
|
||||
<CommandItem
|
||||
key={space.name}
|
||||
value={space.name}
|
||||
onSelect={async (currentValue) => {
|
||||
const storage = new Storage({ area: "local" });
|
||||
if (currentValue === value) {
|
||||
await storage.set("search_space", "");
|
||||
await storage.set("search_space_id", 0);
|
||||
await storage.set("workspace", "");
|
||||
await storage.set("workspace_id", 0);
|
||||
} else {
|
||||
const selectedSpace = searchspaces.find(
|
||||
const selectedSpace = workspaces.find(
|
||||
(space) => space.name === currentValue
|
||||
);
|
||||
await storage.set("search_space", currentValue);
|
||||
await storage.set("search_space_id", selectedSpace.id);
|
||||
await storage.set("workspace", currentValue);
|
||||
await storage.set("workspace_id", selectedSpace.id);
|
||||
}
|
||||
setValue(currentValue === value ? "" : currentValue);
|
||||
setOpen(false);
|
||||
|
|
|
|||
1
surfsense_browser_extension/tsconfig.tsbuildinfo
Normal file
1
surfsense_browser_extension/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -50,8 +50,8 @@ export const IPC_CHANNELS = {
|
|||
GET_SHORTCUTS: 'shortcuts:get',
|
||||
SET_SHORTCUTS: 'shortcuts:set',
|
||||
// Active search space
|
||||
GET_ACTIVE_SEARCH_SPACE: 'search-space:get-active',
|
||||
SET_ACTIVE_SEARCH_SPACE: 'search-space:set-active',
|
||||
GET_ACTIVE_WORKSPACE: 'workspace:get-active',
|
||||
SET_ACTIVE_WORKSPACE: 'workspace:set-active',
|
||||
// Launch on system startup
|
||||
GET_AUTO_LAUNCH: 'auto-launch:get',
|
||||
SET_AUTO_LAUNCH: 'auto-launch:set',
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import {
|
|||
} from '../modules/folder-watcher';
|
||||
import { getShortcuts, setShortcuts, type ShortcutConfig } from '../modules/shortcuts';
|
||||
import { getAutoLaunchState, setAutoLaunch } from '../modules/auto-launch';
|
||||
import { getActiveSearchSpaceId, setActiveSearchSpaceId } from '../modules/active-search-space';
|
||||
import { getActiveWorkspaceId, setActiveWorkspaceId } from '../modules/active-workspace';
|
||||
import { reregisterQuickAsk } from '../modules/quick-ask';
|
||||
import { reregisterGeneralAssist, reregisterScreenshotAssist } from '../modules/tray';
|
||||
import {
|
||||
|
|
@ -205,9 +205,9 @@ export function registerIpcHandlers(): void {
|
|||
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.READ_AGENT_LOCAL_FILE_TEXT,
|
||||
async (_event, virtualPath: string, searchSpaceId?: number | null) => {
|
||||
async (_event, virtualPath: string, workspaceId?: number | null) => {
|
||||
try {
|
||||
const result = await readAgentLocalFileText(virtualPath, searchSpaceId);
|
||||
const result = await readAgentLocalFileText(virtualPath, workspaceId);
|
||||
return { ok: true, path: result.path, content: result.content };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to read local file';
|
||||
|
|
@ -218,9 +218,9 @@ export function registerIpcHandlers(): void {
|
|||
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.WRITE_AGENT_LOCAL_FILE_TEXT,
|
||||
async (_event, virtualPath: string, content: string, searchSpaceId?: number | null) => {
|
||||
async (_event, virtualPath: string, content: string, workspaceId?: number | null) => {
|
||||
try {
|
||||
const result = await writeAgentLocalFileText(virtualPath, content, searchSpaceId);
|
||||
const result = await writeAgentLocalFileText(virtualPath, content, workspaceId);
|
||||
return { ok: true, path: result.path };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to write local file';
|
||||
|
|
@ -321,10 +321,10 @@ export function registerIpcHandlers(): void {
|
|||
},
|
||||
);
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.GET_ACTIVE_SEARCH_SPACE, () => getActiveSearchSpaceId());
|
||||
ipcMain.handle(IPC_CHANNELS.GET_ACTIVE_WORKSPACE, () => getActiveWorkspaceId());
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.SET_ACTIVE_SEARCH_SPACE, (_event, id: string) =>
|
||||
setActiveSearchSpaceId(id)
|
||||
ipcMain.handle(IPC_CHANNELS.SET_ACTIVE_WORKSPACE, (_event, id: string) =>
|
||||
setActiveWorkspaceId(id)
|
||||
);
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.SET_SHORTCUTS, async (_event, config: Partial<ShortcutConfig>) => {
|
||||
|
|
@ -370,12 +370,12 @@ export function registerIpcHandlers(): void {
|
|||
};
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_GET_SETTINGS, (_event, searchSpaceId?: number | null) =>
|
||||
getAgentFilesystemSettings(searchSpaceId)
|
||||
ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_GET_SETTINGS, (_event, workspaceId?: number | null) =>
|
||||
getAgentFilesystemSettings(workspaceId)
|
||||
);
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_GET_MOUNTS, (_event, searchSpaceId?: number | null) =>
|
||||
getAgentFilesystemMounts(searchSpaceId)
|
||||
ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_GET_MOUNTS, (_event, workspaceId?: number | null) =>
|
||||
getAgentFilesystemMounts(workspaceId)
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
|
|
@ -384,7 +384,7 @@ export function registerIpcHandlers(): void {
|
|||
_event,
|
||||
options: {
|
||||
rootPath: string;
|
||||
searchSpaceId?: number | null;
|
||||
workspaceId?: number | null;
|
||||
excludePatterns?: string[] | null;
|
||||
fileExtensions?: string[] | null;
|
||||
}
|
||||
|
|
@ -397,10 +397,10 @@ export function registerIpcHandlers(): void {
|
|||
(
|
||||
_event,
|
||||
payload: {
|
||||
searchSpaceId?: number | null;
|
||||
workspaceId?: number | null;
|
||||
settings: { mode?: 'cloud' | 'desktop_local_folder'; localRootPaths?: string[] | null };
|
||||
}
|
||||
) => setAgentFilesystemSettings(payload?.searchSpaceId, payload?.settings ?? {})
|
||||
) => setAgentFilesystemSettings(payload?.workspaceId, payload?.settings ?? {})
|
||||
);
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_PICK_ROOT, () =>
|
||||
|
|
@ -415,7 +415,7 @@ export function registerIpcHandlers(): void {
|
|||
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.AGENT_FILESYSTEM_TREE_WATCH_STOP,
|
||||
(_event, searchSpaceId?: number | null) =>
|
||||
stopAgentFilesystemTreeWatch(searchSpaceId)
|
||||
(_event, workspaceId?: number | null) =>
|
||||
stopAgentFilesystemTreeWatch(workspaceId)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
const STORE_KEY = 'activeSearchSpaceId';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let store: any = null;
|
||||
|
||||
async function getStore() {
|
||||
if (!store) {
|
||||
const { default: Store } = await import('electron-store');
|
||||
store = new Store({
|
||||
name: 'active-search-space',
|
||||
defaults: { [STORE_KEY]: null as string | null },
|
||||
});
|
||||
}
|
||||
return store;
|
||||
}
|
||||
|
||||
export async function getActiveSearchSpaceId(): Promise<string | null> {
|
||||
const s = await getStore();
|
||||
return (s.get(STORE_KEY) as string | null) ?? null;
|
||||
}
|
||||
|
||||
export async function setActiveSearchSpaceId(id: string): Promise<void> {
|
||||
const s = await getStore();
|
||||
s.set(STORE_KEY, id);
|
||||
}
|
||||
35
surfsense_desktop/src/modules/active-workspace.ts
Normal file
35
surfsense_desktop/src/modules/active-workspace.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
const STORE_KEY = 'activeWorkspaceId';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let store: any = null;
|
||||
|
||||
async function getStore() {
|
||||
if (!store) {
|
||||
const { default: Store } = await import('electron-store');
|
||||
store = new Store({
|
||||
name: 'active-workspace',
|
||||
defaults: { [STORE_KEY]: null as string | null },
|
||||
});
|
||||
// One-time migration from the legacy `active-search-space` store so the
|
||||
// user's last-selected workspace survives the rename.
|
||||
if (store.get(STORE_KEY) == null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const legacy: any = new Store({
|
||||
name: 'active-search-space',
|
||||
defaults: { activeSearchSpaceId: null as string | null },
|
||||
});
|
||||
const prev = legacy.get('activeSearchSpaceId') as string | null;
|
||||
if (prev != null) store.set(STORE_KEY, prev);
|
||||
}
|
||||
}
|
||||
return store;
|
||||
}
|
||||
|
||||
export async function getActiveWorkspaceId(): Promise<string | null> {
|
||||
const s = await getStore();
|
||||
return (s.get(STORE_KEY) as string | null) ?? null;
|
||||
}
|
||||
|
||||
export async function setActiveWorkspaceId(id: string): Promise<void> {
|
||||
const s = await getStore();
|
||||
s.set(STORE_KEY, id);
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ const SAFETY_POLL_MS = 60_000;
|
|||
const EVENT_DEBOUNCE_MS = 700;
|
||||
|
||||
export type AgentFilesystemTreeWatchOptions = {
|
||||
searchSpaceId?: number | null;
|
||||
workspaceId?: number | null;
|
||||
rootPaths: string[];
|
||||
excludePatterns?: string[] | null;
|
||||
fileExtensions?: string[] | null;
|
||||
|
|
@ -17,7 +17,7 @@ export type AgentFilesystemTreeWatchOptions = {
|
|||
type TreeDirtyReason = 'watcher_event' | 'safety_poll';
|
||||
|
||||
type TreeDirtyEvent = {
|
||||
searchSpaceId: number | null;
|
||||
workspaceId: number | null;
|
||||
reason: TreeDirtyReason;
|
||||
rootPath: string;
|
||||
changedPath: string | null;
|
||||
|
|
@ -25,7 +25,7 @@ type TreeDirtyEvent = {
|
|||
};
|
||||
|
||||
type WatchSession = {
|
||||
searchSpaceId: number | null;
|
||||
workspaceId: number | null;
|
||||
optionsSignature: string;
|
||||
rootPaths: string[];
|
||||
excludePatterns: string[];
|
||||
|
|
@ -40,15 +40,15 @@ type WatchSession = {
|
|||
|
||||
const sessions = new Map<string, WatchSession>();
|
||||
|
||||
function normalizeSearchSpaceId(searchSpaceId?: number | null): number | null {
|
||||
if (typeof searchSpaceId === 'number' && Number.isFinite(searchSpaceId) && searchSpaceId > 0) {
|
||||
return searchSpaceId;
|
||||
function normalizeWorkspaceId(workspaceId?: number | null): number | null {
|
||||
if (typeof workspaceId === 'number' && Number.isFinite(workspaceId) && workspaceId > 0) {
|
||||
return workspaceId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getSessionKey(searchSpaceId?: number | null): string {
|
||||
const normalized = normalizeSearchSpaceId(searchSpaceId);
|
||||
function getSessionKey(workspaceId?: number | null): string {
|
||||
const normalized = normalizeWorkspaceId(workspaceId);
|
||||
return normalized === null ? 'default' : String(normalized);
|
||||
}
|
||||
|
||||
|
|
@ -71,13 +71,13 @@ function normalizeExtensions(value: string[] | null | undefined): string[] | nul
|
|||
}
|
||||
|
||||
function buildOptionsSignature(
|
||||
searchSpaceId: number | null,
|
||||
workspaceId: number | null,
|
||||
rootPaths: string[],
|
||||
excludePatterns: string[],
|
||||
fileExtensions: string[] | null
|
||||
): string {
|
||||
return JSON.stringify({
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
rootPaths: [...rootPaths].sort(),
|
||||
excludePatterns: [...excludePatterns].sort(),
|
||||
fileExtensions: fileExtensions ? [...fileExtensions].sort() : null,
|
||||
|
|
@ -99,10 +99,10 @@ async function buildRootSnapshotSignature(
|
|||
rootPath: string
|
||||
): Promise<string> {
|
||||
let hash = 2166136261;
|
||||
hash = hashText(`space:${session.searchSpaceId ?? 'default'}|root:${rootPath}`, hash);
|
||||
hash = hashText(`space:${session.workspaceId ?? 'default'}|root:${rootPath}`, hash);
|
||||
const files = await listAgentFilesystemFiles({
|
||||
rootPath,
|
||||
searchSpaceId: session.searchSpaceId,
|
||||
workspaceId: session.workspaceId,
|
||||
excludePatterns: session.excludePatterns,
|
||||
fileExtensions: session.fileExtensions,
|
||||
});
|
||||
|
|
@ -118,13 +118,13 @@ async function buildRootSnapshotSignature(
|
|||
}
|
||||
|
||||
function sendTreeDirtyEvent(
|
||||
searchSpaceId: number | null,
|
||||
workspaceId: number | null,
|
||||
reason: TreeDirtyReason,
|
||||
rootPath: string,
|
||||
changedPath: string | null
|
||||
): void {
|
||||
const payload: TreeDirtyEvent = {
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
reason,
|
||||
rootPath,
|
||||
changedPath,
|
||||
|
|
@ -158,7 +158,7 @@ function scheduleDirtyEmit(
|
|||
session.pendingDirtyByRoot.clear();
|
||||
for (const [pendingRootPath, payload] of pending) {
|
||||
sendTreeDirtyEvent(
|
||||
session.searchSpaceId,
|
||||
session.workspaceId,
|
||||
payload.reason,
|
||||
pendingRootPath,
|
||||
payload.changedPath
|
||||
|
|
@ -183,21 +183,21 @@ async function closeSession(session: WatchSession): Promise<void> {
|
|||
export async function startAgentFilesystemTreeWatch(
|
||||
options: AgentFilesystemTreeWatchOptions
|
||||
): Promise<{ ok: true }> {
|
||||
const searchSpaceId = normalizeSearchSpaceId(options.searchSpaceId);
|
||||
const workspaceId = normalizeWorkspaceId(options.workspaceId);
|
||||
const rootPaths = Array.from(
|
||||
new Set(normalizeList(options.rootPaths).map((rootPath) => normalizeRootPath(rootPath)))
|
||||
);
|
||||
const excludePatterns = Array.from(new Set(normalizeList(options.excludePatterns)));
|
||||
const fileExtensions = normalizeExtensions(options.fileExtensions);
|
||||
const sessionKey = getSessionKey(searchSpaceId);
|
||||
const sessionKey = getSessionKey(workspaceId);
|
||||
|
||||
if (rootPaths.length === 0) {
|
||||
await stopAgentFilesystemTreeWatch(searchSpaceId);
|
||||
await stopAgentFilesystemTreeWatch(workspaceId);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const optionsSignature = buildOptionsSignature(
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
rootPaths,
|
||||
excludePatterns,
|
||||
fileExtensions
|
||||
|
|
@ -228,7 +228,7 @@ export async function startAgentFilesystemTreeWatch(
|
|||
);
|
||||
|
||||
const session: WatchSession = {
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
optionsSignature,
|
||||
rootPaths,
|
||||
excludePatterns,
|
||||
|
|
@ -291,9 +291,9 @@ export async function startAgentFilesystemTreeWatch(
|
|||
}
|
||||
|
||||
export async function stopAgentFilesystemTreeWatch(
|
||||
searchSpaceId?: number | null
|
||||
workspaceId?: number | null
|
||||
): Promise<{ ok: true }> {
|
||||
const sessionKey = getSessionKey(searchSpaceId);
|
||||
const sessionKey = getSessionKey(workspaceId);
|
||||
const session = sessions.get(sessionKey);
|
||||
if (!session) return { ok: true };
|
||||
sessions.delete(sessionKey);
|
||||
|
|
|
|||
|
|
@ -119,9 +119,9 @@ async function normalizeLocalRootPathsCanonical(paths: unknown): Promise<string[
|
|||
return [...uniquePaths];
|
||||
}
|
||||
|
||||
function normalizeSearchSpaceKey(searchSpaceId?: number | null): string {
|
||||
if (typeof searchSpaceId === "number" && Number.isFinite(searchSpaceId) && searchSpaceId > 0) {
|
||||
return String(searchSpaceId);
|
||||
function normalizeWorkspaceKey(workspaceId?: number | null): string {
|
||||
if (typeof workspaceId === "number" && Number.isFinite(workspaceId) && workspaceId > 0) {
|
||||
return String(workspaceId);
|
||||
}
|
||||
return DEFAULT_SPACE_KEY;
|
||||
}
|
||||
|
|
@ -147,9 +147,9 @@ function getDefaultStore(): AgentFilesystemSettingsStore {
|
|||
|
||||
function getSettingsFromStore(
|
||||
store: AgentFilesystemSettingsStore,
|
||||
searchSpaceId?: number | null
|
||||
workspaceId?: number | null
|
||||
): AgentFilesystemSettings {
|
||||
const key = normalizeSearchSpaceKey(searchSpaceId);
|
||||
const key = normalizeWorkspaceKey(workspaceId);
|
||||
return store.spaces[key] ?? getDefaultSettings();
|
||||
}
|
||||
|
||||
|
|
@ -194,22 +194,22 @@ async function loadAgentFilesystemSettingsStore(): Promise<AgentFilesystemSettin
|
|||
}
|
||||
|
||||
export async function getAgentFilesystemSettings(
|
||||
searchSpaceId?: number | null
|
||||
workspaceId?: number | null
|
||||
): Promise<AgentFilesystemSettings> {
|
||||
const store = await loadAgentFilesystemSettingsStore();
|
||||
return getSettingsFromStore(store, searchSpaceId);
|
||||
return getSettingsFromStore(store, workspaceId);
|
||||
}
|
||||
|
||||
export async function setAgentFilesystemSettings(
|
||||
searchSpaceId: number | null | undefined,
|
||||
workspaceId: number | null | undefined,
|
||||
settings: {
|
||||
mode?: AgentFilesystemMode;
|
||||
localRootPaths?: string[] | null;
|
||||
}
|
||||
): Promise<AgentFilesystemSettings> {
|
||||
const store = await loadAgentFilesystemSettingsStore();
|
||||
const key = normalizeSearchSpaceKey(searchSpaceId);
|
||||
const current = getSettingsFromStore(store, searchSpaceId);
|
||||
const key = normalizeWorkspaceKey(workspaceId);
|
||||
const current = getSettingsFromStore(store, workspaceId);
|
||||
const nextMode =
|
||||
settings.mode === "cloud" || settings.mode === "desktop_local_folder"
|
||||
? settings.mode
|
||||
|
|
@ -291,7 +291,7 @@ export type LocalRootMount = {
|
|||
|
||||
export type AgentFilesystemListOptions = {
|
||||
rootPath: string;
|
||||
searchSpaceId?: number | null;
|
||||
workspaceId?: number | null;
|
||||
excludePatterns?: string[] | null;
|
||||
fileExtensions?: string[] | null;
|
||||
};
|
||||
|
|
@ -332,9 +332,9 @@ function buildRootMounts(rootPaths: string[]): LocalRootMount[] {
|
|||
}
|
||||
|
||||
export async function getAgentFilesystemMounts(
|
||||
searchSpaceId?: number | null
|
||||
workspaceId?: number | null
|
||||
): Promise<LocalRootMount[]> {
|
||||
const rootPaths = await resolveCurrentRootPaths(searchSpaceId);
|
||||
const rootPaths = await resolveCurrentRootPaths(workspaceId);
|
||||
return buildRootMounts(rootPaths);
|
||||
}
|
||||
|
||||
|
|
@ -371,7 +371,7 @@ function normalizeExcludeSet(excludePatterns: string[] | null | undefined): Set<
|
|||
export async function listAgentFilesystemFiles(
|
||||
options: AgentFilesystemListOptions
|
||||
): Promise<AgentFilesystemFileEntry[]> {
|
||||
const allowedRootPaths = await resolveCurrentRootPaths(options.searchSpaceId);
|
||||
const allowedRootPaths = await resolveCurrentRootPaths(options.workspaceId);
|
||||
const requestedRootPath = await canonicalizeRootPath(options.rootPath);
|
||||
const normalizedRequestedRoot = normalizeComparablePath(requestedRootPath);
|
||||
const allowedRoots = new Set(
|
||||
|
|
@ -474,8 +474,8 @@ function toMountedVirtualPath(mount: string, rootPath: string, absolutePath: str
|
|||
return `/${mount}${relativePath}`;
|
||||
}
|
||||
|
||||
async function resolveCurrentRootPaths(searchSpaceId?: number | null): Promise<string[]> {
|
||||
const settings = await getAgentFilesystemSettings(searchSpaceId);
|
||||
async function resolveCurrentRootPaths(workspaceId?: number | null): Promise<string[]> {
|
||||
const settings = await getAgentFilesystemSettings(workspaceId);
|
||||
if (settings.localRootPaths.length === 0) {
|
||||
throw new Error("No local filesystem roots selected");
|
||||
}
|
||||
|
|
@ -484,9 +484,9 @@ async function resolveCurrentRootPaths(searchSpaceId?: number | null): Promise<s
|
|||
|
||||
export async function readAgentLocalFileText(
|
||||
virtualPath: string,
|
||||
searchSpaceId?: number | null
|
||||
workspaceId?: number | null
|
||||
): Promise<{ path: string; content: string }> {
|
||||
const rootPaths = await resolveCurrentRootPaths(searchSpaceId);
|
||||
const rootPaths = await resolveCurrentRootPaths(workspaceId);
|
||||
const mounts = buildRootMounts(rootPaths);
|
||||
const { mount, subPath } = parseMountedVirtualPath(virtualPath, mounts);
|
||||
const rootMount = findMountByName(mounts, mount);
|
||||
|
|
@ -507,9 +507,9 @@ export async function readAgentLocalFileText(
|
|||
export async function writeAgentLocalFileText(
|
||||
virtualPath: string,
|
||||
content: string,
|
||||
searchSpaceId?: number | null
|
||||
workspaceId?: number | null
|
||||
): Promise<{ path: string }> {
|
||||
const rootPaths = await resolveCurrentRootPaths(searchSpaceId);
|
||||
const rootPaths = await resolveCurrentRootPaths(workspaceId);
|
||||
const mounts = buildRootMounts(rootPaths);
|
||||
const { mount, subPath } = parseMountedVirtualPath(virtualPath, mounts);
|
||||
const rootMount = findMountByName(mounts, mount);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import * as path from 'path';
|
|||
import * as fs from 'fs';
|
||||
import { IPC_CHANNELS } from '../ipc/channels';
|
||||
import { trackEvent } from './analytics';
|
||||
import { migrateWatchedFolderConfigs } from './migrate-watched-folders';
|
||||
|
||||
export interface WatchedFolderConfig {
|
||||
path: string;
|
||||
|
|
@ -12,7 +13,7 @@ export interface WatchedFolderConfig {
|
|||
excludePatterns: string[];
|
||||
fileExtensions: string[] | null;
|
||||
rootFolderId: number | null;
|
||||
searchSpaceId: number;
|
||||
workspaceId: number;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -27,7 +28,7 @@ type FolderSyncAction = 'add' | 'change' | 'unlink';
|
|||
export interface FolderSyncFileChangedEvent {
|
||||
id: string;
|
||||
rootFolderId: number | null;
|
||||
searchSpaceId: number;
|
||||
workspaceId: number;
|
||||
folderPath: string;
|
||||
folderName: string;
|
||||
relativePath: string;
|
||||
|
|
@ -68,6 +69,12 @@ async function getStore() {
|
|||
[STORE_KEY]: [] as WatchedFolderConfig[],
|
||||
},
|
||||
});
|
||||
// One-time read-migration: legacy persisted configs stored the workspace as
|
||||
// `searchSpaceId`. Map it to `workspaceId` and write back once so existing
|
||||
// watched folders keep their sync target after the rename.
|
||||
const raw = store.get(STORE_KEY, []) as Array<Record<string, unknown>>;
|
||||
const { configs, migrated } = migrateWatchedFolderConfigs<WatchedFolderConfig>(raw);
|
||||
if (migrated) store.set(STORE_KEY, configs);
|
||||
}
|
||||
return store;
|
||||
}
|
||||
|
|
@ -267,7 +274,7 @@ async function startWatcher(config: WatchedFolderConfig) {
|
|||
if (storedMtime === undefined) {
|
||||
sendFileChangedEvent({
|
||||
rootFolderId: config.rootFolderId,
|
||||
searchSpaceId: config.searchSpaceId,
|
||||
workspaceId: config.workspaceId,
|
||||
folderPath: config.path,
|
||||
folderName: config.name,
|
||||
relativePath: rel,
|
||||
|
|
@ -278,7 +285,7 @@ async function startWatcher(config: WatchedFolderConfig) {
|
|||
} else if (Math.abs(currentMtime - storedMtime) >= MTIME_TOLERANCE_S * 1000) {
|
||||
sendFileChangedEvent({
|
||||
rootFolderId: config.rootFolderId,
|
||||
searchSpaceId: config.searchSpaceId,
|
||||
workspaceId: config.workspaceId,
|
||||
folderPath: config.path,
|
||||
folderName: config.name,
|
||||
relativePath: rel,
|
||||
|
|
@ -295,7 +302,7 @@ async function startWatcher(config: WatchedFolderConfig) {
|
|||
if (!(rel in currentMap)) {
|
||||
sendFileChangedEvent({
|
||||
rootFolderId: config.rootFolderId,
|
||||
searchSpaceId: config.searchSpaceId,
|
||||
workspaceId: config.workspaceId,
|
||||
folderPath: config.path,
|
||||
folderName: config.name,
|
||||
relativePath: rel,
|
||||
|
|
@ -346,7 +353,7 @@ async function startWatcher(config: WatchedFolderConfig) {
|
|||
|
||||
sendFileChangedEvent({
|
||||
rootFolderId: config.rootFolderId,
|
||||
searchSpaceId: config.searchSpaceId,
|
||||
workspaceId: config.workspaceId,
|
||||
folderPath: config.path,
|
||||
folderName: config.name,
|
||||
relativePath,
|
||||
|
|
@ -403,7 +410,7 @@ export async function addWatchedFolder(
|
|||
}
|
||||
|
||||
trackEvent('desktop_folder_watch_added', {
|
||||
search_space_id: config.searchSpaceId,
|
||||
workspace_id: config.workspaceId,
|
||||
root_folder_id: config.rootFolderId,
|
||||
active: config.active,
|
||||
has_exclude_patterns: (config.excludePatterns?.length ?? 0) > 0,
|
||||
|
|
@ -431,7 +438,7 @@ export async function removeWatchedFolder(
|
|||
|
||||
if (removed) {
|
||||
trackEvent('desktop_folder_watch_removed', {
|
||||
search_space_id: removed.searchSpaceId,
|
||||
workspace_id: removed.workspaceId,
|
||||
root_folder_id: removed.rootFolderId,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { migrateWatchedFolderConfigs } from "./migrate-watched-folders.ts";
|
||||
|
||||
// Run with: node --test src/modules/migrate-watched-folders.test.ts
|
||||
test("maps legacy searchSpaceId to workspaceId and flags migration", () => {
|
||||
const { configs, migrated } = migrateWatchedFolderConfigs<{ workspaceId?: number }>([
|
||||
{ path: "/tmp/a", searchSpaceId: 42 },
|
||||
]);
|
||||
assert.equal(migrated, true);
|
||||
assert.equal(configs[0].workspaceId, 42);
|
||||
assert.equal("searchSpaceId" in (configs[0] as object), false);
|
||||
});
|
||||
|
||||
test("leaves configs that already have workspaceId untouched", () => {
|
||||
const { configs, migrated } = migrateWatchedFolderConfigs<{ workspaceId?: number }>([
|
||||
{ path: "/tmp/b", workspaceId: 5 },
|
||||
]);
|
||||
assert.equal(migrated, false);
|
||||
assert.equal(configs[0].workspaceId, 5);
|
||||
});
|
||||
23
surfsense_desktop/src/modules/migrate-watched-folders.ts
Normal file
23
surfsense_desktop/src/modules/migrate-watched-folders.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* One-time read-migration for persisted watched-folder configs: legacy configs
|
||||
* stored the workspace as `searchSpaceId`. Map it to `workspaceId` so existing
|
||||
* watched folders keep their sync target after the rename. Pure + dependency-free
|
||||
* so it can be unit-checked without loading electron-store.
|
||||
*
|
||||
* Returns the migrated configs and whether anything changed (so callers can
|
||||
* write back only when needed).
|
||||
*/
|
||||
export function migrateWatchedFolderConfigs<T>(
|
||||
raw: Array<Record<string, unknown>>
|
||||
): { configs: T[]; migrated: boolean } {
|
||||
let migrated = false;
|
||||
const configs = raw.map((c) => {
|
||||
if (c.workspaceId === undefined && c.searchSpaceId !== undefined) {
|
||||
migrated = true;
|
||||
const { searchSpaceId, ...rest } = c;
|
||||
return { ...rest, workspaceId: searchSpaceId } as unknown as T;
|
||||
}
|
||||
return c as unknown as T;
|
||||
});
|
||||
return { configs, migrated };
|
||||
}
|
||||
|
|
@ -4,14 +4,14 @@ import { IPC_CHANNELS } from '../ipc/channels';
|
|||
import { checkAccessibilityPermission, getFrontmostApp, simulateCopy, simulatePaste } from './platform';
|
||||
import { getServerOrigin } from './server';
|
||||
import { getShortcuts } from './shortcuts';
|
||||
import { getActiveSearchSpaceId } from './active-search-space';
|
||||
import { getActiveWorkspaceId } from './active-workspace';
|
||||
import { trackEvent } from './analytics';
|
||||
|
||||
let currentShortcut = '';
|
||||
let quickAskWindow: BrowserWindow | null = null;
|
||||
let pendingText = '';
|
||||
let pendingMode = '';
|
||||
let pendingSearchSpaceId: string | null = null;
|
||||
let pendingWorkspaceId: string | null = null;
|
||||
let sourceApp = '';
|
||||
let savedClipboard = '';
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ function createQuickAskWindow(x: number, y: number): BrowserWindow {
|
|||
skipTaskbar: true,
|
||||
});
|
||||
|
||||
const spaceId = pendingSearchSpaceId;
|
||||
const spaceId = pendingWorkspaceId;
|
||||
const route = spaceId ? `/dashboard/${spaceId}/new-chat` : '/dashboard';
|
||||
quickAskWindow.loadURL(`${getServerOrigin()}${route}?quickAssist=true`);
|
||||
|
||||
|
|
@ -87,7 +87,7 @@ function createQuickAskWindow(x: number, y: number): BrowserWindow {
|
|||
async function openQuickAsk(text: string): Promise<void> {
|
||||
pendingText = text;
|
||||
pendingMode = 'quick-assist';
|
||||
pendingSearchSpaceId = await getActiveSearchSpaceId();
|
||||
pendingWorkspaceId = await getActiveWorkspaceId();
|
||||
const cursor = screen.getCursorScreenPoint();
|
||||
const pos = clampToScreen(cursor.x, cursor.y, 450, 750);
|
||||
createQuickAskWindow(pos.x, pos.y);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import path from 'path';
|
|||
import { trackEvent } from './analytics';
|
||||
import { showErrorDialog } from './errors';
|
||||
import { getServerOrigin, getServerPort } from './server';
|
||||
import { setActiveSearchSpaceId } from './active-search-space';
|
||||
import { setActiveWorkspaceId } from './active-workspace';
|
||||
|
||||
const isDev = !app.isPackaged;
|
||||
const isMac = process.platform === 'darwin';
|
||||
|
|
@ -140,14 +140,14 @@ export function createMainWindow(initialPath = '/dashboard'): BrowserWindow {
|
|||
});
|
||||
|
||||
// Auto-sync active search space from URL navigation
|
||||
const syncSearchSpace = (url: string) => {
|
||||
const syncWorkspace = (url: string) => {
|
||||
const match = url.match(/\/dashboard\/(\d+)/);
|
||||
if (match) {
|
||||
setActiveSearchSpaceId(match[1]);
|
||||
setActiveWorkspaceId(match[1]);
|
||||
}
|
||||
};
|
||||
mainWindow.webContents.on('did-navigate', (_event, url) => syncSearchSpace(url));
|
||||
mainWindow.webContents.on('did-navigate-in-page', (_event, url) => syncSearchSpace(url));
|
||||
mainWindow.webContents.on('did-navigate', (_event, url) => syncWorkspace(url));
|
||||
mainWindow.webContents.on('did-navigate-in-page', (_event, url) => syncWorkspace(url));
|
||||
|
||||
if (isDev) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
|
|
|
|||
|
|
@ -74,10 +74,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
// Browse files via native dialog
|
||||
browseFiles: () => ipcRenderer.invoke(IPC_CHANNELS.BROWSE_FILES),
|
||||
readLocalFiles: (paths: string[]) => ipcRenderer.invoke(IPC_CHANNELS.READ_LOCAL_FILES, paths),
|
||||
readAgentLocalFileText: (virtualPath: string, searchSpaceId?: number | null) =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.READ_AGENT_LOCAL_FILE_TEXT, virtualPath, searchSpaceId),
|
||||
writeAgentLocalFileText: (virtualPath: string, content: string, searchSpaceId?: number | null) =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.WRITE_AGENT_LOCAL_FILE_TEXT, virtualPath, content, searchSpaceId),
|
||||
readAgentLocalFileText: (virtualPath: string, workspaceId?: number | null) =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.READ_AGENT_LOCAL_FILE_TEXT, virtualPath, workspaceId),
|
||||
writeAgentLocalFileText: (virtualPath: string, content: string, workspaceId?: number | null) =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.WRITE_AGENT_LOCAL_FILE_TEXT, virtualPath, content, workspaceId),
|
||||
|
||||
// Auth token sync across windows
|
||||
getAccessToken: () => ipcRenderer.invoke(IPC_CHANNELS.GET_ACCESS_TOKEN),
|
||||
|
|
@ -104,9 +104,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
ipcRenderer.invoke(IPC_CHANNELS.SET_AUTO_LAUNCH, { enabled, openAsHidden }),
|
||||
|
||||
// Active search space
|
||||
getActiveSearchSpace: () => ipcRenderer.invoke(IPC_CHANNELS.GET_ACTIVE_SEARCH_SPACE),
|
||||
setActiveSearchSpace: (id: string) =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.SET_ACTIVE_SEARCH_SPACE, id),
|
||||
getActiveWorkspace: () => ipcRenderer.invoke(IPC_CHANNELS.GET_ACTIVE_WORKSPACE),
|
||||
setActiveWorkspace: (id: string) =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.SET_ACTIVE_WORKSPACE, id),
|
||||
|
||||
// Analytics bridge — lets posthog-js running inside the Next.js renderer
|
||||
// mirror identify/reset/capture into the Electron main-process PostHog
|
||||
|
|
@ -118,27 +118,27 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
ipcRenderer.invoke(IPC_CHANNELS.ANALYTICS_CAPTURE, { event, properties }),
|
||||
getAnalyticsContext: () => ipcRenderer.invoke(IPC_CHANNELS.ANALYTICS_GET_CONTEXT),
|
||||
// Agent filesystem mode
|
||||
getAgentFilesystemSettings: (searchSpaceId?: number | null) =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_GET_SETTINGS, searchSpaceId),
|
||||
getAgentFilesystemMounts: (searchSpaceId?: number | null) =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_GET_MOUNTS, searchSpaceId),
|
||||
getAgentFilesystemSettings: (workspaceId?: number | null) =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_GET_SETTINGS, workspaceId),
|
||||
getAgentFilesystemMounts: (workspaceId?: number | null) =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_GET_MOUNTS, workspaceId),
|
||||
listAgentFilesystemFiles: (options: {
|
||||
rootPath: string;
|
||||
searchSpaceId?: number | null;
|
||||
workspaceId?: number | null;
|
||||
excludePatterns?: string[] | null;
|
||||
fileExtensions?: string[] | null;
|
||||
}) => ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_LIST_FILES, options),
|
||||
startAgentFilesystemTreeWatch: (options: {
|
||||
searchSpaceId?: number | null;
|
||||
workspaceId?: number | null;
|
||||
rootPaths: string[];
|
||||
excludePatterns?: string[] | null;
|
||||
fileExtensions?: string[] | null;
|
||||
}) => ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_TREE_WATCH_START, options),
|
||||
stopAgentFilesystemTreeWatch: (searchSpaceId?: number | null) =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_TREE_WATCH_STOP, searchSpaceId),
|
||||
stopAgentFilesystemTreeWatch: (workspaceId?: number | null) =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_TREE_WATCH_STOP, workspaceId),
|
||||
onAgentFilesystemTreeDirty: (
|
||||
callback: (data: {
|
||||
searchSpaceId: number | null;
|
||||
workspaceId: number | null;
|
||||
reason: 'watcher_event' | 'safety_poll';
|
||||
rootPath: string;
|
||||
changedPath: string | null;
|
||||
|
|
@ -148,7 +148,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
const listener = (
|
||||
_event: unknown,
|
||||
data: {
|
||||
searchSpaceId: number | null;
|
||||
workspaceId: number | null;
|
||||
reason: 'watcher_event' | 'safety_poll';
|
||||
rootPath: string;
|
||||
changedPath: string | null;
|
||||
|
|
@ -163,7 +163,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
setAgentFilesystemSettings: (settings: {
|
||||
mode?: "cloud" | "desktop_local_folder";
|
||||
localRootPaths?: string[] | null;
|
||||
}, searchSpaceId?: number | null) =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_SET_SETTINGS, { searchSpaceId, settings }),
|
||||
}, workspaceId?: number | null) =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_SET_SETTINGS, { workspaceId, settings }),
|
||||
pickAgentFilesystemRoot: () => ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_PICK_ROOT),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,5 +12,5 @@
|
|||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist", "scripts"]
|
||||
"exclude": ["node_modules", "dist", "scripts", "src/**/*.test.ts"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ Open **Settings → SurfSense** in Obsidian and fill in:
|
|||
| --- | --- |
|
||||
| Server URL | `https://surfsense.com` for SurfSense Cloud, or your self-hosted URL |
|
||||
| API token | Create a personal access token from the *Connectors → Obsidian* dialog or *User settings → API access* in the SurfSense web app |
|
||||
| Search space | Pick the search space this vault should sync into |
|
||||
| Search space | Pick the workspace this vault should sync into |
|
||||
| Vault name | Defaults to your Obsidian vault name; rename if you have multiple vaults |
|
||||
| Sync mode | *Auto* (recommended) or *Manual* |
|
||||
| Exclude patterns | Glob patterns of folders/files to skip (e.g. `.trash`, `_attachments`, `templates/**`) |
|
||||
|
|
@ -82,7 +82,7 @@ addendum.
|
|||
|
||||
- `vault_id`: a random UUID minted in the plugin's `data.json` on first run
|
||||
- `vault_name`: the Obsidian vault folder name
|
||||
- `search_space_id`: the SurfSense search space you picked
|
||||
- `workspace_id`: the SurfSense workspace you picked
|
||||
|
||||
**Sent per note on `/sync`, `/rename`, `/delete`:**
|
||||
|
||||
|
|
|
|||
3153
surfsense_obsidian/pnpm-lock.yaml
generated
Normal file
3153
surfsense_obsidian/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
2
surfsense_obsidian/pnpm-workspace.yaml
Normal file
2
surfsense_obsidian/pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
allowBuilds:
|
||||
esbuild: set this to true or false
|
||||
|
|
@ -7,7 +7,7 @@ import type {
|
|||
NotePayload,
|
||||
RenameAck,
|
||||
RenameItem,
|
||||
SearchSpace,
|
||||
Workspace,
|
||||
SyncAck,
|
||||
} from "./types";
|
||||
|
||||
|
|
@ -94,14 +94,14 @@ export class SurfSenseApiClient {
|
|||
return await this.request<HealthResponse>("GET", "/api/v1/obsidian/health");
|
||||
}
|
||||
|
||||
async listSearchSpaces(): Promise<SearchSpace[]> {
|
||||
const resp = await this.request<SearchSpace[] | { items: SearchSpace[] }>(
|
||||
async listWorkspaces(): Promise<Workspace[]> {
|
||||
const resp = await this.request<Workspace[] | { items: Workspace[] }>(
|
||||
"GET",
|
||||
"/api/v1/searchspaces/"
|
||||
"/api/v1/workspaces/"
|
||||
);
|
||||
if (Array.isArray(resp)) return resp;
|
||||
if (resp && Array.isArray((resp as { items?: SearchSpace[] }).items)) {
|
||||
return (resp as { items: SearchSpace[] }).items;
|
||||
if (resp && Array.isArray((resp as { items?: Workspace[] }).items)) {
|
||||
return (resp as { items: Workspace[] }).items;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
|
@ -114,7 +114,7 @@ export class SurfSenseApiClient {
|
|||
}
|
||||
|
||||
async connect(input: {
|
||||
searchSpaceId: number;
|
||||
workspaceId: number;
|
||||
vaultId: string;
|
||||
vaultName: string;
|
||||
vaultFingerprint: string;
|
||||
|
|
@ -125,7 +125,7 @@ export class SurfSenseApiClient {
|
|||
{
|
||||
vault_id: input.vaultId,
|
||||
vault_name: input.vaultName,
|
||||
search_space_id: input.searchSpaceId,
|
||||
workspace_id: input.workspaceId,
|
||||
vault_fingerprint: input.vaultFingerprint,
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ export default class SurfSensePlugin extends Plugin {
|
|||
name: "Sync current note",
|
||||
checkCallback: (checking) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file || file.extension.toLowerCase() !== "md") return false;
|
||||
if (file?.extension.toLowerCase() !== "md") return false;
|
||||
if (checking) return true;
|
||||
this.queue.enqueueUpsert(file.path);
|
||||
void this.engine.flushQueue();
|
||||
|
|
@ -186,7 +186,7 @@ export default class SurfSensePlugin extends Plugin {
|
|||
if (!changed) return;
|
||||
this.engine?.refreshStatus();
|
||||
this.notifyStatusChange();
|
||||
if (this.settings.searchSpaceId !== null) {
|
||||
if (this.settings.workspaceId !== null) {
|
||||
void this.engine.ensureConnected();
|
||||
}
|
||||
}
|
||||
|
|
@ -252,16 +252,24 @@ export default class SurfSensePlugin extends Plugin {
|
|||
}
|
||||
|
||||
async loadSettings() {
|
||||
const data = (await this.loadData()) as Partial<SurfsensePluginSettings> | null;
|
||||
// One-time migration: the workspace was previously persisted as `searchSpaceId`.
|
||||
// Destructure it out so the migrated value moves into `workspaceId` and the
|
||||
// dead key is not spread back in / re-persisted.
|
||||
const data = (await this.loadData()) as
|
||||
| (Partial<SurfsensePluginSettings> & { searchSpaceId?: number | null })
|
||||
| null;
|
||||
const { searchSpaceId: legacyWorkspaceId, ...persisted } = data ?? {};
|
||||
this.settings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
...(data ?? {}),
|
||||
queue: (data?.queue ?? []).map((i: QueueItem) => ({ ...i })),
|
||||
tombstones: { ...(data?.tombstones ?? {}) },
|
||||
includeFolders: [...(data?.includeFolders ?? [])],
|
||||
excludeFolders: [...(data?.excludeFolders ?? [])],
|
||||
excludePatterns: data?.excludePatterns?.length
|
||||
? [...data.excludePatterns]
|
||||
...persisted,
|
||||
workspaceId:
|
||||
persisted.workspaceId ?? legacyWorkspaceId ?? DEFAULT_SETTINGS.workspaceId,
|
||||
queue: (persisted.queue ?? []).map((i: QueueItem) => ({ ...i })),
|
||||
tombstones: { ...(persisted.tombstones ?? {}) },
|
||||
includeFolders: [...(persisted.includeFolders ?? [])],
|
||||
excludeFolders: [...(persisted.excludeFolders ?? [])],
|
||||
excludePatterns: persisted.excludePatterns?.length
|
||||
? [...persisted.excludePatterns]
|
||||
: [...DEFAULT_SETTINGS.excludePatterns],
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ import { normalizeFolder, parseExcludePatterns } from "./excludes";
|
|||
import { FolderSuggestModal } from "./folder-suggest-modal";
|
||||
import type SurfSensePlugin from "./main";
|
||||
import { STATUS_VISUALS } from "./status-visuals";
|
||||
import type { SearchSpace } from "./types";
|
||||
import type { Workspace } from "./types";
|
||||
|
||||
/** Plugin settings tab. */
|
||||
|
||||
export class SurfSenseSettingTab extends PluginSettingTab {
|
||||
private readonly plugin: SurfSensePlugin;
|
||||
private searchSpaces: SearchSpace[] = [];
|
||||
private workspaces: Workspace[] = [];
|
||||
private loadingSpaces = false;
|
||||
private connectionIndicator: HTMLElement | null = null;
|
||||
private readonly onStatusChange = (): void => this.updateConnectionIndicator();
|
||||
|
|
@ -51,7 +51,7 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
const next = value.trim();
|
||||
const previous = this.plugin.settings.serverUrl;
|
||||
if (previous !== "" && next !== previous) {
|
||||
this.plugin.settings.searchSpaceId = null;
|
||||
this.plugin.settings.workspaceId = null;
|
||||
this.plugin.settings.connectorId = null;
|
||||
}
|
||||
this.plugin.settings.serverUrl = next;
|
||||
|
|
@ -80,7 +80,7 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
const next = value.trim();
|
||||
const previous = this.plugin.settings.apiToken;
|
||||
if (previous !== "" && next !== previous) {
|
||||
this.plugin.settings.searchSpaceId = null;
|
||||
this.plugin.settings.workspaceId = null;
|
||||
this.plugin.settings.connectorId = null;
|
||||
}
|
||||
this.plugin.settings.apiToken = next;
|
||||
|
|
@ -102,7 +102,7 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
await this.plugin.api.verifyToken();
|
||||
new Notice("Surfsense: token verified.");
|
||||
this.plugin.engine.refreshStatus({ force: true });
|
||||
await this.refreshSearchSpaces();
|
||||
await this.refreshWorkspaces();
|
||||
this.display();
|
||||
} catch (err) {
|
||||
this.handleApiError(err);
|
||||
|
|
@ -115,21 +115,21 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
new Setting(containerEl)
|
||||
.setName("Search space")
|
||||
.setDesc(
|
||||
"Which Surfsense search space this vault syncs into. Reload after changing your token.",
|
||||
"Which Surfsense workspace this vault syncs into. Reload after changing your token.",
|
||||
)
|
||||
.addDropdown((drop) => {
|
||||
drop.addOption("", this.loadingSpaces ? "Loading…" : "Select a search space");
|
||||
for (const space of this.searchSpaces) {
|
||||
drop.addOption("", this.loadingSpaces ? "Loading…" : "Select a workspace");
|
||||
for (const space of this.workspaces) {
|
||||
drop.addOption(String(space.id), space.name);
|
||||
}
|
||||
if (settings.searchSpaceId !== null) {
|
||||
drop.setValue(String(settings.searchSpaceId));
|
||||
if (settings.workspaceId !== null) {
|
||||
drop.setValue(String(settings.workspaceId));
|
||||
}
|
||||
drop.onChange(async (value) => {
|
||||
this.plugin.settings.searchSpaceId = value ? Number(value) : null;
|
||||
this.plugin.settings.workspaceId = value ? Number(value) : null;
|
||||
this.plugin.settings.connectorId = null;
|
||||
await this.plugin.saveSettings();
|
||||
if (this.plugin.settings.searchSpaceId !== null) {
|
||||
if (this.plugin.settings.workspaceId !== null) {
|
||||
try {
|
||||
await this.plugin.engine.ensureConnected();
|
||||
await this.plugin.engine.maybeReconcile(true);
|
||||
|
|
@ -144,9 +144,9 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
.addExtraButton((btn) =>
|
||||
btn
|
||||
.setIcon("refresh-ccw")
|
||||
.setTooltip("Reload search spaces")
|
||||
.setTooltip("Reload workspaces")
|
||||
.onClick(async () => {
|
||||
await this.refreshSearchSpaces();
|
||||
await this.refreshWorkspaces();
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
|
|
@ -319,13 +319,13 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
indicator.setAttr("title", visual.label);
|
||||
}
|
||||
|
||||
private async refreshSearchSpaces(): Promise<void> {
|
||||
private async refreshWorkspaces(): Promise<void> {
|
||||
this.loadingSpaces = true;
|
||||
try {
|
||||
this.searchSpaces = await this.plugin.api.listSearchSpaces();
|
||||
this.workspaces = await this.plugin.api.listWorkspaces();
|
||||
} catch (err) {
|
||||
this.handleApiError(err);
|
||||
this.searchSpaces = [];
|
||||
this.workspaces = [];
|
||||
} finally {
|
||||
this.loadingSpaces = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export interface SyncEngineSettings {
|
|||
vaultId: string;
|
||||
apiToken: string;
|
||||
connectorId: number | null;
|
||||
searchSpaceId: number | null;
|
||||
workspaceId: number | null;
|
||||
includeFolders: string[];
|
||||
excludeFolders: string[];
|
||||
excludePatterns: string[];
|
||||
|
|
@ -96,7 +96,7 @@ export class SyncEngine {
|
|||
this.setStatus("syncing", "Connecting to SurfSense…");
|
||||
|
||||
const settings = this.deps.getSettings();
|
||||
if (!settings.searchSpaceId) {
|
||||
if (!settings.workspaceId) {
|
||||
// No target yet — /health still surfaces auth/network errors.
|
||||
try {
|
||||
const health = await this.deps.apiClient.health();
|
||||
|
|
@ -124,7 +124,7 @@ export class SyncEngine {
|
|||
*/
|
||||
async ensureConnected(): Promise<boolean> {
|
||||
const settings = this.deps.getSettings();
|
||||
if (!settings.searchSpaceId) {
|
||||
if (!settings.workspaceId) {
|
||||
this.setStatus("idle");
|
||||
return false;
|
||||
}
|
||||
|
|
@ -132,7 +132,7 @@ export class SyncEngine {
|
|||
try {
|
||||
const fingerprint = await computeVaultFingerprint(this.deps.app);
|
||||
const resp = await this.deps.apiClient.connect({
|
||||
searchSpaceId: settings.searchSpaceId,
|
||||
workspaceId: settings.workspaceId,
|
||||
vaultId: settings.vaultId,
|
||||
vaultName: this.deps.app.vault.getName(),
|
||||
vaultFingerprint: fingerprint,
|
||||
|
|
@ -272,7 +272,7 @@ export class SyncEngine {
|
|||
this.refreshStatus({ force: true });
|
||||
return;
|
||||
}
|
||||
if (!settings.searchSpaceId) {
|
||||
if (!settings.workspaceId) {
|
||||
try {
|
||||
const health = await this.deps.apiClient.health();
|
||||
this.applyHealth(health);
|
||||
|
|
@ -543,7 +543,7 @@ export class SyncEngine {
|
|||
const isError =
|
||||
last === "auth-error" || last === "offline" || last === "error";
|
||||
const s = this.deps.getSettings();
|
||||
const setupComplete = !!(s.apiToken && s.searchSpaceId && s.connectorId);
|
||||
const setupComplete = !!(s.apiToken && s.workspaceId && s.connectorId);
|
||||
if (isError && setupComplete) return;
|
||||
}
|
||||
this.setStatus(this.queueStatusKind(), this.statusDetail());
|
||||
|
|
@ -571,7 +571,7 @@ export class SyncEngine {
|
|||
kind = "needs-setup";
|
||||
detail = this.setupHint(s);
|
||||
} else if (kind !== "auth-error" && kind !== "offline" && kind !== "error") {
|
||||
if (!s.searchSpaceId || !s.connectorId) {
|
||||
if (!s.workspaceId || !s.connectorId) {
|
||||
kind = "needs-setup";
|
||||
detail = this.setupHint(s);
|
||||
}
|
||||
|
|
@ -582,7 +582,7 @@ export class SyncEngine {
|
|||
|
||||
private setupHint(s: SyncEngineSettings): string {
|
||||
if (!s.apiToken) return "Paste your API token in settings.";
|
||||
if (!s.searchSpaceId) return "Pick a search space in settings.";
|
||||
if (!s.workspaceId) return "Pick a workspace in settings.";
|
||||
return "Connecting…";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
export interface SurfsensePluginSettings {
|
||||
serverUrl: string;
|
||||
apiToken: string;
|
||||
searchSpaceId: number | null;
|
||||
workspaceId: number | null;
|
||||
connectorId: number | null;
|
||||
/** UUID for the vault — lives here so Obsidian Sync replicates it across devices. */
|
||||
vaultId: string;
|
||||
|
|
@ -25,7 +25,7 @@ export interface SurfsensePluginSettings {
|
|||
export const DEFAULT_SETTINGS: SurfsensePluginSettings = {
|
||||
serverUrl: "https://surfsense.com",
|
||||
apiToken: "",
|
||||
searchSpaceId: null,
|
||||
workspaceId: null,
|
||||
connectorId: null,
|
||||
vaultId: "",
|
||||
syncIntervalMinutes: 10,
|
||||
|
|
@ -107,7 +107,7 @@ export interface HeadingRef {
|
|||
level: number;
|
||||
}
|
||||
|
||||
export interface SearchSpace {
|
||||
export interface Workspace {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
|
|
@ -117,7 +117,7 @@ export interface SearchSpace {
|
|||
export interface ConnectResponse {
|
||||
connector_id: number;
|
||||
vault_id: string;
|
||||
search_space_id: number;
|
||||
workspace_id: number;
|
||||
capabilities: string[];
|
||||
server_time_utc: string;
|
||||
[key: string]: unknown;
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -12,78 +12,78 @@ export const agentToolsAtom = atomWithQuery((_get) => ({
|
|||
|
||||
const STORAGE_PREFIX = "surfsense-disabled-tools-";
|
||||
|
||||
function loadDisabledTools(searchSpaceId: string): string[] {
|
||||
function loadDisabledTools(workspaceId: string): string[] {
|
||||
if (typeof window === "undefined") return [];
|
||||
try {
|
||||
const raw = localStorage.getItem(`${STORAGE_PREFIX}${searchSpaceId}`);
|
||||
const raw = localStorage.getItem(`${STORAGE_PREFIX}${workspaceId}`);
|
||||
return raw ? (JSON.parse(raw) as string[]) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveDisabledTools(searchSpaceId: string, tools: string[]) {
|
||||
function saveDisabledTools(workspaceId: string, tools: string[]) {
|
||||
if (typeof window === "undefined") return;
|
||||
if (tools.length === 0) {
|
||||
localStorage.removeItem(`${STORAGE_PREFIX}${searchSpaceId}`);
|
||||
localStorage.removeItem(`${STORAGE_PREFIX}${workspaceId}`);
|
||||
} else {
|
||||
localStorage.setItem(`${STORAGE_PREFIX}${searchSpaceId}`, JSON.stringify(tools));
|
||||
localStorage.setItem(`${STORAGE_PREFIX}${workspaceId}`, JSON.stringify(tools));
|
||||
}
|
||||
}
|
||||
|
||||
const disabledToolsBaseAtom = atom<string[]>([]);
|
||||
|
||||
/** Tracks whether the atom has been hydrated from localStorage for the current search space */
|
||||
/** Tracks whether the atom has been hydrated from localStorage for the current workspace */
|
||||
const hydratedForAtom = atom<string | null>(null);
|
||||
|
||||
/**
|
||||
* Read/write atom for the set of disabled tool names.
|
||||
* Persists to localStorage keyed by search space ID.
|
||||
* Persists to localStorage keyed by workspace ID.
|
||||
*/
|
||||
export const disabledToolsAtom = atom(
|
||||
(get) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
const hydratedFor = get(hydratedForAtom);
|
||||
if (searchSpaceId && hydratedFor !== searchSpaceId) {
|
||||
return loadDisabledTools(searchSpaceId);
|
||||
if (workspaceId && hydratedFor !== workspaceId) {
|
||||
return loadDisabledTools(workspaceId);
|
||||
}
|
||||
return get(disabledToolsBaseAtom);
|
||||
},
|
||||
(get, set, update: string[] | ((prev: string[]) => string[])) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
const prev = get(disabledToolsBaseAtom);
|
||||
const next = typeof update === "function" ? update(prev) : update;
|
||||
set(disabledToolsBaseAtom, next);
|
||||
set(hydratedForAtom, searchSpaceId);
|
||||
if (searchSpaceId) {
|
||||
saveDisabledTools(searchSpaceId, next);
|
||||
set(hydratedForAtom, workspaceId);
|
||||
if (workspaceId) {
|
||||
saveDisabledTools(workspaceId, next);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Hydrate disabled tools from localStorage when search space changes.
|
||||
* Call this from a useEffect in a component that has access to the search space.
|
||||
* Hydrate disabled tools from localStorage when workspace changes.
|
||||
* Call this from a useEffect in a component that has access to the workspace.
|
||||
*/
|
||||
export const hydrateDisabledToolsAtom = atom(null, (get, set) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
if (!searchSpaceId) return;
|
||||
const stored = loadDisabledTools(searchSpaceId);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
if (!workspaceId) return;
|
||||
const stored = loadDisabledTools(workspaceId);
|
||||
set(disabledToolsBaseAtom, stored);
|
||||
set(hydratedForAtom, searchSpaceId);
|
||||
set(hydratedForAtom, workspaceId);
|
||||
});
|
||||
|
||||
/** Toggle a single tool's enabled/disabled state */
|
||||
export const toggleToolAtom = atom(null, (get, set, toolName: string) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
const current = get(disabledToolsBaseAtom);
|
||||
const next = current.includes(toolName)
|
||||
? current.filter((t) => t !== toolName)
|
||||
: [...current, toolName];
|
||||
set(disabledToolsBaseAtom, next);
|
||||
set(hydratedForAtom, searchSpaceId);
|
||||
if (searchSpaceId) {
|
||||
saveDisabledTools(searchSpaceId, next);
|
||||
set(hydratedForAtom, workspaceId);
|
||||
if (workspaceId) {
|
||||
saveDisabledTools(workspaceId, next);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -26,15 +26,15 @@ import { cacheKeys } from "@/lib/query-client/cache-keys";
|
|||
import { queryClient } from "@/lib/query-client/client";
|
||||
|
||||
// Cache invalidation strategy:
|
||||
// - Automation writes invalidate the search-space list + the touched detail.
|
||||
// - Automation writes invalidate the workspace list + the touched detail.
|
||||
// - Trigger writes only invalidate the parent automation detail (triggers
|
||||
// come back inline in AutomationDetail).
|
||||
// We deliberately invalidate the whole "automations" prefix on the list side
|
||||
// because list is keyed by (searchSpaceId, limit, offset) and we don't track
|
||||
// because list is keyed by (workspaceId, limit, offset) and we don't track
|
||||
// the active pagination in this layer.
|
||||
|
||||
function invalidateList(searchSpaceId: number) {
|
||||
queryClient.invalidateQueries({ queryKey: ["automations", "list", searchSpaceId] });
|
||||
function invalidateList(workspaceId: number) {
|
||||
queryClient.invalidateQueries({ queryKey: ["automations", "list", workspaceId] });
|
||||
}
|
||||
|
||||
function invalidateDetail(automationId: number) {
|
||||
|
|
@ -113,17 +113,17 @@ export const updateAutomationMutationAtom = atomWithMutation(() => ({
|
|||
|
||||
export const deleteAutomationMutationAtom = atomWithMutation(() => ({
|
||||
meta: { suppressGlobalErrorToast: true },
|
||||
mutationFn: async (vars: { automationId: number; searchSpaceId: number }) => {
|
||||
mutationFn: async (vars: { automationId: number; workspaceId: number }) => {
|
||||
await automationsApiService.deleteAutomation(vars.automationId);
|
||||
return vars;
|
||||
},
|
||||
onSuccess: (vars) => {
|
||||
invalidateList(vars.searchSpaceId);
|
||||
invalidateList(vars.workspaceId);
|
||||
invalidateDetail(vars.automationId);
|
||||
toast.success("Automation deleted");
|
||||
trackAutomationDeleted({
|
||||
automation_id: vars.automationId,
|
||||
workspace_id: vars.searchSpaceId,
|
||||
workspace_id: vars.workspaceId,
|
||||
});
|
||||
},
|
||||
onError: (error: Error, vars) => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { activeWorkspaceIdAtom } from "@/atoms/workspaces/workspace-query.atoms"
|
|||
import { automationsApiService } from "@/lib/apis/automations-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
|
||||
// First page of the active search space's automations.
|
||||
// First page of the active workspace's automations.
|
||||
// Detail + paginated/parameterized reads live in hooks (see use-automation.ts,
|
||||
// use-automation-runs.ts) so atoms stay tied to "current scope" and don't
|
||||
// proliferate atom families for every (id, limit, offset) tuple.
|
||||
|
|
@ -11,18 +11,18 @@ const DEFAULT_LIMIT = 50;
|
|||
const DEFAULT_OFFSET = 0;
|
||||
|
||||
export const automationsListAtom = atomWithQuery((get) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
|
||||
return {
|
||||
queryKey: cacheKeys.automations.list(Number(searchSpaceId ?? 0), DEFAULT_LIMIT, DEFAULT_OFFSET),
|
||||
enabled: !!searchSpaceId,
|
||||
queryKey: cacheKeys.automations.list(Number(workspaceId ?? 0), DEFAULT_LIMIT, DEFAULT_OFFSET),
|
||||
enabled: !!workspaceId,
|
||||
staleTime: 60 * 1000,
|
||||
queryFn: async () => {
|
||||
if (!searchSpaceId) {
|
||||
if (!workspaceId) {
|
||||
return { items: [], total: 0 };
|
||||
}
|
||||
return automationsApiService.listAutomations({
|
||||
workspace_id: Number(searchSpaceId),
|
||||
workspace_id: Number(workspaceId),
|
||||
limit: DEFAULT_LIMIT,
|
||||
offset: DEFAULT_OFFSET,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ import { reportPanelAtom } from "./report-panel.atom";
|
|||
|
||||
interface CurrentThreadState {
|
||||
id: number | null;
|
||||
searchSpaceId: number | null;
|
||||
workspaceId: number | null;
|
||||
visibility: ChatVisibility | null;
|
||||
hasComments: boolean;
|
||||
}
|
||||
|
||||
interface CurrentThreadMetadataPatch {
|
||||
id: number | null;
|
||||
searchSpaceId?: number | null;
|
||||
workspaceId?: number | null;
|
||||
visibility?: ChatVisibility | null;
|
||||
hasComments?: boolean;
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ interface CurrentThreadMetadataUpdate {
|
|||
|
||||
const initialState: CurrentThreadState = {
|
||||
id: null,
|
||||
searchSpaceId: null,
|
||||
workspaceId: null,
|
||||
visibility: null,
|
||||
hasComments: false,
|
||||
};
|
||||
|
|
@ -44,11 +44,11 @@ export const setCurrentThreadMetadataAtom = atom(
|
|||
set(currentThreadAtom, {
|
||||
...current,
|
||||
id: metadata.id,
|
||||
searchSpaceId:
|
||||
"searchSpaceId" in metadata
|
||||
? (metadata.searchSpaceId ?? null)
|
||||
workspaceId:
|
||||
"workspaceId" in metadata
|
||||
? (metadata.workspaceId ?? null)
|
||||
: isSameThread
|
||||
? current.searchSpaceId
|
||||
? current.workspaceId
|
||||
: null,
|
||||
visibility:
|
||||
"visibility" in metadata
|
||||
|
|
|
|||
|
|
@ -13,38 +13,38 @@ import { queryClient } from "@/lib/query-client/client";
|
|||
import { activeWorkspaceIdAtom } from "../workspaces/workspace-query.atoms";
|
||||
|
||||
export const createConnectorMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
|
||||
return {
|
||||
mutationKey: cacheKeys.connectors.all(searchSpaceId ?? ""),
|
||||
enabled: !!searchSpaceId,
|
||||
mutationKey: cacheKeys.connectors.all(workspaceId ?? ""),
|
||||
enabled: !!workspaceId,
|
||||
mutationFn: async (request: CreateConnectorRequest) => {
|
||||
return connectorsApiService.createConnector(request);
|
||||
},
|
||||
|
||||
onSuccess: () => {
|
||||
if (!searchSpaceId) return;
|
||||
if (!workspaceId) return;
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.connectors.all(searchSpaceId),
|
||||
queryKey: cacheKeys.connectors.all(workspaceId),
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const updateConnectorMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
|
||||
return {
|
||||
mutationKey: cacheKeys.connectors.all(searchSpaceId ?? ""),
|
||||
enabled: !!searchSpaceId,
|
||||
mutationKey: cacheKeys.connectors.all(workspaceId ?? ""),
|
||||
enabled: !!workspaceId,
|
||||
mutationFn: async (request: UpdateConnectorRequest) => {
|
||||
return connectorsApiService.updateConnector(request);
|
||||
},
|
||||
|
||||
onSuccess: (_, request: UpdateConnectorRequest) => {
|
||||
if (!searchSpaceId) return;
|
||||
if (!workspaceId) return;
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.connectors.all(searchSpaceId),
|
||||
queryKey: cacheKeys.connectors.all(workspaceId),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.connectors.byId(String(request.id)),
|
||||
|
|
@ -54,19 +54,19 @@ export const updateConnectorMutationAtom = atomWithMutation((get) => {
|
|||
});
|
||||
|
||||
export const deleteConnectorMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
|
||||
return {
|
||||
mutationKey: cacheKeys.connectors.all(searchSpaceId ?? ""),
|
||||
enabled: !!searchSpaceId,
|
||||
mutationKey: cacheKeys.connectors.all(workspaceId ?? ""),
|
||||
enabled: !!workspaceId,
|
||||
mutationFn: async (request: DeleteConnectorRequest) => {
|
||||
return connectorsApiService.deleteConnector(request);
|
||||
},
|
||||
|
||||
onSuccess: (_, request: DeleteConnectorRequest) => {
|
||||
if (!searchSpaceId) return;
|
||||
if (!workspaceId) return;
|
||||
queryClient.setQueryData(
|
||||
cacheKeys.connectors.all(searchSpaceId),
|
||||
cacheKeys.connectors.all(workspaceId),
|
||||
(oldData: GetConnectorsResponse | undefined) => {
|
||||
if (!oldData) return oldData;
|
||||
return oldData.filter((connector) => connector.id !== request.id);
|
||||
|
|
@ -80,19 +80,19 @@ export const deleteConnectorMutationAtom = atomWithMutation((get) => {
|
|||
});
|
||||
|
||||
export const indexConnectorMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
|
||||
return {
|
||||
mutationKey: cacheKeys.connectors.index(),
|
||||
enabled: !!searchSpaceId,
|
||||
enabled: !!workspaceId,
|
||||
mutationFn: async (request: IndexConnectorRequest) => {
|
||||
return connectorsApiService.indexConnector(request);
|
||||
},
|
||||
|
||||
onSuccess: (response: IndexConnectorResponse) => {
|
||||
if (!searchSpaceId) return;
|
||||
if (!workspaceId) return;
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.connectors.all(searchSpaceId),
|
||||
queryKey: cacheKeys.connectors.all(workspaceId),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.connectors.byId(String(response.connector_id)),
|
||||
|
|
|
|||
|
|
@ -4,16 +4,16 @@ import { cacheKeys } from "@/lib/query-client/cache-keys";
|
|||
import { activeWorkspaceIdAtom } from "../workspaces/workspace-query.atoms";
|
||||
|
||||
export const connectorsAtom = atomWithQuery((get) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
|
||||
return {
|
||||
queryKey: cacheKeys.connectors.all(searchSpaceId!),
|
||||
enabled: !!searchSpaceId,
|
||||
queryKey: cacheKeys.connectors.all(workspaceId!),
|
||||
enabled: !!workspaceId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
queryFn: async () => {
|
||||
return connectorsApiService.getConnectors({
|
||||
queryParams: {
|
||||
workspace_id: searchSpaceId!,
|
||||
workspace_id: workspaceId!,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ import { queryClient } from "@/lib/query-client/client";
|
|||
import { globalDocumentsQueryParamsAtom } from "./ui.atoms";
|
||||
|
||||
export const createDocumentMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
const documentsQueryParams = get(globalDocumentsQueryParamsAtom);
|
||||
|
||||
return {
|
||||
mutationKey: cacheKeys.documents.globalQueryParams(documentsQueryParams),
|
||||
enabled: !!searchSpaceId,
|
||||
enabled: !!workspaceId,
|
||||
mutationFn: async (request: CreateDocumentRequest) => {
|
||||
return documentsApiService.createDocument(request);
|
||||
},
|
||||
|
|
@ -34,12 +34,12 @@ export const createDocumentMutationAtom = atomWithMutation((get) => {
|
|||
});
|
||||
|
||||
export const uploadDocumentMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
const documentsQueryParams = get(globalDocumentsQueryParamsAtom);
|
||||
|
||||
return {
|
||||
mutationKey: cacheKeys.documents.globalQueryParams(documentsQueryParams),
|
||||
enabled: !!searchSpaceId,
|
||||
enabled: !!workspaceId,
|
||||
mutationFn: async (request: UploadDocumentRequest) => {
|
||||
return documentsApiService.uploadDocument(request);
|
||||
},
|
||||
|
|
@ -47,19 +47,19 @@ export const uploadDocumentMutationAtom = atomWithMutation((get) => {
|
|||
onSuccess: () => {
|
||||
// Note: Toast notification is handled by the caller (DocumentUploadTab) to use i18n
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.logs.summary(searchSpaceId ?? undefined),
|
||||
queryKey: cacheKeys.logs.summary(workspaceId ?? undefined),
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const updateDocumentMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
const documentsQueryParams = get(globalDocumentsQueryParamsAtom);
|
||||
|
||||
return {
|
||||
mutationKey: cacheKeys.documents.globalQueryParams(documentsQueryParams),
|
||||
enabled: !!searchSpaceId,
|
||||
enabled: !!workspaceId,
|
||||
mutationFn: async (request: UpdateDocumentRequest) => {
|
||||
return documentsApiService.updateDocument(request);
|
||||
},
|
||||
|
|
@ -77,12 +77,12 @@ export const updateDocumentMutationAtom = atomWithMutation((get) => {
|
|||
});
|
||||
|
||||
export const deleteDocumentMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
const documentsQueryParams = get(globalDocumentsQueryParamsAtom);
|
||||
|
||||
return {
|
||||
mutationKey: cacheKeys.documents.globalQueryParams(documentsQueryParams),
|
||||
enabled: !!searchSpaceId,
|
||||
enabled: !!workspaceId,
|
||||
mutationFn: async (request: DeleteDocumentRequest) => {
|
||||
return documentsApiService.deleteDocument(request);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { atom } from "jotai";
|
|||
import { atomWithStorage } from "jotai/utils";
|
||||
|
||||
/**
|
||||
* Set of folder IDs that are currently expanded in the tree, keyed by search space ID.
|
||||
* Set of folder IDs that are currently expanded in the tree, keyed by workspace ID.
|
||||
* Persisted to localStorage so expand/collapse state survives page refreshes.
|
||||
*/
|
||||
export const expandedFolderIdsAtom = atomWithStorage<Record<number, number[]>>(
|
||||
|
|
@ -13,7 +13,7 @@ export const expandedFolderIdsAtom = atomWithStorage<Record<number, number[]>>(
|
|||
);
|
||||
|
||||
/**
|
||||
* Expanded folder keys for Local filesystem tree, keyed by search space ID.
|
||||
* Expanded folder keys for Local filesystem tree, keyed by workspace ID.
|
||||
* Persisted so local tree expansion survives remounts/reloads.
|
||||
*/
|
||||
export const localExpandedFolderKeysAtom = atomWithStorage<Record<number, string[]>>(
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export interface AgentCreatedDocument {
|
|||
id: number;
|
||||
title: string;
|
||||
documentType: string;
|
||||
searchSpaceId: number;
|
||||
workspaceId: number;
|
||||
folderId: number | null;
|
||||
createdById: string | null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ interface EditorPanelState {
|
|||
kind: "document" | "local_file" | "memory";
|
||||
documentId: number | null;
|
||||
localFilePath: string | null;
|
||||
searchSpaceId: number | null;
|
||||
workspaceId: number | null;
|
||||
memoryScope: "user" | "team" | null;
|
||||
title: string | null;
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ const initialState: EditorPanelState = {
|
|||
kind: "document",
|
||||
documentId: null,
|
||||
localFilePath: null,
|
||||
searchSpaceId: null,
|
||||
workspaceId: null,
|
||||
memoryScope: null,
|
||||
title: null,
|
||||
};
|
||||
|
|
@ -33,18 +33,18 @@ export const openEditorPanelAtom = atom(
|
|||
get,
|
||||
set,
|
||||
payload:
|
||||
| { documentId: number; searchSpaceId: number; title?: string; kind?: "document" }
|
||||
| { documentId: number; workspaceId: number; title?: string; kind?: "document" }
|
||||
| {
|
||||
kind: "local_file";
|
||||
localFilePath: string;
|
||||
title?: string;
|
||||
searchSpaceId?: number;
|
||||
workspaceId?: number;
|
||||
}
|
||||
| {
|
||||
kind: "memory";
|
||||
memoryScope: "user" | "team";
|
||||
title?: string;
|
||||
searchSpaceId?: number;
|
||||
workspaceId?: number;
|
||||
}
|
||||
) => {
|
||||
if (!get(editorPanelAtom).isOpen) {
|
||||
|
|
@ -56,7 +56,7 @@ export const openEditorPanelAtom = atom(
|
|||
kind: "local_file",
|
||||
documentId: null,
|
||||
localFilePath: payload.localFilePath,
|
||||
searchSpaceId: payload.searchSpaceId ?? null,
|
||||
workspaceId: payload.workspaceId ?? null,
|
||||
memoryScope: null,
|
||||
title: payload.title ?? null,
|
||||
});
|
||||
|
|
@ -70,7 +70,7 @@ export const openEditorPanelAtom = atom(
|
|||
kind: "memory",
|
||||
documentId: null,
|
||||
localFilePath: null,
|
||||
searchSpaceId: payload.searchSpaceId ?? null,
|
||||
workspaceId: payload.workspaceId ?? null,
|
||||
memoryScope: payload.memoryScope,
|
||||
title: payload.title ?? null,
|
||||
});
|
||||
|
|
@ -83,7 +83,7 @@ export const openEditorPanelAtom = atom(
|
|||
kind: "document",
|
||||
documentId: payload.documentId,
|
||||
localFilePath: null,
|
||||
searchSpaceId: payload.searchSpaceId,
|
||||
workspaceId: payload.workspaceId,
|
||||
memoryScope: null,
|
||||
title: payload.title ?? null,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export const acceptInviteMutationAtom = atomWithMutation(() => ({
|
|||
return invitesApiService.acceptInvite(request);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: cacheKeys.searchSpaces.all });
|
||||
queryClient.invalidateQueries({ queryKey: cacheKeys.workspaces.all });
|
||||
toast.success("Invite accepted successfully");
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
|
|
|
|||
|
|
@ -4,18 +4,18 @@ import { invitesApiService } from "@/lib/apis/invites-api.service";
|
|||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
|
||||
export const invitesAtom = atomWithQuery((get) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
|
||||
return {
|
||||
queryKey: cacheKeys.invites.all(searchSpaceId?.toString() ?? ""),
|
||||
enabled: !!searchSpaceId,
|
||||
queryKey: cacheKeys.invites.all(workspaceId?.toString() ?? ""),
|
||||
enabled: !!workspaceId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
queryFn: async () => {
|
||||
if (!searchSpaceId) {
|
||||
if (!workspaceId) {
|
||||
return [];
|
||||
}
|
||||
return invitesApiService.getInvites({
|
||||
workspace_id: Number(searchSpaceId),
|
||||
workspace_id: Number(workspaceId),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ import { queryClient } from "@/lib/query-client/client";
|
|||
* Create Log Mutation
|
||||
*/
|
||||
export const createLogMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
return {
|
||||
mutationKey: cacheKeys.logs.list(searchSpaceId ?? undefined),
|
||||
enabled: !!searchSpaceId,
|
||||
mutationKey: cacheKeys.logs.list(workspaceId ?? undefined),
|
||||
enabled: !!workspaceId,
|
||||
mutationFn: async (request: CreateLogRequest) => logsApiService.createLog(request),
|
||||
onSuccess: () => {
|
||||
// Invalidate all log-related queries (list, summary, detail, withQueryParams)
|
||||
|
|
@ -29,10 +29,10 @@ export const createLogMutationAtom = atomWithMutation((get) => {
|
|||
* Update Log Mutation
|
||||
*/
|
||||
export const updateLogMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
return {
|
||||
mutationKey: cacheKeys.logs.list(searchSpaceId ?? undefined),
|
||||
enabled: !!searchSpaceId,
|
||||
mutationKey: cacheKeys.logs.list(workspaceId ?? undefined),
|
||||
enabled: !!workspaceId,
|
||||
mutationFn: async ({ logId, data }: { logId: number; data: UpdateLogRequest }) =>
|
||||
logsApiService.updateLog(logId, data),
|
||||
onSuccess: (_data, variables) => {
|
||||
|
|
@ -45,10 +45,10 @@ export const updateLogMutationAtom = atomWithMutation((get) => {
|
|||
* Delete Log Mutation
|
||||
*/
|
||||
export const deleteLogMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
return {
|
||||
mutationKey: cacheKeys.logs.list(searchSpaceId ?? undefined),
|
||||
enabled: !!searchSpaceId,
|
||||
mutationKey: cacheKeys.logs.list(workspaceId ?? undefined),
|
||||
enabled: !!workspaceId,
|
||||
mutationFn: async (request: DeleteLogRequest) => logsApiService.deleteLog(request),
|
||||
onSuccess: (_data, request) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["logs"] });
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { toast } from "sonner";
|
|||
import type {
|
||||
DeleteMembershipRequest,
|
||||
DeleteMembershipResponse,
|
||||
LeaveSearchSpaceRequest,
|
||||
LeaveSearchSpaceResponse,
|
||||
LeaveWorkspaceRequest,
|
||||
LeaveWorkspaceResponse,
|
||||
UpdateMembershipRequest,
|
||||
UpdateMembershipResponse,
|
||||
} from "@/contracts/types/members.types";
|
||||
|
|
@ -48,20 +48,20 @@ export const deleteMemberMutationAtom = atomWithMutation(() => {
|
|||
};
|
||||
});
|
||||
|
||||
export const leaveSearchSpaceMutationAtom = atomWithMutation(() => {
|
||||
export const leaveWorkspaceMutationAtom = atomWithMutation(() => {
|
||||
return {
|
||||
meta: { suppressGlobalErrorToast: true },
|
||||
mutationFn: async (request: LeaveSearchSpaceRequest) => {
|
||||
return membersApiService.leaveSearchSpace(request);
|
||||
mutationFn: async (request: LeaveWorkspaceRequest) => {
|
||||
return membersApiService.leaveWorkspace(request);
|
||||
},
|
||||
onSuccess: (_: LeaveSearchSpaceResponse, request: LeaveSearchSpaceRequest) => {
|
||||
toast.success("Successfully left the search space");
|
||||
onSuccess: (_: LeaveWorkspaceResponse, request: LeaveWorkspaceRequest) => {
|
||||
toast.success("Successfully left the workspace");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.members.all(request.workspace_id.toString()),
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to leave search space");
|
||||
toast.error("Failed to leave workspace");
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,37 +5,37 @@ import { membersApiService } from "@/lib/apis/members-api.service";
|
|||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
|
||||
export const membersAtom = atomWithQuery((get) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
|
||||
return {
|
||||
queryKey: cacheKeys.members.all(searchSpaceId?.toString() ?? ""),
|
||||
enabled: !!searchSpaceId,
|
||||
queryKey: cacheKeys.members.all(workspaceId?.toString() ?? ""),
|
||||
enabled: !!workspaceId,
|
||||
staleTime: 3 * 1000, // 3 seconds - short staleness for live collaboration
|
||||
refetchInterval: 2 * 60 * 1000, // 2 minutes
|
||||
queryFn: async () => {
|
||||
if (!searchSpaceId) {
|
||||
if (!workspaceId) {
|
||||
return [];
|
||||
}
|
||||
return membersApiService.getMembers({
|
||||
workspace_id: Number(searchSpaceId),
|
||||
workspace_id: Number(workspaceId),
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const myAccessAtom = atomWithQuery((get) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
|
||||
return {
|
||||
queryKey: cacheKeys.members.myAccess(searchSpaceId?.toString() ?? ""),
|
||||
enabled: !!searchSpaceId,
|
||||
queryKey: cacheKeys.members.myAccess(workspaceId?.toString() ?? ""),
|
||||
enabled: !!workspaceId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
queryFn: async () => {
|
||||
if (!searchSpaceId) {
|
||||
if (!workspaceId) {
|
||||
return null;
|
||||
}
|
||||
return membersApiService.getMyAccess({
|
||||
workspace_id: Number(searchSpaceId),
|
||||
workspace_id: Number(workspaceId),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,18 +18,18 @@ import { cacheKeys } from "@/lib/query-client/cache-keys";
|
|||
import { queryClient } from "@/lib/query-client/client";
|
||||
import { activeWorkspaceIdAtom } from "../workspaces/workspace-query.atoms";
|
||||
|
||||
function invalidateModelConnections(searchSpaceId: number) {
|
||||
function invalidateModelConnections(workspaceId: number) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.modelConnections.all(searchSpaceId),
|
||||
queryKey: cacheKeys.modelConnections.all(workspaceId),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.modelConnections.roles(searchSpaceId),
|
||||
queryKey: cacheKeys.modelConnections.roles(workspaceId),
|
||||
});
|
||||
}
|
||||
|
||||
function upsertModelConnection(searchSpaceId: number, connection: ConnectionRead) {
|
||||
function upsertModelConnection(workspaceId: number, connection: ConnectionRead) {
|
||||
queryClient.setQueryData<ConnectionRead[]>(
|
||||
cacheKeys.modelConnections.all(searchSpaceId),
|
||||
cacheKeys.modelConnections.all(workspaceId),
|
||||
(current = []) => {
|
||||
if (current.some((item) => item.id === connection.id)) {
|
||||
return current.map((item) => (item.id === connection.id ? connection : item));
|
||||
|
|
@ -40,19 +40,19 @@ function upsertModelConnection(searchSpaceId: number, connection: ConnectionRead
|
|||
}
|
||||
|
||||
export const createModelConnectionMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = Number(get(activeWorkspaceIdAtom));
|
||||
const workspaceId = Number(get(activeWorkspaceIdAtom));
|
||||
return {
|
||||
mutationKey: ["model-connections", "create"],
|
||||
mutationFn: (request: ConnectionCreateRequest) =>
|
||||
modelConnectionsApiService.createConnection(request),
|
||||
onSuccess: (connection: ConnectionRead, request: ConnectionCreateRequest) => {
|
||||
const resolvedSearchSpaceId = Number(
|
||||
request.workspace_id ?? connection.workspace_id ?? searchSpaceId
|
||||
const resolvedWorkspaceId = Number(
|
||||
request.workspace_id ?? connection.workspace_id ?? workspaceId
|
||||
);
|
||||
toast.success("Connection created");
|
||||
if (resolvedSearchSpaceId > 0) {
|
||||
upsertModelConnection(resolvedSearchSpaceId, connection);
|
||||
invalidateModelConnections(resolvedSearchSpaceId);
|
||||
if (resolvedWorkspaceId > 0) {
|
||||
upsertModelConnection(resolvedWorkspaceId, connection);
|
||||
invalidateModelConnections(resolvedWorkspaceId);
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => toast.error(error.message || "Failed to create connection"),
|
||||
|
|
@ -60,34 +60,34 @@ export const createModelConnectionMutationAtom = atomWithMutation((get) => {
|
|||
});
|
||||
|
||||
export const updateModelConnectionMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = Number(get(activeWorkspaceIdAtom));
|
||||
const workspaceId = Number(get(activeWorkspaceIdAtom));
|
||||
return {
|
||||
mutationKey: ["model-connections", "update"],
|
||||
mutationFn: ({ id, data }: { id: number; data: ConnectionUpdateRequest }) =>
|
||||
modelConnectionsApiService.updateConnection(id, data),
|
||||
onSuccess: () => {
|
||||
toast.success("Connection updated");
|
||||
invalidateModelConnections(searchSpaceId);
|
||||
invalidateModelConnections(workspaceId);
|
||||
},
|
||||
onError: (error: Error) => toast.error(error.message || "Failed to update connection"),
|
||||
};
|
||||
});
|
||||
|
||||
export const deleteModelConnectionMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = Number(get(activeWorkspaceIdAtom));
|
||||
const workspaceId = Number(get(activeWorkspaceIdAtom));
|
||||
return {
|
||||
mutationKey: ["model-connections", "delete"],
|
||||
mutationFn: (id: number) => modelConnectionsApiService.deleteConnection(id),
|
||||
onSuccess: () => {
|
||||
toast.success("Connection deleted");
|
||||
invalidateModelConnections(searchSpaceId);
|
||||
invalidateModelConnections(workspaceId);
|
||||
},
|
||||
onError: (error: Error) => toast.error(error.message || "Failed to delete connection"),
|
||||
};
|
||||
});
|
||||
|
||||
export const verifyModelConnectionMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = Number(get(activeWorkspaceIdAtom));
|
||||
const workspaceId = Number(get(activeWorkspaceIdAtom));
|
||||
return {
|
||||
mutationKey: ["model-connections", "verify"],
|
||||
mutationFn: (id: number) => modelConnectionsApiService.verifyConnection(id),
|
||||
|
|
@ -103,14 +103,14 @@ export const verifyModelConnectionMutationAtom = atomWithMutation((get) => {
|
|||
: "Couldn't list models. Chat may still work — add model IDs manually."
|
||||
);
|
||||
}
|
||||
invalidateModelConnections(searchSpaceId);
|
||||
invalidateModelConnections(workspaceId);
|
||||
},
|
||||
onError: (error: Error) => toast.error(error.message || "Failed to verify connection"),
|
||||
};
|
||||
});
|
||||
|
||||
export const discoverConnectionModelsMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = Number(get(activeWorkspaceIdAtom));
|
||||
const workspaceId = Number(get(activeWorkspaceIdAtom));
|
||||
return {
|
||||
mutationKey: ["model-connections", "discover"],
|
||||
mutationFn: (id: number) => modelConnectionsApiService.discoverModels(id),
|
||||
|
|
@ -118,7 +118,7 @@ export const discoverConnectionModelsMutationAtom = atomWithMutation((get) => {
|
|||
toast.success(
|
||||
models.length ? `${models.length} models discovered` : "No models found for this connection"
|
||||
);
|
||||
invalidateModelConnections(searchSpaceId);
|
||||
invalidateModelConnections(workspaceId);
|
||||
},
|
||||
onError: (error: Error) => toast.error(error.message || "Failed to discover models"),
|
||||
};
|
||||
|
|
@ -149,64 +149,64 @@ export const testPreviewModelMutationAtom = atomWithMutation(() => {
|
|||
});
|
||||
|
||||
export const addManualModelMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = Number(get(activeWorkspaceIdAtom));
|
||||
const workspaceId = Number(get(activeWorkspaceIdAtom));
|
||||
return {
|
||||
mutationKey: ["models", "add-manual"],
|
||||
mutationFn: ({ connectionId, data }: { connectionId: number; data: ModelCreateRequest }) =>
|
||||
modelConnectionsApiService.addManualModel(connectionId, data),
|
||||
onSuccess: () => {
|
||||
toast.success("Model added");
|
||||
invalidateModelConnections(searchSpaceId);
|
||||
invalidateModelConnections(workspaceId);
|
||||
},
|
||||
onError: (error: Error) => toast.error(error.message || "Failed to add model"),
|
||||
};
|
||||
});
|
||||
|
||||
export const updateModelMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = Number(get(activeWorkspaceIdAtom));
|
||||
const workspaceId = Number(get(activeWorkspaceIdAtom));
|
||||
return {
|
||||
mutationKey: ["models", "update"],
|
||||
mutationFn: ({ id, data }: { id: number; data: ModelUpdateRequest }) =>
|
||||
modelConnectionsApiService.updateModel(id, data),
|
||||
onSuccess: () => invalidateModelConnections(searchSpaceId),
|
||||
onSuccess: () => invalidateModelConnections(workspaceId),
|
||||
onError: (error: Error) => toast.error(error.message || "Failed to update model"),
|
||||
};
|
||||
});
|
||||
|
||||
export const bulkUpdateModelsMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = Number(get(activeWorkspaceIdAtom));
|
||||
const workspaceId = Number(get(activeWorkspaceIdAtom));
|
||||
return {
|
||||
mutationKey: ["models", "bulk-update"],
|
||||
mutationFn: ({ connectionId, data }: { connectionId: number; data: ModelsBulkUpdateRequest }) =>
|
||||
modelConnectionsApiService.bulkUpdateModels(connectionId, data),
|
||||
onSuccess: () => invalidateModelConnections(searchSpaceId),
|
||||
onSuccess: () => invalidateModelConnections(workspaceId),
|
||||
onError: (error: Error) => toast.error(error.message || "Failed to update models"),
|
||||
};
|
||||
});
|
||||
|
||||
export const testModelMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = Number(get(activeWorkspaceIdAtom));
|
||||
const workspaceId = Number(get(activeWorkspaceIdAtom));
|
||||
return {
|
||||
mutationKey: ["models", "test"],
|
||||
mutationFn: (id: number) => modelConnectionsApiService.testModel(id),
|
||||
onSuccess: (result: VerifyConnectionResponse) => {
|
||||
if (result.ok) toast.success("Model test succeeded");
|
||||
else toast.error(result.message || "Model test failed");
|
||||
invalidateModelConnections(searchSpaceId);
|
||||
invalidateModelConnections(workspaceId);
|
||||
},
|
||||
onError: (error: Error) => toast.error(error.message || "Failed to test model"),
|
||||
};
|
||||
});
|
||||
|
||||
export const updateModelRolesMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = Number(get(activeWorkspaceIdAtom));
|
||||
const workspaceId = Number(get(activeWorkspaceIdAtom));
|
||||
return {
|
||||
mutationKey: ["model-roles", "update"],
|
||||
mutationFn: (roles: ModelRoles) =>
|
||||
modelConnectionsApiService.updateModelRoles(searchSpaceId, roles),
|
||||
modelConnectionsApiService.updateModelRoles(workspaceId, roles),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.modelConnections.roles(searchSpaceId),
|
||||
queryKey: cacheKeys.modelConnections.roles(workspaceId),
|
||||
});
|
||||
},
|
||||
onError: (error: Error) => toast.error(error.message || "Failed to update model roles"),
|
||||
|
|
|
|||
|
|
@ -26,21 +26,21 @@ export const modelProvidersAtom = atomWithQuery(() => ({
|
|||
}));
|
||||
|
||||
export const modelConnectionsAtom = atomWithQuery((get) => {
|
||||
const searchSpaceId = Number(get(activeWorkspaceIdAtom));
|
||||
const workspaceId = Number(get(activeWorkspaceIdAtom));
|
||||
return {
|
||||
queryKey: cacheKeys.modelConnections.all(searchSpaceId),
|
||||
enabled: !!searchSpaceId,
|
||||
queryKey: cacheKeys.modelConnections.all(workspaceId),
|
||||
enabled: !!workspaceId,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
queryFn: () => modelConnectionsApiService.getConnections(searchSpaceId),
|
||||
queryFn: () => modelConnectionsApiService.getConnections(workspaceId),
|
||||
};
|
||||
});
|
||||
|
||||
export const modelRolesAtom = atomWithQuery((get) => {
|
||||
const searchSpaceId = Number(get(activeWorkspaceIdAtom));
|
||||
const workspaceId = Number(get(activeWorkspaceIdAtom));
|
||||
return {
|
||||
queryKey: cacheKeys.modelConnections.roles(searchSpaceId),
|
||||
enabled: !!searchSpaceId,
|
||||
queryKey: cacheKeys.modelConnections.roles(workspaceId),
|
||||
enabled: !!workspaceId,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
queryFn: () => modelConnectionsApiService.getModelRoles(searchSpaceId),
|
||||
queryFn: () => modelConnectionsApiService.getModelRoles(workspaceId),
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,18 +4,18 @@ import { chatThreadsApiService } from "@/lib/apis/chat-threads-api.service";
|
|||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
|
||||
export const publicChatSnapshotsAtom = atomWithQuery((get) => {
|
||||
const searchSpaceId = get(activeWorkspaceIdAtom);
|
||||
const workspaceId = get(activeWorkspaceIdAtom);
|
||||
|
||||
return {
|
||||
queryKey: cacheKeys.publicChatSnapshots.bySearchSpace(Number(searchSpaceId) || 0),
|
||||
enabled: !!searchSpaceId,
|
||||
queryKey: cacheKeys.publicChatSnapshots.byWorkspace(Number(workspaceId) || 0),
|
||||
enabled: !!workspaceId,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
queryFn: async () => {
|
||||
if (!searchSpaceId) {
|
||||
if (!workspaceId) {
|
||||
return { snapshots: [] };
|
||||
}
|
||||
return chatThreadsApiService.listPublicChatSnapshotsForSearchSpace({
|
||||
workspace_id: Number(searchSpaceId),
|
||||
return chatThreadsApiService.listPublicChatSnapshotsForWorkspace({
|
||||
workspace_id: Number(workspaceId),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
21
surfsense_web/atoms/tabs/migrate-tabs.test.ts
Normal file
21
surfsense_web/atoms/tabs/migrate-tabs.test.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { migrateLegacyTabs } from "./migrate-tabs";
|
||||
|
||||
// Run with: pnpm exec tsx --test atoms/tabs/migrate-tabs.test.ts
|
||||
test("maps legacy searchSpaceId to workspaceId on read", () => {
|
||||
const migrated = migrateLegacyTabs({
|
||||
tabs: [{ id: "chat-new", type: "chat", searchSpaceId: 7 } as never],
|
||||
activeTabId: "chat-new",
|
||||
});
|
||||
const tab = migrated.tabs[0] as { workspaceId?: number };
|
||||
assert.equal(tab.workspaceId, 7);
|
||||
});
|
||||
|
||||
test("leaves an already-migrated workspaceId untouched", () => {
|
||||
const migrated = migrateLegacyTabs({
|
||||
tabs: [{ id: "d1", type: "document", workspaceId: 3, searchSpaceId: 9 } as never],
|
||||
});
|
||||
const tab = migrated.tabs[0] as { workspaceId?: number };
|
||||
assert.equal(tab.workspaceId, 3);
|
||||
});
|
||||
19
surfsense_web/atoms/tabs/migrate-tabs.ts
Normal file
19
surfsense_web/atoms/tabs/migrate-tabs.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* One-time read-migration for persisted tabs: legacy state stored the workspace
|
||||
* as `searchSpaceId`. Map it to `workspaceId` on read so already-open tabs keep
|
||||
* their workspace association after the rename. Pure + dependency-free so it can
|
||||
* be unit-checked without loading the atom module.
|
||||
*/
|
||||
export function migrateLegacyTabs<T extends { tabs: Array<{ workspaceId?: number }> }>(
|
||||
state: T
|
||||
): T {
|
||||
return {
|
||||
...state,
|
||||
tabs: state.tabs.map((t) => {
|
||||
const legacy = t as { workspaceId?: number; searchSpaceId?: number };
|
||||
return legacy.workspaceId === undefined && legacy.searchSpaceId !== undefined
|
||||
? { ...t, workspaceId: legacy.searchSpaceId }
|
||||
: t;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { atom } from "jotai";
|
||||
import { atomWithStorage, createJSONStorage } from "jotai/utils";
|
||||
import type { ChatVisibility } from "@/lib/chat/thread-persistence";
|
||||
import { migrateLegacyTabs } from "./migrate-tabs";
|
||||
|
||||
export type TabType = "chat" | "document";
|
||||
|
||||
|
|
@ -15,7 +16,7 @@ export interface Tab {
|
|||
hasComments?: boolean;
|
||||
/** For document tabs */
|
||||
documentId?: number;
|
||||
searchSpaceId?: number;
|
||||
workspaceId?: number;
|
||||
}
|
||||
|
||||
interface TabsState {
|
||||
|
|
@ -40,11 +41,17 @@ const initialState: TabsState = {
|
|||
const deletedChatIdsAtom = atom<Set<number>>(new Set<number>());
|
||||
|
||||
// Persist tabs in localStorage so they survive a hard refresh and let the user
|
||||
// keep tabs open across multiple search spaces (browser-like behavior).
|
||||
// keep tabs open across multiple workspaces (browser-like behavior).
|
||||
const localStorageAdapter = createJSONStorage<TabsState>(
|
||||
() => (typeof window !== "undefined" ? localStorage : undefined) as Storage
|
||||
);
|
||||
|
||||
// Wrap getItem in place so the adapter keeps its original (sync) type while
|
||||
// migrating legacy persisted state on read.
|
||||
const baseGetItem = localStorageAdapter.getItem.bind(localStorageAdapter);
|
||||
localStorageAdapter.getItem = (key, initialValue) =>
|
||||
migrateLegacyTabs(baseGetItem(key, initialValue));
|
||||
|
||||
export const tabsStateAtom = atomWithStorage<TabsState>(
|
||||
"surfsense:tabs",
|
||||
initialState,
|
||||
|
|
@ -81,14 +88,14 @@ export const syncChatTabAtom = atom(
|
|||
chatId,
|
||||
title,
|
||||
chatUrl,
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
visibility,
|
||||
hasComments,
|
||||
}: {
|
||||
chatId: number | null;
|
||||
title?: string;
|
||||
chatUrl?: string;
|
||||
searchSpaceId: number;
|
||||
workspaceId: number;
|
||||
visibility?: ChatVisibility;
|
||||
hasComments?: boolean;
|
||||
}
|
||||
|
|
@ -111,7 +118,7 @@ export const syncChatTabAtom = atom(
|
|||
...t,
|
||||
title: title || t.title,
|
||||
chatUrl: chatUrl || t.chatUrl,
|
||||
searchSpaceId: searchSpaceId ?? t.searchSpaceId,
|
||||
workspaceId: workspaceId ?? t.workspaceId,
|
||||
...(visibility !== undefined ? { visibility } : {}),
|
||||
...(hasComments !== undefined ? { hasComments } : {}),
|
||||
}
|
||||
|
|
@ -122,18 +129,18 @@ export const syncChatTabAtom = atom(
|
|||
}
|
||||
|
||||
// If navigating to a new chat (no chatId), ensure there's a "new chat" tab
|
||||
// scoped to the current search space.
|
||||
// scoped to the current workspace.
|
||||
if (!chatId) {
|
||||
const hasNewChatTab = state.tabs.some((t) => t.id === "chat-new");
|
||||
if (hasNewChatTab) {
|
||||
set(tabsStateAtom, {
|
||||
...state,
|
||||
activeTabId: "chat-new",
|
||||
tabs: state.tabs.map((t) => (t.id === "chat-new" ? { ...t, searchSpaceId, chatUrl } : t)),
|
||||
tabs: state.tabs.map((t) => (t.id === "chat-new" ? { ...t, workspaceId, chatUrl } : t)),
|
||||
});
|
||||
} else {
|
||||
set(tabsStateAtom, {
|
||||
tabs: [...state.tabs, { ...INITIAL_CHAT_TAB, searchSpaceId, chatUrl }],
|
||||
tabs: [...state.tabs, { ...INITIAL_CHAT_TAB, workspaceId, chatUrl }],
|
||||
activeTabId: "chat-new",
|
||||
});
|
||||
}
|
||||
|
|
@ -148,7 +155,7 @@ export const syncChatTabAtom = atom(
|
|||
title: title || "New Chat",
|
||||
chatId,
|
||||
chatUrl,
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
...(visibility !== undefined ? { visibility } : {}),
|
||||
...(hasComments !== undefined ? { hasComments } : {}),
|
||||
};
|
||||
|
|
@ -197,11 +204,7 @@ export const openDocumentTabAtom = atom(
|
|||
(
|
||||
get,
|
||||
set,
|
||||
{
|
||||
documentId,
|
||||
searchSpaceId,
|
||||
title,
|
||||
}: { documentId: number; searchSpaceId: number; title?: string }
|
||||
{ documentId, workspaceId, title }: { documentId: number; workspaceId: number; title?: string }
|
||||
) => {
|
||||
const state = get(tabsStateAtom);
|
||||
const tabId = makeDocumentTabId(documentId);
|
||||
|
|
@ -221,7 +224,7 @@ export const openDocumentTabAtom = atom(
|
|||
type: "document",
|
||||
title: title || `Document ${documentId}`,
|
||||
documentId,
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
};
|
||||
|
||||
set(tabsStateAtom, {
|
||||
|
|
@ -300,7 +303,7 @@ export const removeChatTabAtom = atom(null, (get, set, chatId: number) => {
|
|||
return remaining.find((t) => t.id === newActiveId) ?? null;
|
||||
});
|
||||
|
||||
/** Reset tabs when switching search spaces. */
|
||||
/** Reset tabs when switching workspaces. */
|
||||
export const resetTabsAtom = atom(null, (_get, set) => {
|
||||
set(tabsStateAtom, { ...initialState });
|
||||
set(deletedChatIdsAtom, new Set<number>());
|
||||
|
|
|
|||
|
|
@ -1,96 +1,96 @@
|
|||
import { atomWithMutation } from "jotai-tanstack-query";
|
||||
import { toast } from "sonner";
|
||||
import type {
|
||||
CreateSearchSpaceRequest,
|
||||
DeleteSearchSpaceRequest,
|
||||
UpdateSearchSpaceApiAccessRequest,
|
||||
UpdateSearchSpaceRequest,
|
||||
CreateWorkspaceRequest,
|
||||
DeleteWorkspaceRequest,
|
||||
UpdateWorkspaceApiAccessRequest,
|
||||
UpdateWorkspaceRequest,
|
||||
} from "@/contracts/types/workspace.types";
|
||||
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 { queryClient } from "@/lib/query-client/client";
|
||||
import { activeWorkspaceIdAtom } from "./workspace-query.atoms";
|
||||
|
||||
export const createSearchSpaceMutationAtom = atomWithMutation(() => {
|
||||
export const createWorkspaceMutationAtom = atomWithMutation(() => {
|
||||
return {
|
||||
mutationKey: ["create-search-space"],
|
||||
mutationFn: async (request: CreateSearchSpaceRequest) => {
|
||||
return searchSpacesApiService.createSearchSpace(request);
|
||||
mutationKey: ["create-workspace"],
|
||||
mutationFn: async (request: CreateWorkspaceRequest) => {
|
||||
return workspacesApiService.createWorkspace(request);
|
||||
},
|
||||
|
||||
onSuccess: () => {
|
||||
toast.success("Search space created successfully");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.searchSpaces.all,
|
||||
queryKey: cacheKeys.workspaces.all,
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const updateSearchSpaceMutationAtom = atomWithMutation((get) => {
|
||||
const activeSearchSpaceId = get(activeWorkspaceIdAtom);
|
||||
export const updateWorkspaceMutationAtom = atomWithMutation((get) => {
|
||||
const activeWorkspaceId = get(activeWorkspaceIdAtom);
|
||||
|
||||
return {
|
||||
mutationKey: ["update-search-space", activeSearchSpaceId],
|
||||
enabled: !!activeSearchSpaceId,
|
||||
mutationFn: async (request: UpdateSearchSpaceRequest) => {
|
||||
return searchSpacesApiService.updateSearchSpace(request);
|
||||
mutationKey: ["update-workspace", activeWorkspaceId],
|
||||
enabled: !!activeWorkspaceId,
|
||||
mutationFn: async (request: UpdateWorkspaceRequest) => {
|
||||
return workspacesApiService.updateWorkspace(request);
|
||||
},
|
||||
|
||||
onSuccess: (_, request: UpdateSearchSpaceRequest) => {
|
||||
onSuccess: (_, request: UpdateWorkspaceRequest) => {
|
||||
toast.success("Search space updated successfully");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.searchSpaces.all,
|
||||
queryKey: cacheKeys.workspaces.all,
|
||||
});
|
||||
if (request.id) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.searchSpaces.detail(String(request.id)),
|
||||
queryKey: cacheKeys.workspaces.detail(String(request.id)),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const updateSearchSpaceApiAccessMutationAtom = atomWithMutation((get) => {
|
||||
const activeSearchSpaceId = get(activeWorkspaceIdAtom);
|
||||
export const updateWorkspaceApiAccessMutationAtom = atomWithMutation((get) => {
|
||||
const activeWorkspaceId = get(activeWorkspaceIdAtom);
|
||||
|
||||
return {
|
||||
mutationKey: ["update-search-space-api-access", activeSearchSpaceId],
|
||||
enabled: !!activeSearchSpaceId,
|
||||
mutationFn: async (request: UpdateSearchSpaceApiAccessRequest) => {
|
||||
return searchSpacesApiService.updateSearchSpaceApiAccess(request);
|
||||
mutationKey: ["update-workspace-api-access", activeWorkspaceId],
|
||||
enabled: !!activeWorkspaceId,
|
||||
mutationFn: async (request: UpdateWorkspaceApiAccessRequest) => {
|
||||
return workspacesApiService.updateWorkspaceApiAccess(request);
|
||||
},
|
||||
|
||||
onSuccess: (_, request: UpdateSearchSpaceApiAccessRequest) => {
|
||||
onSuccess: (_, request: UpdateWorkspaceApiAccessRequest) => {
|
||||
toast.success("API access updated successfully");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.searchSpaces.all,
|
||||
queryKey: cacheKeys.workspaces.all,
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.searchSpaces.detail(String(request.id)),
|
||||
queryKey: cacheKeys.workspaces.detail(String(request.id)),
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const deleteSearchSpaceMutationAtom = atomWithMutation((get) => {
|
||||
const activeSearchSpaceId = get(activeWorkspaceIdAtom);
|
||||
export const deleteWorkspaceMutationAtom = atomWithMutation((get) => {
|
||||
const activeWorkspaceId = get(activeWorkspaceIdAtom);
|
||||
|
||||
return {
|
||||
mutationKey: ["delete-search-space", activeSearchSpaceId],
|
||||
enabled: !!activeSearchSpaceId,
|
||||
mutationFn: async (request: DeleteSearchSpaceRequest) => {
|
||||
return searchSpacesApiService.deleteSearchSpace(request);
|
||||
mutationKey: ["delete-workspace", activeWorkspaceId],
|
||||
enabled: !!activeWorkspaceId,
|
||||
mutationFn: async (request: DeleteWorkspaceRequest) => {
|
||||
return workspacesApiService.deleteWorkspace(request);
|
||||
},
|
||||
|
||||
onSuccess: (_, request: DeleteSearchSpaceRequest) => {
|
||||
onSuccess: (_, request: DeleteWorkspaceRequest) => {
|
||||
toast.success("Search space deleted successfully");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.searchSpaces.all,
|
||||
queryKey: cacheKeys.workspaces.all,
|
||||
});
|
||||
if (request.id) {
|
||||
queryClient.removeQueries({
|
||||
queryKey: cacheKeys.searchSpaces.detail(String(request.id)),
|
||||
queryKey: cacheKeys.workspaces.detail(String(request.id)),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
import { atom } from "jotai";
|
||||
import { atomWithQuery } from "jotai-tanstack-query";
|
||||
import type { GetSearchSpacesRequest } from "@/contracts/types/workspace.types";
|
||||
import { searchSpacesApiService } from "@/lib/apis/workspaces-api.service";
|
||||
import type { GetWorkspacesRequest } from "@/contracts/types/workspace.types";
|
||||
import { workspacesApiService } from "@/lib/apis/workspaces-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
|
||||
export const activeWorkspaceIdAtom = atom<string | null>(null);
|
||||
|
||||
export const searchSpacesQueryParamsAtom = atom<GetSearchSpacesRequest["queryParams"]>({
|
||||
export const workspacesQueryParamsAtom = atom<GetWorkspacesRequest["queryParams"]>({
|
||||
skip: 0,
|
||||
limit: 10,
|
||||
owned_only: false,
|
||||
});
|
||||
|
||||
export const searchSpacesAtom = atomWithQuery((get) => {
|
||||
const queryParams = get(searchSpacesQueryParamsAtom);
|
||||
export const workspacesAtom = atomWithQuery((get) => {
|
||||
const queryParams = get(workspacesQueryParamsAtom);
|
||||
|
||||
return {
|
||||
queryKey: cacheKeys.searchSpaces.withQueryParams(queryParams),
|
||||
queryKey: cacheKeys.workspaces.withQueryParams(queryParams),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
queryFn: async () => {
|
||||
return searchSpacesApiService.getSearchSpaces({
|
||||
return workspacesApiService.getWorkspaces({
|
||||
queryParams,
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ This release brings major improvements to **collaboration and user experience**.
|
|||
#### New Chat-First Interface
|
||||
|
||||
- **Dashboard Removed**: Users now land directly in a chat for faster access
|
||||
- **Redesigned Search Spaces**: Moved to a left column with color-coded icons and hover tooltips
|
||||
- **Redesigned Workspaces**: Moved to a left column with color-coded icons and hover tooltips
|
||||
- **Collapsible Sidebar**: New sidebar design with collapsible sections for private and group chats
|
||||
- **Streamlined Settings**: Accessible through intuitive dropdown menus
|
||||
- **Mobile-Responsive Design**: Better experience on all devices
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ This update brings **public sharing, image generation**, a redesigned Documents
|
|||
#### Public Sharing
|
||||
|
||||
- **Public Chats**: Share snapshots of chats via public links.
|
||||
- **Sharing Permissions**: Search Space owners control who can create and manage public links.
|
||||
- **Link Management Page**: View and revoke all public chats from Search Space Settings.
|
||||
- **Sharing Permissions**: Workspace owners control who can create and manage public links.
|
||||
- **Link Management Page**: View and revoke all public chats from Workspace Settings.
|
||||
|
||||
#### Auto (Load Balanced) Mode
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ SurfSense now treats every sensitive AI action as an explicit, reviewable step.
|
|||
|
||||
- **Mobile-First Polish**: Mobile citation drawer, long-press actions in chats and documents, a responsive documents sidebar, and a mobile-friendly onboarding tour.
|
||||
- **Reworked Composer**: Tool actions are grouped into a cleaner menu with better icons, plus a helpful "connect tools" banner.
|
||||
- **Settings & Team**: New tabbed user settings page (profile + API keys), team roles management with pagination, and a search space settings dialog.
|
||||
- **Settings & Team**: New tabbed user settings page (profile + API keys), team roles management with pagination, and a workspace settings dialog.
|
||||
- **Right Panel & Docked Sidebar**: A tabbed Sources/Report panel with smooth transitions, plus an optional docked documents sidebar.
|
||||
- **Community Prompts**: Public prompt library with copy support, inline share toggles, and a see-more/less toggle for long prompts.
|
||||
- **New Homepage**: Smooth scrolling, a use-cases grid, an updated walkthrough hero, a GitHub stars badge, and a new carousel for AI-generated video.
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ The SurfSense desktop app becomes a serious always-on **AI like ChatGPT** that a
|
|||
- **Multi-Suggestion UI**: The suggestion popup now offers up to **3 options** to pick from, with clean cards and quick-assist detection.
|
||||
- **Knowledge Base Grounding**: Suggestions are grounded in your connected knowledge base, not just generic model output.
|
||||
- **macOS Permission Onboarding**: A clearer onboarding page walks macOS users through the permissions the app needs, only when they're actually needed.
|
||||
- **Switch Search Spaces from the Overlay**: Change the active search space without opening the main app.
|
||||
- **Switch Workspaces from the Overlay**: Change the active workspace without opening the main app.
|
||||
- **General Assist**: A new general-purpose assist mode with cleaner shortcut icons and descriptions.
|
||||
- **Stay Signed In Everywhere**: Sign-in is now synchronized between the desktop app and the web app.
|
||||
- **Vision Model Settings**: A dedicated Vision Models tab in Settings lets you pick and manage vision models, including a dynamic model list from OpenRouter.
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Turn your knowledge and profile details into export-ready resume documents.
|
|||
|
||||
- **Cross-Site Anonymous Chat Cookies**: Anonymous chat cookies now adapt their SameSite and Secure settings based on deployment context, making hosted and cross-domain setups more reliable.
|
||||
- **Better Anonymous Chat History**: Message history handling in anonymous chat is more dependable, especially when users move between public chat states.
|
||||
- **Safer Form Inputs**: Login, registration, profile, search space, and role forms now enforce sensible max-length limits directly in the UI.
|
||||
- **Safer Form Inputs**: Login, registration, profile, workspace, and role forms now enforce sensible max-length limits directly in the UI.
|
||||
- **Cleaner Page Landmarks**: Home and free-chat pages no longer nest main landmarks, improving HTML semantics and screen-reader navigation.
|
||||
- **SEO Metadata Refresh**: Titles and descriptions across key pages now better communicate SurfSense's open-source, privacy-focused positioning.
|
||||
|
||||
|
|
|
|||
|
|
@ -491,7 +491,7 @@ export const AssistantMessage: FC = () => {
|
|||
const commentPanelRef = useRef<HTMLDivElement>(null);
|
||||
const commentTriggerRef = useRef<HTMLButtonElement>(null);
|
||||
const messageId = useAuiState(({ message }) => message?.id);
|
||||
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom);
|
||||
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
|
||||
const dbMessageId = parseMessageId(messageId);
|
||||
const commentsEnabled = useAtomValue(commentsEnabledAtom);
|
||||
|
||||
|
|
@ -520,7 +520,7 @@ export const AssistantMessage: FC = () => {
|
|||
const commentCount = commentsData?.total_count ?? 0;
|
||||
const hasComments = commentCount > 0;
|
||||
|
||||
const showCommentTrigger = searchSpaceId && commentsEnabled && !isMessageStreaming && dbMessageId;
|
||||
const showCommentTrigger = workspaceId && commentsEnabled && !isMessageStreaming && dbMessageId;
|
||||
|
||||
// Close floating panel when clicking outside (but not on portaled popover/dropdown content)
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@ interface ConnectorIndicatorProps {
|
|||
|
||||
export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, ConnectorIndicatorProps>(
|
||||
(_props, ref) => {
|
||||
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom);
|
||||
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
|
||||
|
||||
// Real-time document type counts via Zero (updates instantly as docs are indexed)
|
||||
const documentTypeCounts = useZeroDocumentTypeCounts(searchSpaceId);
|
||||
const documentTypeCounts = useZeroDocumentTypeCounts(workspaceId);
|
||||
// Read status inbox items from shared atom (populated by LayoutDataProvider)
|
||||
// instead of creating a duplicate useInbox("status") hook.
|
||||
const statusInboxItems = useAtomValue(statusInboxItemsAtom);
|
||||
|
|
@ -124,7 +124,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
|||
loading: connectorsLoading,
|
||||
error: connectorsError,
|
||||
refreshConnectors: refreshConnectorsSync,
|
||||
} = useConnectorsSync(searchSpaceId);
|
||||
} = useConnectorsSync(workspaceId);
|
||||
|
||||
const useSyncData = connectorsFromSync.length > 0 || (connectorsLoading && !connectorsError);
|
||||
const connectors = useSyncData ? connectorsFromSync : allConnectors || [];
|
||||
|
|
@ -142,7 +142,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
|||
inboxItems
|
||||
);
|
||||
|
||||
// Get document types that have documents in the search space
|
||||
// Get document types that have documents in the workspace
|
||||
const activeDocumentTypes = documentTypeCounts
|
||||
? Object.entries(documentTypeCounts).filter(([, count]) => count > 0)
|
||||
: [];
|
||||
|
|
@ -163,7 +163,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
|||
open: () => handleOpenChange(true),
|
||||
}));
|
||||
|
||||
if (!searchSpaceId) return null;
|
||||
if (!workspaceId) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} modal={false} onOpenChange={handleOpenChange}>
|
||||
|
|
@ -191,8 +191,8 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
|||
>
|
||||
<DialogTitle className="sr-only">Manage Connectors</DialogTitle>
|
||||
{/* YouTube Crawler View - shown when adding YouTube videos */}
|
||||
{isYouTubeView && searchSpaceId ? (
|
||||
<YouTubeCrawlerView searchSpaceId={searchSpaceId} onBack={handleBackFromYouTube} />
|
||||
{isYouTubeView && workspaceId ? (
|
||||
<YouTubeCrawlerView workspaceId={workspaceId} onBack={handleBackFromYouTube} />
|
||||
) : viewingMCPList ? (
|
||||
<ConnectorAccountsListView
|
||||
connectorType="MCP_CONNECTOR"
|
||||
|
|
@ -253,7 +253,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
|||
isSaving={isSaving}
|
||||
isDisconnecting={isDisconnecting}
|
||||
isIndexing={indexingConnectorIds.has(editingConnector.id)}
|
||||
searchSpaceId={searchSpaceId?.toString()}
|
||||
workspaceId={workspaceId?.toString()}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
onPeriodicEnabledChange={setPeriodicEnabled}
|
||||
|
|
@ -346,7 +346,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
|||
<TabsContent value="all" className="m-0">
|
||||
<AllConnectorsTab
|
||||
searchQuery={searchQuery}
|
||||
searchSpaceId={searchSpaceId}
|
||||
workspaceId={workspaceId}
|
||||
connectedTypes={connectedTypes}
|
||||
connectingId={connectingId}
|
||||
allConnectors={connectors}
|
||||
|
|
|
|||
|
|
@ -159,17 +159,17 @@ export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
|
|||
|
||||
<div className="h-px bg-border/60" />
|
||||
|
||||
{/* Step 4 — Pick search space */}
|
||||
{/* Step 4 — Pick workspace */}
|
||||
<article>
|
||||
<header className="mb-3 flex items-center gap-2">
|
||||
<div className="flex size-7 items-center justify-center rounded-md border border-slate-400/30 text-xs font-medium">
|
||||
4
|
||||
</div>
|
||||
<h3 className="text-sm font-medium sm:text-base">Pick this search space</h3>
|
||||
<h3 className="text-sm font-medium sm:text-base">Pick this workspace</h3>
|
||||
</header>
|
||||
<p className="text-[11px] text-muted-foreground sm:text-xs">
|
||||
In the plugin's <span className="font-medium">Search space</span> setting, choose the
|
||||
search space you want this vault to sync into. The connector will appear here
|
||||
workspace you want this vault to sync into. The connector will appear here
|
||||
automatically once the plugin makes its first sync.
|
||||
</p>
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export function getConnectorBenefits(connectorType: string): string[] | null {
|
|||
LINEAR_CONNECTOR: [
|
||||
"Search through all your Linear issues and comments",
|
||||
"Access issue titles, descriptions, and full discussion threads",
|
||||
"Connect your team's project management directly to your search space",
|
||||
"Connect your team's project management directly to your workspace",
|
||||
"Keep your search results up-to-date with latest Linear content",
|
||||
"Index your Linear issues for enhanced search capabilities",
|
||||
],
|
||||
|
|
@ -36,63 +36,63 @@ export function getConnectorBenefits(connectorType: string): string[] | null {
|
|||
SLACK_CONNECTOR: [
|
||||
"Search through all your Slack messages and conversations",
|
||||
"Access messages from public and private channels",
|
||||
"Connect your team's communications directly to your search space",
|
||||
"Connect your team's communications directly to your workspace",
|
||||
"Keep your search results up-to-date with latest Slack content",
|
||||
"Index your Slack conversations for enhanced search capabilities",
|
||||
],
|
||||
DISCORD_CONNECTOR: [
|
||||
"Search through all your Discord messages and conversations",
|
||||
"Access messages from all accessible channels",
|
||||
"Connect your community's communications directly to your search space",
|
||||
"Connect your community's communications directly to your workspace",
|
||||
"Keep your search results up-to-date with latest Discord content",
|
||||
"Index your Discord conversations for enhanced search capabilities",
|
||||
],
|
||||
NOTION_CONNECTOR: [
|
||||
"Search through all your Notion pages and databases",
|
||||
"Access page content, properties, and metadata",
|
||||
"Connect your knowledge base directly to your search space",
|
||||
"Connect your knowledge base directly to your workspace",
|
||||
"Keep your search results up-to-date with latest Notion content",
|
||||
"Index your Notion workspace for enhanced search capabilities",
|
||||
],
|
||||
CONFLUENCE_CONNECTOR: [
|
||||
"Search through all your Confluence pages and spaces",
|
||||
"Access page content, comments, and attachments",
|
||||
"Connect your team's documentation directly to your search space",
|
||||
"Connect your team's documentation directly to your workspace",
|
||||
"Keep your search results up-to-date with latest Confluence content",
|
||||
"Index your Confluence workspace for enhanced search capabilities",
|
||||
],
|
||||
BOOKSTACK_CONNECTOR: [
|
||||
"Search through all your BookStack pages and books",
|
||||
"Access page content, chapters, and documentation",
|
||||
"Connect your documentation directly to your search space",
|
||||
"Connect your documentation directly to your workspace",
|
||||
"Keep your search results up-to-date with latest BookStack content",
|
||||
"Index your BookStack instance for enhanced search capabilities",
|
||||
],
|
||||
GITHUB_CONNECTOR: [
|
||||
"Search through code, issues, and documentation from GitHub repositories",
|
||||
"Access repository content, pull requests, and discussions",
|
||||
"Connect your codebase directly to your search space",
|
||||
"Connect your codebase directly to your workspace",
|
||||
"Keep your search results up-to-date with latest GitHub content",
|
||||
"Index your GitHub repositories for enhanced search capabilities",
|
||||
],
|
||||
JIRA_CONNECTOR: [
|
||||
"Search through all your Jira issues and tickets",
|
||||
"Access issue descriptions, comments, and project data",
|
||||
"Connect your project management directly to your search space",
|
||||
"Connect your project management directly to your workspace",
|
||||
"Keep your search results up-to-date with latest Jira content",
|
||||
"Index your Jira projects for enhanced search capabilities",
|
||||
],
|
||||
CLICKUP_CONNECTOR: [
|
||||
"Search through all your ClickUp tasks and projects",
|
||||
"Access task descriptions, comments, and project data",
|
||||
"Connect your task management directly to your search space",
|
||||
"Connect your task management directly to your workspace",
|
||||
"Keep your search results up-to-date with latest ClickUp content",
|
||||
"Index your ClickUp workspace for enhanced search capabilities",
|
||||
],
|
||||
LUMA_CONNECTOR: [
|
||||
"Search through all your Luma events",
|
||||
"Access event details, descriptions, and attendee information",
|
||||
"Connect your events directly to your search space",
|
||||
"Connect your events directly to your workspace",
|
||||
"Keep your search results up-to-date with latest Luma content",
|
||||
"Index your Luma events for enhanced search capabilities",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ export const CirclebackConfig: FC<CirclebackConfigProps> = ({ connector, onNameC
|
|||
<AlertDescription>
|
||||
Configure this URL in Circleback Settings → Automations → Create automation → Send
|
||||
webhook request. The webhook will automatically send meeting notes, transcripts, and
|
||||
action items to this search space.
|
||||
action items to this workspace.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export interface ConnectorConfigProps {
|
|||
connector: SearchSourceConnector;
|
||||
onConfigChange?: (config: Record<string, unknown>) => void;
|
||||
onNameChange?: (name: string) => void;
|
||||
searchSpaceId?: string;
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
export type ConnectorConfigComponent = FC<ConnectorConfigProps>;
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ interface ConnectorEditViewProps {
|
|||
isSaving: boolean;
|
||||
isDisconnecting: boolean;
|
||||
isIndexing?: boolean;
|
||||
searchSpaceId?: string;
|
||||
workspaceId?: string;
|
||||
onStartDateChange: (date: Date | undefined) => void;
|
||||
onEndDateChange: (date: Date | undefined) => void;
|
||||
onPeriodicEnabledChange: (enabled: boolean) => void;
|
||||
|
|
@ -65,7 +65,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
|
|||
isSaving,
|
||||
isDisconnecting,
|
||||
isIndexing = false,
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
onStartDateChange,
|
||||
onEndDateChange,
|
||||
onPeriodicEnabledChange,
|
||||
|
|
@ -78,7 +78,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
|
|||
onConfigChange,
|
||||
onNameChange,
|
||||
}) => {
|
||||
const searchSpaceIdAtom = useAtomValue(activeWorkspaceIdAtom);
|
||||
const workspaceIdAtom = useAtomValue(activeWorkspaceIdAtom);
|
||||
const isAuthExpired = connector.config?.auth_expired === true;
|
||||
const reauthEndpoint = getReauthEndpoint(connector);
|
||||
const [reauthing, setReauthing] = useState(false);
|
||||
|
|
@ -91,7 +91,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
|
|||
(connector.is_indexable || connector.connector_type === EnumConnectorName.OBSIDIAN_CONNECTOR);
|
||||
|
||||
const handleReauth = useCallback(async () => {
|
||||
const spaceId = searchSpaceId ?? searchSpaceIdAtom;
|
||||
const spaceId = workspaceId ?? workspaceIdAtom;
|
||||
if (!spaceId || !reauthEndpoint) return;
|
||||
setReauthing(true);
|
||||
try {
|
||||
|
|
@ -119,7 +119,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
|
|||
} finally {
|
||||
setReauthing(false);
|
||||
}
|
||||
}, [searchSpaceId, searchSpaceIdAtom, reauthEndpoint, connector.id]);
|
||||
}, [workspaceId, workspaceIdAtom, reauthEndpoint, connector.id]);
|
||||
|
||||
// Get connector-specific config component (MCP-backed connectors use a generic view)
|
||||
const ConnectorConfigComponent = useMemo(() => {
|
||||
|
|
@ -273,7 +273,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
|
|||
connector={connector}
|
||||
onConfigChange={onConfigChange}
|
||||
onNameChange={onNameChange}
|
||||
searchSpaceId={searchSpaceId}
|
||||
workspaceId={workspaceId}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ function clearOAuthResultCookie(): void {
|
|||
}
|
||||
|
||||
export const useConnectorDialog = () => {
|
||||
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom);
|
||||
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
|
||||
const { data: allConnectors, refetch: refetchAllConnectors } = useAtomValue(connectorsAtom);
|
||||
const { mutateAsync: indexConnector } = useAtomValue(indexConnectorMutationAtom);
|
||||
const { mutateAsync: updateConnector } = useAtomValue(updateConnectorMutationAtom);
|
||||
|
|
@ -140,7 +140,7 @@ export const useConnectorDialog = () => {
|
|||
|
||||
const handleAutoIndex = useCallback(
|
||||
async (connector: SearchSourceConnector, connectorTitle: string, connectorType: string) => {
|
||||
if (!searchSpaceId || isAutoIndexingRef.current) return;
|
||||
if (!workspaceId || isAutoIndexingRef.current) return;
|
||||
isAutoIndexingRef.current = true;
|
||||
|
||||
const defaults = AUTO_INDEX_DEFAULTS[connectorType];
|
||||
|
|
@ -165,13 +165,13 @@ export const useConnectorDialog = () => {
|
|||
await indexConnector({
|
||||
connector_id: connector.id,
|
||||
queryParams: {
|
||||
workspace_id: searchSpaceId,
|
||||
workspace_id: workspaceId,
|
||||
start_date: format(startDate, "yyyy-MM-dd"),
|
||||
end_date: format(endDate, "yyyy-MM-dd"),
|
||||
},
|
||||
});
|
||||
|
||||
trackIndexWithDateRangeStarted(Number(searchSpaceId), connectorType, connector.id, {
|
||||
trackIndexWithDateRangeStarted(Number(workspaceId), connectorType, connector.id, {
|
||||
hasStartDate: true,
|
||||
hasEndDate: true,
|
||||
});
|
||||
|
|
@ -188,13 +188,13 @@ export const useConnectorDialog = () => {
|
|||
});
|
||||
} finally {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
||||
queryKey: cacheKeys.logs.summary(Number(workspaceId)),
|
||||
});
|
||||
await refetchAllConnectors();
|
||||
isAutoIndexingRef.current = false;
|
||||
}
|
||||
},
|
||||
[searchSpaceId, indexConnector, updateConnector, refetchAllConnectors]
|
||||
[workspaceId, indexConnector, updateConnector, refetchAllConnectors]
|
||||
);
|
||||
|
||||
// YouTube view state
|
||||
|
|
@ -206,7 +206,7 @@ export const useConnectorDialog = () => {
|
|||
// Consume OAuth result from cookie (set by /connectors/callback route handler)
|
||||
useEffect(() => {
|
||||
const raw = readOAuthResultCookie();
|
||||
if (!raw || !searchSpaceId) return;
|
||||
if (!raw || !workspaceId) return;
|
||||
clearOAuthResultCookie();
|
||||
|
||||
const result = parseOAuthCallbackResult(raw);
|
||||
|
|
@ -221,7 +221,7 @@ export const useConnectorDialog = () => {
|
|||
|
||||
if (oauthConnector) {
|
||||
trackConnectorSetupFailure(
|
||||
Number(searchSpaceId),
|
||||
Number(workspaceId),
|
||||
oauthConnector.connectorType,
|
||||
result.error,
|
||||
"oauth_callback"
|
||||
|
|
@ -292,7 +292,7 @@ export const useConnectorDialog = () => {
|
|||
const connectorValidation = searchSourceConnector.safeParse(newConnector);
|
||||
if (connectorValidation.success) {
|
||||
trackConnectorConnected(
|
||||
Number(searchSpaceId),
|
||||
Number(workspaceId),
|
||||
oauthConnector.connectorType,
|
||||
newConnector.id
|
||||
);
|
||||
|
|
@ -338,20 +338,20 @@ export const useConnectorDialog = () => {
|
|||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchSpaceId, handleAutoIndex, refetchAllConnectors, setIsOpen]);
|
||||
}, [workspaceId, handleAutoIndex, refetchAllConnectors, setIsOpen]);
|
||||
|
||||
// Handle OAuth connection
|
||||
const handleConnectOAuth = useCallback(
|
||||
async (connector: (typeof OAUTH_CONNECTORS)[number] | (typeof COMPOSIO_CONNECTORS)[number]) => {
|
||||
if (!searchSpaceId || !connector.authEndpoint) return;
|
||||
if (!workspaceId || !connector.authEndpoint) return;
|
||||
|
||||
// Set connecting state immediately to disable button and show spinner
|
||||
setConnectingId(connector.id);
|
||||
|
||||
trackConnectorSetupStarted(Number(searchSpaceId), connector.connectorType, "oauth_click");
|
||||
trackConnectorSetupStarted(Number(workspaceId), connector.connectorType, "oauth_click");
|
||||
|
||||
try {
|
||||
const url = buildBackendUrl(connector.authEndpoint, { space_id: searchSpaceId });
|
||||
const url = buildBackendUrl(connector.authEndpoint, { space_id: workspaceId });
|
||||
|
||||
const response = await authenticatedFetch(url, { method: "GET" });
|
||||
|
||||
|
|
@ -370,7 +370,7 @@ export const useConnectorDialog = () => {
|
|||
} catch (error) {
|
||||
console.error(`Error connecting to ${connector.title}:`, error);
|
||||
trackConnectorSetupFailure(
|
||||
Number(searchSpaceId),
|
||||
Number(workspaceId),
|
||||
connector.connectorType,
|
||||
error instanceof Error ? error.message : "oauth_initiation_failed",
|
||||
"oauth_init"
|
||||
|
|
@ -384,22 +384,22 @@ export const useConnectorDialog = () => {
|
|||
setConnectingId(null);
|
||||
}
|
||||
},
|
||||
[searchSpaceId]
|
||||
[workspaceId]
|
||||
);
|
||||
|
||||
// Handle creating YouTube crawler (not a connector, shows view in popup)
|
||||
const handleCreateYouTubeCrawler = useCallback(() => {
|
||||
if (!searchSpaceId) return;
|
||||
if (!workspaceId) return;
|
||||
setIsYouTubeView(true);
|
||||
}, [searchSpaceId]);
|
||||
}, [workspaceId]);
|
||||
|
||||
// Handle creating webcrawler connector
|
||||
const handleCreateWebcrawler = useCallback(async () => {
|
||||
if (!searchSpaceId) return;
|
||||
if (!workspaceId) return;
|
||||
|
||||
setConnectingId("webcrawler-connector");
|
||||
trackConnectorSetupStarted(
|
||||
Number(searchSpaceId),
|
||||
Number(workspaceId),
|
||||
EnumConnectorName.WEBCRAWLER_CONNECTOR,
|
||||
"webcrawler_quick_add"
|
||||
);
|
||||
|
|
@ -418,7 +418,7 @@ export const useConnectorDialog = () => {
|
|||
enable_vision_llm: false,
|
||||
},
|
||||
queryParams: {
|
||||
workspace_id: searchSpaceId,
|
||||
workspace_id: workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -433,7 +433,7 @@ export const useConnectorDialog = () => {
|
|||
if (connectorValidation.success) {
|
||||
// Track webcrawler connector connected
|
||||
trackConnectorConnected(
|
||||
Number(searchSpaceId),
|
||||
Number(workspaceId),
|
||||
EnumConnectorName.WEBCRAWLER_CONNECTOR,
|
||||
connector.id
|
||||
);
|
||||
|
|
@ -453,7 +453,7 @@ export const useConnectorDialog = () => {
|
|||
} catch (error) {
|
||||
console.error("Error creating webcrawler connector:", error);
|
||||
trackConnectorSetupFailure(
|
||||
Number(searchSpaceId),
|
||||
Number(workspaceId),
|
||||
EnumConnectorName.WEBCRAWLER_CONNECTOR,
|
||||
error instanceof Error ? error.message : "webcrawler_create_failed",
|
||||
"webcrawler_quick_add"
|
||||
|
|
@ -462,18 +462,18 @@ export const useConnectorDialog = () => {
|
|||
} finally {
|
||||
setConnectingId(null);
|
||||
}
|
||||
}, [searchSpaceId, createConnector, refetchAllConnectors, setIsOpen]);
|
||||
}, [workspaceId, createConnector, refetchAllConnectors, setIsOpen]);
|
||||
|
||||
// Handle connecting non-OAuth connectors (like Tavily API, Obsidian plugin, etc.)
|
||||
const handleConnectNonOAuth = useCallback(
|
||||
(connectorType: string) => {
|
||||
if (!searchSpaceId) return;
|
||||
if (!workspaceId) return;
|
||||
|
||||
trackConnectorSetupStarted(Number(searchSpaceId), connectorType, "non_oauth_click");
|
||||
trackConnectorSetupStarted(Number(workspaceId), connectorType, "non_oauth_click");
|
||||
|
||||
setConnectingConnectorType(connectorType);
|
||||
},
|
||||
[searchSpaceId]
|
||||
[workspaceId]
|
||||
);
|
||||
|
||||
// Handle submitting connect form
|
||||
|
|
@ -495,7 +495,7 @@ export const useConnectorDialog = () => {
|
|||
},
|
||||
onIndexingStart?: (connectorId: number) => void
|
||||
) => {
|
||||
if (!searchSpaceId || !connectingConnectorType) {
|
||||
if (!workspaceId || !connectingConnectorType) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -519,7 +519,7 @@ export const useConnectorDialog = () => {
|
|||
enable_vision_llm: false,
|
||||
},
|
||||
queryParams: {
|
||||
workspace_id: searchSpaceId,
|
||||
workspace_id: workspaceId,
|
||||
},
|
||||
});
|
||||
// Refetch connectors to get the new one
|
||||
|
|
@ -536,7 +536,7 @@ export const useConnectorDialog = () => {
|
|||
const currentConnectorType = connectingConnectorType;
|
||||
|
||||
// Track connector connected event for non-OAuth connectors
|
||||
trackConnectorConnected(Number(searchSpaceId), currentConnectorType, connector.id);
|
||||
trackConnectorConnected(Number(workspaceId), currentConnectorType, connector.id);
|
||||
|
||||
// Find connector title from constants
|
||||
const connectorInfo = OTHER_CONNECTORS.find(
|
||||
|
|
@ -612,7 +612,7 @@ export const useConnectorDialog = () => {
|
|||
await indexConnector({
|
||||
connector_id: connector.id,
|
||||
queryParams: {
|
||||
workspace_id: searchSpaceId,
|
||||
workspace_id: workspaceId,
|
||||
start_date: startDateStr,
|
||||
end_date: endDateStr,
|
||||
},
|
||||
|
|
@ -631,7 +631,7 @@ export const useConnectorDialog = () => {
|
|||
setIndexingConnectorConfig(null);
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
||||
queryKey: cacheKeys.logs.summary(Number(workspaceId)),
|
||||
});
|
||||
|
||||
await refetchAllConnectors();
|
||||
|
|
@ -684,7 +684,7 @@ export const useConnectorDialog = () => {
|
|||
} catch (error) {
|
||||
console.error("Error creating connector:", error);
|
||||
trackConnectorSetupFailure(
|
||||
Number(searchSpaceId),
|
||||
Number(workspaceId),
|
||||
connectingConnectorType ?? formData.connector_type,
|
||||
error instanceof Error ? error.message : "connector_create_failed",
|
||||
"non_oauth_form"
|
||||
|
|
@ -698,7 +698,7 @@ export const useConnectorDialog = () => {
|
|||
},
|
||||
[
|
||||
connectingConnectorType,
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
createConnector,
|
||||
refetchAllConnectors,
|
||||
updateConnector,
|
||||
|
|
@ -724,7 +724,7 @@ export const useConnectorDialog = () => {
|
|||
// Handle viewing accounts list for OAuth connector type
|
||||
const handleViewAccountsList = useCallback(
|
||||
(connectorType: string, _connectorTitle?: string) => {
|
||||
if (!searchSpaceId) return;
|
||||
if (!workspaceId) return;
|
||||
|
||||
const oauthConnector =
|
||||
OAUTH_CONNECTORS.find((c) => c.connectorType === connectorType) ||
|
||||
|
|
@ -736,7 +736,7 @@ export const useConnectorDialog = () => {
|
|||
});
|
||||
}
|
||||
},
|
||||
[searchSpaceId]
|
||||
[workspaceId]
|
||||
);
|
||||
|
||||
// Handle going back from accounts list view
|
||||
|
|
@ -746,9 +746,9 @@ export const useConnectorDialog = () => {
|
|||
|
||||
// Handle viewing MCP list
|
||||
const handleViewMCPList = useCallback(() => {
|
||||
if (!searchSpaceId) return;
|
||||
if (!workspaceId) return;
|
||||
setViewingMCPList(true);
|
||||
}, [searchSpaceId]);
|
||||
}, [workspaceId]);
|
||||
|
||||
// Handle going back from MCP list view
|
||||
const handleBackFromMCPList = useCallback(() => {
|
||||
|
|
@ -765,7 +765,7 @@ export const useConnectorDialog = () => {
|
|||
// Handle starting indexing
|
||||
const handleStartIndexing = useCallback(
|
||||
async (refreshConnectors: () => void) => {
|
||||
if (!indexingConfig || !searchSpaceId) return;
|
||||
if (!indexingConfig || !workspaceId) return;
|
||||
|
||||
// Validate date range (skip for Google Drive, Composio Drive, OneDrive, Dropbox, and Webcrawler)
|
||||
if (
|
||||
|
|
@ -847,7 +847,7 @@ export const useConnectorDialog = () => {
|
|||
await indexConnector({
|
||||
connector_id: indexingConfig.connectorId,
|
||||
queryParams: {
|
||||
workspace_id: searchSpaceId,
|
||||
workspace_id: workspaceId,
|
||||
},
|
||||
body: {
|
||||
folders: selectedFolders || [],
|
||||
|
|
@ -870,14 +870,14 @@ export const useConnectorDialog = () => {
|
|||
await indexConnector({
|
||||
connector_id: indexingConfig.connectorId,
|
||||
queryParams: {
|
||||
workspace_id: searchSpaceId,
|
||||
workspace_id: workspaceId,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await indexConnector({
|
||||
connector_id: indexingConfig.connectorId,
|
||||
queryParams: {
|
||||
workspace_id: searchSpaceId,
|
||||
workspace_id: workspaceId,
|
||||
start_date: startDateStr,
|
||||
end_date: endDateStr,
|
||||
},
|
||||
|
|
@ -886,7 +886,7 @@ export const useConnectorDialog = () => {
|
|||
|
||||
// Track index with date range started event
|
||||
trackIndexWithDateRangeStarted(
|
||||
Number(searchSpaceId),
|
||||
Number(workspaceId),
|
||||
indexingConfig.connectorType,
|
||||
indexingConfig.connectorId,
|
||||
{
|
||||
|
|
@ -898,7 +898,7 @@ export const useConnectorDialog = () => {
|
|||
// Track periodic indexing started if enabled
|
||||
if (periodicEnabled) {
|
||||
trackPeriodicIndexingStarted(
|
||||
Number(searchSpaceId),
|
||||
Number(workspaceId),
|
||||
indexingConfig.connectorType,
|
||||
indexingConfig.connectorId,
|
||||
parseInt(frequencyMinutes, 10)
|
||||
|
|
@ -915,7 +915,7 @@ export const useConnectorDialog = () => {
|
|||
|
||||
refreshConnectors();
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
||||
queryKey: cacheKeys.logs.summary(Number(workspaceId)),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error starting indexing:", error);
|
||||
|
|
@ -926,7 +926,7 @@ export const useConnectorDialog = () => {
|
|||
},
|
||||
[
|
||||
indexingConfig,
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
startDate,
|
||||
endDate,
|
||||
indexConnector,
|
||||
|
|
@ -951,7 +951,7 @@ export const useConnectorDialog = () => {
|
|||
// Handle starting edit mode
|
||||
const handleStartEdit = useCallback(
|
||||
(connector: SearchSourceConnector) => {
|
||||
if (!searchSpaceId) return;
|
||||
if (!workspaceId) return;
|
||||
|
||||
// For MCP connectors from "All Connectors" tab, show the list view instead of directly editing
|
||||
// (unless we're already in the MCP list view or on the Active tab where individual MCPs are shown)
|
||||
|
|
@ -986,11 +986,7 @@ export const useConnectorDialog = () => {
|
|||
|
||||
// Track index with date range opened event
|
||||
if (connector.is_indexable) {
|
||||
trackIndexWithDateRangeOpened(
|
||||
Number(searchSpaceId),
|
||||
connector.connector_type,
|
||||
connector.id
|
||||
);
|
||||
trackIndexWithDateRangeOpened(Number(workspaceId), connector.connector_type, connector.id);
|
||||
}
|
||||
|
||||
setEditingConnector(connector);
|
||||
|
|
@ -1001,13 +997,13 @@ export const useConnectorDialog = () => {
|
|||
setStartDate(undefined);
|
||||
setEndDate(undefined);
|
||||
},
|
||||
[searchSpaceId, viewingAccountsType, viewingMCPList, handleViewMCPList, activeTab]
|
||||
[workspaceId, viewingAccountsType, viewingMCPList, handleViewMCPList, activeTab]
|
||||
);
|
||||
|
||||
// Handle saving connector changes
|
||||
const handleSaveConnector = useCallback(
|
||||
async (refreshConnectors: () => void) => {
|
||||
if (!editingConnector || !searchSpaceId || isSaving) return;
|
||||
if (!editingConnector || !workspaceId || isSaving) return;
|
||||
|
||||
// Validate date range (skip for Google Drive/OneDrive/Dropbox which uses folder selection, Webcrawler which uses config, and non-indexable connectors)
|
||||
if (
|
||||
|
|
@ -1114,7 +1110,7 @@ export const useConnectorDialog = () => {
|
|||
await indexConnector({
|
||||
connector_id: editingConnector.id,
|
||||
queryParams: {
|
||||
workspace_id: searchSpaceId,
|
||||
workspace_id: workspaceId,
|
||||
},
|
||||
body: {
|
||||
folders: selectedFolders || [],
|
||||
|
|
@ -1134,7 +1130,7 @@ export const useConnectorDialog = () => {
|
|||
await indexConnector({
|
||||
connector_id: editingConnector.id,
|
||||
queryParams: {
|
||||
workspace_id: searchSpaceId,
|
||||
workspace_id: workspaceId,
|
||||
},
|
||||
});
|
||||
indexingDescription = "Re-indexing started with updated configuration.";
|
||||
|
|
@ -1143,7 +1139,7 @@ export const useConnectorDialog = () => {
|
|||
await indexConnector({
|
||||
connector_id: editingConnector.id,
|
||||
queryParams: {
|
||||
workspace_id: searchSpaceId,
|
||||
workspace_id: workspaceId,
|
||||
start_date: startDateStr,
|
||||
end_date: endDateStr,
|
||||
},
|
||||
|
|
@ -1157,7 +1153,7 @@ export const useConnectorDialog = () => {
|
|||
(indexingDescription.includes("Re-indexing") || indexingDescription.includes("indexing"))
|
||||
) {
|
||||
trackIndexWithDateRangeStarted(
|
||||
Number(searchSpaceId),
|
||||
Number(workspaceId),
|
||||
editingConnector.connector_type,
|
||||
editingConnector.id,
|
||||
{
|
||||
|
|
@ -1170,7 +1166,7 @@ export const useConnectorDialog = () => {
|
|||
// Track periodic indexing if enabled
|
||||
if (periodicEnabled && editingConnector.is_indexable) {
|
||||
trackPeriodicIndexingStarted(
|
||||
Number(searchSpaceId),
|
||||
Number(workspaceId),
|
||||
editingConnector.connector_type,
|
||||
editingConnector.id,
|
||||
frequency || parseInt(frequencyMinutes, 10)
|
||||
|
|
@ -1190,7 +1186,7 @@ export const useConnectorDialog = () => {
|
|||
|
||||
refreshConnectors();
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
||||
queryKey: cacheKeys.logs.summary(Number(workspaceId)),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error saving connector:", error);
|
||||
|
|
@ -1201,7 +1197,7 @@ export const useConnectorDialog = () => {
|
|||
},
|
||||
[
|
||||
editingConnector,
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
isSaving,
|
||||
startDate,
|
||||
endDate,
|
||||
|
|
@ -1220,7 +1216,7 @@ export const useConnectorDialog = () => {
|
|||
// Handle disconnecting connector
|
||||
const handleDisconnectConnector = useCallback(
|
||||
async (refreshConnectors: () => void) => {
|
||||
if (!editingConnector || !searchSpaceId) return;
|
||||
if (!editingConnector || !workspaceId) return;
|
||||
|
||||
setIsDisconnecting(true);
|
||||
try {
|
||||
|
|
@ -1230,7 +1226,7 @@ export const useConnectorDialog = () => {
|
|||
|
||||
// Track connector deleted event
|
||||
trackConnectorDeleted(
|
||||
Number(searchSpaceId),
|
||||
Number(workspaceId),
|
||||
editingConnector.connector_type,
|
||||
editingConnector.id
|
||||
);
|
||||
|
|
@ -1255,7 +1251,7 @@ export const useConnectorDialog = () => {
|
|||
|
||||
refreshConnectors();
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
||||
queryKey: cacheKeys.logs.summary(Number(workspaceId)),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error disconnecting connector:", error);
|
||||
|
|
@ -1264,7 +1260,7 @@ export const useConnectorDialog = () => {
|
|||
setIsDisconnecting(false);
|
||||
}
|
||||
},
|
||||
[editingConnector, searchSpaceId, deleteConnector, cameFromMCPList, setIsOpen]
|
||||
[editingConnector, workspaceId, deleteConnector, cameFromMCPList, setIsOpen]
|
||||
);
|
||||
|
||||
// Handle quick index (index with selected date range, or backend defaults if none selected)
|
||||
|
|
@ -1276,7 +1272,7 @@ export const useConnectorDialog = () => {
|
|||
startDate?: Date,
|
||||
endDate?: Date
|
||||
) => {
|
||||
if (!searchSpaceId) {
|
||||
if (!workspaceId) {
|
||||
if (stopIndexing) {
|
||||
stopIndexing(connectorId);
|
||||
}
|
||||
|
|
@ -1285,7 +1281,7 @@ export const useConnectorDialog = () => {
|
|||
|
||||
// Track quick index clicked event
|
||||
if (connectorType) {
|
||||
trackQuickIndexClicked(Number(searchSpaceId), connectorType, connectorId);
|
||||
trackQuickIndexClicked(Number(workspaceId), connectorType, connectorId);
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -1296,7 +1292,7 @@ export const useConnectorDialog = () => {
|
|||
await indexConnector({
|
||||
connector_id: connectorId,
|
||||
queryParams: {
|
||||
workspace_id: searchSpaceId,
|
||||
workspace_id: workspaceId,
|
||||
start_date: startDateStr,
|
||||
end_date: endDateStr,
|
||||
},
|
||||
|
|
@ -1305,7 +1301,7 @@ export const useConnectorDialog = () => {
|
|||
|
||||
// Invalidate queries to refresh data
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
||||
queryKey: cacheKeys.logs.summary(Number(workspaceId)),
|
||||
});
|
||||
// Note: Don't call stopIndexing here - let useIndexingConnectors hook
|
||||
// detect when last_indexed_at changes via real-time sync
|
||||
|
|
@ -1318,7 +1314,7 @@ export const useConnectorDialog = () => {
|
|||
}
|
||||
}
|
||||
},
|
||||
[searchSpaceId, indexConnector]
|
||||
[workspaceId, indexConnector]
|
||||
);
|
||||
|
||||
// Handle going back from edit view
|
||||
|
|
@ -1406,7 +1402,7 @@ export const useConnectorDialog = () => {
|
|||
periodicEnabled,
|
||||
frequencyMinutes,
|
||||
enableVisionLlm,
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
allConnectors,
|
||||
viewingAccountsType,
|
||||
viewingMCPList,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export function getConnectorDisplayName(fullName: string): string {
|
|||
|
||||
interface AllConnectorsTabProps {
|
||||
searchQuery: string;
|
||||
searchSpaceId: string;
|
||||
workspaceId: string;
|
||||
connectedTypes: Set<string>;
|
||||
connectingId: string | null;
|
||||
allConnectors: SearchSourceConnector[] | undefined;
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
|||
isConnecting = false,
|
||||
addButtonText,
|
||||
}) => {
|
||||
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom);
|
||||
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
|
||||
const [reauthingId, setReauthingId] = useState<number | null>(null);
|
||||
const [confirmDisconnectId, setConfirmDisconnectId] = useState<number | null>(null);
|
||||
const [disconnectingId, setDisconnectingId] = useState<number | null>(null);
|
||||
|
|
@ -58,13 +58,13 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
|||
const handleReauth = useCallback(
|
||||
async (connector: SearchSourceConnector) => {
|
||||
const endpoint = getReauthEndpoint(connector);
|
||||
if (!searchSpaceId || !endpoint) return;
|
||||
if (!workspaceId || !endpoint) return;
|
||||
setReauthingId(connector.id);
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
buildBackendUrl(endpoint, {
|
||||
connector_id: connector.id,
|
||||
space_id: searchSpaceId,
|
||||
space_id: workspaceId,
|
||||
return_url: window.location.pathname,
|
||||
})
|
||||
);
|
||||
|
|
@ -86,7 +86,7 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
|||
setReauthingId(null);
|
||||
}
|
||||
},
|
||||
[searchSpaceId]
|
||||
[workspaceId]
|
||||
);
|
||||
|
||||
// Filter connectors to only show those of this type
|
||||
|
|
|
|||
|
|
@ -38,11 +38,11 @@ function extractYoutubeUrls(text: string): string[] {
|
|||
}
|
||||
|
||||
interface YouTubeCrawlerViewProps {
|
||||
searchSpaceId: string;
|
||||
workspaceId: string;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ searchSpaceId, onBack }) => {
|
||||
export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ workspaceId, onBack }) => {
|
||||
const t = useTranslations("add_youtube");
|
||||
const [videoTags, setVideoTags] = useState<TagType[]>([]);
|
||||
const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null);
|
||||
|
|
@ -165,7 +165,7 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ searchSpaceId,
|
|||
{
|
||||
document_type: "YOUTUBE_VIDEO",
|
||||
content: videoUrls,
|
||||
workspace_id: parseInt(searchSpaceId, 10),
|
||||
workspace_id: parseInt(workspaceId, 10),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
|
|
|
|||
|
|
@ -90,9 +90,9 @@ const DocumentUploadPopupContent: FC<{
|
|||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}> = ({ isOpen, onOpenChange }) => {
|
||||
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom);
|
||||
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
|
||||
|
||||
if (!searchSpaceId) return null;
|
||||
if (!workspaceId) return null;
|
||||
|
||||
const handleSuccess = () => {
|
||||
onOpenChange(false);
|
||||
|
|
@ -112,12 +112,12 @@ const DocumentUploadPopupContent: FC<{
|
|||
Upload Documents
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-base text-muted-foreground/80 line-clamp-1">
|
||||
Upload and sync your documents to your search space
|
||||
Upload and sync your documents to your workspace
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="px-4 sm:px-6 pb-4 sm:pb-6">
|
||||
<DocumentUploadTab searchSpaceId={searchSpaceId} onSuccess={handleSuccess} />
|
||||
<DocumentUploadTab workspaceId={workspaceId} onSuccess={handleSuccess} />
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ function FilePathLink({ path, className }: { path: string; className?: string })
|
|||
const openEditorPanel = useSetAtom(openEditorPanelAtom);
|
||||
const params = useParams();
|
||||
const electronAPI = useElectronAPI();
|
||||
const resolvedSearchSpaceId = getWorkspaceIdNumber(params);
|
||||
const resolvedWorkspaceId = getWorkspaceIdNumber(params);
|
||||
|
||||
const { displayName, isFolder } = getVirtualPathDisplay(path);
|
||||
const icon = isFolder ? <FolderIcon className="size-3.5" /> : <FileIcon className="size-3.5" />;
|
||||
|
|
@ -204,7 +204,7 @@ function FilePathLink({ path, className }: { path: string; className?: string })
|
|||
if (electronAPI.getAgentFilesystemMounts) {
|
||||
try {
|
||||
const mounts = (await electronAPI.getAgentFilesystemMounts(
|
||||
resolvedSearchSpaceId
|
||||
resolvedWorkspaceId
|
||||
)) as AgentFilesystemMount[];
|
||||
resolvedLocalPath = normalizeLocalVirtualPathForEditor(path, mounts);
|
||||
} catch {
|
||||
|
|
@ -215,21 +215,21 @@ function FilePathLink({ path, className }: { path: string; className?: string })
|
|||
kind: "local_file",
|
||||
localFilePath: resolvedLocalPath,
|
||||
title: resolvedLocalPath.split("/").pop() || resolvedLocalPath,
|
||||
searchSpaceId: resolvedSearchSpaceId,
|
||||
workspaceId: resolvedWorkspaceId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resolvedSearchSpaceId || !path.startsWith("/documents/")) return;
|
||||
if (!resolvedWorkspaceId || !path.startsWith("/documents/")) return;
|
||||
try {
|
||||
const doc = await documentsApiService.getDocumentByVirtualPath({
|
||||
workspace_id: resolvedSearchSpaceId,
|
||||
workspace_id: resolvedWorkspaceId,
|
||||
virtual_path: path,
|
||||
});
|
||||
openEditorPanel({
|
||||
kind: "document",
|
||||
documentId: doc.id,
|
||||
searchSpaceId: resolvedSearchSpaceId,
|
||||
workspaceId: resolvedWorkspaceId,
|
||||
title: doc.title,
|
||||
});
|
||||
} catch {
|
||||
|
|
@ -237,7 +237,7 @@ function FilePathLink({ path, className }: { path: string; className?: string })
|
|||
}
|
||||
})();
|
||||
},
|
||||
[electronAPI, openEditorPanel, path, resolvedSearchSpaceId]
|
||||
[electronAPI, openEditorPanel, path, resolvedWorkspaceId]
|
||||
);
|
||||
|
||||
// Folders cannot open in the editor panel — keep them as visual chips.
|
||||
|
|
|
|||
|
|
@ -895,7 +895,7 @@ const Composer: FC = () => {
|
|||
<ComposerSuggestionPopoverContent side="top">
|
||||
<DocumentMentionPicker
|
||||
ref={documentPickerRef}
|
||||
searchSpaceId={workspaceId ?? 0}
|
||||
workspaceId={workspaceId ?? 0}
|
||||
enableChatMentions
|
||||
currentChatId={threadId}
|
||||
onSelectionChange={handleDocumentsMention}
|
||||
|
|
@ -961,7 +961,7 @@ const Composer: FC = () => {
|
|||
</div>
|
||||
<ComposerAction
|
||||
isBlockedByOtherUser={isBlockedByOtherUser}
|
||||
searchSpaceId={workspaceId ?? 0}
|
||||
workspaceId={workspaceId ?? 0}
|
||||
onChatModelSelected={handleChatModelSelected}
|
||||
/>
|
||||
<ConnectorIndicator showTrigger={false} />
|
||||
|
|
@ -982,13 +982,13 @@ const Composer: FC = () => {
|
|||
|
||||
interface ComposerActionProps {
|
||||
isBlockedByOtherUser?: boolean;
|
||||
searchSpaceId: number;
|
||||
workspaceId: number;
|
||||
onChatModelSelected?: () => void;
|
||||
}
|
||||
|
||||
const ComposerAction: FC<ComposerActionProps> = ({
|
||||
isBlockedByOtherUser = false,
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
onChatModelSelected,
|
||||
}) => {
|
||||
const mentionedDocuments = useAtomValue(mentionedDocumentsAtom);
|
||||
|
|
@ -1564,7 +1564,7 @@ const ComposerAction: FC<ComposerActionProps> = ({
|
|||
)}
|
||||
<div className="ml-auto flex min-w-0 shrink-0 items-center gap-2">
|
||||
<ChatHeader
|
||||
searchSpaceId={searchSpaceId}
|
||||
workspaceId={workspaceId}
|
||||
className="h-9 max-w-[44vw] px-2 sm:max-w-[220px] sm:px-3"
|
||||
onChatModelSelected={onChatModelSelected}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -76,33 +76,33 @@ const UserTextPart: FC = () => {
|
|||
const openEditorPanel = useSetAtom(openEditorPanelAtom);
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const resolvedSearchSpaceId = getWorkspaceIdNumber(params);
|
||||
const resolvedWorkspaceId = getWorkspaceIdNumber(params);
|
||||
|
||||
const handleOpenDoc = useCallback(
|
||||
(docId: number, title: string) => {
|
||||
if (!resolvedSearchSpaceId) {
|
||||
toast.error("Cannot open document outside a search space.");
|
||||
if (!resolvedWorkspaceId) {
|
||||
toast.error("Cannot open document outside a workspace.");
|
||||
return;
|
||||
}
|
||||
openEditorPanel({
|
||||
kind: "document",
|
||||
documentId: docId,
|
||||
searchSpaceId: resolvedSearchSpaceId,
|
||||
workspaceId: resolvedWorkspaceId,
|
||||
title,
|
||||
});
|
||||
},
|
||||
[openEditorPanel, resolvedSearchSpaceId]
|
||||
[openEditorPanel, resolvedWorkspaceId]
|
||||
);
|
||||
|
||||
const handleOpenThread = useCallback(
|
||||
(threadId: number) => {
|
||||
if (!resolvedSearchSpaceId) {
|
||||
toast.error("Cannot open chat outside a search space.");
|
||||
if (!resolvedWorkspaceId) {
|
||||
toast.error("Cannot open chat outside a workspace.");
|
||||
return;
|
||||
}
|
||||
router.push(`/dashboard/${resolvedSearchSpaceId}/new-chat/${threadId}`);
|
||||
router.push(`/dashboard/${resolvedWorkspaceId}/new-chat/${threadId}`);
|
||||
},
|
||||
[resolvedSearchSpaceId, router]
|
||||
[resolvedWorkspaceId, router]
|
||||
);
|
||||
|
||||
const segments = parseMentionSegments(text, mentionedDocs);
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ export const CitationPanelContent: FC<CitationPanelContentProps> = ({
|
|||
if (!data) return;
|
||||
openEditorPanel({
|
||||
documentId: data.id,
|
||||
searchSpaceId: data.workspace_id,
|
||||
workspaceId: data.workspace_id,
|
||||
title: data.title,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export interface FolderDisplay {
|
|||
name: string;
|
||||
position: string;
|
||||
parentId: number | null;
|
||||
searchSpaceId: number;
|
||||
workspaceId: number;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ export function EditorPanelContent({
|
|||
documentId,
|
||||
localFilePath,
|
||||
memoryScope,
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
title,
|
||||
onClose,
|
||||
}: {
|
||||
|
|
@ -154,7 +154,7 @@ export function EditorPanelContent({
|
|||
documentId?: number;
|
||||
localFilePath?: string;
|
||||
memoryScope?: "user" | "team";
|
||||
searchSpaceId?: number;
|
||||
workspaceId?: number;
|
||||
title: string | null;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
|
|
@ -186,14 +186,14 @@ export function EditorPanelContent({
|
|||
}
|
||||
try {
|
||||
const mounts = (await electronAPI.getAgentFilesystemMounts(
|
||||
searchSpaceId
|
||||
workspaceId
|
||||
)) as AgentFilesystemMount[];
|
||||
return normalizeLocalVirtualPathForEditor(candidatePath, mounts);
|
||||
} catch {
|
||||
return candidatePath;
|
||||
}
|
||||
},
|
||||
[electronAPI, searchSpaceId]
|
||||
[electronAPI, workspaceId]
|
||||
);
|
||||
|
||||
const plateMaxBytes = editorDoc?.editor_plate_max_bytes ?? LARGE_DOCUMENT_THRESHOLD;
|
||||
|
|
@ -234,7 +234,7 @@ export function EditorPanelContent({
|
|||
const resolvedLocalPath = await resolveLocalVirtualPath(localFilePath);
|
||||
const readResult = await electronAPI.readAgentLocalFileText(
|
||||
resolvedLocalPath,
|
||||
searchSpaceId
|
||||
workspaceId
|
||||
);
|
||||
if (!readResult.ok) {
|
||||
throw new Error(readResult.error || "Failed to read local file");
|
||||
|
|
@ -257,7 +257,7 @@ export function EditorPanelContent({
|
|||
if (!memoryScope) throw new Error("Missing memory context");
|
||||
const { document, limits } = await fetchMemoryEditorDocument({
|
||||
scope: memoryScope,
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
title,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
|
@ -271,12 +271,12 @@ export function EditorPanelContent({
|
|||
return;
|
||||
}
|
||||
|
||||
if (!documentId || !searchSpaceId) {
|
||||
if (!documentId || !workspaceId) {
|
||||
throw new Error("Missing document context");
|
||||
}
|
||||
const response = await authenticatedFetch(
|
||||
buildBackendUrl(
|
||||
`/api/v1/workspaces/${searchSpaceId}/documents/${documentId}/editor-content`
|
||||
`/api/v1/workspaces/${workspaceId}/documents/${documentId}/editor-content`
|
||||
),
|
||||
{ method: "GET" }
|
||||
);
|
||||
|
|
@ -323,7 +323,7 @@ export function EditorPanelContent({
|
|||
localFilePath,
|
||||
memoryScope,
|
||||
resolveLocalVirtualPath,
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
title,
|
||||
]);
|
||||
|
||||
|
|
@ -382,7 +382,7 @@ export function EditorPanelContent({
|
|||
const writeResult = await electronAPI.writeAgentLocalFileText(
|
||||
resolvedLocalPath,
|
||||
contentToSave,
|
||||
searchSpaceId
|
||||
workspaceId
|
||||
);
|
||||
if (!writeResult.ok) {
|
||||
throw new Error(writeResult.error || "Failed to save local file");
|
||||
|
|
@ -395,7 +395,7 @@ export function EditorPanelContent({
|
|||
if (!memoryScope) throw new Error("Missing memory context");
|
||||
const { markdown: savedContent, limits } = await saveMemoryMarkdown({
|
||||
scope: memoryScope,
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
markdown: markdownRef.current,
|
||||
});
|
||||
markdownRef.current = savedContent;
|
||||
|
|
@ -408,11 +408,11 @@ export function EditorPanelContent({
|
|||
return true;
|
||||
}
|
||||
|
||||
if (!searchSpaceId || !documentId) {
|
||||
if (!workspaceId || !documentId) {
|
||||
throw new Error("Missing document context");
|
||||
}
|
||||
const response = await authenticatedFetch(
|
||||
buildBackendUrl(`/api/v1/workspaces/${searchSpaceId}/documents/${documentId}/save`),
|
||||
buildBackendUrl(`/api/v1/workspaces/${workspaceId}/documents/${documentId}/save`),
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
@ -460,7 +460,7 @@ export function EditorPanelContent({
|
|||
plateMaxBytes,
|
||||
plateMaxLines,
|
||||
resolveLocalVirtualPath,
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
@ -515,12 +515,12 @@ export function EditorPanelContent({
|
|||
}, [editorDoc?.source_markdown]);
|
||||
|
||||
const handleDownloadMarkdown = useCallback(async () => {
|
||||
if (!searchSpaceId || !documentId) return;
|
||||
if (!workspaceId || !documentId) return;
|
||||
setDownloading(true);
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
buildBackendUrl(
|
||||
`/api/v1/workspaces/${searchSpaceId}/documents/${documentId}/download-markdown`
|
||||
`/api/v1/workspaces/${workspaceId}/documents/${documentId}/download-markdown`
|
||||
),
|
||||
{ method: "GET" }
|
||||
);
|
||||
|
|
@ -542,7 +542,7 @@ export function EditorPanelContent({
|
|||
} finally {
|
||||
setDownloading(false);
|
||||
}
|
||||
}, [documentId, editorDoc?.title, searchSpaceId]);
|
||||
}, [documentId, editorDoc?.title, workspaceId]);
|
||||
|
||||
const largeDocAlert = viewerMode === "monaco" && !isLocalFileMode && editorDoc && (
|
||||
<Alert className="m-4 shrink-0">
|
||||
|
|
@ -890,7 +890,7 @@ function DesktopEditorPanel() {
|
|||
|
||||
const hasTarget =
|
||||
panelState.kind === "document"
|
||||
? !!panelState.documentId && !!panelState.searchSpaceId
|
||||
? !!panelState.documentId && !!panelState.workspaceId
|
||||
: panelState.kind === "local_file"
|
||||
? !!panelState.localFilePath
|
||||
: !!panelState.memoryScope;
|
||||
|
|
@ -903,7 +903,7 @@ function DesktopEditorPanel() {
|
|||
documentId={panelState.documentId ?? undefined}
|
||||
localFilePath={panelState.localFilePath ?? undefined}
|
||||
memoryScope={panelState.memoryScope ?? undefined}
|
||||
searchSpaceId={panelState.searchSpaceId ?? undefined}
|
||||
workspaceId={panelState.workspaceId ?? undefined}
|
||||
title={panelState.title}
|
||||
onClose={closePanel}
|
||||
/>
|
||||
|
|
@ -919,7 +919,7 @@ function MobileEditorDrawer() {
|
|||
|
||||
const hasTarget =
|
||||
panelState.kind === "document"
|
||||
? !!panelState.documentId && !!panelState.searchSpaceId
|
||||
? !!panelState.documentId && !!panelState.workspaceId
|
||||
: !!panelState.memoryScope;
|
||||
if (!hasTarget) return null;
|
||||
|
||||
|
|
@ -943,7 +943,7 @@ function MobileEditorDrawer() {
|
|||
documentId={panelState.documentId ?? undefined}
|
||||
localFilePath={panelState.localFilePath ?? undefined}
|
||||
memoryScope={panelState.memoryScope ?? undefined}
|
||||
searchSpaceId={panelState.searchSpaceId ?? undefined}
|
||||
workspaceId={panelState.workspaceId ?? undefined}
|
||||
title={panelState.title}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -957,7 +957,7 @@ export function EditorPanel() {
|
|||
const isDesktop = useMediaQuery("(min-width: 1024px)");
|
||||
const hasTarget =
|
||||
panelState.kind === "document"
|
||||
? !!panelState.documentId && !!panelState.searchSpaceId
|
||||
? !!panelState.documentId && !!panelState.workspaceId
|
||||
: panelState.kind === "local_file"
|
||||
? !!panelState.localFilePath
|
||||
: !!panelState.memoryScope;
|
||||
|
|
@ -977,7 +977,7 @@ export function MobileEditorPanel() {
|
|||
const isDesktop = useMediaQuery("(min-width: 1024px)");
|
||||
const hasTarget =
|
||||
panelState.kind === "document"
|
||||
? !!panelState.documentId && !!panelState.searchSpaceId
|
||||
? !!panelState.documentId && !!panelState.workspaceId
|
||||
: panelState.kind === "local_file"
|
||||
? !!panelState.localFilePath
|
||||
: !!panelState.memoryScope;
|
||||
|
|
|
|||
|
|
@ -24,10 +24,10 @@ interface MemoryReadResponse {
|
|||
limits?: MemoryLimits;
|
||||
}
|
||||
|
||||
function getMemoryPath(scope: MemoryScope, searchSpaceId?: number | null) {
|
||||
function getMemoryPath(scope: MemoryScope, workspaceId?: number | null) {
|
||||
if (scope === "user") return "/api/v1/users/me/memory";
|
||||
if (!searchSpaceId) throw new Error("Missing search space context");
|
||||
return `/api/v1/workspaces/${searchSpaceId}/memory`;
|
||||
if (!workspaceId) throw new Error("Missing workspace context");
|
||||
return `/api/v1/workspaces/${workspaceId}/memory`;
|
||||
}
|
||||
|
||||
export function getMemoryLimitState(length: number, limits?: MemoryLimits | null) {
|
||||
|
|
@ -53,16 +53,16 @@ export function getMemoryLimitState(length: number, limits?: MemoryLimits | null
|
|||
|
||||
export async function fetchMemoryEditorDocument({
|
||||
scope,
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
title,
|
||||
signal,
|
||||
}: {
|
||||
scope: MemoryScope;
|
||||
searchSpaceId?: number | null;
|
||||
workspaceId?: number | null;
|
||||
title?: string | null;
|
||||
signal?: AbortSignal;
|
||||
}) {
|
||||
const response = await authenticatedFetch(buildBackendUrl(getMemoryPath(scope, searchSpaceId)), {
|
||||
const response = await authenticatedFetch(buildBackendUrl(getMemoryPath(scope, workspaceId)), {
|
||||
method: "GET",
|
||||
signal,
|
||||
});
|
||||
|
|
@ -87,14 +87,14 @@ export async function fetchMemoryEditorDocument({
|
|||
|
||||
export async function saveMemoryMarkdown({
|
||||
scope,
|
||||
searchSpaceId,
|
||||
workspaceId,
|
||||
markdown,
|
||||
}: {
|
||||
scope: MemoryScope;
|
||||
searchSpaceId?: number | null;
|
||||
workspaceId?: number | null;
|
||||
markdown: string;
|
||||
}) {
|
||||
const response = await authenticatedFetch(buildBackendUrl(getMemoryPath(scope, searchSpaceId)), {
|
||||
const response = await authenticatedFetch(buildBackendUrl(getMemoryPath(scope, workspaceId)), {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ memory_md: markdown }),
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ export type {
|
|||
IconRailProps,
|
||||
NavItem,
|
||||
PageUsage,
|
||||
SearchSpace,
|
||||
SidebarSectionProps,
|
||||
User,
|
||||
Workspace,
|
||||
} from "./types/layout.types";
|
||||
export {
|
||||
ChatListItem,
|
||||
CreateSearchSpaceDialog,
|
||||
CreateWorkspaceDialog,
|
||||
CreditBalanceDisplay,
|
||||
Header,
|
||||
IconRail,
|
||||
|
|
@ -20,10 +20,10 @@ export {
|
|||
MobileSidebarTrigger,
|
||||
NavIcon,
|
||||
NavSection,
|
||||
SearchSpaceAvatar,
|
||||
Sidebar,
|
||||
SidebarCollapseButton,
|
||||
SidebarHeader,
|
||||
SidebarSection,
|
||||
SidebarUserProfile,
|
||||
WorkspaceAvatar,
|
||||
} from "./ui";
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ import { useLoginGate } from "@/contexts/login-gate";
|
|||
import { useAnnouncements } from "@/hooks/use-announcements";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { anonymousChatApiService } from "@/lib/apis/anonymous-chat-api.service";
|
||||
import type { ChatItem, NavItem, PageUsage, SearchSpace } from "../types/layout.types";
|
||||
import type { ChatItem, NavItem, PageUsage, Workspace } from "../types/layout.types";
|
||||
import { LayoutShell } from "../ui/shell";
|
||||
|
||||
interface FreeLayoutDataProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const GUEST_SPACE: SearchSpace = {
|
||||
const GUEST_SPACE: Workspace = {
|
||||
id: 0,
|
||||
name: "SurfSense Free",
|
||||
description: "Free AI chat without login",
|
||||
|
|
@ -94,19 +94,16 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
|
|||
|
||||
const handleAnnouncements = useCallback(() => gate("see what's new"), [gate]);
|
||||
|
||||
const handleSearchSpaceSelect = useCallback(
|
||||
(_id: number) => gate("switch search spaces"),
|
||||
[gate]
|
||||
);
|
||||
const handleWorkspaceSelect = useCallback((_id: number) => gate("switch workspaces"), [gate]);
|
||||
|
||||
return (
|
||||
<LayoutShell
|
||||
searchSpaces={[GUEST_SPACE]}
|
||||
activeSearchSpaceId={0}
|
||||
onSearchSpaceSelect={handleSearchSpaceSelect}
|
||||
onSearchSpaceSettings={gatedAction("search space settings")}
|
||||
onAddSearchSpace={gatedAction("create search spaces")}
|
||||
searchSpace={GUEST_SPACE}
|
||||
workspaces={[GUEST_SPACE]}
|
||||
activeWorkspaceId={0}
|
||||
onWorkspaceSelect={handleWorkspaceSelect}
|
||||
onWorkspaceSettings={gatedAction("workspace settings")}
|
||||
onAddWorkspace={gatedAction("create workspaces")}
|
||||
workspace={GUEST_SPACE}
|
||||
navItems={navItems}
|
||||
onNavItemClick={handleNavItemClick}
|
||||
chats={[]}
|
||||
|
|
@ -121,7 +118,7 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
|
|||
email: "Guest",
|
||||
name: "Guest",
|
||||
}}
|
||||
onSettings={gatedAction("search space settings")}
|
||||
onSettings={gatedAction("workspace settings")}
|
||||
onManageMembers={gatedAction("team management")}
|
||||
onUserSettings={gatedAction("account settings")}
|
||||
onAnnouncements={handleAnnouncements}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue