From c3b0219313389ebc71bf6e290588628e7973fce5 Mon Sep 17 00:00:00 2001
From: NathanViaud <nathan.viaud@inria.fr>
Date: Tue, 6 Jun 2023 17:32:00 +0200
Subject: [PATCH] Adding undo redo on text type input

---
 .../forms/components/GenericField.vue         | 20 +++++++++++++++++--
 .../forms/components/inputs/GenericInput.vue  |  5 +++++
 .../forms/components/inputs/HtmlInput.vue     | 11 ++++++++++
 .../forms/components/inputs/ScoreInput.vue    | 13 +++++++++++-
 .../forms/components/inputs/TextAreaInput.vue | 13 +++++++++++-
 .../forms/components/inputs/TextInput.vue     | 13 +++++++++++-
 src/shared/interfaces/undoRedo.interface.ts   | 11 +++++++++-
 src/shared/services/graph/content.service.ts  |  5 +++++
 src/shared/stores/undoRedo/functions/form.ts  | 16 +++++++++++----
 src/shared/stores/undoRedo/functions/node.ts  |  3 +--
 src/shared/stores/undoRedo/undoRedoStore.ts   |  7 +++++--
 src/views/EditorPage.vue                      |  9 +++++++--
 12 files changed, 110 insertions(+), 16 deletions(-)

diff --git a/src/features/forms/components/GenericField.vue b/src/features/forms/components/GenericField.vue
index dcdbe352..bb0fa9d1 100644
--- a/src/features/forms/components/GenericField.vue
+++ b/src/features/forms/components/GenericField.vue
@@ -1,11 +1,12 @@
 <script setup lang="ts">
-import { Input } from '@/src/shared/interfaces';
+import { FormUpdatedAction, Input } from '@/src/shared/interfaces';
 import GenericInput from './inputs/GenericInput.vue';
-import { useEditorStore } from '@/src/shared/stores';
+import { useEditorStore, useUndoRedoStore } from '@/src/shared/stores';
 import { graphService } from '@/src/shared/services';
 import { deleteElement, changeContentOrder } from '@/src/shared/services/graph';
 
 const editorStore = useEditorStore();
+const undoRedoStore = useUndoRedoStore();
 
 defineProps<{
     inputs: Input[];
@@ -125,6 +126,20 @@ function onCheck(value: boolean, id:string) {
     graphService.writeProjectData();
 }
 
+function addUndoAction(event: { oldValue: string, newValue: string }, id: string) {
+    const { oldValue, newValue } = event;
+    
+    const action: FormUpdatedAction = {
+        type: 'formUpdated',
+        nodeId: currentNode.id,
+        elementId: editorStore.openedElementId,
+        oldValue,
+        newValue,
+        formValueId: id
+    };
+    undoRedoStore.addAction(action);
+}
+
 </script>
 
 <template>
@@ -139,6 +154,7 @@ function onCheck(value: boolean, id:string) {
         @input="onInput($event, input.id)"
         @check="onCheck($event, input.id)"
         @repeat-input="onRepeatInput($event, input.id)"
+        @add-undo-action="addUndoAction($event, input.id)"
     />
 </template>
 
diff --git a/src/features/forms/components/inputs/GenericInput.vue b/src/features/forms/components/inputs/GenericInput.vue
index 8c76525c..2ae31676 100644
--- a/src/features/forms/components/inputs/GenericInput.vue
+++ b/src/features/forms/components/inputs/GenericInput.vue
@@ -24,6 +24,7 @@ const emit = defineEmits<{
     (e: 'input', value: string): void;
     (e: 'repeatInput', value): void;
     (e: 'check', value: boolean): void;
+    (e: 'add-undo-action', value: { oldValue: string, newValue: string }): void;
 }>();
 
 </script>
@@ -36,6 +37,7 @@ const emit = defineEmits<{
         :input-value="inputValue"
         :inside-card="insideCard"
         @input="emit('input', $event)"
+        @add-undo-action="emit('add-undo-action', $event)"
     />
     <HtmlInput
         v-if="input.type === 'html'"
@@ -44,6 +46,7 @@ const emit = defineEmits<{
         :input-value="inputValue"
         :inside-card="insideCard"
         @input="emit('input', $event)"
+        @add-undo-action="emit('add-undo-action', $event)"
     />
     <TextAreaInput 
         v-if="input.type === 'textarea'"
@@ -52,6 +55,7 @@ const emit = defineEmits<{
         :input-value="inputValue"
         :inside-card="insideCard"
         @input="emit('input', $event)"
+        @add-undo-action="emit('add-undo-action', $event)"
     />
     <FileInput 
         v-if="input.type === 'file'"
@@ -66,6 +70,7 @@ const emit = defineEmits<{
         :label="input.label"
         :input-value="inputValue"
         @input="emit('input', $event)"
+        @add-undo-action="emit('add-undo-action', $event)"
     />
     <CheckBoxInput
         v-if="input.type === 'checkbox'"
diff --git a/src/features/forms/components/inputs/HtmlInput.vue b/src/features/forms/components/inputs/HtmlInput.vue
index 6ba10b84..fc72fd21 100644
--- a/src/features/forms/components/inputs/HtmlInput.vue
+++ b/src/features/forms/components/inputs/HtmlInput.vue
@@ -14,6 +14,7 @@ const props = defineProps<{
 
 const emit = defineEmits<{
     (e: 'input', value: string): void;
+    (e: 'add-undo-action', value: { oldValue: string, newValue: string }): void;
 }>();
 
 const editor = ref(null);
@@ -74,6 +75,14 @@ function handleFilePicker(callback) {
     });
 }
 
+let initialValue = null;
+
+function addUndoAction() {
+    if(initialValue !== props.inputValue) {
+        emit('add-undo-action', { oldValue: initialValue, newValue: props.inputValue });
+    }
+}
+
 </script>
 
 <template>
@@ -98,6 +107,8 @@ function handleFilePicker(callback) {
         }"
         @init="init"
         @drop.stop.prevent="drop"
+        @focus="initialValue = content"
+        @blur="addUndoAction"
         @keydown="ignoreUndoRedoOnFocus"
     />
 </template>
\ No newline at end of file
diff --git a/src/features/forms/components/inputs/ScoreInput.vue b/src/features/forms/components/inputs/ScoreInput.vue
index 666784aa..e9532976 100644
--- a/src/features/forms/components/inputs/ScoreInput.vue
+++ b/src/features/forms/components/inputs/ScoreInput.vue
@@ -1,12 +1,13 @@
 <script setup lang="ts">
 import { ignoreUndoRedoOnFocus } from '@/src/shared/stores/undoRedo/functions';
 
-defineProps<{
+const props = defineProps<{
     inputValue: string;
 }>();
 
 const emit = defineEmits<{
     (e: 'input', value: string): void;
+    (e: 'add-undo-action', value: { oldValue: string, newValue: string }): void;
 }>();
 
 function minus(inputValue: string) {
@@ -19,6 +20,14 @@ function plus(inputValue: string) {
     emit('input', `${value}`);
 }
 
+let initialValue = null;
+
+function addUndoAction() {
+    if(initialValue !== props.inputValue) {
+        emit('add-undo-action', { oldValue: initialValue, newValue: props.inputValue });
+    }
+}
+
 </script>
 
 <template>
@@ -29,6 +38,8 @@ function plus(inputValue: string) {
             type="number"
             :value="inputValue"
             @input="emit('input', ($event.target as HTMLInputElement).value)"
+            @focus="initialValue = inputValue"
+            @blur="addUndoAction"
             @keydown="ignoreUndoRedoOnFocus"
         >
         <button @click="plus(inputValue)"><i class="icon-plus-circle"></i></button>
diff --git a/src/features/forms/components/inputs/TextAreaInput.vue b/src/features/forms/components/inputs/TextAreaInput.vue
index a1163498..d6bad82a 100644
--- a/src/features/forms/components/inputs/TextAreaInput.vue
+++ b/src/features/forms/components/inputs/TextAreaInput.vue
@@ -1,7 +1,7 @@
 <script setup lang="ts">
 import { ignoreUndoRedoOnFocus } from '@/src/shared/stores/undoRedo/functions';
 
-defineProps<{
+const props = defineProps<{
     label: string;
     placeholder: string;
     inputValue: string;
@@ -11,8 +11,17 @@ defineProps<{
 
 const emit = defineEmits<{
     (e: 'input', value: string): void;
+    (e: 'add-undo-action', value: { oldValue: string, newValue: string }): void;
 }>();
 
+let initialValue = null;
+
+function addUndoAction() {
+    if(initialValue !== props.inputValue) {
+        emit('add-undo-action', { oldValue: initialValue, newValue: props.inputValue });
+    }
+}
+
 </script>
 
 <template>
@@ -24,6 +33,8 @@ const emit = defineEmits<{
         :placeholder="placeholder"
         :value="inputValue"
         @input="emit('input', ($event.target as HTMLInputElement).value)"
+        @focus="initialValue = inputValue"
+        @blur="addUndoAction"
         @keydown="ignoreUndoRedoOnFocus"
     ></textarea>
 </template>
\ No newline at end of file
diff --git a/src/features/forms/components/inputs/TextInput.vue b/src/features/forms/components/inputs/TextInput.vue
index 59c39a34..03ed0154 100644
--- a/src/features/forms/components/inputs/TextInput.vue
+++ b/src/features/forms/components/inputs/TextInput.vue
@@ -1,7 +1,7 @@
 <script setup lang="ts">
 import { ignoreUndoRedoOnFocus } from '@/src/shared/stores/undoRedo/functions';
 
-defineProps<{
+const props = defineProps<{
     label: string;
     placeholder?: string;
     inputValue: string;
@@ -10,8 +10,17 @@ defineProps<{
 
 const emit = defineEmits<{
     (e: 'input', value: string): void;
+    (e: 'add-undo-action', value: { oldValue: string, newValue: string }): void;
 }>();
 
+let initialValue = null;
+
+function addUndoAction() {
+    if(initialValue !== props.inputValue) {
+        emit('add-undo-action', { oldValue: initialValue, newValue: props.inputValue });
+    }
+}
+
 </script>
 
 <template>
@@ -24,6 +33,8 @@ const emit = defineEmits<{
         :placeholder="placeholder"
         :value="inputValue"
         @input="emit('input', ($event.target as HTMLInputElement).value)"
+        @focus="initialValue = inputValue"
+        @blur="addUndoAction"
         @keydown="ignoreUndoRedoOnFocus"
     >
 </template>
\ No newline at end of file
diff --git a/src/shared/interfaces/undoRedo.interface.ts b/src/shared/interfaces/undoRedo.interface.ts
index 99e1485f..12bdb780 100644
--- a/src/shared/interfaces/undoRedo.interface.ts
+++ b/src/shared/interfaces/undoRedo.interface.ts
@@ -1,7 +1,7 @@
 import { GraphEdge } from '@vue-flow/core';
 
 export interface UndoRedoAction {
-    type: 'nodeMoved' | 'nodeAdded' | 'nodeRemoved' | 'nodeUpdated' | 'edgeAdded' | 'edgeUpdated' | 'edgeRemoved';
+    type: 'nodeMoved' | 'nodeAdded' | 'nodeRemoved' | 'nodeUpdated' | 'edgeAdded' | 'edgeUpdated' | 'edgeRemoved' | 'formUpdated';
 }
 
 export interface NodeMovedAction extends UndoRedoAction {
@@ -31,4 +31,13 @@ export interface EdgeUpdatedAction extends UndoRedoAction {
     type: 'edgeUpdated';
     newEdge: GraphEdge;
     oldEdge: GraphEdge
+}
+
+export interface FormUpdatedAction extends UndoRedoAction {
+    type: 'formUpdated';
+    nodeId: string;
+    elementId: string;
+    formValueId: string;
+    oldValue: string;
+    newValue: string;
 }
\ No newline at end of file
diff --git a/src/shared/services/graph/content.service.ts b/src/shared/services/graph/content.service.ts
index 662dd055..02f7ecb5 100644
--- a/src/shared/services/graph/content.service.ts
+++ b/src/shared/services/graph/content.service.ts
@@ -93,4 +93,9 @@ export function getContentDefaultValues(type) {
         }, {});
         return {...acc, ...keyValues};
     }, {});
+}
+
+export function updateElementValue(elementId: string, nodeId: string, valueId: string, value: string): void {
+    const node = findNode(nodeId);
+    node.data.formValues[valueId] = value;
 }
\ No newline at end of file
diff --git a/src/shared/stores/undoRedo/functions/form.ts b/src/shared/stores/undoRedo/functions/form.ts
index 9f9b9809..411357f3 100644
--- a/src/shared/stores/undoRedo/functions/form.ts
+++ b/src/shared/stores/undoRedo/functions/form.ts
@@ -1,9 +1,17 @@
-import { UndoRedoAction } from '@/src/shared/interfaces';
+import { FormUpdatedAction, UndoRedoAction } from '@/src/shared/interfaces';
+import { updateElementValue } from '@/src/shared/services/graph';
 
-export function formUpdated(action: UndoRedoAction, reverseStack: UndoRedoAction[]): void {
-    const reverseAction: UndoRedoAction = {
-        type: action.type,
+export function formUpdatedAction(action: FormUpdatedAction, reverseStack: UndoRedoAction[]): void {
+    const { elementId, nodeId, formValueId, oldValue, newValue } = action;
+    
+    updateElementValue(elementId, nodeId, formValueId, oldValue);
+    
+    const reverseAction: FormUpdatedAction = {
+        ...action,
+        oldValue: newValue,
+        newValue: oldValue,
     };
+
     reverseStack.push(reverseAction);
 }
 
diff --git a/src/shared/stores/undoRedo/functions/node.ts b/src/shared/stores/undoRedo/functions/node.ts
index 1c033681..d526e25a 100644
--- a/src/shared/stores/undoRedo/functions/node.ts
+++ b/src/shared/stores/undoRedo/functions/node.ts
@@ -39,7 +39,6 @@ export function addNodeAction(action: NodeMutatedAction, reverseStack: UndoRedoA
         edges: action.edges
     };
     reverseStack.push(reverseAction);
-    
 }
 
 export function updateNodeAction(): void {
@@ -62,4 +61,4 @@ export function moveNodeAction(action: NodeMovedAction, reverseStack: UndoRedoAc
         }
     };
     reverseStack.push(reverseAction);
-}
+}
\ No newline at end of file
diff --git a/src/shared/stores/undoRedo/undoRedoStore.ts b/src/shared/stores/undoRedo/undoRedoStore.ts
index 51889268..97c1e748 100644
--- a/src/shared/stores/undoRedo/undoRedoStore.ts
+++ b/src/shared/stores/undoRedo/undoRedoStore.ts
@@ -1,6 +1,6 @@
 import { defineStore } from 'pinia';
-import { UndoRedoAction, NodeMovedAction, NodeMutatedAction, EdgeUpdatedAction, EdgeMutatedAction } from '../../interfaces';
-import { addEdgeAction, addNodeAction, deleteEdgeAction, deleteNodeAction, moveNodeAction, updateEdgeAction, updateNodeAction } from './functions';
+import { UndoRedoAction, NodeMovedAction, NodeMutatedAction, EdgeUpdatedAction, EdgeMutatedAction, FormUpdatedAction } from '../../interfaces';
+import { addEdgeAction, addNodeAction, deleteEdgeAction, deleteNodeAction, formUpdatedAction, moveNodeAction, updateEdgeAction, updateNodeAction } from './functions';
 
 interface UndoRedoState {
     undoStack: UndoRedoAction[];
@@ -56,6 +56,9 @@ export const useUndoRedoStore = defineStore('epoc', {
             case 'edgeRemoved':
                 addEdgeAction(action as EdgeMutatedAction, reverseStack);
                 break;
+            case 'formUpdated':
+                formUpdatedAction(action as FormUpdatedAction, reverseStack);
+                break;
             }
         },
     }
diff --git a/src/views/EditorPage.vue b/src/views/EditorPage.vue
index eb75d459..dcf5e2fb 100644
--- a/src/views/EditorPage.vue
+++ b/src/views/EditorPage.vue
@@ -15,7 +15,8 @@ const undoRedoStore = useUndoRedoStore();
 editorService.setup();
 
 document.body.addEventListener('keydown', function(event) {
-    const key = event.key;
+    const { key, ctrlKey, metaKey }= event;
+
     if ((key === 'Backspace' || key === 'Delete')) {
         if((event.target as HTMLElement).className.indexOf('vue-flow') !== -1 || event.target === document.body) {
             event.stopPropagation();
@@ -23,11 +24,15 @@ document.body.addEventListener('keydown', function(event) {
         }
     }
 
-    if (event.ctrlKey || event.metaKey) {
+    if (ctrlKey || metaKey) {
         if (key === 'z') {
+            event.preventDefault();
+            event.stopPropagation();
             undo();
         }
         if (key === 'y') {
+            event.preventDefault();
+            event.stopPropagation();
             redo();
         }
     }
-- 
GitLab