feat(workspaces): add workspace sessions and management
Introduce workspace support to group repositories and store per-workspace open tabs and the last active repository. Integrate a workspace picker into the repository tab bar and add workspace controls to the dashboard to create, edit, switch and delete workspaces. Persist and migrate workspace preferences robustly and include tests for migration, corruption and edge cases. - Persist per-workspace sessions and active repo to localStorage - Add workspace picker in tabs and dashboard management hooks - Include migration/validation logic and unit tests for edge cases
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
export interface Workspace {
|
||||
id: string;
|
||||
name: string;
|
||||
repositories: string[];
|
||||
openPaths: string[];
|
||||
activePath: string;
|
||||
}
|
||||
export interface WorkspaceState {
|
||||
selectedId: string;
|
||||
workspaces: Workspace[];
|
||||
defaultSession: { openPaths: string[]; activePath: string };
|
||||
}
|
||||
export const WORKSPACES_KEY = "gitlite.workspaces.v1";
|
||||
const paths = (value: unknown): string[] => Array.isArray(value)
|
||||
? [...new Set(value.filter((path): path is string => typeof path === "string" && !!path.trim()))] : [];
|
||||
export function readWorkspaces(storage: Pick<Storage, "getItem">, openPaths: string[]): WorkspaceState {
|
||||
const fallback: WorkspaceState = { selectedId: "", workspaces: [], defaultSession: { openPaths, activePath: "" } };
|
||||
try {
|
||||
const raw = storage.getItem(WORKSPACES_KEY);
|
||||
if (raw) {
|
||||
const saved = JSON.parse(raw);
|
||||
const workspaces: Workspace[] = Array.isArray(saved.workspaces) ? saved.workspaces.flatMap((item: any) => {
|
||||
if (!item || typeof item.id !== "string" || !item.id.startsWith("workspace-") || typeof item.name !== "string" || !item.name.trim()) return [];
|
||||
const repositories = paths(item.repositories);
|
||||
const openPaths = paths(item.openPaths).filter(path => repositories.includes(path));
|
||||
return [{ id: item.id, name: item.name, repositories, openPaths,
|
||||
activePath: openPaths.includes(item.activePath) ? item.activePath : openPaths[0] ?? "" }];
|
||||
}) : [];
|
||||
const defaultPaths = paths(saved.defaultSession?.openPaths);
|
||||
return { workspaces, selectedId: workspaces.some(item => item.id === saved.selectedId) ? saved.selectedId : "",
|
||||
defaultSession: { openPaths: defaultPaths, activePath: defaultPaths.includes(saved.defaultSession?.activePath) ? saved.defaultSession.activePath : defaultPaths[0] ?? "" } };
|
||||
}
|
||||
const legacy = JSON.parse(storage.getItem("gitty.dashboard.v1") ?? "{}");
|
||||
if (Array.isArray(legacy.workspaces)) fallback.workspaces = legacy.workspaces.flatMap((item: any) => {
|
||||
if (!item || typeof item.id !== "string" || !item.id.startsWith("workspace-") || typeof item.name !== "string" || !item.name.trim()) return [];
|
||||
const repositories = Object.entries(legacy.assignments ?? {}).filter(([, id]) => id === item.id).map(([path]) => path);
|
||||
const tabs = openPaths.filter(path => repositories.includes(path));
|
||||
return [{ ...item, repositories, openPaths: tabs, activePath: tabs[0] ?? "" }];
|
||||
});
|
||||
} catch { /* Invalid optional preferences must not block startup. */ }
|
||||
return fallback;
|
||||
}
|
||||
Reference in New Issue
Block a user