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

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

View file

@ -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,
}
);

View file

@ -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],
};
}

View file

@ -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;
}

View file

@ -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…";
}

View file

@ -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;