From c4e269805e117ef33aba754e6517aa699ba7fc02 Mon Sep 17 00:00:00 2001
From: VIAUD Nathan <nathan.viaud@inria.fr>
Date: Wed, 23 Aug 2023 16:52:37 +0200
Subject: [PATCH] Functionnal badge system

---
 package-lock.json                             |  11 +-
 package.json                                  |   2 +-
 .../components/ConditionElementSelector.vue   |   2 +-
 .../badge/components/ConditionItem.vue        | 129 ++++++++++-----
 .../badge/components/ConditionModal.vue       |  42 ++++-
 .../badge/components/ConditionValue.vue       |  91 +++++++++++
 .../components/inputs/badges/BadgesInput.vue  |  12 +-
 src/shared/classes/epoc-v1.ts                 |   6 +-
 src/shared/interfaces/badge.interface.ts      |  30 +++-
 src/shared/services/badge.service.ts          |  83 ++++++++++
 src/shared/services/graph.service.ts          |  30 ++--
 src/shared/services/graph/content.service.ts  |  14 +-
 .../services/graph/copyPaste.service.ts       |  92 +++++++++++
 src/shared/services/graph/element.service.ts  |  95 +++++++++++
 src/shared/services/graph/index.ts            |   4 +-
 src/shared/services/graph/node.service.ts     | 151 ++----------------
 src/shared/services/import.service.ts         |   2 +-
 src/shared/services/index.ts                  |   3 +-
 src/shared/stores/editorStore.ts              |   7 +-
 19 files changed, 575 insertions(+), 231 deletions(-)
 create mode 100644 src/features/badge/components/ConditionValue.vue
 create mode 100644 src/shared/services/badge.service.ts
 create mode 100644 src/shared/services/graph/copyPaste.service.ts
 create mode 100644 src/shared/services/graph/element.service.ts

diff --git a/package-lock.json b/package-lock.json
index aac9bbfe..f8d5b15b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
       "version": "0.1.4-beta",
       "license": "CeCILL-B",
       "dependencies": {
-        "@epoc/epoc-types": "^2.0.0-beta.7",
+        "@epoc/epoc-types": "git+https://gitlab.inria.fr/learninglab/epoc/epoc-types.git",
         "@meforma/vue-toaster": "^1.3.0",
         "@tinymce/tinymce-vue": "^5.1.0",
         "@vue-flow/additional-components": "^1.3.3",
@@ -287,8 +287,8 @@
     },
     "node_modules/@epoc/epoc-types": {
       "version": "2.0.0-beta.7",
-      "resolved": "https://registry.npmjs.org/@epoc/epoc-types/-/epoc-types-2.0.0-beta.7.tgz",
-      "integrity": "sha512-H9jlOWAkiHbuMQlWArWm4hsdKMhavsVBWngQUniv120JRJUjmHHj/CtGOu/crJ+gYV7R+4avjCDZI/L51Bv3sg=="
+      "resolved": "git+https://gitlab.inria.fr/learninglab/epoc/epoc-types.git#a5ba071f37736b019aa325fe88ad0f4e87eda22d",
+      "license": "CeCILL-B"
     },
     "node_modules/@esbuild/android-arm": {
       "version": "0.17.19",
@@ -13386,9 +13386,8 @@
       }
     },
     "@epoc/epoc-types": {
-      "version": "2.0.0-beta.7",
-      "resolved": "https://registry.npmjs.org/@epoc/epoc-types/-/epoc-types-2.0.0-beta.7.tgz",
-      "integrity": "sha512-H9jlOWAkiHbuMQlWArWm4hsdKMhavsVBWngQUniv120JRJUjmHHj/CtGOu/crJ+gYV7R+4avjCDZI/L51Bv3sg=="
+      "version": "git+https://gitlab.inria.fr/learninglab/epoc/epoc-types.git#a5ba071f37736b019aa325fe88ad0f4e87eda22d",
+      "from": "@epoc/epoc-types@git+https://gitlab.inria.fr/learninglab/epoc/epoc-types.git"
     },
     "@esbuild/android-arm": {
       "version": "0.17.19",
diff --git a/package.json b/package.json
index a69f1dbf..27f8637a 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
     "debug-mac": "'/Applications/ePoc Editor.app/Contents/MacOS/ePoc Editor' --remote-debugging-port=8315"
   },
   "dependencies": {
-    "@epoc/epoc-types": "^2.0.0-beta.7",
+    "@epoc/epoc-types": "git+https://gitlab.inria.fr/learninglab/epoc/epoc-types.git",
     "@meforma/vue-toaster": "^1.3.0",
     "@tinymce/tinymce-vue": "^5.1.0",
     "@vue-flow/additional-components": "^1.3.3",
diff --git a/src/features/badge/components/ConditionElementSelector.vue b/src/features/badge/components/ConditionElementSelector.vue
index 169e42f4..4aadce8f 100644
--- a/src/features/badge/components/ConditionElementSelector.vue
+++ b/src/features/badge/components/ConditionElementSelector.vue
@@ -17,7 +17,7 @@ function onClick() {
     <div class="select">
         Activité
         <div class="select-input" @click="onClick">
-            <p>{{ inputValue || 'Veuillez sélectionner un élément' }}</p>
+            <p>{{ inputValue || 'Veuillez sélectionner' }}</p>
             <i class="icon-cible"></i>
         </div>
     </div>
diff --git a/src/features/badge/components/ConditionItem.vue b/src/features/badge/components/ConditionItem.vue
index 70226204..0338b22f 100644
--- a/src/features/badge/components/ConditionItem.vue
+++ b/src/features/badge/components/ConditionItem.vue
@@ -1,35 +1,53 @@
 <script setup lang="ts">
-import { Ref, ref } from 'vue';
-import ConditionElementSelector from '@/src/features/badge/components/ConditionElementSelector.vue';
-
+import { computed, ComputedRef } from 'vue';
+import ConditionElementSelector from './ConditionElementSelector.vue';
+import ConditionValue from './ConditionValue.vue';
+import { getVerbs, getValueType } from '@/src/shared/services';
+import { Condition, ElementType } from '@/src/shared/interfaces';
+import { getElementType } from '@/src/shared/services/graph';
     
 const props = defineProps<{
-    inputValue: any;
+    inputValue: Condition;
     conditionIndex: number;
 }>();
 
 const emit = defineEmits<{
     (e: 'removeCondition'): void;
+    (e: 'updateCondition', values: { value: string, key: string }): void;
 }>();
 
-const conditions = [
-    'Vue',
-    'Plus que',
-    'Moins que',
-    'Egal à',
-];
 
-const logicChoice: Ref<'and' | 'or'> = ref('and');
+// const logicChoice: Ref<'and' | 'or'> = ref('and');
+
+const currentCondition: ComputedRef<Condition> = computed(() => {
+    return {
+        element: props.inputValue.element || '',
+        verb: props.inputValue.verb || '',
+        value: props.inputValue.value || ''
+    };
+});
+
+
+const verbDisabled: ComputedRef<boolean> = computed(() => {
+    return !currentCondition.value.element;
+});
+
+const valueDisabled: ComputedRef<boolean> = computed(() => {
+    return !currentCondition.value.element || !currentCondition.value.verb;
+});
+
+const elementType: ComputedRef<ElementType> = computed(() => {
+    if(!currentCondition.value.element) return null;
+    return getElementType(currentCondition.value.element);
+});
 
 function removeCondition() {
     emit('removeCondition');
 }
 
-const currentCondition = ref({
-    element: props.inputValue.element || '',
-    condition: props.inputValue.condition || '',
-    value: props.inputValue.value || '',
-});
+function updateCondition(value: string, key: string) {
+    emit('updateCondition', { value, key });
+}
 
 </script>
 
@@ -37,30 +55,46 @@ const currentCondition = ref({
     <div v-if="conditionIndex !== 0" class="gap"></div>
     <article>
         <i class="icon-supprimer delete" @click.stop="removeCondition"></i>
-        <div v-if="conditionIndex !== 0" class="logic-condition">
+        <div class="logic-condition">
+            <button class="logic-choice active">
+                ET
+            </button>
+        </div>
+        <!-- Condition switch -->
+        <!-- <div v-if="conditionIndex !== 0" class="logic-condition">
             <button class="logic-choice" :class="{ 'active' : logicChoice === 'and'}" @click="logicChoice = 'and'">
                 ET
             </button>
             <button class="logic-choice" :class="{ 'active' : logicChoice === 'or'}" @click="logicChoice = 'or'">
                 OU
             </button>
-        </div>
-        <ConditionElementSelector
-            :input-value="currentCondition.element"
-            :index="conditionIndex"
-        />
-        <div class="select">
-            Condition    
-            <select id="condition">
-                <option v-for="(condition, index) of conditions" :key="index" :value="condition">{{ condition }}</option> 
-            </select>
-        </div>
-        <div class="select">
-            Valeur    
-            <select id="value">
-                <option value="true">True</option>
-                <option value="false">False</option>
-            </select>
+        </div> -->
+        <div class="grid-container">
+            <ConditionElementSelector
+                :input-value="currentCondition.element"
+                :index="conditionIndex"
+                class="grid-item"
+            />
+            <div class="select">
+                Condition    
+                <select
+                    id="condition"
+                    :disabled="verbDisabled"
+                    class="grid-item"
+                    :value="currentCondition.verb"
+                    @change="updateCondition(($event.target as HTMLSelectElement).value, 'verb')"
+                >
+                    <option default value="">Veuillez choisir</option>
+                    <option v-for="(description, verb) in getVerbs(elementType)" :key="verb" :value="verb">{{ description.label }}</option> 
+                </select>
+            </div>
+            <ConditionValue 
+                :disabled="valueDisabled"
+                :input-value="currentCondition.value"
+                :value-type="valueDisabled ? null : getValueType(currentCondition.verb)"
+                class="grid-item"
+                @change="updateCondition($event, 'value')"
+            />
         </div>
     </article>
 </template>
@@ -90,13 +124,15 @@ const currentCondition = ref({
         padding: .5rem .75rem;
         color: var(--inria-grey);
         
-        &:first-child {
-            border-right: none;
-            border-radius: 8px 0 0 8px;
-        }
-        &:last-child {
-            border-radius: 0 8px 8px 0;
-        }
+        border-radius: 8px;
+        
+        // &:first-child {
+        //     border-right: none;
+        //     border-radius: 8px 0 0 8px;
+        // }
+        // &:last-child {
+        //     border-radius: 0 8px 8px 0;
+        // }
         
         &.active {
             background: var(--inria-grey);
@@ -138,6 +174,17 @@ article {
         background-repeat: no-repeat;
         background-position: right 0.7rem top 50%;
         background-size: .8rem auto;
+        
+        &:disabled {
+            cursor: not-allowed;
+        }
     }
 }
+
+.grid-container {
+    display: grid;
+    width: 100%;
+    grid-template-columns: 1fr 1fr 1fr;
+    grid-gap: 1rem;
+}
 </style>
\ No newline at end of file
diff --git a/src/features/badge/components/ConditionModal.vue b/src/features/badge/components/ConditionModal.vue
index 8018cfa3..546d579a 100644
--- a/src/features/badge/components/ConditionModal.vue
+++ b/src/features/badge/components/ConditionModal.vue
@@ -1,23 +1,44 @@
 <script setup lang="ts">
-import { ref } from 'vue';
 import ConditionItem from './ConditionItem.vue';
 import { useEditorStore } from '@/src/shared/stores';
+import { getConditions, createRule } from '@/src/shared/services';
+import { computed } from 'vue';
 
 const editorStore = useEditorStore();
 
 const currentBadge = editorStore.getEpocNode.data.formValues['badges'][editorStore.openedBadgeId];
-const localConditions = ref(currentBadge.conditions);
+
+const allConditionsValid = computed(() => {
+    return editorStore.tempConditions.every((condition) => {
+        return condition.element && condition.verb && condition.value;
+    });
+});
+
+if(!editorStore.editingConditions) {
+    getConditions(currentBadge);
+    editorStore.editingConditions = true;
+}
+
 
 function addCondition() {
-    localConditions.value.push({});
+    editorStore.tempConditions.push({});
 }
 
 function close() {
+    editorStore.tempConditions = [];
     editorStore.conditionModal = false;
+    editorStore.editingConditions = false;
+}
+
+
+function updateCondition(values: { value: string, key: string }, index: number) {
+    const { value, key } = values;
+    editorStore.tempConditions[index][key] = value;
 }
 
 function save() {
-    currentBadge.conditions = localConditions.value;
+    currentBadge.rule = createRule(editorStore.tempConditions);
+
     close();
 }
 
@@ -34,11 +55,12 @@ function save() {
         </header>
         <div class="content conditions">
             <ConditionItem
-                v-for="(condition, index) in localConditions"
+                v-for="(condition, index) in editorStore.tempConditions"
                 :key="index" 
                 :input-value="condition"
                 :condition-index="index"
-                @remove-condition="localConditions.splice(index, 1)"
+                @remove-condition="editorStore.tempConditions.splice(index, 1)"
+                @update-condition="updateCondition($event, index)"
             />
             <button class="add btn btn-form" @click="addCondition">
                 <i class="icon-plus"></i>
@@ -49,7 +71,7 @@ function save() {
         <footer>
             <div class="content">
                 <button class="btn-choice cancel" @click="close">ANNULER</button>
-                <button class="btn-choice save" @click="save">ENREGISTRER</button>
+                <button :disabled="!allConditionsValid" class="btn-choice save" @click="save">ENREGISTRER</button>
             </div> 
         </footer>
     </article>
@@ -146,6 +168,12 @@ h3 {
     &.save{
         background-color: #E93100;
         color: #fff;
+        
+        &:disabled {
+            cursor: not-allowed;
+            filter:opacity(40%);
+        }
     }
+    
 }
 </style>
\ No newline at end of file
diff --git a/src/features/badge/components/ConditionValue.vue b/src/features/badge/components/ConditionValue.vue
new file mode 100644
index 00000000..1204a8b3
--- /dev/null
+++ b/src/features/badge/components/ConditionValue.vue
@@ -0,0 +1,91 @@
+<script setup lang="ts">
+
+defineProps<{
+    disabled: boolean;
+    valueType?: 'boolean' | 'number';
+    inputValue: string | boolean | number;
+}>();
+
+const emit = defineEmits<{
+    (e: 'change', value: string): void;
+}>();
+
+function onChange(event: Event) {
+    emit('change', (event.target as HTMLSelectElement).value);
+}
+
+</script>
+
+<template>
+    <div class="select">
+        Valeur    
+        <select
+            v-if="valueType === 'boolean'"
+            id="value"
+            :value="inputValue"
+            :disabled="disabled"
+            @change="onChange"
+        >
+            <option default value="">Veuillez choisir</option>
+            <option value="true">True</option>
+            <option value="false">False</option>
+        </select>
+        <input 
+            v-else-if="valueType === 'number'"
+            id="value"
+            :value="inputValue"
+            :disabled="disabled"
+            type="number"
+            class="number-input"
+            @change="onChange"
+        >
+        <input 
+            v-else
+            type="text"
+            class="disabled-input"
+            disabled
+        >
+    </div>
+</template>
+
+<style scoped lang="scss">
+.select {
+    display: flex;
+    flex-direction: column;
+    flex-grow: 1;
+    margin-top: .75rem;
+
+    label {
+        margin-bottom: 0.5rem;
+    }
+    
+    select, .disabled-input, .number-input {
+        appearance: none;
+        padding: .5rem;
+        border: 1px solid var(--border);
+        border-radius: 4px;
+        background-color: var(--item-background);
+        cursor: pointer;
+        font-size: 1rem;
+        color: var(--text);
+
+        &:disabled {
+            cursor: not-allowed;
+        }
+    }
+
+    input::-webkit-outer-spin-button,
+    input::-webkit-inner-spin-button {
+        -webkit-appearance: none;
+        margin: 0;
+    }
+
+    select {
+        background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgd2lkdGg9IjExcHgiIGhlaWdodD0iN3B4IiB2aWV3Qm94PSIwIDAgMTEuMCA3LjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxkZWZzPjxjbGlwUGF0aCBpZD0iaTAiPjxwYXRoIGQ9Ik0yNDE4LDAgTDI0MTgsMjQyNiBMMCwyNDI2IEwwLDAgTDI0MTgsMCBaIj48L3BhdGg+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImkxIj48cGF0aCBkPSJNOS4yMDE3MjIyNywwIEM5LjYxNjUwNDA2LDAgOS45OTA4OTcyMSwwLjI3MzU1MDk4NyAxMC4xNDk4ODU5LDAuNjk0MzM1OTM3IEMxMC4zMDg4NzQ2LDEuMTE1MTIwODkgMTAuMjQ5ODk0OSwxLjU5OTYwOTM3IDkuOTU0OTk2NjMsMS45MTk1MzE0NiBMNS44ODA5MDQ1NSw2LjQxOTUzMTQ2IEM1LjY1MzMxOTM0LDYuNjQxMDE1NDEgNS4zOTA0NzQ3Niw2Ljc1IDUuMTI3NjMwMTksNi43NSBDNC44NjQ3ODU2Miw2Ljc1IDQuNjAyNTgxNzgsNi42NDAxMzY3MiA0LjQwMjI0Mjg2LDYuNDIwNDEwMTYgTDAuMzI4MTUwMjkxLDEuOTIwNDEwMTYgQzAuMDA2MTQ2NTU1MTksMS41OTk2MDkzNyAtMC4wODE2OTQ5MjIzLDEuMTE0NDUzMDIgMC4wNzcxMDE1NTQ3LDAuNjk2MDkzODU3IEMwLjIzNTg5ODAzMiwwLjI3NzczNDY5NyAwLjYxMDIyNzEwNiwwIDEuMDI0Njg4NTMsMCBMOS4yMDE3MjIyNywwIFoiPjwvcGF0aD48L2NsaXBQYXRoPjwvZGVmcz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTMwNC4wIC0xMDYyLjApIj48ZyBjbGlwLXBhdGg9InVybCgjaTApIj48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg5ODAuMCA5MC4wKSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjAuMCA3MjkuMCkiPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAuMCA1MS4wKSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTUuMCAxNTAuMCkiPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAuMCAyNC4wKSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjg5LjM3MjM2OTgwNjgwMSAxOC4wKSI+PGcgY2xpcC1wYXRoPSJ1cmwoI2kxKSI+PHBvbHlnb24gcG9pbnRzPSItMS4xMTAyMjMwMmUtMTYsMCAxMC4yMzYwMTA0LDAgMTAuMjM2MDEwNCw2Ljc1IC0xLjExMDIyMzAyZS0xNiw2Ljc1IC0xLjExMDIyMzAyZS0xNiwwIiBzdHJva2U9Im5vbmUiIGZpbGw9IiMzNTQyNTgiPjwvcG9seWdvbj48L2c+PC9nPjwvZz48L2c+PC9nPjwvZz48L2c+PC9nPjwvZz48L3N2Zz4=");
+        background-repeat: no-repeat;
+        background-position: right 0.7rem top 50%;
+        background-size: .8rem auto;
+    }
+}
+
+</style>
\ No newline at end of file
diff --git a/src/features/forms/components/inputs/badges/BadgesInput.vue b/src/features/forms/components/inputs/badges/BadgesInput.vue
index 0ae6e18b..9fd67a6e 100644
--- a/src/features/forms/components/inputs/badges/BadgesInput.vue
+++ b/src/features/forms/components/inputs/badges/BadgesInput.vue
@@ -2,9 +2,9 @@
 import BadgeItem from '@/src/features/badge/components/BadgeItem.vue';
 import AddBadge from './components/AddBadge.vue';
 import { useEditorStore } from '@/src/shared/stores/editorStore';
-import { Badge } from '@/src/shared/interfaces';
 import { computed, ComputedRef } from 'vue';
 import { generateContentId } from '@/src/shared/services';
+import { Badge } from '@/src/shared/interfaces';
 
 const editorStore = useEditorStore();
 
@@ -22,9 +22,9 @@ const badges: ComputedRef<Badge[]> = computed(() => {
         const newBadge: Badge = {
             id: value,
             title: props.inputValue[value]['title'],
-            icon: props.inputValue[value]['icon'],
-            conditions: props.inputValue[value]['conditions'],
             description: props.inputValue[value]['description'],
+            icon: props.inputValue[value]['icon'],
+            rule: props.inputValue[value]['rule'],
         };
         res.push(newBadge);
     }
@@ -40,8 +40,10 @@ function addNewBadge() {
     epocNode.data.formValues['badges'][id] = {
         title: '',
         icon: '',
-        conditions: [],
         description: '',
+        rule: {
+            'and': []
+        }
     };
     openBadge(id);
 }
@@ -58,7 +60,7 @@ function addNewBadge() {
             v-for="(badge, index) in badges"
             :key="index"
             :badge="badge"
-            :invalid="badge.conditions.length === 0"
+            :invalid="Object.keys(badge.rule.and).length === 0"
             @click="openBadge(badge.id)"
         />
     </div>
diff --git a/src/shared/classes/epoc-v1.ts b/src/shared/classes/epoc-v1.ts
index 84e5976c..5f0d5b61 100644
--- a/src/shared/classes/epoc-v1.ts
+++ b/src/shared/classes/epoc-v1.ts
@@ -1,6 +1,6 @@
-import { Chapter, Content, Epoc, html, Parameters, uid } from '@epoc/epoc-types/dist/v1';
-import { Question } from '@epoc/epoc-types/dist/v1/question';
-import { Author } from '@epoc/epoc-types/dist/v1/author';
+import { Chapter, Content, Epoc, html, Parameters, uid } from '@epoc/epoc-types/src/v1';
+import { Question } from '@epoc/epoc-types/src/v1/question';
+import { Author } from '@epoc/epoc-types/src/v1/author';
 
 export class EpocV1 implements Epoc {
     id: string;
diff --git a/src/shared/interfaces/badge.interface.ts b/src/shared/interfaces/badge.interface.ts
index a73a21ac..66b7173d 100644
--- a/src/shared/interfaces/badge.interface.ts
+++ b/src/shared/interfaces/badge.interface.ts
@@ -1,8 +1,26 @@
+import { Rule, uid } from '@epoc/epoc-types/src/v2';
 
 export interface Badge {
-   id: string;
-   title: string;
-   description: string;
-   icon: string;
-   conditions: any;
-}
\ No newline at end of file
+    id?: uid;
+    title: string;
+    description: string;
+    icon: string;
+    rule: Rule
+}
+
+export interface Condition {
+    element?: string;
+    verb?: string;
+    value?: string | number | boolean;
+    elementType?: 'contents' | 'chapters';
+}
+
+export interface VerbDescription {
+    label: string;
+    valueType: 'number' | 'boolean';
+}
+
+export type ElementType = 'chapter' | 'page' | 'text' | 'video' | 'audio' | 'activity' | 'question';
+export type VerbKey = 'started' | 'completed' | 'viewed' | 'read' | 'played' | 'watched' | 'listened' | 'attempted' | 'scored';
+
+export type Verbs = { [key in VerbKey]?: VerbDescription; }
\ No newline at end of file
diff --git a/src/shared/services/badge.service.ts b/src/shared/services/badge.service.ts
new file mode 100644
index 00000000..4cbb2ade
--- /dev/null
+++ b/src/shared/services/badge.service.ts
@@ -0,0 +1,83 @@
+import { useEditorStore } from '@/src/shared/stores';
+import { Condition } from '@/src/shared/interfaces';
+import { Verbs, VerbKey, ElementType } from '@/src/shared/interfaces/badge.interface';
+import { Operand, Operands, Rule } from '@epoc/epoc-types/src/v2';
+
+const verbs: Verbs = {
+    'started': { label: 'Commencé', valueType: 'boolean' },
+    'completed': { label: 'Terminé', valueType: 'boolean' },
+    'viewed': { label: 'Vu', valueType: 'boolean' },
+    'read': { label: 'Lu', valueType: 'boolean' },
+    'played': { label: 'Joué', valueType: 'boolean' },
+    'watched': { label: 'Regardé', valueType: 'boolean' },
+    'listened': { label: 'Écouté', valueType: 'boolean' },
+    'attempted': { label: 'Tenté', valueType: 'boolean' },
+    'scored': { label: 'Obtenu un score de', valueType: 'number' }
+};
+
+const elementVerbs: Record<ElementType, VerbKey[]> = {
+    'chapter': ['started', 'completed'],
+    'page': ['viewed'],
+    'text': ['read'],
+    'video': ['played', 'watched'],
+    'audio': ['played', 'listened'],
+    'activity': ['started', 'completed', 'scored'],
+    'question': ['attempted', 'completed', 'scored']
+};
+
+export function getVerbs(type: ElementType): Verbs {
+    if(!type || !elementVerbs[type]) return;
+
+    const verbsKeys = elementVerbs[type];
+    const res: Verbs = {};
+
+    for (const key of verbsKeys) {
+        res[key] = verbs[key];
+    }
+    
+    return res;
+}
+
+export function getValueType(verbKey:string): 'number' | 'boolean' {
+    if(!verbs[verbKey]) return;
+    return verbs[verbKey].valueType;
+}
+
+
+export function getConditions(currentBadge): void {
+    const editorStore = useEditorStore();    
+    const rules = currentBadge.rule['and'];
+
+    for(const key1 in rules) {
+        // key2 is the operator
+        for(const key2 in rules[key1]) {
+            const [verbs, value] = rules[key1][key2];
+            
+            const [type, element,verb] = verbs['var'].split('.');
+            
+            console.log('type detected');
+
+            const conditionObj: Condition = {
+                element,
+                verb,
+                value,
+                elementType: type
+            };
+            
+            editorStore.tempConditions.push(conditionObj);
+        }
+    }
+}
+
+export function createRule(entry: Condition[]): Rule {
+    const rules: Operands = entry.map((item) => {
+        const entryRule: Operand = {
+            '===': [
+                { 'var': `${item.elementType}.${item.element}.${item.verb}` }, item.value
+            ]
+        };
+        return entryRule;
+    }) as Operands;
+    
+    return { 'and': rules };
+}
\ No newline at end of file
diff --git a/src/shared/services/graph.service.ts b/src/shared/services/graph.service.ts
index afdc9286..d6576411 100644
--- a/src/shared/services/graph.service.ts
+++ b/src/shared/services/graph.service.ts
@@ -1,15 +1,15 @@
 import { ApiInterface } from '@/src/shared/interfaces/api.interface';
 import { getConnectedEdges, GraphNode, useVueFlow } from '@vue-flow/core';
 import { EpocV1 } from '@/src/shared/classes/epoc-v1';
-import {Assessment, ChoiceCondition, Content, Html, SimpleQuestion, uid, Video, Audio} from '@epoc/epoc-types/dist/v1';
-import { Question } from '@epoc/epoc-types/dist/v1/question';
+import { Assessment, ChoiceCondition, Content, Html, SimpleQuestion, uid, Video, Audio } from '@epoc/epoc-types/src/v1';
 import {questions} from '@/src/shared/data';
 import { useEditorStore } from '@/src/shared/stores';
-import { getContentIdFromId } from '@/src/shared/services/graph';
+import { changeChapterSelectability, getContentIdFromId } from '@/src/shared/services/graph';
+import { Question } from '@epoc/epoc-types/src/v2';
 
 declare const api: ApiInterface;
 
-const { toObject, nodes, edges }  = useVueFlow({ id: 'main' });
+const { nodes, edges, findNode, toObject }  = useVueFlow({ id: 'main' });
 
 function writeProjectData(): void {
     debounceFunction(500, () => {
@@ -37,8 +37,6 @@ const debounceFunction = function (delay, cb) {
     timerId = setTimeout(cb, delay);
 };
 
-
-
 function createContentJSON() : EpocV1 {
 
     const ePocNode = nodes.value.find((node) => { return node.type === 'epoc'; });
@@ -253,28 +251,30 @@ let openedConditionIndex: number | null = null;
 export function enterSelectNodeMode(conditionIndex: number): void {
     const editorStore = useEditorStore();
     editorStore.enterSelectNodeMode();
+    changeChapterSelectability(true);
 
     disableGraph();
     openedConditionIndex = conditionIndex;
 }
 
+function getElementType(id: string): 'contents' | 'chapters' {
+    const node = findNode(id);
+    if(!node || node.type !== 'chapter' ) return 'contents';
+    else return 'chapters';
+}
+
 export function exitSelectNodeMode(selectedId?: string): void {
     const editorStore = useEditorStore();
     editorStore.exitSelectNodeMode();
-
-    const epocNode = editorStore.getEpocNode;
-    const currentBadge = editorStore.openedBadgeId;
-
-    const conditions = epocNode.data.formValues['badges'][currentBadge].conditions;
-
-    conditions[openedConditionIndex].element = getContentIdFromId(selectedId);
+    changeChapterSelectability(false);
+    
+    editorStore.tempConditions[openedConditionIndex].elementType = getElementType(selectedId);
+    editorStore.tempConditions[openedConditionIndex].element = getContentIdFromId(selectedId);
 
     enableGraph();
-
     openedConditionIndex = null;
 }
 
-
 function disableGraph(): void {
     nodes.value.forEach(node => node.draggable = false);
     edges.value.forEach(edge => {
diff --git a/src/shared/services/graph/content.service.ts b/src/shared/services/graph/content.service.ts
index 488236cc..c856f8c1 100644
--- a/src/shared/services/graph/content.service.ts
+++ b/src/shared/services/graph/content.service.ts
@@ -5,7 +5,7 @@ import { deleteNode } from './node.service';
 import {generateContentId, generateId} from '../graph.service';
 import * as forms from '@/src/shared/data/forms';
 
-const { findNode } = useVueFlow({ id: 'main' });
+const { nodes, findNode } = useVueFlow({ id: 'main' });
 
 const editorStore = useEditorStore();
 
@@ -99,4 +99,16 @@ export function getContentDefaultValues(type) {
         }, {});
         return {...acc, ...keyValues};
     }, {});
+}
+
+export function getContentByContentId(contentId: string) {
+    for(const node of nodes.value) {
+        if(node.data.elements) {
+            for(const element of node.data.elements) {
+                if(element.contentId === contentId) {
+                    return element;
+                }
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/shared/services/graph/copyPaste.service.ts b/src/shared/services/graph/copyPaste.service.ts
new file mode 100644
index 00000000..c7e7e150
--- /dev/null
+++ b/src/shared/services/graph/copyPaste.service.ts
@@ -0,0 +1,92 @@
+import { ApiInterface } from '@/src/shared/interfaces/api.interface';
+import { getSelectedNodes, alignNode } from '.';
+import { useVueFlow, Node } from '@vue-flow/core';
+import { generateId, generateContentId } from '../graph.service';
+
+const { addNodes } = useVueFlow({ id: 'main' });
+
+declare const api: ApiInterface;
+
+function setupCopyPaste(): void {
+    api.receive('graphPasted', (data: string) => {
+        const parsedData = JSON.parse(data);
+        const { selectedPages, position } = parsedData;
+        handleGraphPaste(selectedPages, position);
+    });
+}
+
+setupCopyPaste();
+
+export function graphCopy(selection?: Node[]): void {
+    if(!selection) selection = getSelectedNodes();
+    const selectedPages = selection.filter(node => node.type === 'page' || node.type === 'activity');
+    
+    const data = JSON.stringify({ pages: selectedPages });
+    
+    api.send('graphCopy', data);
+}
+
+export function graphPaste(position?: { x: number, y: number }) {
+    const data = JSON.stringify({ position });
+    api.send('graphPaste', data);
+}
+
+function handleGraphPaste(selectedPages, position: { x: number, y: number }): void {
+    if(!selectedPages) return;
+    
+    let offsetX;
+    let offsetY;
+    
+    if(position) {
+        offsetX = position.x - selectedPages[0].position.x;
+        offsetY = position.y - selectedPages[0].position.y;
+    } else {
+        offsetX = 100;
+        offsetY = 100;
+    }
+
+    const newPages = [];
+    for(const page of selectedPages) {
+        const pageId = generateId();
+        
+        const elements = page.data.elements.map(element => {
+            const newElement = JSON.parse(JSON.stringify(element));
+            newElement.id = generateId();
+            newElement.parentId = pageId;
+            return newElement;
+        });
+
+        const newPage: Node = {
+            id: pageId,
+            type: page.type,
+            data: {
+                type: page.data.type,
+                elements,
+                contentId: generateContentId(),
+                formType: page.data.formType,
+                formValues: JSON.parse(JSON.stringify(page.data.formValues)),
+            },
+            position: { x: page.position.x + offsetX, y: page.position.y + offsetY },
+            deletable: true
+        };
+        newPages.push(newPage);
+    }
+    
+    // deselect the old pages
+    for(const page of selectedPages) {
+        page.selected = false;
+    }
+    
+    // select the new pages
+    for(const page of newPages) {
+        page.selected = true;
+    }
+    // automatically copy the new pages so that they can be pasted again without superposition
+    // api.send('graphCopy', { selectedPages: newPages, selectionRectHeight: offsetY });
+
+    addNodes(newPages);
+    
+    for(const page of newPages) {
+        alignNode(page.id);
+    }
+}
\ No newline at end of file
diff --git a/src/shared/services/graph/element.service.ts b/src/shared/services/graph/element.service.ts
new file mode 100644
index 00000000..0bcb507d
--- /dev/null
+++ b/src/shared/services/graph/element.service.ts
@@ -0,0 +1,95 @@
+import { useVueFlow, Node, MarkerType, Edge } from '@vue-flow/core';
+import { deleteContent, deleteNode, getContentByContentId } from './';
+import { useEditorStore } from '@/src/shared/stores';
+import { generateId } from '../graph.service';
+import { ElementType, NodeElement } from '../../interfaces';
+
+const { nodes, findNode, addEdges } = useVueFlow({ id: 'main' });
+const editorStore = useEditorStore();
+
+export function deleteSelection(selection: Node[]) {
+    selection.forEach(node => deleteElement(node.id));
+}
+
+export function deleteElement(id: string, pageId?: string): void {
+    const pageToDelete = findNode(id);
+
+    if(pageId || !pageToDelete) deleteContent(pageId ?? editorStore.openedNodeId, id);
+    else deleteNode(id);
+
+    editorStore.closeFormPanel();
+}
+
+export function createEdge(sourceId: string, targetId: string): void {
+    const newEdge: Edge = {
+        id: generateId(),
+        source: sourceId,
+        target: targetId,
+        type: 'default',
+        updatable: true,
+        style: {stroke: '#384257', strokeWidth: 2.5},
+        markerEnd: {type: MarkerType.ArrowClosed, color: '#384257'}
+    };
+
+    addEdges([newEdge]);
+}
+
+export function getContentIdFromId(id: string): string {
+    if(id === '1') return 'ePoc';
+    const node = findNode(id);
+    let contentId;
+    if(!node) {
+        for(const node of nodes.value) {
+            if(node.data.elements) {
+                for(const element of node.data.elements) {
+                    if(element.id === id) contentId = element.contentId;
+                }
+            }
+        }
+    } else {
+        contentId = node.data.contentId;
+    }
+
+    return contentId;
+}
+
+export function getElementByContentId(contentId: string): Node | NodeElement {
+    const node = nodes.value.find(node => node.data.contentId === contentId);
+
+    if(!node) {
+        for(const node of nodes.value) {
+            if(node.data.elements) {
+                for(const element of node.data.elements) {
+                    if(element.contentId === contentId) {
+                        return element;
+                    }
+                }
+            }
+        }
+    }
+    
+    return node;
+}
+
+
+const questions = ['choice', 'drag-and-drop', 'reorder', 'swipe', 'dropdown-list'];
+function getContentType(contentId: string): ElementType {
+    const content = getContentByContentId(contentId);
+
+    if(!content) return;
+    if(questions.includes(content.action.type)) return 'question';
+    else return content.action.type;
+}
+
+
+export function getElementType(contentId: string): ElementType {
+    if(contentId === 'ePoc') return;
+
+    const node = nodes.value.find(node => node.data.contentId === contentId);
+
+    if(!node) {
+        return getContentType(contentId);
+    }     
+    
+    return node.type as ElementType;
+}
\ No newline at end of file
diff --git a/src/shared/services/graph/index.ts b/src/shared/services/graph/index.ts
index e77bda7e..b735ddb1 100644
--- a/src/shared/services/graph/index.ts
+++ b/src/shared/services/graph/index.ts
@@ -1,2 +1,4 @@
 export * from './content.service';
-export * from './node.service';
\ No newline at end of file
+export * from './node.service';
+export * from './element.service';
+export * from './copyPaste.service';
\ No newline at end of file
diff --git a/src/shared/services/graph/node.service.ts b/src/shared/services/graph/node.service.ts
index a7d1d905..e63eed39 100644
--- a/src/shared/services/graph/node.service.ts
+++ b/src/shared/services/graph/node.service.ts
@@ -1,15 +1,15 @@
-import { Chapter } from '@epoc/epoc-types/dist/v1';
+import { Chapter } from '@epoc/epoc-types/src/v1';
 import { EpocV1 } from '../../classes/epoc-v1';
 import { useEditorStore, useGraphStore } from '../../stores';
-import { useVueFlow, Node, MarkerType, Edge, getConnectedEdges } from '@vue-flow/core';
+import { useVueFlow, Node, getConnectedEdges } from '@vue-flow/core';
 import { NodeElement, SideAction } from '../../interfaces';
 import { nextTick, toRaw, watch } from 'vue';
 
-import { addContentToPage, deleteContent } from './content.service';
+import { addContentToPage } from './content.service';
 import { generateContentId, generateId, graphService } from '../graph.service';
-import { ApiInterface } from '../../interfaces/api.interface';
+import { deleteElement, deleteSelection, createEdge } from '.';
 
-const { nodes, edges, addNodes, addEdges, findNode, applyEdgeChanges, applyNodeChanges } = useVueFlow({ id: 'main' });
+const { nodes, edges, addNodes, findNode, applyEdgeChanges, applyNodeChanges } = useVueFlow({ id: 'main' });
 
 const editorStore = useEditorStore();
 
@@ -212,13 +212,10 @@ export function insertAtEnd(chapterId: string, action: SideAction): void {
         nextNode = graphService.getNextNode(findNode(nextNode.id));
     }
     
-    console.log('finalNode', savedId);
-    
     insertAfter(savedId, action);
 }
 
 export function insertAtStart(chapterId: string, action: SideAction): void {
-    console.log('insertAtStart');
     const nextNode = graphService.getNextNode(findNode(chapterId));
 
     if(nextNode) insertBefore(nextNode.id, action);
@@ -255,20 +252,6 @@ export function createPageFromContent(position: { x: number, y: number }, elemen
     });
 }
 
-export function createEdge(sourceId: string, targetId: string): void {
-    const newEdge: Edge = {
-        id: generateId(),
-        source: sourceId,
-        target: targetId,
-        type: 'default',
-        updatable: true,
-        style: {stroke: '#384257', strokeWidth: 2.5},
-        markerEnd: {type: MarkerType.ArrowClosed, color: '#384257'}
-    };
-
-    addEdges([newEdge]);
-}
-
 export function deleteSelectedNodes(): void {
     const isChild = Boolean(editorStore.openedNodeId);
 
@@ -283,9 +266,6 @@ export function deleteSelectedNodes(): void {
     editorStore.closeValidationModal();
 }
 
-export function deleteSelection(selection: Node[]) {
-    selection.forEach(node => deleteElement(node.id));
-}
 
 export function deleteNode(nodeId: string): void {
     const nodeToDelete = findNode(nodeId);
@@ -294,14 +274,6 @@ export function deleteNode(nodeId: string): void {
     if(nodeToDelete.type === 'chapter') updateNextChapter(nodeToDelete.id);
 }
 
-export function deleteElement(id: string, pageId?: string): void {
-    const pageToDelete = findNode(id);
-
-    if(pageId || !pageToDelete) deleteContent(pageId ?? editorStore.openedNodeId, id);
-    else deleteNode(id);
-
-    editorStore.closeFormPanel();
-}
 
 export function duplicatePage(pageId?: string): void {
     const pageNode = findNode(pageId ?? editorStore.openedElementId);
@@ -408,118 +380,17 @@ export function isFormButtonDisabled(isDisabledFunction: (node) => boolean): boo
     return isDisabledFunction(nodeData);
 }
 
-export function unselectAllNodes(): void {
-    nodes.value.forEach(node => node.selected = false);
-}
-
-
-// Copy/Paste
-declare const api: ApiInterface;
-
-function setupCopyPaste(): void {
-    api.receive('graphPasted', (data: string) => {
-        const parsedData = JSON.parse(data);
-        const { selectedPages, position } = parsedData;
-        handleGraphPaste(selectedPages, position);
-    });
-}
-
-setupCopyPaste();
-
-export function graphCopy(selection?: Node[]): void {
-    if(!selection) selection = getSelectedNodes();
-    const selectedPages = selection.filter(node => node.type === 'page' || node.type === 'activity');
-    
-    const data = JSON.stringify({ pages: selectedPages });
-    
-    api.send('graphCopy', data);
-}
-
-export function graphPaste(position?: { x: number, y: number }) {
-    const data = JSON.stringify({ position });
-    api.send('graphPaste', data);
-}
-
-function handleGraphPaste(selectedPages, position: { x: number, y: number }): void {
-    if(!selectedPages) return;
-    
-    let offsetX;
-    let offsetY;
-    
-    if(position) {
-        offsetX = position.x - selectedPages[0].position.x;
-        offsetY = position.y - selectedPages[0].position.y;
-    } else {
-        offsetX = 100;
-        offsetY = 100;
-    }
-
-    const newPages = [];
-    for(const page of selectedPages) {
-        const pageId = generateId();
-        
-        const elements = page.data.elements.map(element => {
-            const newElement = JSON.parse(JSON.stringify(element));
-            newElement.id = generateId();
-            newElement.parentId = pageId;
-            return newElement;
-        });
-
-        const newPage: Node = {
-            id: pageId,
-            type: page.type,
-            data: {
-                type: page.data.type,
-                elements,
-                contentId: generateContentId(),
-                formType: page.data.formType,
-                formValues: JSON.parse(JSON.stringify(page.data.formValues)),
-            },
-            position: { x: page.position.x + offsetX, y: page.position.y + offsetY },
-            deletable: true
-        };
-        newPages.push(newPage);
-    }
-    
-    // deselect the old pages
-    for(const page of selectedPages) {
-        page.selected = false;
-    }
-    
-    // select the new pages
-    for(const page of newPages) {
-        page.selected = true;
+export function changeChapterSelectability(value: boolean): void {
+    for(const node of nodes.value) {
+        if(node.type === 'chapter') node.selectable = value;
     }
-    // automatically copy the new pages so that they can be pasted again without superposition
-    // api.send('graphCopy', { selectedPages: newPages, selectionRectHeight: offsetY });
+}
 
-    addNodes(newPages);
-    
-    for(const page of newPages) {
-        alignNode(page.id);
-    }
+export function unselectAllNodes(): void {
+    nodes.value.forEach(node => node.selected = false);
 }
 
 export function deleteBadge(id: string) {
     const epocNode = findNode('1');
     delete epocNode.data.formValues.badges[id];
-}
-
-export function getContentIdFromId(id: string): string {
-    if(id === '1') return 'ePoc';
-    const node = findNode(id);
-    let contentId;
-    if(!node) {
-        for(const node of nodes.value) {
-            if(node.data.elements) {
-                for(const element of node.data.elements) {
-                    if(element.id === id) contentId = element.contentId;
-                }
-            }
-        }
-    } else {
-        contentId = node.data.contentId;
-    }
-
-    return contentId;
 }
\ No newline at end of file
diff --git a/src/shared/services/import.service.ts b/src/shared/services/import.service.ts
index d5da0c09..830d218c 100644
--- a/src/shared/services/import.service.ts
+++ b/src/shared/services/import.service.ts
@@ -2,7 +2,7 @@ import { EpocV1 } from '@/src/shared/classes/epoc-v1';
 import { addChapter, createLinkedPage, setEpocNodeData } from '@/src/shared/services/graph';
 import { generateId } from '@/src/shared/services/graph.service';
 import { questions, standardPages } from '@/src/shared/data';
-import { Assessment, ChoiceCondition, SimpleQuestion } from '@epoc/epoc-types/dist/v1';
+import { Assessment, ChoiceCondition, SimpleQuestion } from '@epoc/epoc-types/src/v1';
 
 const mapType = {
     'video': standardPages.find(s => s.type === 'video'),
diff --git a/src/shared/services/index.ts b/src/shared/services/index.ts
index 2731ec0d..c76024e2 100644
--- a/src/shared/services/index.ts
+++ b/src/shared/services/index.ts
@@ -1,2 +1,3 @@
 export * from './editor.service';
-export * from './graph.service';
\ No newline at end of file
+export * from './graph.service';
+export * from './badge.service';
\ No newline at end of file
diff --git a/src/shared/stores/editorStore.ts b/src/shared/stores/editorStore.ts
index 763190aa..8acdad17 100644
--- a/src/shared/stores/editorStore.ts
+++ b/src/shared/stores/editorStore.ts
@@ -1,7 +1,6 @@
 import { defineStore } from 'pinia';
-import { ePocProject, Form, NodeElement, PageModel, SideAction} from '@/src/shared/interfaces';
+import { ePocProject, Form, NodeElement, PageModel, SideAction, Condition } from '@/src/shared/interfaces';
 import { GraphNode, useVueFlow } from '@vue-flow/core';
-
 import { formsModel, questions, standardPages } from '@/src/shared/data';
 
 const { nodes, findNode } = useVueFlow({ id: 'main' });
@@ -51,6 +50,8 @@ interface EditorState {
 
     // Mode
     selectNodeMode: boolean;
+    tempConditions: Condition[];
+    editingConditions: boolean;
 }
 
 export const useEditorStore = defineStore('editor', {
@@ -88,6 +89,8 @@ export const useEditorStore = defineStore('editor', {
 
         // Mode
         selectNodeMode: false,
+        tempConditions: [],
+        editingConditions: false,
     }),
     
     getters: {
-- 
GitLab