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,45 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import ts from 'typescript';
|
||||
|
||||
const source = readFileSync(new URL('../src/lib/workspaces.ts', import.meta.url), 'utf8');
|
||||
const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 } }).outputText;
|
||||
const { readWorkspaces, WORKSPACES_KEY } = await import(`data:text/javascript;base64,${Buffer.from(compiled).toString('base64')}`);
|
||||
const storage = values => ({ getItem: key => values[key] ?? null });
|
||||
|
||||
test('migrates dashboard assignments without copying unrelated tabs into a workspace', () => {
|
||||
const result = readWorkspaces(storage({ 'gitty.dashboard.v1': JSON.stringify({
|
||||
workspaces: [{ id: 'workspace-a', name: 'A' }, { id: 'workspace-b', name: 'B' }],
|
||||
assignments: { '/a': 'workspace-a', '/b': 'workspace-b', '/closed': 'workspace-a' },
|
||||
}) }), ['/a', '/b', '/outside']);
|
||||
assert.deepEqual(result.workspaces[0], { id: 'workspace-a', name: 'A', repositories: ['/a', '/closed'], openPaths: ['/a'], activePath: '/a' });
|
||||
assert.deepEqual(result.workspaces[1].openPaths, ['/b']);
|
||||
assert.deepEqual(result.defaultSession.openPaths, ['/a', '/b', '/outside']);
|
||||
});
|
||||
|
||||
test('restores independent tab order and last active repository', () => {
|
||||
const saved = { selectedId: 'workspace-a', workspaces: [
|
||||
{ id: 'workspace-a', name: 'A', repositories: ['/a', '/b'], openPaths: ['/b', '/a'], activePath: '/a' },
|
||||
{ id: 'workspace-b', name: 'B', repositories: ['/b'], openPaths: [], activePath: '' },
|
||||
], defaultSession: { openPaths: ['/outside'], activePath: '/outside' } };
|
||||
assert.deepEqual(readWorkspaces(storage({ [WORKSPACES_KEY]: JSON.stringify(saved) }), []), saved);
|
||||
});
|
||||
|
||||
test('drops invalid memberships and stale active paths from saved sessions', () => {
|
||||
const saved = { selectedId: 'deleted', workspaces: [null, { id: 'workspace-a', name: 'A',
|
||||
repositories: ['/a', '/a', null], openPaths: ['/removed', '/a', 3], activePath: '/removed' }],
|
||||
defaultSession: { openPaths: ['/outside'], activePath: '/removed' } };
|
||||
const result = readWorkspaces(storage({ [WORKSPACES_KEY]: JSON.stringify(saved) }), []);
|
||||
assert.equal(result.selectedId, '');
|
||||
assert.deepEqual(result.workspaces[0].openPaths, ['/a']);
|
||||
assert.equal(result.workspaces[0].activePath, '/a');
|
||||
assert.deepEqual(result.workspaces[0].repositories, ['/a']);
|
||||
assert.equal(result.defaultSession.activePath, '/outside');
|
||||
});
|
||||
|
||||
test('unavailable or corrupt preferences preserve existing repository tabs', () => {
|
||||
for (const source of [storage({ [WORKSPACES_KEY]: '{broken' }), { getItem() { throw new Error('Storage unavailable'); } }]) {
|
||||
assert.deepEqual(readWorkspaces(source, ['/existing']).defaultSession.openPaths, ['/existing']);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user