diff --git a/src/stores/workspace/types.ts b/src/stores/workspace/types.ts index f03f674..f9485cb 100644 --- a/src/stores/workspace/types.ts +++ b/src/stores/workspace/types.ts @@ -117,6 +117,7 @@ export interface WorkspaceActions { executeTypeTransaction: ( operation: () => T, rollback: () => void, - operationName: string + operationName: string, + documentId?: string ) => T | null; } diff --git a/src/stores/workspaceStore.test.ts b/src/stores/workspaceStore.test.ts index 4029bb4..e133a6b 100644 --- a/src/stores/workspaceStore.test.ts +++ b/src/stores/workspaceStore.test.ts @@ -784,6 +784,159 @@ describe("workspaceStore", () => { }); }); + describe("Auto-save behavior for type operations", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("should trigger auto-save after updating a label", async () => { + const { createDocument, addLabelToDocument, updateLabelInDocument } = + useWorkspaceStore.getState(); + + const docId = createDocument("Test Doc"); + + // Add a label + addLabelToDocument(docId, { + id: "test-label", + name: "Test Label", + color: "#ff0000", + appliesTo: "both", + }); + + // Clear the dirty flag before updating + const metadata = useWorkspaceStore.getState().documentMetadata.get(docId); + if (metadata) { + metadata.isDirty = false; + } + + // Update the label + updateLabelInDocument(docId, "test-label", { + name: "Updated Label", + }); + + // Check that document is marked dirty + const metadataAfterUpdate = useWorkspaceStore.getState().documentMetadata.get(docId); + expect(metadataAfterUpdate?.isDirty).toBe(true); + + // Fast-forward 1 second to trigger auto-save + vi.advanceTimersByTime(1000); + + // Check that document is no longer dirty (auto-save completed) + const metadataAfterSave = useWorkspaceStore.getState().documentMetadata.get(docId); + expect(metadataAfterSave?.isDirty).toBe(false); + }); + + it("should trigger auto-save after updating a tangible", async () => { + const { createDocument, addTangibleToDocument, updateTangibleInDocument } = + useWorkspaceStore.getState(); + + const docId = createDocument("Test Doc"); + + // Add a tangible + addTangibleToDocument(docId, { + name: "Test Tangible", + mode: "filter", + filterLabels: ["label-1"], + } as TangibleConfig); + + // Get the tangible ID + const doc = useWorkspaceStore.getState().documents.get(docId); + const tangibleId = doc!.tangibles![0].id!; + + // Clear the dirty flag before updating + const metadata = useWorkspaceStore.getState().documentMetadata.get(docId); + if (metadata) { + metadata.isDirty = false; + } + + // Update the tangible + updateTangibleInDocument(docId, tangibleId, { + name: "Updated Tangible", + }); + + // Check that document is marked dirty + const metadataAfterUpdate = useWorkspaceStore.getState().documentMetadata.get(docId); + expect(metadataAfterUpdate?.isDirty).toBe(true); + + // Fast-forward 1 second to trigger auto-save + vi.advanceTimersByTime(1000); + + // Check that document is no longer dirty (auto-save completed) + const metadataAfterSave = useWorkspaceStore.getState().documentMetadata.get(docId); + expect(metadataAfterSave?.isDirty).toBe(false); + }); + + it("should trigger auto-save after adding a node type", async () => { + const { createDocument, addNodeTypeToDocument } = + useWorkspaceStore.getState(); + + const docId = createDocument("Test Doc"); + + // Clear the dirty flag + const metadata = useWorkspaceStore.getState().documentMetadata.get(docId); + if (metadata) { + metadata.isDirty = false; + } + + // Add a node type + addNodeTypeToDocument(docId, { + id: "test-type", + label: "Test Type", + color: "#00ff00", + shape: "circle", + icon: "Person", + }); + + // Check that document is marked dirty + const metadataAfterUpdate = useWorkspaceStore.getState().documentMetadata.get(docId); + expect(metadataAfterUpdate?.isDirty).toBe(true); + + // Fast-forward 1 second to trigger auto-save + vi.advanceTimersByTime(1000); + + // Check that document is no longer dirty (auto-save completed) + const metadataAfterSave = useWorkspaceStore.getState().documentMetadata.get(docId); + expect(metadataAfterSave?.isDirty).toBe(false); + }); + + it("should trigger auto-save after updating an edge type", async () => { + const { createDocument, updateEdgeTypeInDocument } = + useWorkspaceStore.getState(); + + const docId = createDocument("Test Doc"); + + // Get the first edge type from default types + const doc = useWorkspaceStore.getState().documents.get(docId); + const edgeTypeId = doc!.edgeTypes[0].id; + + // Clear the dirty flag + const metadata = useWorkspaceStore.getState().documentMetadata.get(docId); + if (metadata) { + metadata.isDirty = false; + } + + // Update the edge type + updateEdgeTypeInDocument(docId, edgeTypeId, { + label: "Updated Type", + }); + + // Check that document is marked dirty + const metadataAfterUpdate = useWorkspaceStore.getState().documentMetadata.get(docId); + expect(metadataAfterUpdate?.isDirty).toBe(true); + + // Fast-forward 1 second to trigger auto-save + vi.advanceTimersByTime(1000); + + // Check that document is no longer dirty (auto-save completed) + const metadataAfterSave = useWorkspaceStore.getState().documentMetadata.get(docId); + expect(metadataAfterSave?.isDirty).toBe(false); + }); + }); + describe("Edge Cases", () => { it("should handle rapid document creation", () => { const { createDocument } = useWorkspaceStore.getState(); diff --git a/src/stores/workspaceStore.ts b/src/stores/workspaceStore.ts index 83c0609..aeed6f2 100644 --- a/src/stores/workspaceStore.ts +++ b/src/stores/workspaceStore.ts @@ -1005,15 +1005,25 @@ export const useWorkspaceStore = create((set, get) * @param operation - Function that performs the operation (can throw) * @param rollback - Function to rollback changes on failure * @param operationName - Human-readable name for error messages + * @param documentId - Document ID to trigger auto-save for * @returns Operation result or null on failure */ executeTypeTransaction: ( operation: () => T, rollback: () => void, - operationName: string + operationName: string, + documentId?: string ): T | null => { try { const result = operation(); + + // Trigger auto-save after successful operation (consistent with undo/redo) + if (documentId) { + setTimeout(() => { + get().saveDocument(documentId); + }, 1000); + } + return result; } catch (error) { console.error(`${operationName} failed:`, error); @@ -1095,7 +1105,8 @@ export const useWorkspaceStore = create((set, get) useGraphStore.getState().setNodeTypes(doc.nodeTypes); } }, - 'add node type' + 'add node type', + documentId ); }, @@ -1142,7 +1153,8 @@ export const useWorkspaceStore = create((set, get) useGraphStore.getState().setNodeTypes(doc.nodeTypes); } }, - 'update node type' + 'update node type', + documentId ); }, @@ -1187,7 +1199,8 @@ export const useWorkspaceStore = create((set, get) useGraphStore.getState().setNodeTypes(doc.nodeTypes); } }, - 'delete node type' + 'delete node type', + documentId ); }, @@ -1223,7 +1236,8 @@ export const useWorkspaceStore = create((set, get) useGraphStore.getState().setEdgeTypes(doc.edgeTypes); } }, - 'add edge type' + 'add edge type', + documentId ); }, @@ -1261,7 +1275,8 @@ export const useWorkspaceStore = create((set, get) useGraphStore.getState().setEdgeTypes(doc.edgeTypes); } }, - 'update edge type' + 'update edge type', + documentId ); }, @@ -1297,7 +1312,8 @@ export const useWorkspaceStore = create((set, get) useGraphStore.getState().setEdgeTypes(doc.edgeTypes); } }, - 'delete edge type' + 'delete edge type', + documentId ); }, @@ -1339,7 +1355,8 @@ export const useWorkspaceStore = create((set, get) useGraphStore.getState().setLabels(doc.labels); } }, - 'add label' + 'add label', + documentId ); }, @@ -1382,7 +1399,8 @@ export const useWorkspaceStore = create((set, get) useGraphStore.getState().setLabels(doc.labels); } }, - 'update label' + 'update label', + documentId ); }, @@ -1539,7 +1557,8 @@ export const useWorkspaceStore = create((set, get) useGraphStore.getState().setTangibles(doc.tangibles || []); } }, - 'delete label' + 'delete label', + documentId ); }, @@ -1606,7 +1625,8 @@ export const useWorkspaceStore = create((set, get) useGraphStore.getState().setTangibles(doc.tangibles); } }, - 'add tangible' + 'add tangible', + documentId ); }, @@ -1668,7 +1688,8 @@ export const useWorkspaceStore = create((set, get) useGraphStore.getState().setTangibles(doc.tangibles); } }, - 'update tangible' + 'update tangible', + documentId ); }, @@ -1709,7 +1730,8 @@ export const useWorkspaceStore = create((set, get) useGraphStore.getState().setTangibles(doc.tangibles); } }, - 'delete tangible' + 'delete tangible', + documentId ); }, }));