From f6f813d3824f0671aaa788442964f83cae118c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Thu, 18 Dec 2025 16:49:24 +0100 Subject: [PATCH] refactor: collapse migrations --- src/components/LayoutControls.tsx | 3 +- src/components/WorkspaceSettings.tsx | 3 +- src/core/logic.test.ts | 4 +- src/core/logic.ts | 29 ++---- src/core/state.ts | 12 +-- src/lib/migrations.test.ts | 127 +++------------------------ src/lib/migrations.ts | 63 ++++--------- src/lib/nostr-schema.ts | 10 ++- src/types/app.ts | 2 +- 9 files changed, 52 insertions(+), 201 deletions(-) diff --git a/src/components/LayoutControls.tsx b/src/components/LayoutControls.tsx index 77b5a00..898b4ec 100644 --- a/src/components/LayoutControls.tsx +++ b/src/components/LayoutControls.tsx @@ -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(); diff --git a/src/components/WorkspaceSettings.tsx b/src/components/WorkspaceSettings.tsx index faa9880..269d7fb 100644 --- a/src/components/WorkspaceSettings.tsx +++ b/src/components/WorkspaceSettings.tsx @@ -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) { diff --git a/src/core/logic.test.ts b/src/core/logic.test.ts index ea8aaee..95b7e72 100644 --- a/src/core/logic.test.ts +++ b/src/core/logic.test.ts @@ -134,16 +134,16 @@ describe("addWindow", () => { layoutConfig: LayoutConfig, existingLayout: MosaicNode | 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 }, }, }); diff --git a/src/core/logic.ts b/src/core/logic.ts index f0a94db..e2ad634 100644 --- a/src/core/logic.ts +++ b/src/core/logic.ts @@ -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, ): 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, }, }; }; diff --git a/src/core/state.ts b/src/core/state.ts index 66de270..4936a6d 100644 --- a/src/core/state.ts +++ b/src/core/state.ts @@ -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 - }, }, }, }; diff --git a/src/lib/migrations.test.ts b/src/lib/migrations.test.ts index 9e15172..f80e045 100644 --- a/src/lib/migrations.test.ts +++ b/src/lib/migrations.test.ts @@ -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 = { diff --git a/src/lib/migrations.ts b/src/lib/migrations.ts index 4bbb7e4..d75e931 100644 --- a/src/lib/migrations.ts +++ b/src/lib/migrations.ts @@ -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 = { 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 = {}; - - // Add default layoutConfig to each workspace - for (const [id, workspace] of Object.entries(state.workspaces || {})) { - const ws = workspace as Record; - 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 = {}; - - for (const [id, workspace] of Object.entries(state.workspaces || {})) { - const ws = workspace as Record; - 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; diff --git a/src/lib/nostr-schema.ts b/src/lib/nostr-schema.ts index f3b668d..a1cb508 100644 --- a/src/lib/nostr-schema.ts +++ b/src/lib/nostr-schema.ts @@ -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; diff --git a/src/types/app.ts b/src/types/app.ts index d1d1905..27c142e 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -64,7 +64,6 @@ export interface Workspace { label?: string; // Optional user-editable label layout: MosaicNode | null; windowIds: string[]; - layoutConfig: LayoutConfig; // Per-workspace configuration for window insertion } export interface RelayInfo { @@ -84,6 +83,7 @@ export interface GrimoireState { windows: Record; workspaces: Record; activeWorkspaceId: string; + layoutConfig: LayoutConfig; // Global configuration for window insertion behavior activeAccount?: { pubkey: string; relays?: UserRelays;