Browse Source

feat(admin): migrate theme + api + utilities to vue 3 composition

Nicolas Giard 3 years ago
parent
commit
8fb29cfc21

+ 14 - 13
ux/package.json

@@ -12,26 +12,26 @@
   },
   "dependencies": {
     "@apollo/client": "3.6.8",
-    "@codemirror/autocomplete": "0.20.3",
+    "@codemirror/autocomplete": "6.0.2",
     "@codemirror/basic-setup": "0.20.0",
     "@codemirror/closebrackets": "0.19.2",
-    "@codemirror/commands": "0.20.0",
+    "@codemirror/commands": "6.0.0",
     "@codemirror/comment": "0.19.1",
     "@codemirror/fold": "0.19.4",
     "@codemirror/gutter": "0.19.9",
     "@codemirror/highlight": "0.19.8",
     "@codemirror/history": "0.19.2",
-    "@codemirror/lang-css": "0.20.0",
-    "@codemirror/lang-html": "0.20.0",
-    "@codemirror/lang-javascript": "0.20.1",
-    "@codemirror/lang-json": "0.20.0",
-    "@codemirror/lang-markdown": "0.20.1",
+    "@codemirror/lang-css": "6.0.0",
+    "@codemirror/lang-html": "6.0.0",
+    "@codemirror/lang-javascript": "6.0.0",
+    "@codemirror/lang-json": "6.0.0",
+    "@codemirror/lang-markdown": "6.0.0",
     "@codemirror/matchbrackets": "0.19.4",
-    "@codemirror/search": "0.20.1",
-    "@codemirror/state": "0.20.1",
+    "@codemirror/search": "6.0.0",
+    "@codemirror/state": "6.0.1",
     "@codemirror/tooltip": "0.19.16",
-    "@codemirror/view": "0.20.7",
-    "@lezer/common": "0.16.1",
+    "@codemirror/view": "6.0.1",
+    "@lezer/common": "1.0.0",
     "@quasar/extras": "1.14.0",
     "@tiptap/core": "2.0.0-beta.176",
     "@tiptap/extension-code-block": "2.0.0-beta.37",
@@ -61,6 +61,7 @@
     "apollo-upload-client": "17.0.0",
     "browser-fs-access": "0.29.6",
     "clipboard": "2.0.11",
+    "codemirror": "6.0.0",
     "filesize": "9.0.9",
     "filesize-parser": "1.5.0",
     "graphql": "16.5.0",
@@ -71,12 +72,12 @@
     "luxon": "2.4.0",
     "pinia": "2.0.14",
     "pug": "3.0.2",
-    "quasar": "2.7.2",
+    "quasar": "2.7.3",
     "tippy.js": "6.3.7",
     "uuid": "8.3.2",
     "v-network-graph": "0.5.19",
     "vue": "3.2.37",
-    "vue-codemirror": "5.1.0",
+    "vue-codemirror": "6.0.0",
     "vue-i18n": "9.1.10",
     "vue-router": "4.0.16",
     "vuedraggable": "4.1.0",

+ 0 - 1
ux/src/components/UserCreateDialog.vue

@@ -178,7 +178,6 @@ defineEmits([
 
 const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
 const $q = useQuasar()
-defineExpose({ $q })
 
 // I18N
 

+ 0 - 1
ux/src/components/UserEditOverlay.vue

@@ -510,7 +510,6 @@ import UtilCodeEditor from './UtilCodeEditor.vue'
 // QUASAR
 
 const $q = useQuasar()
-defineExpose({ $q })
 
 // STORES
 

+ 0 - 1
ux/src/components/UtilCodeEditor.vue

@@ -12,7 +12,6 @@ import { EditorState } from '@codemirror/state'
 import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands'
 import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language'
 import { ref, shallowRef, onBeforeMount, onMounted, watch } from 'vue'
-import { json } from '@codemirror/lang-json'
 
 // PROPS
 

+ 0 - 1
ux/src/components/WebhookEditDialog.vue

@@ -218,7 +218,6 @@ defineEmits([
 
 const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
 const $q = useQuasar()
-defineExpose({ $q })
 
 // I18N
 

+ 0 - 1
ux/src/layouts/AdminLayout.vue

@@ -203,7 +203,6 @@ const overlays = {
 // QUASAR
 
 const $q = useQuasar()
-defineExpose({ $q })
 
 // STORES
 

+ 155 - 154
ux/src/pages/AdminApi.vue

@@ -4,16 +4,16 @@ q-page.admin-api
     .col-auto
       img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-rest-api.svg')
     .col.q-pl-md
-      .text-h5.text-primary.animated.fadeInLeft {{ $t('admin.api.title') }}
-      .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ $t('admin.api.subtitle') }}
+      .text-h5.text-primary.animated.fadeInLeft {{ t('admin.api.title') }}
+      .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.api.subtitle') }}
     .col
       .flex.items-center
-        template(v-if='enabled')
+        template(v-if='state.enabled')
           q-spinner-rings.q-mr-sm(color='green')
-          .text-caption.text-green {{$t('admin.api.enabled')}}
+          .text-caption.text-green {{t('admin.api.enabled')}}
         template(v-else)
           q-spinner-rings.q-mr-sm(color='red', size='md')
-          .text-caption.text-red {{$t('admin.api.disabled')}}
+          .text-caption.text-red {{t('admin.api.disabled')}}
     .col-auto
       q-btn.q-mr-sm.q-ml-md.acrylic-btn(
         icon='las la-question-circle'
@@ -27,24 +27,24 @@ q-page.admin-api
         icon='las la-redo-alt'
         flat
         color='secondary'
-        :loading='loading > 0'
+        :loading='state.loading > 0'
         @click='load'
         )
       q-btn.q-mr-sm(
         unelevated
         icon='las la-power-off'
-        :label='!enabled ? $t(`admin.api.enableButton`) : $t(`admin.api.disableButton`)'
-        :color='!enabled ? `positive` : `negative`'
+        :label='!state.enabled ? t(`admin.api.enableButton`) : t(`admin.api.disableButton`)'
+        :color='!state.enabled ? `positive` : `negative`'
         @click='globalSwitch'
-        :disabled='loading > 0'
+        :disabled='state.loading > 0'
       )
       q-btn(
         unelevated
         icon='las la-plus'
-        :label='$t(`admin.api.newKeyButton`)'
+        :label='t(`admin.api.newKeyButton`)'
         color='primary'
         @click='newKey'
-        :disabled='loading > 0'
+        :disabled='state.loading > 0'
       )
   q-separator(inset)
   .row.q-pa-md.q-col-gutter-md
@@ -114,165 +114,166 @@ q-page.admin-api
 //-         v-btn(color='red', dark, @click='revokeConfirm', :loading='revokeLoading') {{$t('admin.api.revoke')}}
 </template>
 
-<script>
-import _get from 'lodash/get'
-import cloneDeep from 'lodash/cloneDeep'
+<script setup>
 import gql from 'graphql-tag'
-import { createMetaMixin } from 'quasar'
-// import { StatusIndicator } from 'vue-status-indicator'
+import cloneDeep from 'lodash/cloneDeep'
+import { useI18n } from 'vue-i18n'
+import { useMeta, useQuasar } from 'quasar'
+import { computed, onMounted, reactive, watch } from 'vue'
+
+// QUASAR
+
+const $q = useQuasar()
+
+// I18N
+
+const { t } = useI18n()
+
+// META
+
+useMeta({
+  title: t('admin.api.title')
+})
+
+// DATA
 
-// import CreateApiKey from './admin-api-create.vue'
+const state = reactive({
+  enabled: false,
+  loading: 0,
+  keys: [],
+  isCreateDialogShown: false,
+  isRevokeConfirmDialogShown: false,
+  revokeLoading: false,
+  current: {}
+})
 
-export default {
-  components: {
-    // StatusIndicator,
-    // CreateApiKey
-  },
-  mixins: [
-    createMetaMixin(function () {
-      return {
-        title: this.$t('admin.api.title')
+// METHODS
+
+async function load () {
+  state.loading++
+  $q.loading.show()
+  const resp = await APOLLO_CLIENT.query({
+    query: gql`
+      query getHooks {
+        apiKeys {
+          id
+          name
+          keyShort
+          expiration
+          isRevoked
+          createdAt
+          updatedAt
+        }
+        apiState
       }
-    })
-  ],
-  data () {
-    return {
-      enabled: false,
-      loading: 0,
-      keys: [],
-      isCreateDialogShown: false,
-      isRevokeConfirmDialogShown: false,
-      revokeLoading: false,
-      current: {}
-    }
-  },
-  mounted () {
-    this.load()
-  },
-  methods: {
-    async load () {
-      this.loading++
-      this.$q.loading.show()
-      const resp = await this.$apollo.query({
-        query: gql`
-          query getHooks {
-            apiKeys {
-              id
-              name
-              keyShort
-              expiration
-              isRevoked
-              createdAt
-              updatedAt
-            }
-            apiState
-          }
-        `,
-        fetchPolicy: 'network-only'
-      })
-      this.keys = cloneDeep(resp?.data?.apiKeys) ?? []
-      this.enabled = resp?.data?.apiState === true
-      this.$q.loading.hide()
-      this.loading--
-    },
-    async globalSwitch () {
-      this.isToggleLoading = true
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation ($enabled: Boolean!) {
-              authentication {
-                setApiState (enabled: $enabled) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
+    `,
+    fetchPolicy: 'network-only'
+  })
+  state.keys = cloneDeep(resp?.data?.apiKeys) ?? []
+  state.enabled = resp?.data?.apiState === true
+  $q.loading.hide()
+  state.loading--
+}
+
+async function globalSwitch () {
+  state.isToggleLoading = true
+  try {
+    const resp = await APOLLO_CLIENT.mutate({
+      mutation: gql`
+        mutation ($enabled: Boolean!) {
+          authentication {
+            setApiState (enabled: $enabled) {
+              responseResult {
+                succeeded
+                errorCode
+                slug
+                message
               }
             }
-          `,
-          variables: {
-            enabled: !this.enabled
-          },
-          watchLoading (isLoading) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-toggle')
           }
-        })
-        if (_get(resp, 'data.authentication.setApiState.responseResult.succeeded', false)) {
-          this.$store.commit('showNotification', {
-            style: 'success',
-            message: this.enabled ? this.$t('admin.api.toggleStateDisabledSuccess') : this.$t('admin.api.toggleStateEnabledSuccess'),
-            icon: 'check'
-          })
-          await this.load()
-        } else {
-          this.$store.commit('showNotification', {
-            style: 'red',
-            message: _get(resp, 'data.authentication.setApiState.responseResult.message', 'An unexpected error occurred.'),
-            icon: 'alert'
-          })
         }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
+      `,
+      variables: {
+        enabled: !state.enabled
       }
-      this.isToggleLoading = false
-    },
-    async newKey () {
-      this.isCreateDialogShown = true
-    },
-    revoke (key) {
-      this.current = key
-      this.isRevokeConfirmDialogShown = true
-    },
-    async revokeConfirm () {
-      this.revokeLoading = true
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation ($id: Int!) {
-              authentication {
-                revokeApiKey (id: $id) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
+    })
+    if (resp?.data?.setApiState?.operation?.succeeded) {
+      $q.notify({
+        type: 'positive',
+        message: state.enabled ? t('admin.api.toggleStateDisabledSuccess') : t('admin.api.toggleStateEnabledSuccess')
+      })
+      await load()
+    } else {
+      throw new Error(resp?.data?.setApiState?.operation.message || 'An unexpected error occurred.')
+    }
+  } catch (err) {
+    $q.notify({
+      type: 'negative',
+      message: 'Failed to switch API state.',
+      caption: err.message
+    })
+  }
+  state.isToggleLoading = false
+}
+
+async function newKey () {
+  state.isCreateDialogShown = true
+}
+
+function revoke (key) {
+  state.current = key
+  state.isRevokeConfirmDialogShown = true
+}
+
+async function revokeConfirm () {
+  state.revokeLoading = true
+  try {
+    const resp = await APOLLO_CLIENT.mutate({
+      mutation: gql`
+        mutation ($id: Int!) {
+          authentication {
+            revokeApiKey (id: $id) {
+              responseResult {
+                succeeded
+                errorCode
+                slug
+                message
               }
             }
-          `,
-          variables: {
-            id: this.current.id
-          },
-          watchLoading (isLoading) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-revoke')
           }
-        })
-        if (_get(resp, 'data.authentication.revokeApiKey.responseResult.succeeded', false)) {
-          this.$store.commit('showNotification', {
-            style: 'success',
-            message: this.$t('admin.api.revokeSuccess'),
-            icon: 'check'
-          })
-          this.load()
-        } else {
-          this.$store.commit('showNotification', {
-            style: 'red',
-            message: _get(resp, 'data.authentication.revokeApiKey.responseResult.message', 'An unexpected error occurred.'),
-            icon: 'alert'
-          })
         }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
+      `,
+      variables: {
+        id: state.current.id
       }
-      this.isRevokeConfirmDialogShown = false
-      this.revokeLoading = false
-    }
+    })
+    // if (_get(resp, 'data.authentication.revokeApiKey.responseResult.succeeded', false)) {
+    //   this.$store.commit('showNotification', {
+    //     style: 'success',
+    //     message: this.$t('admin.api.revokeSuccess'),
+    //     icon: 'check'
+    //   })
+    //   this.load()
+    // } else {
+    //   this.$store.commit('showNotification', {
+    //     style: 'red',
+    //     message: _get(resp, 'data.authentication.revokeApiKey.responseResult.message', 'An unexpected error occurred.'),
+    //     icon: 'alert'
+    //   })
+    // }
+  } catch (err) {
+    // this.$store.commit('pushGraphError', err)
   }
+  state.isRevokeConfirmDialogShown = false
+  state.revokeLoading = false
 }
+
+// MOUNTED
+
+onMounted(() => {
+  load()
+})
+
 </script>
 
 <style lang='scss'>

+ 237 - 220
ux/src/pages/AdminTheme.vue

@@ -4,8 +4,8 @@ q-page.admin-theme
     .col-auto
       img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-paint-roller.svg')
     .col.q-pl-md
-      .text-h5.text-primary.animated.fadeInLeft {{ $t('admin.theme.title') }}
-      .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ $t('admin.theme.subtitle') }}
+      .text-h5.text-primary.animated.fadeInLeft {{ t('admin.theme.title') }}
+      .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.theme.subtitle') }}
     .col-auto
       q-btn.q-mr-sm.acrylic-btn(
         icon='las la-question-circle'
@@ -19,16 +19,16 @@ q-page.admin-theme
         icon='las la-redo-alt'
         flat
         color='secondary'
-        :loading='loading > 0'
+        :loading='state.loading > 0'
         @click='load'
         )
       q-btn(
         unelevated
-        icon='mdi-check'
-        :label='$t(`common.actions.apply`)'
+        icon='fa-solid fa-check'
+        :label='t(`common.actions.apply`)'
         color='secondary'
         @click='save'
-        :loading='loading > 0'
+        :loading='state.loading > 0'
       )
   q-separator(inset)
   .row.q-pa-md.q-col-gutter-md
@@ -38,11 +38,11 @@ q-page.admin-theme
       //- -----------------------
       q-card.shadow-1.q-pb-sm
         q-card-section.flex.items-center
-          .text-subtitle1 {{$t('admin.theme.options')}}
+          .text-subtitle1 {{t('admin.theme.options')}}
           q-space
           q-btn.acrylic-btn(
             icon='las la-redo-alt'
-            :label='$t(`admin.theme.resetDefaults`)'
+            :label='t(`admin.theme.resetDefaults`)'
             flat
             size='sm'
             color='pink'
@@ -51,25 +51,25 @@ q-page.admin-theme
         q-item(tag='label', v-ripple)
           blueprint-icon(icon='light-on')
           q-item-section
-            q-item-label {{$t(`admin.theme.darkMode`)}}
-            q-item-label(caption) {{$t(`admin.theme.darkModeHint`)}}
+            q-item-label {{t(`admin.theme.darkMode`)}}
+            q-item-label(caption) {{t(`admin.theme.darkModeHint`)}}
           q-item-section(avatar)
             q-toggle(
-              v-model='config.dark'
+              v-model='state.config.dark'
               color='primary'
               checked-icon='las la-check'
               unchecked-icon='las la-times'
-              :aria-label='$t(`admin.theme.darkMode`)'
+              :aria-label='t(`admin.theme.darkMode`)'
               )
         template(v-for='(cl, idx) of colorKeys', :key='cl')
           q-separator.q-my-sm(inset)
           q-item
             blueprint-icon(icon='fill-color')
             q-item-section
-              q-item-label {{$t(`admin.theme.` + cl + `Color`)}}
-              q-item-label(caption) {{$t(`admin.theme.` + cl + `ColorHint`)}}
+              q-item-label {{t(`admin.theme.` + cl + `Color`)}}
+              q-item-label(caption) {{t(`admin.theme.` + cl + `ColorHint`)}}
             q-item-section(side)
-              .text-caption.text-grey-6 {{config[`color` + startCase(cl)]}}
+              .text-caption.text-grey-6 {{state.config[`color` + startCase(cl)]}}
             q-item-section(side)
               q-btn.q-mr-sm(
                 :key='`btnpick-` + cl'
@@ -77,14 +77,14 @@ q-page.admin-theme
                 padding='xs md'
                 no-caps
                 size='sm'
-                :style='`background-color: ` + config[`color` + startCase(cl)] + `;`'
+                :style='`background-color: ` + state.config[`color` + startCase(cl)] + `;`'
                 text-color='white'
                 )
                 q-icon(name='las la-fill', size='xs', left)
                 span Pick...
                 q-menu
                   q-color(
-                    v-model='config[`color` + startCase(cl)]'
+                    v-model='state.config[`color` + startCase(cl)]'
                   )
 
       //- -----------------------
@@ -92,69 +92,63 @@ q-page.admin-theme
       //- -----------------------
       q-card.shadow-1.q-pb-sm.q-mt-md
         q-card-section
-          .text-subtitle1 {{$t('admin.theme.layout')}}
+          .text-subtitle1 {{t('admin.theme.layout')}}
         q-item
           blueprint-icon(icon='right-navigation-toolbar')
           q-item-section
-            q-item-label {{$t(`admin.theme.sidebarPosition`)}}
-            q-item-label(caption) {{$t(`admin.theme.sidebarPositionHint`)}}
+            q-item-label {{t(`admin.theme.sidebarPosition`)}}
+            q-item-label(caption) {{t(`admin.theme.sidebarPositionHint`)}}
           q-item-section.col-auto
             q-btn-toggle(
-              v-model='config.sidebarPosition'
+              v-model='state.config.sidebarPosition'
               push
               glossy
               no-caps
               toggle-color='primary'
-              :options=`[
-                { label: 'Left', value: 'left' },
-                { label: 'Right', value: 'right' }
-              ]`
+              :options='rightLeftOptions'
             )
         q-separator.q-my-sm(inset)
         q-item
           blueprint-icon(icon='index')
           q-item-section
-            q-item-label {{$t(`admin.theme.tocPosition`)}}
-            q-item-label(caption) {{$t(`admin.theme.tocPositionHint`)}}
+            q-item-label {{t(`admin.theme.tocPosition`)}}
+            q-item-label(caption) {{t(`admin.theme.tocPositionHint`)}}
           q-item-section.col-auto
             q-btn-toggle(
-              v-model='config.tocPosition'
+              v-model='state.config.tocPosition'
               push
               glossy
               no-caps
               toggle-color='primary'
-              :options=`[
-                { label: 'Left', value: 'left' },
-                { label: 'Right', value: 'right' }
-              ]`
+              :options='rightLeftOptions'
             )
         q-separator.q-my-sm(inset)
         q-item(tag='label', v-ripple)
           blueprint-icon(icon='share')
           q-item-section
-            q-item-label {{$t(`admin.theme.showSharingMenu`)}}
-            q-item-label(caption) {{$t(`admin.theme.showSharingMenuHint`)}}
+            q-item-label {{t(`admin.theme.showSharingMenu`)}}
+            q-item-label(caption) {{t(`admin.theme.showSharingMenuHint`)}}
           q-item-section(avatar)
             q-toggle(
-              v-model='config.showSharingMenu'
+              v-model='state.config.showSharingMenu'
               color='primary'
               checked-icon='las la-check'
               unchecked-icon='las la-times'
-              :aria-label='$t(`admin.theme.showSharingMenu`)'
+              :aria-label='t(`admin.theme.showSharingMenu`)'
               )
         q-separator.q-my-sm(inset)
         q-item(tag='label', v-ripple)
           blueprint-icon(icon='print')
           q-item-section
-            q-item-label {{$t(`admin.theme.showPrintBtn`)}}
-            q-item-label(caption) {{$t(`admin.theme.showPrintBtnHint`)}}
+            q-item-label {{t(`admin.theme.showPrintBtn`)}}
+            q-item-label(caption) {{t(`admin.theme.showPrintBtnHint`)}}
           q-item-section(avatar)
             q-toggle(
-              v-model='config.showPrintBtn'
+              v-model='state.config.showPrintBtn'
               color='primary'
               checked-icon='las la-check'
               unchecked-icon='las la-times'
-              :aria-label='$t(`admin.theme.showPrintBtn`)'
+              :aria-label='t(`admin.theme.showPrintBtn`)'
               )
 
     .col-6
@@ -163,234 +157,257 @@ q-page.admin-theme
       //- -----------------------
       q-card.shadow-1.q-pb-sm
         q-card-section
-          .text-subtitle1 {{$t('admin.theme.codeInjection')}}
+          .text-subtitle1 {{t('admin.theme.codeInjection')}}
         q-item
           blueprint-icon(icon='css')
           q-item-section
-            q-item-label {{$t(`admin.theme.cssOverride`)}}
-            q-item-label(caption) {{$t(`admin.theme.cssOverrideHint`)}}
+            q-item-label {{t(`admin.theme.cssOverride`)}}
+            q-item-label(caption) {{t(`admin.theme.cssOverrideHint`)}}
         q-item
           q-item-section
-            q-no-ssr(:placeholder='$t(`common.loading`)')
+            q-no-ssr(:placeholder='t(`common.loading`)')
               util-code-editor.admin-theme-cm(
                 ref='cmCSS'
-                v-model='config.injectCSS'
+                v-model='state.config.injectCSS'
                 language='css'
               )
         q-separator.q-my-sm(inset)
         q-item
           blueprint-icon(icon='html')
           q-item-section
-            q-item-label {{$t(`admin.theme.headHtmlInjection`)}}
-            q-item-label(caption) {{$t(`admin.theme.headHtmlInjectionHint`)}}
+            q-item-label {{t(`admin.theme.headHtmlInjection`)}}
+            q-item-label(caption) {{t(`admin.theme.headHtmlInjectionHint`)}}
         q-item
           q-item-section
-            q-no-ssr(:placeholder='$t(`common.loading`)')
+            q-no-ssr(:placeholder='t(`common.loading`)')
               util-code-editor.admin-theme-cm(
                 ref='cmHead'
-                v-model='config.injectHead'
+                v-model='state.config.injectHead'
                 language='html'
               )
         q-separator.q-my-sm(inset)
         q-item
           blueprint-icon(icon='html')
           q-item-section
-            q-item-label {{$t(`admin.theme.bodyHtmlInjection`)}}
-            q-item-label(caption) {{$t(`admin.theme.bodyHtmlInjectionHint`)}}
+            q-item-label {{t(`admin.theme.bodyHtmlInjection`)}}
+            q-item-label(caption) {{t(`admin.theme.bodyHtmlInjectionHint`)}}
         q-item
           q-item-section
-            q-no-ssr(:placeholder='$t(`common.loading`)')
+            q-no-ssr(:placeholder='t(`common.loading`)')
               util-code-editor.admin-theme-cm(
                 ref='cmBody'
-                v-model='config.injectBody'
+                v-model='state.config.injectBody'
                 language='html'
               )
 </template>
 
-<script>
+<script setup>
 import gql from 'graphql-tag'
-import { get } from 'vuex-pathify'
-import _get from 'lodash/get'
 import cloneDeep from 'lodash/cloneDeep'
 import startCase from 'lodash/startCase'
-import { createMetaMixin, useQuasar } from 'quasar'
+import { useI18n } from 'vue-i18n'
+import { useMeta, useQuasar } from 'quasar'
+import { onMounted, reactive, watch } from 'vue'
+
+import { useAdminStore } from 'src/stores/admin'
+import { useSiteStore } from 'src/stores/site'
+
 import UtilCodeEditor from '../components/UtilCodeEditor.vue'
 
-export default {
-  components: {
-    UtilCodeEditor
-  },
-  mixins: [
-    createMetaMixin(function () {
-      return {
-        title: this.$t('admin.theme.title')
-      }
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const adminStore = useAdminStore()
+const siteStore = useSiteStore()
+
+// I18N
+
+const { t } = useI18n()
+
+// META
+
+useMeta({
+  title: t('admin.theme.title')
+})
+
+// DATA
+
+const state = reactive({
+  loading: 0,
+  config: {
+    dark: false,
+    injectCSS: '',
+    injectHead: '',
+    injectBody: '',
+    colorPrimary: '#1976D2',
+    colorSecondary: '#02C39A',
+    colorAccent: '#f03a47',
+    colorHeader: '#000',
+    colorSidebar: '#1976D2',
+    sidebarPosition: 'left',
+    tocPosition: 'right',
+    showSharingMenu: true,
+    showPrintBtn: true
+  }
+})
+
+const colorKeys = [
+  'primary',
+  'secondary',
+  'accent',
+  'header',
+  'sidebar'
+]
+
+const rightLeftOptions = [
+  { label: 'Left', value: 'left' },
+  { label: 'Right', value: 'right' }
+]
+
+// WATCHERS
+
+watch(() => adminStore.currentSiteId, (newValue) => {
+  load()
+})
+
+// METHODS
+
+function resetColors () {
+  state.config.dark = false
+  state.config.colorPrimary = '#1976D2'
+  state.config.colorSecondary = '#02C39A'
+  state.config.colorAccent = '#f03a47'
+  state.config.colorHeader = '#000'
+  state.config.colorSidebar = '#1976D2'
+}
+
+async function load () {
+  state.loading++
+  $q.loading.show()
+  try {
+    const resp = await APOLLO_CLIENT.query({
+      query: gql`
+        query fetchThemeConfig (
+          $id: UUID!
+        ) {
+          siteById(
+            id: $id
+          ) {
+            theme {
+              dark
+              colorPrimary
+              colorSecondary
+              colorAccent
+              colorHeader
+              colorSidebar
+              injectCSS
+              injectHead
+              injectBody
+              sidebarPosition
+              tocPosition
+              showSharingMenu
+              showPrintBtn
+            }
+          }
+        }
+      `,
+      variables: {
+        id: adminStore.currentSiteId
+      },
+      fetchPolicy: 'network-only'
     })
-  ],
-  data () {
-    return {
-      qsr: useQuasar(),
-      loading: 0,
-      colorKeys: [
-        'primary',
-        'secondary',
-        'accent',
-        'header',
-        'sidebar'
-      ],
-      config: {
-        dark: false,
-        injectCSS: '',
-        injectHead: '',
-        injectBody: '',
-        colorPrimary: '#1976D2',
-        colorSecondary: '#02C39A',
-        colorAccent: '#f03a47',
-        colorHeader: '#000',
-        colorSidebar: '#1976D2',
-        sidebarPosition: 'left',
-        tocPosition: 'right',
-        showSharingMenu: true,
-        showPrintBtn: true
-      }
-    }
-  },
-  computed: {
-    currentSiteId: get('admin/currentSiteId', false)
-  },
-  watch: {
-    currentSiteId () {
-      this.load()
+    if (!resp?.data?.siteById?.theme) {
+      throw new Error('Failed to fetch theme config.')
     }
-  },
-  mounted () {
-    this.load()
-  },
-  methods: {
-    startCase,
-    resetColors () {
-      this.config.dark = false
-      this.config.colorPrimary = '#1976D2'
-      this.config.colorSecondary = '#02C39A'
-      this.config.colorAccent = '#f03a47'
-      this.config.colorHeader = '#000'
-      this.config.colorSidebar = '#1976D2'
-    },
-    async load () {
-      if (!this.currentSiteId) { return }
+    state.config = cloneDeep(resp.data.siteById.theme)
+  } catch (err) {
+    $q.notify({
+      type: 'negative',
+      message: 'Failed to fetch site theme config'
+    })
+  }
+  $q.loading.hide()
+  state.loading--
+}
 
-      this.loading++
-      try {
-        const resp = await this.$apollo.query({
-          query: gql`
-            query fetchThemeConfig (
-              $id: UUID!
+async function save () {
+  state.loading++
+  try {
+    const patchTheme = {
+      dark: state.config.dark,
+      colorPrimary: state.config.colorPrimary,
+      colorSecondary: state.config.colorSecondary,
+      colorAccent: state.config.colorAccent,
+      colorHeader: state.config.colorHeader,
+      colorSidebar: state.config.colorSidebar,
+      injectCSS: state.config.injectCSS,
+      injectHead: state.config.injectHead,
+      injectBody: state.config.injectBody,
+      sidebarPosition: state.config.sidebarPosition,
+      tocPosition: state.config.tocPosition,
+      showSharingMenu: state.config.showSharingMenu,
+      showPrintBtn: state.config.showPrintBtn
+    }
+    const respRaw = await APOLLO_CLIENT.mutate({
+      mutation: gql`
+        mutation saveTheme (
+          $id: UUID!
+          $patch: SiteUpdateInput!
+          ) {
+          updateSite (
+            id: $id,
+            patch: $patch
             ) {
-              siteById(
-                id: $id
-              ) {
-                theme {
-                  dark
-                  colorPrimary
-                  colorSecondary
-                  colorAccent
-                  colorHeader
-                  colorSidebar
-                  injectCSS
-                  injectHead
-                  injectBody
-                  sidebarPosition
-                  tocPosition
-                  showSharingMenu
-                  showPrintBtn
-                }
-              }
-            }
-          `,
-          variables: {
-            id: this.currentSiteId
-          },
-          fetchPolicy: 'network-only'
-        })
-        if (!resp?.data?.siteById?.theme) {
-          throw new Error('Failed to fetch theme config.')
-        }
-        this.config = cloneDeep(resp.data.siteById.theme)
-      } catch (err) {
-        this.$q.notify({
-          type: 'negative',
-          message: 'Failed to fetch site theme config'
-        })
-      }
-      this.loading--
-    },
-    async save () {
-      this.loading++
-      try {
-        const patchTheme = {
-          dark: this.config.dark,
-          colorPrimary: this.config.colorPrimary,
-          colorSecondary: this.config.colorSecondary,
-          colorAccent: this.config.colorAccent,
-          colorHeader: this.config.colorHeader,
-          colorSidebar: this.config.colorSidebar,
-          injectCSS: this.config.injectCSS,
-          injectHead: this.config.injectHead,
-          injectBody: this.config.injectBody,
-          sidebarPosition: this.config.sidebarPosition,
-          tocPosition: this.config.tocPosition,
-          showSharingMenu: this.config.showSharingMenu,
-          showPrintBtn: this.config.showPrintBtn
-        }
-        const respRaw = await this.$apollo.mutate({
-          mutation: gql`
-            mutation saveTheme (
-              $id: UUID!
-              $patch: SiteUpdateInput!
-              ) {
-              updateSite (
-                id: $id,
-                patch: $patch
-                ) {
-                status {
-                  succeeded
-                  slug
-                  message
-                }
-              }
+            operation {
+              succeeded
+              slug
+              message
             }
-          `,
-          variables: {
-            id: this.currentSiteId,
-            patch: {
-              theme: patchTheme
-            }
-          }
-        })
-        const resp = _get(respRaw, 'data.updateSite.status', {})
-        if (resp.succeeded) {
-          if (this.currentSiteId === this.$store.get('site/id')) {
-            this.$store.set('site/theme', patchTheme)
-            this.$q.dark.set(this.config.dark)
           }
-          this.$q.notify({
-            type: 'positive',
-            message: this.$t('admin.theme.saveSuccess')
-          })
-        } else {
-          throw new Error(resp.message)
         }
-      } catch (err) {
-        this.$q.notify({
-          type: 'negative',
-          message: 'Failed to save site theme config',
-          caption: err.message
+      `,
+      variables: {
+        id: adminStore.currentSiteId,
+        patch: {
+          theme: patchTheme
+        }
+      }
+    })
+    if (respRaw?.data?.updateSite?.operation?.succeeded) {
+      if (adminStore.currentSiteId === siteStore.id) {
+        siteStore.$patch({
+          theme: patchTheme
         })
+        $q.dark.set(state.config.dark)
       }
-      this.loading--
+      $q.notify({
+        type: 'positive',
+        message: t('admin.theme.saveSuccess')
+      })
+    } else {
+      throw new Error(respRaw?.data?.updateSite?.operation?.message || 'An unexpected error occured.')
     }
+  } catch (err) {
+    $q.notify({
+      type: 'negative',
+      message: 'Failed to save site theme config',
+      caption: err.message
+    })
   }
+  state.loading--
 }
+
+// MOUNTED
+
+onMounted(() => {
+  if (adminStore.currentSiteId) {
+    load()
+  }
+})
+
 </script>
 
 <style lang='scss'>

+ 46 - 27
ux/src/pages/AdminUtilities.vue

@@ -4,14 +4,14 @@ q-page.admin-utilities
     .col-auto
       img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-swiss-army-knife.svg')
     .col.q-pl-md
-      .text-h5.text-primary.animated.fadeInLeft {{ $t('admin.utilities.title') }}
-      .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ $t('admin.utilities.subtitle') }}
+      .text-h5.text-primary.animated.fadeInLeft {{ t('admin.utilities.title') }}
+      .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.utilities.subtitle') }}
     .col-auto
       q-btn.q-mr-sm.acrylic-btn(
         icon='las la-question-circle'
         flat
         color='grey'
-        href='https://docs.requarks.io/admin/utilities'
+        href='https://docs.js.wiki/admin/utilities'
         target='_blank'
         type='a'
         )
@@ -22,38 +22,31 @@ q-page.admin-utilities
         q-item
           blueprint-icon(icon='matches', :hue-rotate='45')
           q-item-section
-            q-item-label {{$t(`admin.utilities.invalidAuthCertificates`)}}
-            q-item-label(caption) {{$t(`admin.utilities.invalidAuthCertificatesHint`)}}
+            q-item-label {{t(`admin.utilities.invalidAuthCertificates`)}}
+            q-item-label(caption) {{t(`admin.utilities.invalidAuthCertificatesHint`)}}
           q-item-section(side)
             q-btn.acrylic-btn(
               flat
               icon='las la-arrow-circle-right'
               color='primary'
               @click=''
-              :label='$t(`common.actions.proceed`)'
+              :label='t(`common.actions.proceed`)'
             )
         q-item
           blueprint-icon(icon='historical', :hue-rotate='45')
           q-item-section
-            q-item-label {{$t(`admin.utilities.purgeHistory`)}}
-            q-item-label(caption) {{$t(`admin.utilities.purgeHistoryHint`)}}
+            q-item-label {{t(`admin.utilities.purgeHistory`)}}
+            q-item-label(caption) {{t(`admin.utilities.purgeHistoryHint`)}}
           q-item-section(side)
             q-select(
               outlined
-              :label='$t(`admin.utilities.purgeHistoryTimeframe`)'
-              v-model='purgeHistoryTimeframe'
+              :label='t(`admin.utilities.purgeHistoryTimeframe`)'
+              v-model='state.purgeHistoryTimeframe'
               style='min-width: 175px;'
               emit-value
               map-options
               dense
-              :options=`[
-                { value: '24h', label: $t('admin.utitilies.purgeHistoryToday') },
-                { value: '1m', label: $tc('admin.utitilies.purgeHistoryMonth', 1, { count: 1 }) },
-                { value: '3m', label: $tc('admin.utitilies.purgeHistoryMonth', 3, { count: 3 }) },
-                { value: '6m', label: $tc('admin.utitilies.purgeHistoryMonth', 6, { count: 6 }) },
-                { value: '1y', label: $tc('admin.utitilies.purgeHistoryYear', 1, { count: 1 }) },
-                { value: '2y', label: $tc('admin.utitilies.purgeHistoryYear', 2, { count: 2 }) }
-              ]`
+              :options='purgeHistoryTimeframes'
             )
           q-separator.q-ml-sm(vertical)
           q-item-section(side)
@@ -62,20 +55,46 @@ q-page.admin-utilities
               icon='las la-arrow-circle-right'
               color='primary'
               @click=''
-              :label='$t(`common.actions.proceed`)'
+              :label='t(`common.actions.proceed`)'
             )
 
 </template>
 
-<script>
+<script setup>
+import { computed, reactive } from 'vue'
+import { useMeta, useQuasar } from 'quasar'
+import { useI18n } from 'vue-i18n'
 
-export default {
-  data () {
-    return {
-      purgeHistoryTimeframe: '1y'
-    }
-  }
-}
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES / ROUTERS / i18n
+
+const { t } = useI18n()
+
+// META
+
+useMeta({
+  title: t('admin.utilities.title')
+})
+
+// DATA
+
+const state = reactive({
+  purgeHistoryTimeframe: '1y'
+})
+
+// COMPUTED
+
+const purgeHistoryTimeframes = computed(() => ([
+  { value: '24h', label: t('admin.utitilies.purgeHistoryToday') },
+  { value: '1m', label: t('admin.utitilies.purgeHistoryMonth', 1, { count: 1 }) },
+  { value: '3m', label: t('admin.utitilies.purgeHistoryMonth', 3, { count: 3 }) },
+  { value: '6m', label: t('admin.utitilies.purgeHistoryMonth', 6, { count: 6 }) },
+  { value: '1y', label: t('admin.utitilies.purgeHistoryYear', 1, { count: 1 }) },
+  { value: '2y', label: t('admin.utitilies.purgeHistoryYear', 2, { count: 2 }) }
+]))
 </script>
 
 <style lang='scss'>

+ 4 - 4
ux/src/router/routes.js

@@ -36,20 +36,20 @@ const routes = [
       { path: ':siteid/locale', component: () => import('../pages/AdminLocale.vue') },
       { path: ':siteid/login', component: () => import('../pages/AdminLogin.vue') },
       // { path: ':siteid/navigation', component: () => import('../pages/AdminNavigation.vue') },
-      { path: ':siteid/storage/:id?', component: () => import('../pages/AdminStorage.vue') },
       // { path: ':siteid/rendering', component: () => import('../pages/AdminRendering.vue') },
-      // { path: ':siteid/theme', component: () => import('../pages/AdminTheme.vue') },
+      { path: ':siteid/storage/:id?', component: () => import('../pages/AdminStorage.vue') },
+      { path: ':siteid/theme', component: () => import('../pages/AdminTheme.vue') },
       // -> Users
       // { path: 'auth', component: () => import('../pages/AdminAuth.vue') },
       { path: 'groups/:id?/:section?', component: () => import('../pages/AdminGroups.vue') },
       { path: 'users/:id?/:section?', component: () => import('../pages/AdminUsers.vue') },
       // -> System
-      // { path: 'api', component: () => import('../pages/AdminApi.vue') },
+      { path: 'api', component: () => import('../pages/AdminApi.vue') },
       { path: 'extensions', component: () => import('../pages/AdminExtensions.vue') },
       { path: 'mail', component: () => import('../pages/AdminMail.vue') },
       { path: 'security', component: () => import('../pages/AdminSecurity.vue') },
       { path: 'system', component: () => import('../pages/AdminSystem.vue') },
-      // { path: 'utilities', component: () => import('../pages/AdminUtilities.vue') },
+      { path: 'utilities', component: () => import('../pages/AdminUtilities.vue') },
       { path: 'webhooks', component: () => import('../pages/AdminWebhooks.vue') },
       { path: 'flags', component: () => import('../pages/AdminFlags.vue') }
     ]

+ 139 - 189
ux/yarn.lock

@@ -73,31 +73,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@codemirror/autocomplete@npm:0.20.3":
-  version: 0.20.3
-  resolution: "@codemirror/autocomplete@npm:0.20.3"
-  dependencies:
-    "@codemirror/language": ^0.20.0
-    "@codemirror/state": ^0.20.0
-    "@codemirror/view": ^0.20.0
-    "@lezer/common": ^0.16.0
-  checksum: 7dfc6b98f343382845b4bf074a7a449fbdd4d204309f4a492209395ac560c92aa6cd24824735606263de1efc25ea6a67c2cd5a46a56a4ff4d4351e1e3c953bf6
-  languageName: node
-  linkType: hard
-
-"@codemirror/autocomplete@npm:^0.20.0":
-  version: 0.20.0
-  resolution: "@codemirror/autocomplete@npm:0.20.0"
-  dependencies:
-    "@codemirror/language": ^0.20.0
-    "@codemirror/state": ^0.20.0
-    "@codemirror/view": ^0.20.0
-    "@lezer/common": ^0.16.0
-  checksum: aeb8fd255c110650853789851ae59270c1aad5ccb2228a62a76575c2220957cccf11e6004e17cc7466a714a2c1fc44d529cff146b381e719af3a0f78c67a5297
-  languageName: node
-  linkType: hard
-
-"@codemirror/autocomplete@npm:^6.0.0":
+"@codemirror/autocomplete@npm:6.0.2, @codemirror/autocomplete@npm:^6.0.0":
   version: 6.0.2
   resolution: "@codemirror/autocomplete@npm:6.0.2"
   dependencies:
@@ -114,6 +90,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@codemirror/autocomplete@npm:^0.20.0":
+  version: 0.20.0
+  resolution: "@codemirror/autocomplete@npm:0.20.0"
+  dependencies:
+    "@codemirror/language": ^0.20.0
+    "@codemirror/state": ^0.20.0
+    "@codemirror/view": ^0.20.0
+    "@lezer/common": ^0.16.0
+  checksum: aeb8fd255c110650853789851ae59270c1aad5ccb2228a62a76575c2220957cccf11e6004e17cc7466a714a2c1fc44d529cff146b381e719af3a0f78c67a5297
+  languageName: node
+  linkType: hard
+
 "@codemirror/basic-setup@npm:0.20.0":
   version: 0.20.0
   resolution: "@codemirror/basic-setup@npm:0.20.0"
@@ -142,19 +130,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@codemirror/commands@npm:0.20.0, @codemirror/commands@npm:^0.20.0":
-  version: 0.20.0
-  resolution: "@codemirror/commands@npm:0.20.0"
-  dependencies:
-    "@codemirror/language": ^0.20.0
-    "@codemirror/state": ^0.20.0
-    "@codemirror/view": ^0.20.0
-    "@lezer/common": ^0.16.0
-  checksum: 4538de7200f9ac4c8482fd99bee8d49ae983ca2be3b81d5f1f3e3cf7d7821f64f3ddf396213dd56b2fd9bd46f6d72467846dad0e4c42a3b9c8ba8243522e0bc6
-  languageName: node
-  linkType: hard
-
-"@codemirror/commands@npm:6.x, @codemirror/commands@npm:^6.0.0":
+"@codemirror/commands@npm:6.0.0, @codemirror/commands@npm:6.x, @codemirror/commands@npm:^6.0.0":
   version: 6.0.0
   resolution: "@codemirror/commands@npm:6.0.0"
   dependencies:
@@ -166,6 +142,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@codemirror/commands@npm:^0.20.0":
+  version: 0.20.0
+  resolution: "@codemirror/commands@npm:0.20.0"
+  dependencies:
+    "@codemirror/language": ^0.20.0
+    "@codemirror/state": ^0.20.0
+    "@codemirror/view": ^0.20.0
+    "@lezer/common": ^0.16.0
+  checksum: 4538de7200f9ac4c8482fd99bee8d49ae983ca2be3b81d5f1f3e3cf7d7821f64f3ddf396213dd56b2fd9bd46f6d72467846dad0e4c42a3b9c8ba8243522e0bc6
+  languageName: node
+  linkType: hard
+
 "@codemirror/comment@npm:0.19.1":
   version: 0.19.1
   resolution: "@codemirror/comment@npm:0.19.1"
@@ -225,83 +213,69 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@codemirror/lang-css@npm:0.20.0, @codemirror/lang-css@npm:^0.20.0":
-  version: 0.20.0
-  resolution: "@codemirror/lang-css@npm:0.20.0"
-  dependencies:
-    "@codemirror/autocomplete": ^0.20.0
-    "@codemirror/language": ^0.20.0
-    "@codemirror/state": ^0.20.0
-    "@lezer/css": ^0.16.0
-  checksum: a922c76fe51a13d5af6e019fe36cd344d9d855aeb8a6ee8b01d2c757d015b3615907984c99dfa1866a81ef1fac465251b959a08e4d15d79b8ed2a4a35f3eed5e
-  languageName: node
-  linkType: hard
-
-"@codemirror/lang-html@npm:0.20.0, @codemirror/lang-html@npm:^0.20.0":
-  version: 0.20.0
-  resolution: "@codemirror/lang-html@npm:0.20.0"
+"@codemirror/lang-css@npm:6.0.0, @codemirror/lang-css@npm:^6.0.0":
+  version: 6.0.0
+  resolution: "@codemirror/lang-css@npm:6.0.0"
   dependencies:
-    "@codemirror/autocomplete": ^0.20.0
-    "@codemirror/lang-css": ^0.20.0
-    "@codemirror/lang-javascript": ^0.20.0
-    "@codemirror/language": ^0.20.0
-    "@codemirror/state": ^0.20.0
-    "@lezer/common": ^0.16.0
-    "@lezer/html": ^0.16.0
-  checksum: ee02952c0409da4040e6f8aa522e69b2f5f7b0c16c704360cf0c7b2ac6010e8aed2a643f6096ccc877f8c3f784d1b5ce7136c92300dc63ddf5e971793aad0944
+    "@codemirror/autocomplete": ^6.0.0
+    "@codemirror/language": ^6.0.0
+    "@codemirror/state": ^6.0.0
+    "@lezer/css": ^1.0.0
+  checksum: 855a040ec2e22cf074fe1ef433488911b6f628878cdaf94c6deaa30df188860bfbf0f629ca339b56ad66549f00c8011df81d570245266c86ad8311367a448f92
   languageName: node
   linkType: hard
 
-"@codemirror/lang-javascript@npm:0.20.1":
-  version: 0.20.1
-  resolution: "@codemirror/lang-javascript@npm:0.20.1"
+"@codemirror/lang-html@npm:6.0.0, @codemirror/lang-html@npm:^6.0.0":
+  version: 6.0.0
+  resolution: "@codemirror/lang-html@npm:6.0.0"
   dependencies:
-    "@codemirror/autocomplete": ^0.20.0
-    "@codemirror/language": ^0.20.0
-    "@codemirror/lint": ^0.20.0
-    "@codemirror/state": ^0.20.0
-    "@codemirror/view": ^0.20.0
-    "@lezer/common": ^0.16.1
-    "@lezer/javascript": ^0.16.0
-  checksum: 169ed50ec2ef0171cc0719c48a971371a12b2a36180d42151f592bae3611f13331f422f67dd0cbbfc6938a5840fe8a96c90278de5606234896345391f15eff46
+    "@codemirror/autocomplete": ^6.0.0
+    "@codemirror/lang-css": ^6.0.0
+    "@codemirror/lang-javascript": ^6.0.0
+    "@codemirror/language": ^6.0.0
+    "@codemirror/state": ^6.0.0
+    "@lezer/common": ^1.0.0
+    "@lezer/html": ^1.0.0
+  checksum: 1cb0aa2ed481d2ef644a23afc8a706f745201c67a22da6f214cdaff1b2c7173e8933dfb9f3b60522b777cdcdd74cb5f88786ef4df61e6a640439eb155cc1c777
   languageName: node
   linkType: hard
 
-"@codemirror/lang-javascript@npm:^0.20.0":
-  version: 0.20.0
-  resolution: "@codemirror/lang-javascript@npm:0.20.0"
+"@codemirror/lang-javascript@npm:6.0.0, @codemirror/lang-javascript@npm:^6.0.0":
+  version: 6.0.0
+  resolution: "@codemirror/lang-javascript@npm:6.0.0"
   dependencies:
-    "@codemirror/autocomplete": ^0.20.0
-    "@codemirror/language": ^0.20.0
-    "@codemirror/lint": ^0.20.0
-    "@codemirror/state": ^0.20.0
-    "@codemirror/view": ^0.20.0
-    "@lezer/javascript": ^0.16.0
-  checksum: 5de37bcdb8c7410d4530663c9038a02f0b318fdf1f32b8b720e46dc65aea9e51d46acce5a219506379dd8fc10b96c42f22b9aca588b5ff1dcdae6ff974d17d51
+    "@codemirror/autocomplete": ^6.0.0
+    "@codemirror/language": ^6.0.0
+    "@codemirror/lint": ^6.0.0
+    "@codemirror/state": ^6.0.0
+    "@codemirror/view": ^6.0.0
+    "@lezer/common": ^1.0.0
+    "@lezer/javascript": ^1.0.0
+  checksum: 6ec2f286c685b8e6556e207fb278637b8918a763344504c8e13c8a00bef06bc05e744858df9c21fbd47cb01bb7a4cbeb7bb5ef6ba2608eb57dfe1b8192d6736e
   languageName: node
   linkType: hard
 
-"@codemirror/lang-json@npm:0.20.0":
-  version: 0.20.0
-  resolution: "@codemirror/lang-json@npm:0.20.0"
+"@codemirror/lang-json@npm:6.0.0":
+  version: 6.0.0
+  resolution: "@codemirror/lang-json@npm:6.0.0"
   dependencies:
-    "@codemirror/language": ^0.20.0
-    "@lezer/json": ^0.16.0
-  checksum: 20bbf0a480852fd11b0904561d4c7fe151f7a5cbe49b36516890ce7d729b4fd51e7c0443fb27463fd25e69e5ed693c3ce5e346ba60669a012f313cf8b06db13e
+    "@codemirror/language": ^6.0.0
+    "@lezer/json": ^1.0.0
+  checksum: 9eb7b1842e407f87683420ac74c06af224142719daf4d8dad254bffc34e826deaee2a318481b3e875e87051acaac8a56e85b4cc2ed7de53bd0297fc630e8452f
   languageName: node
   linkType: hard
 
-"@codemirror/lang-markdown@npm:0.20.1":
-  version: 0.20.1
-  resolution: "@codemirror/lang-markdown@npm:0.20.1"
+"@codemirror/lang-markdown@npm:6.0.0":
+  version: 6.0.0
+  resolution: "@codemirror/lang-markdown@npm:6.0.0"
   dependencies:
-    "@codemirror/lang-html": ^0.20.0
-    "@codemirror/language": ^0.20.0
-    "@codemirror/state": ^0.20.0
-    "@codemirror/view": ^0.20.0
-    "@lezer/common": ^0.16.0
-    "@lezer/markdown": ^0.16.0
-  checksum: cc8ba82f8d5c1e279ad5d64d096c476327b9b83e5f9dda70c63258fcf21d97be3d66ada90d32af7f4fdd6f6bea51bf493e247a5875f0d05b8a34c34b701bcab3
+    "@codemirror/lang-html": ^6.0.0
+    "@codemirror/language": ^6.0.0
+    "@codemirror/state": ^6.0.0
+    "@codemirror/view": ^6.0.0
+    "@lezer/common": ^1.0.0
+    "@lezer/markdown": ^1.0.0
+  checksum: 5cb9ee7053e758745e9376b67d4b22011329f3f74d8642870567d307018a0e823be8d8f59dac2002704f04908e329cb1bd206bcbcfd8111572e0a7e7819cb890
   languageName: node
   linkType: hard
 
@@ -388,18 +362,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@codemirror/search@npm:0.20.1, @codemirror/search@npm:^0.20.0":
-  version: 0.20.1
-  resolution: "@codemirror/search@npm:0.20.1"
-  dependencies:
-    "@codemirror/state": ^0.20.0
-    "@codemirror/view": ^0.20.0
-    crelt: ^1.0.5
-  checksum: eb324d7967183652bc4c636f1b3c7f5bc7266a0d36d1138f03f2aa05585ee17c45666fe4d0e9b8d6e55b39f9338d06b76a333048b5e6ff523400cabad8fbd19f
-  languageName: node
-  linkType: hard
-
-"@codemirror/search@npm:^6.0.0":
+"@codemirror/search@npm:6.0.0, @codemirror/search@npm:^6.0.0":
   version: 6.0.0
   resolution: "@codemirror/search@npm:6.0.0"
   dependencies:
@@ -410,14 +373,18 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@codemirror/state@npm:0.20.1":
+"@codemirror/search@npm:^0.20.0":
   version: 0.20.1
-  resolution: "@codemirror/state@npm:0.20.1"
-  checksum: 9ad924314d2b88eecfdf7aac6da89e9c92154ab2fb2afb8b4de1581bdc24fcafbd3cdf1c435d592042cf4c044815c2306265e39cb22f8041e76f0f7121d7ebd5
+  resolution: "@codemirror/search@npm:0.20.1"
+  dependencies:
+    "@codemirror/state": ^0.20.0
+    "@codemirror/view": ^0.20.0
+    crelt: ^1.0.5
+  checksum: eb324d7967183652bc4c636f1b3c7f5bc7266a0d36d1138f03f2aa05585ee17c45666fe4d0e9b8d6e55b39f9338d06b76a333048b5e6ff523400cabad8fbd19f
   languageName: node
   linkType: hard
 
-"@codemirror/state@npm:6.x, @codemirror/state@npm:^6.0.0":
+"@codemirror/state@npm:6.0.1, @codemirror/state@npm:6.x, @codemirror/state@npm:^6.0.0":
   version: 6.0.1
   resolution: "@codemirror/state@npm:6.0.1"
   checksum: fcb5aa5e1ce455ed1261616beb6cdb9e855f9cf11618a1ef7f61142622a0aba4551605356d381d2fa96f021f932b71323808369bf8edadfa69d745252b7e6ccd
@@ -457,18 +424,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@codemirror/view@npm:0.20.7":
-  version: 0.20.7
-  resolution: "@codemirror/view@npm:0.20.7"
-  dependencies:
-    "@codemirror/state": ^0.20.0
-    style-mod: ^4.0.0
-    w3c-keyname: ^2.2.4
-  checksum: 51799e4e53d0ec0c2cee28e4462abef5542b7dee2551b0fe1c69c1fe3622033243130c04c2b7bc6080561d74950188814660b1bc8eebdb4ce2ea173a9e48f11a
-  languageName: node
-  linkType: hard
-
-"@codemirror/view@npm:6.x, @codemirror/view@npm:^6.0.0":
+"@codemirror/view@npm:6.0.1, @codemirror/view@npm:6.x, @codemirror/view@npm:^6.0.0":
   version: 6.0.1
   resolution: "@codemirror/view@npm:6.0.1"
   dependencies:
@@ -697,10 +653,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@lezer/common@npm:0.16.1, @lezer/common@npm:^0.16.1":
-  version: 0.16.1
-  resolution: "@lezer/common@npm:0.16.1"
-  checksum: ee27598a8c2a4e5bcba285cf091b7c1aee36e5a5e8352b63ce65520e7279f6305e1272d8adede150fc4aee0073c1e3e93fb11271d538815db72bdff341c4be65
+"@lezer/common@npm:1.0.0, @lezer/common@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "@lezer/common@npm:1.0.0"
+  checksum: 0ba652b39f9ff073a6a8a3376a74279f2c2d2ccdd4d2bb57c7b607341dbdbf64baf9c23a196314f09349d175623bc73a6a0b6a0eeb2cc63f3a1190fd631f7c31
   languageName: node
   linkType: hard
 
@@ -718,20 +674,13 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@lezer/common@npm:^1.0.0":
+"@lezer/css@npm:^1.0.0":
   version: 1.0.0
-  resolution: "@lezer/common@npm:1.0.0"
-  checksum: 0ba652b39f9ff073a6a8a3376a74279f2c2d2ccdd4d2bb57c7b607341dbdbf64baf9c23a196314f09349d175623bc73a6a0b6a0eeb2cc63f3a1190fd631f7c31
-  languageName: node
-  linkType: hard
-
-"@lezer/css@npm:^0.16.0":
-  version: 0.16.0
-  resolution: "@lezer/css@npm:0.16.0"
+  resolution: "@lezer/css@npm:1.0.0"
   dependencies:
-    "@lezer/highlight": ^0.16.0
-    "@lezer/lr": ^0.16.0
-  checksum: c88151f2d0f3cbc00e21a9f2f18960298ffe566a321996fa0e2b676b804535c1b20053bc8f81f8510c8523169a00cbbabe808775b5e980c371b64912d19c6dc9
+    "@lezer/highlight": ^1.0.0
+    "@lezer/lr": ^1.0.0
+  checksum: 094b178254c509b3236d0e3a5c21831d5cfe00884328a6990dcb8aa913f5096ad37ed1b4adce9d7cc2d8a1b14bac1f7b8e6455a23249d9a5fbd9ee6c094f75a2
   languageName: node
   linkType: hard
 
@@ -753,33 +702,33 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@lezer/html@npm:^0.16.0":
-  version: 0.16.0
-  resolution: "@lezer/html@npm:0.16.0"
+"@lezer/html@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "@lezer/html@npm:1.0.0"
   dependencies:
-    "@lezer/highlight": ^0.16.0
-    "@lezer/lr": ^0.16.0
-  checksum: 31124c7619cecc79fc43639abdd7229dafa36636d00c95b32f6b6a41e5e04f7dc898e45ba5fca2c456356b920bb11deacc61b07edb0737313a7fa5573754d940
+    "@lezer/highlight": ^1.0.0
+    "@lezer/lr": ^1.0.0
+  checksum: 91e2716ec2850d62238b01c1af4170c1102ecec7c8726fc658182a44de524aa376d2743ee147a8d455d1dde51f0c2255c1993f5a393d6e3aa9e3ac41cc109a2c
   languageName: node
   linkType: hard
 
-"@lezer/javascript@npm:^0.16.0":
-  version: 0.16.0
-  resolution: "@lezer/javascript@npm:0.16.0"
+"@lezer/javascript@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "@lezer/javascript@npm:1.0.0"
   dependencies:
-    "@lezer/highlight": ^0.16.0
-    "@lezer/lr": ^0.16.0
-  checksum: 164c1dcadc47610e588fee1e1c1f1cb478a296373ab3654e04b41dd6feb375f0abe0df6b5d339e70aaa444df573873cf2302d21abe3f372cbaa0bebfd1cf7f5a
+    "@lezer/highlight": ^1.0.0
+    "@lezer/lr": ^1.0.0
+  checksum: f6e104791f124e592f7343e4405224cd4a793b688db378b7e7878ddaa1b78525873fadb895fda10b1959fe83a01b498f4c48571eb827c4510b9e1f87f2327970
   languageName: node
   linkType: hard
 
-"@lezer/json@npm:^0.16.0":
-  version: 0.16.0
-  resolution: "@lezer/json@npm:0.16.0"
+"@lezer/json@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "@lezer/json@npm:1.0.0"
   dependencies:
-    "@lezer/highlight": ^0.16.0
-    "@lezer/lr": ^0.16.0
-  checksum: 3fe14dcd8879c9318dc74e77c3543fff21e2f7b45b344b93e58e67eae48ea0c603c6b602129967d85a8ade1fa88b2c6abe43b3772b57829890fd91b111676f13
+    "@lezer/highlight": ^1.0.0
+    "@lezer/lr": ^1.0.0
+  checksum: c1ca0cdf681415b58a383a669944bed66da3aa830870d32d1e471d545cff0fe43d9ac8a0d2a318a96daa99cd5a645b1d58ba8fbdd2e8d7ca4d33a62c7582cbab
   languageName: node
   linkType: hard
 
@@ -810,13 +759,13 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@lezer/markdown@npm:^0.16.0":
-  version: 0.16.0
-  resolution: "@lezer/markdown@npm:0.16.0"
+"@lezer/markdown@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "@lezer/markdown@npm:1.0.0"
   dependencies:
-    "@lezer/common": ^0.16.0
-    "@lezer/highlight": ^0.16.0
-  checksum: ad60b95cfcbadadd626464227dfa11803d86888721c759ca9f3f9c62d3e587c495d87cabdeb0617c726830aa904dc548e68fa939cfd8a81e4b9877754dc805a0
+    "@lezer/common": ^1.0.0
+    "@lezer/highlight": ^1.0.0
+  checksum: 6d5ff7df69c720d7d25e7b7e527c678a0958948a4c4e004b1c5d737353d21f4b42a8ce7e02c05ac747b51afe8ca16d6a7f58916110c5d3af511d3752e87f0a02
   languageName: node
   linkType: hard
 
@@ -2473,7 +2422,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"codemirror@npm:6.x":
+"codemirror@npm:6.0.0":
   version: 6.0.0
   resolution: "codemirror@npm:6.0.0"
   dependencies:
@@ -5914,10 +5863,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"quasar@npm:2.7.2":
-  version: 2.7.2
-  resolution: "quasar@npm:2.7.2"
-  checksum: 6f1bbed4bf9b5df0067581dd81f1ec751dfed776a6c50394821169825a281d1e9598aa8aa447c48fdae12a9cba8989b1831e72cf8e28128f95cd4c9f2af0c176
+"quasar@npm:2.7.3":
+  version: 2.7.3
+  resolution: "quasar@npm:2.7.3"
+  checksum: 42ae066c7a7e477731e3063bfa6b097bc89a670dac4781dd3e72d3c16e0db61a909f09eea21ef342592953fd6167f80a4c9add20722d4df0229616d3a86f5810
   languageName: node
   linkType: hard
 
@@ -6828,27 +6777,27 @@ __metadata:
   resolution: "ux@workspace:."
   dependencies:
     "@apollo/client": 3.6.8
-    "@codemirror/autocomplete": 0.20.3
+    "@codemirror/autocomplete": 6.0.2
     "@codemirror/basic-setup": 0.20.0
     "@codemirror/closebrackets": 0.19.2
-    "@codemirror/commands": 0.20.0
+    "@codemirror/commands": 6.0.0
     "@codemirror/comment": 0.19.1
     "@codemirror/fold": 0.19.4
     "@codemirror/gutter": 0.19.9
     "@codemirror/highlight": 0.19.8
     "@codemirror/history": 0.19.2
-    "@codemirror/lang-css": 0.20.0
-    "@codemirror/lang-html": 0.20.0
-    "@codemirror/lang-javascript": 0.20.1
-    "@codemirror/lang-json": 0.20.0
-    "@codemirror/lang-markdown": 0.20.1
+    "@codemirror/lang-css": 6.0.0
+    "@codemirror/lang-html": 6.0.0
+    "@codemirror/lang-javascript": 6.0.0
+    "@codemirror/lang-json": 6.0.0
+    "@codemirror/lang-markdown": 6.0.0
     "@codemirror/matchbrackets": 0.19.4
-    "@codemirror/search": 0.20.1
-    "@codemirror/state": 0.20.1
+    "@codemirror/search": 6.0.0
+    "@codemirror/state": 6.0.1
     "@codemirror/tooltip": 0.19.16
-    "@codemirror/view": 0.20.7
+    "@codemirror/view": 6.0.1
     "@intlify/vite-plugin-vue-i18n": 3.4.0
-    "@lezer/common": 0.16.1
+    "@lezer/common": 1.0.0
     "@quasar/app-vite": 1.0.2
     "@quasar/extras": 1.14.0
     "@tiptap/core": 2.0.0-beta.176
@@ -6881,6 +6830,7 @@ __metadata:
     autoprefixer: 10.4.7
     browser-fs-access: 0.29.6
     clipboard: 2.0.11
+    codemirror: 6.0.0
     eslint: 8.18.0
     eslint-config-standard: 17.0.0
     eslint-plugin-import: 2.26.0
@@ -6897,12 +6847,12 @@ __metadata:
     luxon: 2.4.0
     pinia: 2.0.14
     pug: 3.0.2
-    quasar: 2.7.2
+    quasar: 2.7.3
     tippy.js: 6.3.7
     uuid: 8.3.2
     v-network-graph: 0.5.19
     vue: 3.2.37
-    vue-codemirror: 5.1.0
+    vue-codemirror: 6.0.0
     vue-i18n: 9.1.10
     vue-router: 4.0.16
     vuedraggable: 4.1.0
@@ -7002,19 +6952,19 @@ __metadata:
   languageName: node
   linkType: hard
 
-"vue-codemirror@npm:5.1.0":
-  version: 5.1.0
-  resolution: "vue-codemirror@npm:5.1.0"
+"vue-codemirror@npm:6.0.0":
+  version: 6.0.0
+  resolution: "vue-codemirror@npm:6.0.0"
   dependencies:
     "@codemirror/commands": 6.x
     "@codemirror/language": 6.x
     "@codemirror/state": 6.x
     "@codemirror/view": 6.x
-    codemirror: 6.x
     csstype: ^2.6.8
   peerDependencies:
+    codemirror: 6.x
     vue: 3.x
-  checksum: 6eeee0dfee3352cb1dbf9dc822f8f2ebc64413760abc12bcc91fae23ddeed1086585add2d0591a491d1b03c5393361b87db6d1af4a934445da2cd855017777f2
+  checksum: aa762ebfe8b8f9f43c98b76ba822842c79af2fc16be1b2639142d3c85d86f73376c1467d0372bdae1b5e73fd4b76405f23b0266d6e5c72505408677164505e0e
   languageName: node
   linkType: hard