refactor: collapse migrations

This commit is contained in:
Alejandro Gómez
2025-12-18 16:49:24 +01:00
parent f283ef6208
commit f6f813d382
9 changed files with 52 additions and 201 deletions

View File

@@ -24,7 +24,7 @@ import { useState } from "react";
export function LayoutControls() {
const { state, applyPresetLayout, updateLayoutConfig } = useGrimoire();
const { workspaces, activeWorkspaceId } = state;
const { workspaces, activeWorkspaceId, layoutConfig } = state;
// Local state for immediate slider feedback (debounced persistence)
const [localSplitPercentage, setLocalSplitPercentage] = useState<
@@ -32,7 +32,6 @@ export function LayoutControls() {
>(null);
const activeWorkspace = workspaces[activeWorkspaceId];
const layoutConfig = activeWorkspace?.layoutConfig;
const windowCount = activeWorkspace?.windowIds.length || 0;
const presets = getAllPresets();

View File

@@ -20,8 +20,7 @@ export function WorkspaceSettings({
children,
}: WorkspaceSettingsProps) {
const { state, updateLayoutConfig } = useGrimoire();
const activeWorkspace = state.workspaces[state.activeWorkspaceId];
const config = activeWorkspace?.layoutConfig;
const config = state.layoutConfig;
// Early return if no config available
if (!config) {

View File

@@ -134,16 +134,16 @@ describe("addWindow", () => {
layoutConfig: LayoutConfig,
existingLayout: MosaicNode<string> | null = null,
): GrimoireState => ({
__version: 9,
__version: 8,
windows: {},
activeWorkspaceId: "test-workspace",
layoutConfig, // Global layout config
workspaces: {
"test-workspace": {
id: "test-workspace",
number: 1,
windowIds: [],
layout: existingLayout,
layoutConfig, // Per-workspace layout config
},
},
});

View File

@@ -53,12 +53,6 @@ export const createWorkspace = (
label,
layout: null,
windowIds: [],
layoutConfig: {
insertionMode: "smart",
splitPercentage: 50,
insertionPosition: "second",
autoPreset: undefined,
},
},
},
};
@@ -87,8 +81,8 @@ export const addWindow = (
commandString: payload.commandString,
};
// Insert window using workspace's layout configuration
const newLayout = insertWindow(ws.layout, newWindowId, ws.layoutConfig);
// Insert window using global layout configuration
const newLayout = insertWindow(ws.layout, newWindowId, state.layoutConfig);
return {
...state,
@@ -358,27 +352,18 @@ export const updateWindow = (
};
/**
* Updates the active workspace's layout configuration.
* Controls how new windows are inserted into the active workspace.
* Updates the global layout configuration.
* Controls how new windows are inserted across all workspaces.
*/
export const updateLayoutConfig = (
state: GrimoireState,
layoutConfig: Partial<LayoutConfig>,
): GrimoireState => {
const activeId = state.activeWorkspaceId;
const activeWorkspace = state.workspaces[activeId];
return {
...state,
workspaces: {
...state.workspaces,
[activeId]: {
...activeWorkspace,
layoutConfig: {
...activeWorkspace.layoutConfig,
...layoutConfig,
},
},
layoutConfig: {
...state.layoutConfig,
...layoutConfig,
},
};
};

View File

@@ -17,18 +17,18 @@ const initialState: GrimoireState = {
__version: CURRENT_VERSION,
windows: {},
activeWorkspaceId: "default",
layoutConfig: {
insertionMode: "smart", // Smart auto-balancing by default
splitPercentage: 50, // Equal split
insertionPosition: "second", // New windows on right/bottom
autoPreset: undefined, // No preset maintenance
},
workspaces: {
default: {
id: "default",
number: 1,
windowIds: [],
layout: null,
layoutConfig: {
insertionMode: "smart", // Smart auto-balancing by default
splitPercentage: 50, // Equal split
insertionPosition: "second", // New windows on right/bottom
autoPreset: undefined, // No preset maintenance
},
},
},
};

View File

@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
import { migrateState, validateState, CURRENT_VERSION } from "./migrations";
describe("migrations", () => {
describe("v6 to v9 migration (v6→v7→v8→v9)", () => {
describe("v6 to v8 migration (v6→v7→v8)", () => {
it("should convert numeric labels to number field and add global layoutConfig", () => {
const oldState = {
__version: 6,
@@ -26,7 +26,7 @@ describe("migrations", () => {
const migrated = migrateState(oldState);
// Should migrate to v9
// Should migrate to v8
expect(migrated.__version).toBe(CURRENT_VERSION);
// v6→v7: numeric labels converted to number
@@ -35,14 +35,8 @@ describe("migrations", () => {
expect(migrated.workspaces.ws2.number).toBe(2);
expect(migrated.workspaces.ws2.label).toBeUndefined();
// v7→v8→v9: layoutConfig added to each workspace
expect(migrated.workspaces.ws1.layoutConfig).toEqual({
insertionMode: "smart",
splitPercentage: 50,
insertionPosition: "second",
autoPreset: undefined,
});
expect(migrated.workspaces.ws2.layoutConfig).toEqual({
// v7→v8: global layoutConfig added
expect(migrated.layoutConfig).toEqual({
insertionMode: "smart",
splitPercentage: 50,
insertionPosition: "second",
@@ -50,7 +44,7 @@ describe("migrations", () => {
});
});
it("should convert non-numeric labels to number with label and add per-workspace layoutConfig", () => {
it("should convert non-numeric labels to number with label and add global layoutConfig", () => {
const oldState = {
__version: 6,
windows: {},
@@ -81,12 +75,11 @@ describe("migrations", () => {
expect(migrated.workspaces.ws2.number).toBe(2);
expect(migrated.workspaces.ws2.label).toBe("Development");
// v7→v8→v9: layoutConfig added to each workspace
expect(migrated.workspaces.ws1.layoutConfig).toBeDefined();
expect(migrated.workspaces.ws2.layoutConfig).toBeDefined();
// v7→v8: global layoutConfig added
expect(migrated.layoutConfig).toBeDefined();
});
it("should handle mixed numeric and text labels and add per-workspace layoutConfig", () => {
it("should handle mixed numeric and text labels and add global layoutConfig", () => {
const oldState = {
__version: 6,
windows: {},
@@ -125,10 +118,8 @@ describe("migrations", () => {
expect(migrated.workspaces.ws3.number).toBe(3);
expect(migrated.workspaces.ws3.label).toBeUndefined();
// v7→v8→v9: layoutConfig added to each workspace
expect(migrated.workspaces.ws1.layoutConfig).toBeDefined();
expect(migrated.workspaces.ws2.layoutConfig).toBeDefined();
expect(migrated.workspaces.ws3.layoutConfig).toBeDefined();
// v7→v8: global layoutConfig added
expect(migrated.layoutConfig).toBeDefined();
});
it("should validate migrated state", () => {
@@ -151,104 +142,6 @@ describe("migrations", () => {
});
});
describe("v8 to v9 migration", () => {
it("should preserve per-workspace layoutConfig", () => {
const v8State = {
__version: 8,
windows: {
w1: { id: "w1", appId: "profile", props: {} },
w2: { id: "w2", appId: "nip", props: {} },
},
activeWorkspaceId: "ws1",
workspaces: {
ws1: {
id: "ws1",
number: 1,
label: undefined,
layout: null,
windowIds: [],
layoutConfig: {
insertionMode: "smart",
splitPercentage: 50,
insertionPosition: "second",
autoPreset: undefined,
},
},
ws2: {
id: "ws2",
number: 2,
label: "Development",
layout: {
direction: "row",
first: "w1",
second: "w2",
splitPercentage: 50,
},
windowIds: ["w1", "w2"],
layoutConfig: {
insertionMode: "row",
splitPercentage: 70,
insertionPosition: "first",
autoPreset: undefined,
},
},
},
};
const migrated = migrateState(v8State);
expect(migrated.__version).toBe(9);
// layoutConfig should remain per-workspace
expect(migrated.workspaces.ws1.layoutConfig).toEqual({
insertionMode: "smart",
splitPercentage: 50,
insertionPosition: "second",
autoPreset: undefined,
});
expect(migrated.workspaces.ws2.layoutConfig).toEqual({
insertionMode: "row",
splitPercentage: 70,
insertionPosition: "first",
autoPreset: undefined,
});
// Other fields should be preserved
expect(migrated.workspaces.ws2.label).toBe("Development");
expect(migrated.workspaces.ws2.layout).toEqual({
direction: "row",
first: "w1",
second: "w2",
splitPercentage: 50,
});
});
it("should validate v8→v9 migrated state", () => {
const v8State = {
__version: 8,
windows: { w1: { id: "w1", appId: "profile", props: {} } },
activeWorkspaceId: "ws1",
workspaces: {
ws1: {
id: "ws1",
number: 1,
layout: "w1",
windowIds: ["w1"],
layoutConfig: {
insertionMode: "smart",
splitPercentage: 50,
insertionPosition: "second",
autoPreset: undefined,
},
},
},
};
const migrated = migrateState(v8State);
expect(validateState(migrated)).toBe(true);
});
});
describe("validateState", () => {
it("should validate correct state structure", () => {
const state = {

View File

@@ -8,7 +8,7 @@
import { GrimoireState } from "@/types/app";
import { toast } from "sonner";
export const CURRENT_VERSION = 9;
export const CURRENT_VERSION = 8;
/**
* Migration function type
@@ -67,52 +67,18 @@ const migrations: Record<number, MigrationFn> = {
workspaces: migratedWorkspaces,
};
},
// Migration from v7 to v8 - adds layoutConfig to workspaces
// Migration from v7 to v8 - adds global layoutConfig
7: (state: any) => {
const migratedWorkspaces: Record<string, any> = {};
// Add default layoutConfig to each workspace
for (const [id, workspace] of Object.entries(state.workspaces || {})) {
const ws = workspace as Record<string, any>;
migratedWorkspaces[id] = {
...ws,
layoutConfig: {
insertionMode: "smart", // New smart default (auto-balance)
splitPercentage: 50, // Matches old 50/50 behavior
insertionPosition: "second", // Matches old right/bottom behavior
autoPreset: undefined, // No preset by default
},
};
}
// Add global layoutConfig with smart defaults
return {
...state,
__version: 8,
workspaces: migratedWorkspaces,
};
},
// Migration from v8 to v9 - preserve per-workspace layoutConfig
8: (state: any) => {
// Ensure all workspaces have layoutConfig (add default if missing)
const migratedWorkspaces: Record<string, any> = {};
for (const [id, workspace] of Object.entries(state.workspaces || {})) {
const ws = workspace as Record<string, any>;
migratedWorkspaces[id] = {
...ws,
layoutConfig: ws.layoutConfig || {
insertionMode: "smart",
splitPercentage: 50,
insertionPosition: "second",
autoPreset: undefined,
},
};
}
return {
...state,
__version: 9,
workspaces: migratedWorkspaces,
layoutConfig: {
insertionMode: "smart", // Smart auto-balancing
splitPercentage: 50, // Equal split
insertionPosition: "second", // New windows on right/bottom
autoPreset: undefined, // No preset by default
},
};
},
};
@@ -133,11 +99,17 @@ export function validateState(state: any): state is GrimoireState {
!state.windows ||
!state.workspaces ||
!state.activeWorkspaceId ||
!state.layoutConfig ||
typeof state.__version !== "number"
) {
return false;
}
// layoutConfig must be an object
if (typeof state.layoutConfig !== "object") {
return false;
}
// Windows must be an object
if (typeof state.windows !== "object") {
return false;
@@ -154,16 +126,11 @@ export function validateState(state: any): state is GrimoireState {
}
// All window IDs in workspaces must exist in windows
// Each workspace must have layoutConfig
for (const workspace of Object.values(state.workspaces)) {
const ws = workspace as any;
if (!Array.isArray(ws.windowIds)) {
return false;
}
// Verify workspace has layoutConfig
if (!ws.layoutConfig || typeof ws.layoutConfig !== "object") {
return false;
}
for (const windowId of ws.windowIds) {
if (!state.windows[windowId]) {
return false;

View File

@@ -123,7 +123,15 @@ export function parseTagStructure(tag: TagDefinition): {
parts.push(`${current.either.join(" | ")}`);
} else {
// Replace 'free' with 'text' for better readability
const type = current.type === "free" ? "text" : current.type;
let type = current.type === "free" ? "text" : current.type;
// Add examples for specific types
if (type === "url") {
type = "url (e.g. https://grimoire.rocks)";
} else if (type === "relay") {
type = "relay (e.g. wss://grimoire.rocks)";
}
parts.push(type);
}
current = current.next;

View File

@@ -64,7 +64,6 @@ export interface Workspace {
label?: string; // Optional user-editable label
layout: MosaicNode<string> | null;
windowIds: string[];
layoutConfig: LayoutConfig; // Per-workspace configuration for window insertion
}
export interface RelayInfo {
@@ -84,6 +83,7 @@ export interface GrimoireState {
windows: Record<string, WindowInstance>;
workspaces: Record<string, Workspace>;
activeWorkspaceId: string;
layoutConfig: LayoutConfig; // Global configuration for window insertion behavior
activeAccount?: {
pubkey: string;
relays?: UserRelays;