From 21426e80e8faefb3e8d5c043692ddb7cd2753462 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 11:11:08 +0000 Subject: [PATCH 1/4] Initial plan From 1deb0b2631fbb9af21748e1e7edec310e01904e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 11:16:17 +0000 Subject: [PATCH 2/4] Add auto-save trigger to label and tangible operations - Modified executeTypeTransaction to accept documentId and trigger auto-save - Added 1-second timeout before saving (consistent with undo/redo) - Updated all label, tangible, and type management operations to pass documentId - This ensures dirty indicator clears after save completes Co-authored-by: jhbruhn <1036566+jhbruhn@users.noreply.github.com> --- src/stores/workspaceStore.ts | 48 ++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 13 deletions(-) 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 ); }, })); From 8051a466f8a643b9b54d9b8401f65711c876da1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 11:17:38 +0000 Subject: [PATCH 3/4] Add comprehensive tests for auto-save behavior - Added tests to verify auto-save triggers after label/tangible/type updates - Uses fake timers to verify 1-second auto-save delay - Tests verify dirty flag is cleared after auto-save completes - Covers label, tangible, node type, and edge type operations Co-authored-by: jhbruhn <1036566+jhbruhn@users.noreply.github.com> --- src/stores/workspaceStore.test.ts | 153 ++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) 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(); From b93b36664e146360031ca7c42f90e5981bd045b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:45:53 +0000 Subject: [PATCH 4/4] Fix executeTypeTransaction type definition to include documentId parameter The type definition in workspace/types.ts was missing the optional documentId parameter that was added to the implementation. Co-authored-by: jhbruhn <1036566+jhbruhn@users.noreply.github.com> --- src/stores/workspace/types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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; }