diff --git a/src/components/SideMenu.vue b/src/components/SideMenu.vue new file mode 100644 index 0000000000000000000000000000000000000000..ad61da789fdf22ce61efef55f7039825db0c282a --- /dev/null +++ b/src/components/SideMenu.vue @@ -0,0 +1,55 @@ +<script setup lang="ts"> + +defineProps<{ + title: string; +}>(); + +const emits = defineEmits<{ + (e: 'close'): void; +}>(); + +function close() { + emits('close'); +} +</script> + +<template> + <div class="side-menu"> + <header> + <h3>{{ title }}</h3> + <button class="btn btn-close" @click="close"><i class="icon-x"></i></button> + </header> + <hr class="separator" /> + <div class="side-menu-content"> + <slot /> + </div> + </div> +</template> + +<style scoped lang="scss"> +.side-menu { + z-index: 100; + position: fixed; + top: 80px; + left: 100px; + height: 100vh; + background-color: white; + border-right: 1px solid var(--border); + + width: fit-content; + + header { + h3 { + margin-left: 1rem; + } + } + + hr { + margin-bottom: 1rem; + } + + &-content { + padding: 1rem; + } +} +</style> \ No newline at end of file diff --git a/src/features/badge/components/BadgeItem.vue b/src/features/badge/components/BadgeItem.vue index cb15f6020cbc7991068209fd080884719d3df042..fffb0b9ff58d039aaf241dbfd0a6ffae078a6625 100644 --- a/src/features/badge/components/BadgeItem.vue +++ b/src/features/badge/components/BadgeItem.vue @@ -2,6 +2,8 @@ import { Badge } from '@/src/shared/interfaces'; import { computed } from 'vue'; import { iconsPath } from '@/src/shared/data'; +import { useEditorStore } from '@/src/shared/stores'; + const props = defineProps<{ viewMode?: boolean; @@ -17,6 +19,8 @@ const emit = defineEmits<{ (e: 'remove-icon'): void; }>(); +const editorStore = useEditorStore(); + function onClick() { if (props.viewMode) return; @@ -35,6 +39,11 @@ function getIconPath() { if (icon.startsWith('blob')) return icon; return icon.endsWith('.svg') ? `assets://${icon}` : `${iconsPath}/${icon}.svg`; } + +const isActive = computed(() => { + return editorStore.openedBadgeId === props.badge?.id; +}); + </script> <template> @@ -42,7 +51,7 @@ function getIconPath() { <button v-if="deleteButton" class="btn btn-close" @click="emit('remove-icon')"><i class="icon-x"></i></button> <div class="badge-background" - :class="{ clickable: !viewMode && !inactive, inactive: inactive }" + :class="{ clickable: !viewMode && !inactive, inactive: inactive, active: isActive }" @mouseup.stop @click="onClick" > @@ -94,7 +103,7 @@ function getIconPath() { &-background { user-select: none; - background: var(--node); + background: var(--content); border-radius: 8px; position: relative; width: 100px; @@ -105,6 +114,10 @@ function getIconPath() { &.inactive { border: 2px dashed var(--border); } + + &.active { + border: 2px solid var(--editor-yellow) + } } .empty-badge { diff --git a/src/features/ePocFlow/nodes/ActivityNode.vue b/src/features/ePocFlow/nodes/ActivityNode.vue index 34f7c9514875e4d2818ae6d2d93997582aff04a8..7e1bda7b58e42fb4de3c4268ce9d25b93762a615 100644 --- a/src/features/ePocFlow/nodes/ActivityNode.vue +++ b/src/features/ePocFlow/nodes/ActivityNode.vue @@ -122,6 +122,10 @@ const activityIndex = computed(() => { <style scoped lang="scss"> .node { border: 2px dashed var(--dashed-border); + + &.highlight{ + border: 2px dashed var(--editor-yellow) !important; + } } .vue-flow__node.selected .node { diff --git a/src/features/sideBar/SideBar.vue b/src/features/sideBar/SideBar.vue index c08f9002b29abcdc109ac5eb5fc3c3c2e39472c2..a89396d2329ae0258ed1c5d5a82ccf14370cd401 100644 --- a/src/features/sideBar/SideBar.vue +++ b/src/features/sideBar/SideBar.vue @@ -1,18 +1,12 @@ <script setup lang="ts"> import SideActions from './components/SideActions.vue'; -import ModelMenu from './components/ModelMenu.vue'; -import { useEditorStore } from '@/src/shared/stores'; -const editorStore = useEditorStore(); </script> <template> <div class="side-bar"> <img src="/img/epoc.svg" alt="logo ePoc" draggable="false" /> - <div class="side-bar-actions"> - <SideActions /> - </div> - <ModelMenu v-if="editorStore.modelMenu" /> + <SideActions class="side-bar-actions" /> </div> </template> @@ -28,11 +22,8 @@ const editorStore = useEditorStore(); user-select: none; } &-actions { - display: flex; - flex-direction: column; align-items: center; margin-top: 2rem; - gap: 1rem; } } </style> diff --git a/src/features/sideBar/components/BadgeMenu.vue b/src/features/sideBar/components/BadgeMenu.vue new file mode 100644 index 0000000000000000000000000000000000000000..5f39fb3a359088baa595533a7c048dc4a6962761 --- /dev/null +++ b/src/features/sideBar/components/BadgeMenu.vue @@ -0,0 +1,57 @@ +<script setup lang="ts"> +import SideMenu from '@/src/components/SideMenu.vue'; +import { computed, ComputedRef } from 'vue'; +import { Badge } from '@/src/shared/interfaces'; +import { useVueFlow } from '@vue-flow/core'; +import { useEditorStore } from '@/src/shared/stores'; +import BadgeItem from '@/src/features/badge/components/BadgeItem.vue'; +import { isBadgeValid, openBadge } from '@/src/shared/services'; + +const { findNode } = useVueFlow({ id: 'main' }); + +const editorStore = useEditorStore(); + +const epocNode = findNode('1'); +const rules = epocNode.data.formValues.badges; + +const badges: ComputedRef<Badge[]> = computed(() => { + const res: Badge[] = []; + for (let value in rules) { + const newBadge: Badge = { + id: value, + title: rules[value]['title'], + description: rules[value]['description'], + icon: rules[value]['icon'], + rule: rules[value]['rule'], + }; + res.push(newBadge); + } + + return res; +}); +</script> + +<template> + <SideMenu title="Badges" @close="editorStore.badgeMenu = false"> + <div class="badges"> + <BadgeItem + v-for="(badge, index) in badges" + :key="index" + :badge="badge" + :invalid="!isBadgeValid(badge)" + @click="openBadge(badge.id)" + /> + </div> + </SideMenu> +</template> + +<style scoped lang="scss"> +.badges { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 1rem; + //display: flex; + //flex-wrap: wrap; + //gap: 1rem; +} +</style> \ No newline at end of file diff --git a/src/features/sideBar/components/ModelMenu.vue b/src/features/sideBar/components/ModelMenu.vue index 246f4daeadf265ba7ad8a0a6d5298d6b5aa6c102..eccdcaf9a02497b97564a52e090c7bf70a4c2a02 100644 --- a/src/features/sideBar/components/ModelMenu.vue +++ b/src/features/sideBar/components/ModelMenu.vue @@ -1,17 +1,13 @@ <script setup lang="ts"> import PageTemplate from './PageTemplate.vue'; import { useEditorStore } from '@/src/shared/stores'; +import SideMenu from '@/src/components/SideMenu.vue'; const editorStore = useEditorStore(); </script> <template> - <div class="model-menu"> - <header> - <h3>Modèles de page</h3> - <button class="btn btn-close"><i class="icon-x"></i></button> - </header> - <hr class="separator" /> + <SideMenu title="Modèles de page" @close="editorStore.modelMenu = false"> <div v-if="editorStore.pageModels.length > 0" class="models"> <PageTemplate v-for="(model, index) in editorStore.pageModels" @@ -23,47 +19,23 @@ const editorStore = useEditorStore(); <div v-else> <h4 class="empty">Aucun modèle de page n'as été créé</h4> </div> - </div> + </SideMenu> </template> <style scoped lang="scss"> -.model-menu { - z-index: 100; - position: fixed; - top: 0; - left: 100px; - height: 100vh; - background-color: white; - border-right: 1px solid var(--border); - - // All the padding + 2 nodes + all border - width: calc(8rem + 120px + 16px); - - header { - h3 { - margin-left: 1rem; - } - } - - hr { - margin-bottom: 1rem; - } - - .empty { - width: calc(6rem + 120px); - text-align: center; - padding: 1rem; - h4 { - margin: 1rem; - } - } +.models { + display: flex; + gap: 2rem; + flex-wrap: wrap; + margin: 0; +} - .models { - display: flex; - gap: 2rem; - padding: 1rem; - flex-wrap: wrap; - margin: 0; +.empty { + width: calc(6rem + 120px); + text-align: center; + padding: 1rem; + h4 { + margin: 1rem; } } </style> diff --git a/src/features/sideBar/components/SideActions.vue b/src/features/sideBar/components/SideActions.vue index ef68b6bbf2ea0d5b1be33a553589fade3dd6c469..2d21e6350124ee65ae0d5b929f4e35aa0650a4b0 100644 --- a/src/features/sideBar/components/SideActions.vue +++ b/src/features/sideBar/components/SideActions.vue @@ -16,6 +16,7 @@ const standardContent = editorStore.standardPages.filter(({ type }) => { const questionContent = editorStore.standardPages.find(({ type }) => type === 'question'); const conditionContent = editorStore.standardPages.find(({ type }) => type === 'condition'); const modelContent = editorStore.standardPages.find(({ type }) => type === 'model'); +const badgeContent = editorStore.standardPages.find(({ type }) => type === 'badge'); const dragging = ref(false); @@ -50,122 +51,146 @@ function showQuestionsMenu() { function showTemplateMenu() { editorStore.modelMenu = !editorStore.modelMenu; } + +function showBadgeMenu() { + editorStore.badgeMenu = !editorStore.badgeMenu; +} + </script> <template> - <VueDraggable - v-bind="dragOptions" - key="draggable" - :model-value="standardContent" - item-key="index" - class="contents-list" - > - <!--suppress VueUnrecognizedSlot --> - <template #item="{ element, index }"> - <div> - <!--suppress VueUnrecognizedDirective --> - <ContentButton - :key="index" - v-tippy="{ - content: element.tooltip, - placement: 'right', - arrow: true, - arrowType: 'round', - animation: 'fade', - }" - :data-testid="`${element.type}-content`" - :icon="element.icon" - :is-draggable="true" - :class-list="{ 'btn-content-blue': true }" - @dragstart="dragStart($event, element)" - /> + <div class="actions-list"> + <VueDraggable + v-bind="dragOptions" + key="draggable" + :model-value="standardContent" + item-key="index" + class="contents-list" + > + <!--suppress VueUnrecognizedSlot --> + <template #item="{ element, index }"> + <div> + <!--suppress VueUnrecognizedDirective --> + <ContentButton + :key="index" + v-tippy="{ + content: element.tooltip, + placement: 'right', + arrow: true, + arrowType: 'round', + animation: 'fade', + }" + :data-testid="`${element.type}-content`" + :icon="element.icon" + :is-draggable="true" + :class-list="{ 'btn-content-blue': true }" + @dragstart="dragStart($event, element)" + /> + </div> + </template> + </VueDraggable> + <div class="question"> + <!--suppress VueUnrecognizedDirective --> + <ContentButton + v-tippy="{ + content: questionContent.tooltip, + placement: 'right', + arrow: true, + arrowType: 'round', + animation: 'fade', + }" + data-testid="questions-menu" + :icon="questionContent.icon" + :is-draggable="false" + :class-list="classList(questionContent)" + :is-active="editorStore.questionMenu" + @mouseup.stop + @click="showQuestionsMenu" + /> + <div v-if="editorStore.questionMenu" data-testid="floating-menu" class="floating-menu" @click.stop> + <div class="arrow-wrapper"> + <div class="arrow"></div> + </div> + <VueDraggable + v-bind="dragOptions" + key="draggable" + :model-value="editorStore.questions" + item-key="id" + class="questions-list" + > + <!--suppress VueUnrecognizedSlot --> + <template #item="{ element, index }"> + <div> + <!--suppress VueUnrecognizedDirective --> + <ContentButton + :key="index" + v-tippy="{ + content: element.label, + placement: 'right', + arrow: true, + arrowType: 'round', + animation: 'fade', + }" + :data-testid="`${element.type}-content`" + :icon="element.icon" + :class-list="{ 'btn-content-blue': true }" + :is-draggable="true" + @dragstart="dragStart($event, element)" + /> + </div> + </template> + </VueDraggable> </div> - </template> - </VueDraggable> - <div class="question"> - <!--suppress VueUnrecognizedDirective --> - <ContentButton - v-tippy="{ - content: questionContent.tooltip, - placement: 'right', - arrow: true, - arrowType: 'round', - animation: 'fade', - }" - data-testid="questions-menu" - :icon="questionContent.icon" - :is-draggable="false" - :class-list="classList(questionContent)" - :is-active="editorStore.questionMenu" - @mouseup.stop - @click="showQuestionsMenu" - /> - <div v-if="editorStore.questionMenu" data-testid="floating-menu" class="floating-menu" @click.stop> - <div class="arrow-wrapper"> - <div class="arrow"></div> - </div> - <VueDraggable - v-bind="dragOptions" - key="draggable" - :model-value="editorStore.questions" - item-key="id" - class="questions-list" - > - <!--suppress VueUnrecognizedSlot --> - <template #item="{ element, index }"> - <div> - <!--suppress VueUnrecognizedDirective --> - <ContentButton - :key="index" - v-tippy="{ - content: element.label, - placement: 'right', - arrow: true, - arrowType: 'round', - animation: 'fade', - }" - :data-testid="`${element.type}-content`" - :icon="element.icon" - :class-list="{ 'btn-content-blue': true }" - :is-draggable="true" - @dragstart="dragStart($event, element)" - /> - </div> - </template> - </VueDraggable> </div> - </div> - <div v-if="env.isDev"> + <div v-if="env.isDev"> + <!--suppress VueUnrecognizedDirective --> + <ContentButton + v-tippy="{ + content: conditionContent.tooltip, + placement: 'right', + arrow: true, + arrowType: 'round', + animation: 'fade', + }" + :icon="conditionContent.icon" + :class-list="{ 'btn-content-blue': true }" + :is-draggable="true" + @dragstart="dragStart($event, conditionContent)" + @dragend="dragging = false" + /> + <hr /> + <!--suppress VueUnrecognizedDirective --> + <ContentButton + v-tippy="{ + content: modelContent.tooltip, + placement: 'right', + arrow: true, + arrowType: 'round', + animation: 'fade', + }" + class="model-menu" + :icon="modelContent.icon" + :is-draggable="false" + :is-active="editorStore.modelMenu" + :class-list="classList(modelContent)" + @click="showTemplateMenu" + /> + </div> + <!--suppress VueUnrecognizedDirective --> <ContentButton v-tippy="{ - content: conditionContent.tooltip, + content: badgeContent.tooltip, placement: 'right', arrow: true, arrowType: 'round', animation: 'fade', }" - :icon="conditionContent.icon" - :class-list="{ 'btn-content-blue': true }" + class="badge-menu" + :icon="badgeContent.icon" :is-draggable="true" - @dragstart="dragStart($event, conditionContent)" - @dragend="dragging = false" - /> - <hr /> - <!--suppress VueUnrecognizedDirective --> - <ContentButton - v-tippy="{ - content: modelContent.tooltip, - placement: 'right', - arrow: true, - arrowType: 'round', - animation: 'fade', - }" - :icon="modelContent.icon" - :is-draggable="false" - :is-active="editorStore.modelMenu" - :class-list="classList(modelContent)" - @click="showTemplateMenu" + :class-list="classList(badgeContent)" + @click="showBadgeMenu" /> </div> </template> @@ -190,13 +215,16 @@ hr { transition: all 0.15s ease-in-out; } -.questions-list, -.contents-list { +.questions-list, .actions-list, .contents-list { display: flex; flex-direction: column; gap: 1rem; } +.actions-list { + height: 100%; +} + .question { position: relative; } @@ -233,4 +261,9 @@ hr { .container { position: relative; } + +.model-menu { + flex: 1; +} + </style> diff --git a/src/shared/data/forms/nodeForm.data.ts b/src/shared/data/forms/nodeForm.data.ts index 66658238e901d4ece8d32602581078b0dc1d5765..c1fdf3c1f43e0a9006fc505d69b60f1f4be9cadf 100644 --- a/src/shared/data/forms/nodeForm.data.ts +++ b/src/shared/data/forms/nodeForm.data.ts @@ -258,17 +258,6 @@ export const epocForm: Form = { }, ], }, - { - name: 'Badges', - inputs: [ - { - id: 'badges', - label: '', - type: 'badge', - value: [], - }, - ], - }, { name: 'Paramètres :', inputs: [ diff --git a/src/shared/data/sideBar.data.ts b/src/shared/data/sideBar.data.ts index 393f3e6d48cf1cc8347b526b25195250e58c66f7..8ec7d3120c3b56923c323cee6b839cba21648c8a 100644 --- a/src/shared/data/sideBar.data.ts +++ b/src/shared/data/sideBar.data.ts @@ -93,16 +93,16 @@ export const standardPages: SideAction[] = [ label: 'Conditions (legacy)', tooltip: 'Glisser/déposer pour ajouter une condition', }, - { - icon: 'icon-badge', - type: 'badge', - label: 'Badge', - tooltip: 'Cliquer pour ouvrir le menu badge', - }, { icon: 'icon-modele', type: 'model', label: 'Modèle', tooltip: 'Cliquer pour ouvrir le menu modèle', }, + { + icon: 'icon-badge', + type: 'badge', + label: 'Badge', + tooltip: 'Cliquer pour ouvrir le menu badge', + }, ]; diff --git a/src/shared/services/graph.service.ts b/src/shared/services/graph.service.ts index 9a12bb7cbd32af1bf3f534599da04d793b2b33e3..6215f983dd2b465813c1df082a52cec66ef5d090 100644 --- a/src/shared/services/graph.service.ts +++ b/src/shared/services/graph.service.ts @@ -449,4 +449,6 @@ export function closeFormPanel() { editorStore.formPanel.form = null; editorStore.openedElementId = null; editorStore.openedNodeId = null; -} \ No newline at end of file + + editorStore.dismissModals(); +} diff --git a/src/shared/stores/editorStore.ts b/src/shared/stores/editorStore.ts index 52b68714a02f7228a175a3c8bd2c793caa5891fb..f5b30eb45d70e54989da4ab85023b752e0b198f3 100644 --- a/src/shared/stores/editorStore.ts +++ b/src/shared/stores/editorStore.ts @@ -36,6 +36,7 @@ interface EditorState { // Panel/ Menu questionMenu: boolean; modelMenu: boolean; + badgeMenu: boolean; formPanel: { form: Form | null; width: number; @@ -84,6 +85,7 @@ export const useEditorStore = defineStore('editor', { // Panel/ Menu questionMenu: false, modelMenu: false, + badgeMenu: false, formPanel: { form: null, width: 0, @@ -139,6 +141,7 @@ export const useEditorStore = defineStore('editor', { dismissModals(): void { this.questionMenu = false; this.modelMenu = false; + this.badgeMenu = false; this.hamburgerMenu = false; }, diff --git a/src/views/EditorPage.vue b/src/views/EditorPage.vue index 6fff0d61f6fd17d96cc54e54da695420271ed353..db0ad768c066a64f2f23c2c90fd277e28ea56fe0 100644 --- a/src/views/EditorPage.vue +++ b/src/views/EditorPage.vue @@ -12,6 +12,8 @@ import { confirmDelete, graphPaste } from '@/src/shared/services/graph'; import { saveState, setupUndo } from '../shared/services/undoRedo.service'; import { setupContextMenu } from '../shared/services/contextMenu.service'; import { computed } from 'vue'; +import ModelMenu from '@/src/features/sideBar/components/ModelMenu.vue'; +import BadgeMenu from '@/src/features/sideBar/components/BadgeMenu.vue'; const editorStore = useEditorStore(); @@ -88,8 +90,6 @@ const editorDisplay = computed(() => (editorStore.selectNodeMode ? 'editor-flex' class="editor-container" @drop="onRemoveCursor" @dragend="onRemoveCursor" - @mouseup="editorStore.dismissModals" - @click="editorStore.dismissModals" > <SideBar v-if="!editorStore.selectNodeMode" class="side-bar" @dragover="onCursorNotAllowed" /> <TopBar v-if="!editorStore.selectNodeMode" class="top-bar" @dragover="onCursorNotAllowed" /> @@ -108,6 +108,9 @@ const editorDisplay = computed(() => (editorStore.selectNodeMode ? 'editor-flex' <ValidationModal v-if="editorStore.validationModal" /> <ConditionModal v-if="editorStore.conditionModal && !editorStore.selectNodeMode" /> <IconModal v-if="editorStore.iconModal" /> + + <ModelMenu v-if="editorStore.modelMenu" /> + <BadgeMenu v-if="editorStore.badgeMenu" /> </div> </template>