mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-24 19:41:14 +02:00
feat(core): standardize storage + workspace isolation for persist stores
Unify all client-side persistence through StorageAdapter and add
workspace-scoped key namespacing (${key}:${wsId}).
- createPersistStorage: bridge for Zustand persist → StorageAdapter DI
- createWorkspaceAwareStorage: dynamic namespace by current workspace
- Migrate 6 persist stores (navigation, draft, view, scope, my-issues-view, chat)
- Rehydration registry: stores auto-rehydrate on workspace switch
- clearWorkspaceStorage: cleanup on workspace delete / member removal
- Chat store: namespace keys + rehydrate on workspace switch
- Factory view stores (createIssueViewStore): auto-register for rehydration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import type { StorageAdapter } from "../types";
|
import type { StorageAdapter } from "../types";
|
||||||
|
import { getCurrentWorkspaceId, registerForWorkspaceRehydration } from "../platform/workspace-storage";
|
||||||
|
|
||||||
const AGENT_STORAGE_KEY = "multica:chat:selectedAgentId";
|
const AGENT_STORAGE_KEY = "multica:chat:selectedAgentId";
|
||||||
const SESSION_STORAGE_KEY = "multica:chat:activeSessionId";
|
const SESSION_STORAGE_KEY = "multica:chat:activeSessionId";
|
||||||
@@ -39,12 +40,17 @@ export interface ChatStoreOptions {
|
|||||||
export function createChatStore(options: ChatStoreOptions) {
|
export function createChatStore(options: ChatStoreOptions) {
|
||||||
const { storage } = options;
|
const { storage } = options;
|
||||||
|
|
||||||
return create<ChatState>((set) => ({
|
const wsKey = (base: string) => {
|
||||||
|
const wsId = getCurrentWorkspaceId();
|
||||||
|
return wsId ? `${base}:${wsId}` : base;
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = create<ChatState>((set) => ({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
isFullscreen: false,
|
isFullscreen: false,
|
||||||
activeSessionId: storage.getItem(SESSION_STORAGE_KEY),
|
activeSessionId: storage.getItem(wsKey(SESSION_STORAGE_KEY)),
|
||||||
pendingTaskId: null,
|
pendingTaskId: null,
|
||||||
selectedAgentId: storage.getItem(AGENT_STORAGE_KEY),
|
selectedAgentId: storage.getItem(wsKey(AGENT_STORAGE_KEY)),
|
||||||
showHistory: false,
|
showHistory: false,
|
||||||
timelineItems: [],
|
timelineItems: [],
|
||||||
setOpen: (open) =>
|
setOpen: (open) =>
|
||||||
@@ -57,15 +63,15 @@ export function createChatStore(options: ChatStoreOptions) {
|
|||||||
toggleFullscreen: () => set((s) => ({ isFullscreen: !s.isFullscreen })),
|
toggleFullscreen: () => set((s) => ({ isFullscreen: !s.isFullscreen })),
|
||||||
setActiveSession: (id) => {
|
setActiveSession: (id) => {
|
||||||
if (id) {
|
if (id) {
|
||||||
storage.setItem(SESSION_STORAGE_KEY, id);
|
storage.setItem(wsKey(SESSION_STORAGE_KEY), id);
|
||||||
} else {
|
} else {
|
||||||
storage.removeItem(SESSION_STORAGE_KEY);
|
storage.removeItem(wsKey(SESSION_STORAGE_KEY));
|
||||||
}
|
}
|
||||||
set({ activeSessionId: id });
|
set({ activeSessionId: id });
|
||||||
},
|
},
|
||||||
setPendingTask: (taskId) => set({ pendingTaskId: taskId, timelineItems: [] }),
|
setPendingTask: (taskId) => set({ pendingTaskId: taskId, timelineItems: [] }),
|
||||||
setSelectedAgentId: (id) => {
|
setSelectedAgentId: (id) => {
|
||||||
storage.setItem(AGENT_STORAGE_KEY, id);
|
storage.setItem(wsKey(AGENT_STORAGE_KEY), id);
|
||||||
set({ selectedAgentId: id });
|
set({ selectedAgentId: id });
|
||||||
},
|
},
|
||||||
setShowHistory: (show) => set({ showHistory: show }),
|
setShowHistory: (show) => set({ showHistory: show }),
|
||||||
@@ -80,4 +86,14 @@ export function createChatStore(options: ChatStoreOptions) {
|
|||||||
}),
|
}),
|
||||||
clearTimeline: () => set({ timelineItems: [] }),
|
clearTimeline: () => set({ timelineItems: [] }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
registerForWorkspaceRehydration(() => {
|
||||||
|
store.setState({
|
||||||
|
activeSessionId: storage.getItem(wsKey(SESSION_STORAGE_KEY)),
|
||||||
|
selectedAgentId: storage.getItem(wsKey(AGENT_STORAGE_KEY)),
|
||||||
|
timelineItems: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return store;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { createJSONStorage, persist } from "zustand/middleware";
|
import { createJSONStorage, persist } from "zustand/middleware";
|
||||||
import type { IssueStatus, IssuePriority, IssueAssigneeType } from "../../types";
|
import type { IssueStatus, IssuePriority, IssueAssigneeType } from "../../types";
|
||||||
import { createWorkspaceAwareStorage } from "../../platform/workspace-storage";
|
import { createWorkspaceAwareStorage, registerForWorkspaceRehydration } from "../../platform/workspace-storage";
|
||||||
import { defaultStorage } from "../../platform/storage";
|
import { defaultStorage } from "../../platform/storage";
|
||||||
|
|
||||||
interface IssueDraft {
|
interface IssueDraft {
|
||||||
@@ -49,3 +49,5 @@ export const useIssueDraftStore = create<IssueDraftStore>()(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
registerForWorkspaceRehydration(() => useIssueDraftStore.persist.rehydrate());
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { createJSONStorage, persist } from "zustand/middleware";
|
import { createJSONStorage, persist } from "zustand/middleware";
|
||||||
import { createWorkspaceAwareStorage } from "../../platform/workspace-storage";
|
import { createWorkspaceAwareStorage, registerForWorkspaceRehydration } from "../../platform/workspace-storage";
|
||||||
import { defaultStorage } from "../../platform/storage";
|
import { defaultStorage } from "../../platform/storage";
|
||||||
|
|
||||||
export type IssuesScope = "all" | "members" | "agents";
|
export type IssuesScope = "all" | "members" | "agents";
|
||||||
@@ -24,3 +24,5 @@ export const useIssuesScopeStore = create<IssuesScopeState>()(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
registerForWorkspaceRehydration(() => useIssuesScopeStore.persist.rehydrate());
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
viewStoreSlice,
|
viewStoreSlice,
|
||||||
viewStorePersistOptions,
|
viewStorePersistOptions,
|
||||||
} from "./view-store";
|
} from "./view-store";
|
||||||
|
import { registerForWorkspaceRehydration } from "../../platform/workspace-storage";
|
||||||
|
|
||||||
export type MyIssuesScope = "assigned" | "created" | "agents";
|
export type MyIssuesScope = "assigned" | "created" | "agents";
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ export interface MyIssuesViewState extends IssueViewState {
|
|||||||
|
|
||||||
const basePersist = viewStorePersistOptions("multica_my_issues_view");
|
const basePersist = viewStorePersistOptions("multica_my_issues_view");
|
||||||
|
|
||||||
export const myIssuesViewStore: StoreApi<MyIssuesViewState> = createStore<MyIssuesViewState>()(
|
const _myIssuesViewStore = createStore<MyIssuesViewState>()(
|
||||||
persist(
|
persist(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
...viewStoreSlice(set as unknown as StoreApi<IssueViewState>["setState"]),
|
...viewStoreSlice(set as unknown as StoreApi<IssueViewState>["setState"]),
|
||||||
@@ -34,3 +35,7 @@ export const myIssuesViewStore: StoreApi<MyIssuesViewState> = createStore<MyIssu
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const myIssuesViewStore: StoreApi<MyIssuesViewState> = _myIssuesViewStore;
|
||||||
|
|
||||||
|
registerForWorkspaceRehydration(() => _myIssuesViewStore.persist.rehydrate());
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { createStore, type StoreApi } from "zustand/vanilla";
|
|||||||
import { createJSONStorage, persist } from "zustand/middleware";
|
import { createJSONStorage, persist } from "zustand/middleware";
|
||||||
import type { IssueStatus, IssuePriority } from "../../types";
|
import type { IssueStatus, IssuePriority } from "../../types";
|
||||||
import { ALL_STATUSES } from "../config";
|
import { ALL_STATUSES } from "../config";
|
||||||
import { createWorkspaceAwareStorage } from "../../platform/workspace-storage";
|
import { createWorkspaceAwareStorage, registerForWorkspaceRehydration } from "../../platform/workspace-storage";
|
||||||
import { defaultStorage } from "../../platform/storage";
|
import { defaultStorage } from "../../platform/storage";
|
||||||
|
|
||||||
export type ViewMode = "board" | "list";
|
export type ViewMode = "board" | "list";
|
||||||
@@ -183,9 +183,11 @@ export const viewStorePersistOptions = (name: string) => ({
|
|||||||
|
|
||||||
/** Factory: creates a vanilla StoreApi for use with React Context. */
|
/** Factory: creates a vanilla StoreApi for use with React Context. */
|
||||||
export function createIssueViewStore(persistKey: string): StoreApi<IssueViewState> {
|
export function createIssueViewStore(persistKey: string): StoreApi<IssueViewState> {
|
||||||
return createStore<IssueViewState>()(
|
const store = createStore<IssueViewState>()(
|
||||||
persist(viewStoreSlice, viewStorePersistOptions(persistKey))
|
persist(viewStoreSlice, viewStorePersistOptions(persistKey))
|
||||||
);
|
);
|
||||||
|
registerForWorkspaceRehydration(() => store.persist.rehydrate());
|
||||||
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Global singleton for the /issues page. */
|
/** Global singleton for the /issues page. */
|
||||||
@@ -193,6 +195,8 @@ export const useIssueViewStore = create<IssueViewState>()(
|
|||||||
persist(viewStoreSlice, viewStorePersistOptions("multica_issues_view"))
|
persist(viewStoreSlice, viewStorePersistOptions("multica_issues_view"))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
registerForWorkspaceRehydration(() => useIssueViewStore.persist.rehydrate());
|
||||||
|
|
||||||
// Clear filters on all registered view stores when workspace switches.
|
// Clear filters on all registered view stores when workspace switches.
|
||||||
const _syncedStores = new Set<StoreApi<IssueViewState>>();
|
const _syncedStores = new Set<StoreApi<IssueViewState>>();
|
||||||
let _workspaceSyncInitialized = false;
|
let _workspaceSyncInitialized = false;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { persist } from "zustand/middleware";
|
import { createJSONStorage, persist } from "zustand/middleware";
|
||||||
|
import { createPersistStorage } from "../platform/persist-storage";
|
||||||
|
import { defaultStorage } from "../platform/storage";
|
||||||
|
|
||||||
const EXCLUDED_PREFIXES = ["/login", "/pair/"];
|
const EXCLUDED_PREFIXES = ["/login", "/pair/"];
|
||||||
|
|
||||||
@@ -23,6 +25,7 @@ export const useNavigationStore = create<NavigationState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "multica_navigation",
|
name: "multica_navigation",
|
||||||
|
storage: createJSONStorage(() => createPersistStorage(defaultStorage)),
|
||||||
partialize: (state) => ({ lastPath: state.lastPath }),
|
partialize: (state) => ({ lastPath: state.lastPath }),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ export type { CoreProviderProps } from "./types";
|
|||||||
export { AuthInitializer } from "./auth-initializer";
|
export { AuthInitializer } from "./auth-initializer";
|
||||||
export { defaultStorage } from "./storage";
|
export { defaultStorage } from "./storage";
|
||||||
export { createPersistStorage } from "./persist-storage";
|
export { createPersistStorage } from "./persist-storage";
|
||||||
export { createWorkspaceAwareStorage, setCurrentWorkspaceId, getCurrentWorkspaceId } from "./workspace-storage";
|
export { createWorkspaceAwareStorage, setCurrentWorkspaceId, getCurrentWorkspaceId, registerForWorkspaceRehydration, rehydrateAllWorkspaceStores } from "./workspace-storage";
|
||||||
|
export { clearWorkspaceStorage } from "./storage-cleanup";
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function mockAdapter(): StorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("createPersistStorage", () => {
|
describe("createPersistStorage", () => {
|
||||||
it("delegates to StorageAdapter without namespace", () => {
|
it("delegates to StorageAdapter", () => {
|
||||||
const adapter = mockAdapter();
|
const adapter = mockAdapter();
|
||||||
const storage = createPersistStorage(adapter);
|
const storage = createPersistStorage(adapter);
|
||||||
|
|
||||||
@@ -27,28 +27,6 @@ describe("createPersistStorage", () => {
|
|||||||
expect(result).toEqual(JSON.stringify("value"));
|
expect(result).toEqual(JSON.stringify("value"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("namespaces keys when wsId is provided", () => {
|
|
||||||
const adapter = mockAdapter();
|
|
||||||
const storage = createPersistStorage(adapter, "ws_123");
|
|
||||||
|
|
||||||
storage.setItem("draft", JSON.stringify({ title: "test" }));
|
|
||||||
expect(adapter.setItem).toHaveBeenCalledWith(
|
|
||||||
"draft:ws_123",
|
|
||||||
JSON.stringify({ title: "test" }),
|
|
||||||
);
|
|
||||||
|
|
||||||
storage.getItem("draft");
|
|
||||||
expect(adapter.getItem).toHaveBeenCalledWith("draft:ws_123");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removeItem namespaces correctly", () => {
|
|
||||||
const adapter = mockAdapter();
|
|
||||||
const storage = createPersistStorage(adapter, "ws_abc");
|
|
||||||
|
|
||||||
storage.removeItem("draft");
|
|
||||||
expect(adapter.removeItem).toHaveBeenCalledWith("draft:ws_abc");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns null for missing keys", () => {
|
it("returns null for missing keys", () => {
|
||||||
const adapter = mockAdapter();
|
const adapter = mockAdapter();
|
||||||
const storage = createPersistStorage(adapter);
|
const storage = createPersistStorage(adapter);
|
||||||
@@ -56,4 +34,12 @@ describe("createPersistStorage", () => {
|
|||||||
const result = storage.getItem("nonexistent");
|
const result = storage.getItem("nonexistent");
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("removeItem delegates correctly", () => {
|
||||||
|
const adapter = mockAdapter();
|
||||||
|
const storage = createPersistStorage(adapter);
|
||||||
|
|
||||||
|
storage.removeItem("key");
|
||||||
|
expect(adapter.removeItem).toHaveBeenCalledWith("key");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { StateStorage } from "zustand/middleware";
|
import type { StateStorage } from "zustand/middleware";
|
||||||
import type { StorageAdapter } from "../types/storage";
|
import type { StorageAdapter } from "../types/storage";
|
||||||
|
|
||||||
export function createPersistStorage(
|
/**
|
||||||
adapter: StorageAdapter,
|
* Bridge between Zustand persist middleware and our StorageAdapter DI system.
|
||||||
wsId?: string,
|
* For workspace-scoped stores, use createWorkspaceAwareStorage instead.
|
||||||
): StateStorage {
|
*/
|
||||||
const resolve = (key: string) => (wsId ? `${key}:${wsId}` : key);
|
export function createPersistStorage(adapter: StorageAdapter): StateStorage {
|
||||||
return {
|
return {
|
||||||
getItem: (key) => adapter.getItem(resolve(key)),
|
getItem: (key) => adapter.getItem(key),
|
||||||
setItem: (key, value) => adapter.setItem(resolve(key), value),
|
setItem: (key, value) => adapter.setItem(key, value),
|
||||||
removeItem: (key) => adapter.removeItem(resolve(key)),
|
removeItem: (key) => adapter.removeItem(key),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
22
packages/core/platform/storage-cleanup.test.ts
Normal file
22
packages/core/platform/storage-cleanup.test.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
import { clearWorkspaceStorage } from "./storage-cleanup";
|
||||||
|
|
||||||
|
describe("clearWorkspaceStorage", () => {
|
||||||
|
it("removes all workspace-scoped keys for given wsId", () => {
|
||||||
|
const adapter = {
|
||||||
|
getItem: vi.fn(),
|
||||||
|
setItem: vi.fn(),
|
||||||
|
removeItem: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
clearWorkspaceStorage(adapter, "ws_123");
|
||||||
|
|
||||||
|
expect(adapter.removeItem).toHaveBeenCalledWith("multica_issue_draft:ws_123");
|
||||||
|
expect(adapter.removeItem).toHaveBeenCalledWith("multica_issues_view:ws_123");
|
||||||
|
expect(adapter.removeItem).toHaveBeenCalledWith("multica_issues_scope:ws_123");
|
||||||
|
expect(adapter.removeItem).toHaveBeenCalledWith("multica_my_issues_view:ws_123");
|
||||||
|
expect(adapter.removeItem).toHaveBeenCalledWith("multica:chat:selectedAgentId:ws_123");
|
||||||
|
expect(adapter.removeItem).toHaveBeenCalledWith("multica:chat:activeSessionId:ws_123");
|
||||||
|
expect(adapter.removeItem).toHaveBeenCalledTimes(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
27
packages/core/platform/storage-cleanup.ts
Normal file
27
packages/core/platform/storage-cleanup.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { StorageAdapter } from "../types/storage";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keys that are namespaced per workspace (stored as `${key}:${wsId}`).
|
||||||
|
*
|
||||||
|
* IMPORTANT: When adding a new workspace-scoped persist store or storage key,
|
||||||
|
* add its key here so that workspace deletion and logout properly clean it up.
|
||||||
|
* Also ensure the store uses `createWorkspaceAwareStorage` for its persist config.
|
||||||
|
*/
|
||||||
|
const WORKSPACE_SCOPED_KEYS = [
|
||||||
|
"multica_issue_draft",
|
||||||
|
"multica_issues_view",
|
||||||
|
"multica_issues_scope",
|
||||||
|
"multica_my_issues_view",
|
||||||
|
"multica:chat:selectedAgentId",
|
||||||
|
"multica:chat:activeSessionId",
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Remove all workspace-scoped storage entries for the given workspace. */
|
||||||
|
export function clearWorkspaceStorage(
|
||||||
|
adapter: StorageAdapter,
|
||||||
|
wsId: string,
|
||||||
|
) {
|
||||||
|
for (const key of WORKSPACE_SCOPED_KEYS) {
|
||||||
|
adapter.removeItem(`${key}:${wsId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,24 @@ import type { StateStorage } from "zustand/middleware";
|
|||||||
import type { StorageAdapter } from "../types/storage";
|
import type { StorageAdapter } from "../types/storage";
|
||||||
|
|
||||||
let _currentWsId: string | null = null;
|
let _currentWsId: string | null = null;
|
||||||
|
const _rehydrateFns: Array<() => void> = [];
|
||||||
|
|
||||||
export function setCurrentWorkspaceId(wsId: string | null) {
|
export function setCurrentWorkspaceId(wsId: string | null) {
|
||||||
_currentWsId = wsId;
|
_currentWsId = wsId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Register a persist store's rehydrate function to be called on workspace switch. */
|
||||||
|
export function registerForWorkspaceRehydration(fn: () => void) {
|
||||||
|
_rehydrateFns.push(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rehydrate all registered workspace-scoped persist stores from the new namespace. */
|
||||||
|
export function rehydrateAllWorkspaceStores() {
|
||||||
|
for (const fn of _rehydrateFns) {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getCurrentWorkspaceId(): string | null {
|
export function getCurrentWorkspaceId(): string | null {
|
||||||
return _currentWsId;
|
return _currentWsId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import type { StoreApi, UseBoundStore } from "zustand";
|
|||||||
import type { AuthState } from "../auth/store";
|
import type { AuthState } from "../auth/store";
|
||||||
import type { WorkspaceStore } from "../workspace/store";
|
import type { WorkspaceStore } from "../workspace/store";
|
||||||
import { createLogger } from "../logger";
|
import { createLogger } from "../logger";
|
||||||
|
import { clearWorkspaceStorage } from "../platform/storage-cleanup";
|
||||||
|
import { defaultStorage } from "../platform/storage";
|
||||||
import { issueKeys } from "../issues/queries";
|
import { issueKeys } from "../issues/queries";
|
||||||
import { projectKeys } from "../projects/queries";
|
import { projectKeys } from "../projects/queries";
|
||||||
import { runtimeKeys } from "../runtimes/queries";
|
import { runtimeKeys } from "../runtimes/queries";
|
||||||
@@ -238,6 +240,7 @@ export function useRealtimeSync(
|
|||||||
|
|
||||||
const unsubWsDeleted = ws.on("workspace:deleted", (p) => {
|
const unsubWsDeleted = ws.on("workspace:deleted", (p) => {
|
||||||
const { workspace_id } = p as WorkspaceDeletedPayload;
|
const { workspace_id } = p as WorkspaceDeletedPayload;
|
||||||
|
clearWorkspaceStorage(defaultStorage, workspace_id);
|
||||||
const currentWs = workspaceStore.getState().workspace;
|
const currentWs = workspaceStore.getState().workspace;
|
||||||
if (currentWs?.id === workspace_id) {
|
if (currentWs?.id === workspace_id) {
|
||||||
logger.warn("current workspace deleted, switching");
|
logger.warn("current workspace deleted, switching");
|
||||||
@@ -250,6 +253,8 @@ export function useRealtimeSync(
|
|||||||
const { user_id } = p as MemberRemovedPayload;
|
const { user_id } = p as MemberRemovedPayload;
|
||||||
const myUserId = authStore.getState().user?.id;
|
const myUserId = authStore.getState().user?.id;
|
||||||
if (user_id === myUserId) {
|
if (user_id === myUserId) {
|
||||||
|
const wsId = workspaceStore.getState().workspace?.id;
|
||||||
|
if (wsId) clearWorkspaceStorage(defaultStorage, wsId);
|
||||||
logger.warn("removed from workspace, switching");
|
logger.warn("removed from workspace, switching");
|
||||||
onToast?.("You were removed from this workspace", "info");
|
onToast?.("You were removed from this workspace", "info");
|
||||||
workspaceStore.getState().refreshWorkspaces();
|
workspaceStore.getState().refreshWorkspaces();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { create } from "zustand";
|
|||||||
import type { Workspace, StorageAdapter } from "../types";
|
import type { Workspace, StorageAdapter } from "../types";
|
||||||
import type { ApiClient } from "../api/client";
|
import type { ApiClient } from "../api/client";
|
||||||
import { createLogger } from "../logger";
|
import { createLogger } from "../logger";
|
||||||
import { setCurrentWorkspaceId } from "../platform/workspace-storage";
|
import { setCurrentWorkspaceId, rehydrateAllWorkspaceStores } from "../platform/workspace-storage";
|
||||||
|
|
||||||
const logger = createLogger("workspace-store");
|
const logger = createLogger("workspace-store");
|
||||||
|
|
||||||
@@ -59,6 +59,7 @@ export function createWorkspaceStore(api: ApiClient, options?: WorkspaceStoreOpt
|
|||||||
if (!nextWorkspace) {
|
if (!nextWorkspace) {
|
||||||
api.setWorkspaceId(null);
|
api.setWorkspaceId(null);
|
||||||
setCurrentWorkspaceId(null);
|
setCurrentWorkspaceId(null);
|
||||||
|
rehydrateAllWorkspaceStores();
|
||||||
storage?.removeItem("multica_workspace_id");
|
storage?.removeItem("multica_workspace_id");
|
||||||
set({ workspace: null });
|
set({ workspace: null });
|
||||||
return null;
|
return null;
|
||||||
@@ -66,6 +67,7 @@ export function createWorkspaceStore(api: ApiClient, options?: WorkspaceStoreOpt
|
|||||||
|
|
||||||
api.setWorkspaceId(nextWorkspace.id);
|
api.setWorkspaceId(nextWorkspace.id);
|
||||||
setCurrentWorkspaceId(nextWorkspace.id);
|
setCurrentWorkspaceId(nextWorkspace.id);
|
||||||
|
rehydrateAllWorkspaceStores();
|
||||||
storage?.setItem("multica_workspace_id", nextWorkspace.id);
|
storage?.setItem("multica_workspace_id", nextWorkspace.id);
|
||||||
set({ workspace: nextWorkspace });
|
set({ workspace: nextWorkspace });
|
||||||
logger.debug("hydrate workspace", nextWorkspace.name, nextWorkspace.id);
|
logger.debug("hydrate workspace", nextWorkspace.name, nextWorkspace.id);
|
||||||
@@ -142,6 +144,7 @@ export function createWorkspaceStore(api: ApiClient, options?: WorkspaceStoreOpt
|
|||||||
clearWorkspace: () => {
|
clearWorkspace: () => {
|
||||||
api.setWorkspaceId(null);
|
api.setWorkspaceId(null);
|
||||||
setCurrentWorkspaceId(null);
|
setCurrentWorkspaceId(null);
|
||||||
|
rehydrateAllWorkspaceStores();
|
||||||
set({ workspace: null, workspaces: [] });
|
set({ workspace: null, workspaces: [] });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user