mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-10 07:27:23 +02:00
refactor: collapse migrations
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user