Sfoglia il codice sorgente

feat(admin): migrate locale + login + storage views to vue 3 composable

Nicolas Giard 3 anni fa
parent
commit
36ba1eb192

+ 51 - 13
.devcontainer/Dockerfile

@@ -1,21 +1,58 @@
+# Based of https://github.com/microsoft/vscode-dev-containers/blob/main/containers/javascript-node/.devcontainer/base.Dockerfile
+
 # [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 16, 14, 12, 16-bullseye, 14-bullseye, 12-bullseye, 16-buster, 14-buster, 12-buster
-ARG VARIANT=16-bullseye
-FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
+ARG VARIANT=18-bullseye
+FROM node:${VARIANT}
 
-# [Optional] Uncomment this section to install additional OS packages.
-# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
-#     && apt-get -y install --no-install-recommends <your-package-list-here>
+ENV DEBIAN_FRONTEND=noninteractive
 
-# [Optional] Uncomment if you want to install an additional version of node using nvm
-# ARG EXTRA_NODE_VERSION=10
-# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
+# Copy library scripts to execute
+ADD https://raw.githubusercontent.com/microsoft/vscode-dev-containers/main/containers/javascript-node/.devcontainer/library-scripts/common-debian.sh /tmp/library-scripts/
+ADD https://raw.githubusercontent.com/microsoft/vscode-dev-containers/main/containers/javascript-node/.devcontainer/library-scripts/node-debian.sh /tmp/library-scripts/
+ADD https://raw.githubusercontent.com/microsoft/vscode-dev-containers/main/containers/javascript-node/.devcontainer/library-scripts/meta.env /tmp/library-scripts/
 
-# [Optional] Uncomment if you want to install more global node modules
-# RUN su node -c "npm install -g <your-package-list-here>"
+# [Option] Install zsh
+ARG INSTALL_ZSH="true"
+# [Option] Upgrade OS packages to their latest versions
+ARG UPGRADE_PACKAGES="true"
 
-EXPOSE 3000
+# Install needed packages, yarn, nvm and setup non-root user. Use a separate RUN statement to add your own dependencies.
+ARG USERNAME=node
+ARG USER_UID=1000
+ARG USER_GID=$USER_UID
+ARG NPM_GLOBAL=/usr/local/share/npm-global
+ENV NVM_DIR=/usr/local/share/nvm
+ENV NVM_SYMLINK_CURRENT=true \
+    PATH=${NPM_GLOBAL}/bin:${NVM_DIR}/current/bin:${PATH}
+RUN apt-get update \
+    # Remove imagemagick due to https://security-tracker.debian.org/tracker/CVE-2019-10131
+    && apt-get purge -y imagemagick imagemagick-6-common \
+    # Install common packages, non-root user, update yarn and install nvm
+    && bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true" \
+    # Install yarn, nvm
+    && rm -rf /opt/yarn-* /usr/local/bin/yarn /usr/local/bin/yarnpkg \
+    && bash /tmp/library-scripts/node-debian.sh "${NVM_DIR}" "none" "${USERNAME}" \
+    # Configure global npm install location, use group to adapt to UID/GID changes
+    && if ! cat /etc/group | grep -e "^npm:" > /dev/null 2>&1; then groupadd -r npm; fi \
+    && usermod -a -G npm ${USERNAME} \
+    && umask 0002 \
+    && mkdir -p ${NPM_GLOBAL} \
+    && touch /usr/local/etc/npmrc \
+    && chown ${USERNAME}:npm ${NPM_GLOBAL} /usr/local/etc/npmrc \
+    && chmod g+s ${NPM_GLOBAL} \
+    && npm config -g set prefix ${NPM_GLOBAL} \
+    && sudo -u ${USERNAME} npm config -g set prefix ${NPM_GLOBAL} \
+    # Install eslint
+    && su ${USERNAME} -c "umask 0002 && npm install -g eslint" \
+    && npm cache clean --force > /dev/null 2>&1 \
+    # Install python-is-python3 on bullseye to prevent node-gyp regressions
+    && . /etc/os-release \
+    && if [ "${VERSION_CODENAME}" = "bullseye" ]; then apt-get -y install --no-install-recommends python-is-python3; fi \
+    # Clean up
+    && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* /root/.gnupg /tmp/library-scripts
 
-ENV DEBIAN_FRONTEND=noninteractive
+
+EXPOSE 3000
 
 # Add Docker Source
 RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
@@ -33,6 +70,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive && apt-get install -
     git \
     gnupg2 \
     nano \
+    netcat \
     pandoc \
     unzip \
     wget
@@ -48,7 +86,7 @@ ENV npm_config_fund false
 RUN sed -i 's/#force_color_prompt=/force_color_prompt=/' /root/.bashrc
 
 # Fetch wait-for utility
-ADD https://raw.githubusercontent.com/eficode/wait-for/v2.1.3/wait-for /usr/local/bin/
+ADD https://raw.githubusercontent.com/eficode/wait-for/v2.2.3/wait-for /usr/local/bin/
 RUN chmod +rx /usr/local/bin/wait-for
 
 # Copy the startup file

+ 4 - 0
.devcontainer/app-init.sh

@@ -2,7 +2,11 @@
 
 cd /workspace
 
+echo "Disabling git info in terminal..."
 git config codespaces-theme.hide-status 1
 git config oh-my-zsh.hide-info 1
 
+echo "Waiting for DB container to come online..."
+/usr/local/bin/wait-for localhost:5432 -- echo "DB ready"
+
 echo "Ready!"

+ 2 - 2
.devcontainer/devcontainer.json

@@ -32,10 +32,10 @@
     "arcanis.vscode-zipfs",
 		"dbaeumer.vscode-eslint",
 		"eamodio.gitlens",
-		"johnsoncodehk.volar",
+		"Vue.volar",
 		"oderwat.indent-rainbow",
 		"redhat.vscode-yaml",
-		"visualstudioexptteam.vscodeintellicode",
+		"VisualStudioExptTeam.vscodeintellicode",
     "editorconfig.editorconfig",
     "lokalise.i18n-ally",
     "mrmlnc.vscode-duplicate",

+ 2 - 2
.devcontainer/docker-compose.yml

@@ -6,10 +6,10 @@ services:
       context: .
       dockerfile: Dockerfile
       args:
-        # Update 'VARIANT' to pick an LTS version of Node.js: 16, 14, 12.
+        # Update 'VARIANT' to pick an LTS version of Node.js: 18, 16, 14, 12.
         # Append -bullseye or -buster to pin to an OS version.
         # Use -bullseye variants on local arm64/Apple Silicon.
-        VARIANT: 16-bullseye
+        VARIANT: 18-bullseye
 
     volumes:
       - ..:/workspace

+ 2 - 63
server/core/system.js

@@ -1,6 +1,3 @@
-const _ = require('lodash')
-const cfgHelper = require('../helpers/config')
-const Promise = require('bluebird')
 const fs = require('fs-extra')
 const path = require('path')
 
@@ -11,71 +8,13 @@ module.exports = {
     channel: 'BETA',
     version: WIKI.version,
     releaseDate: WIKI.releaseDate,
-    minimumVersionRequired: '2.0.0-beta.0',
-    minimumNodeRequired: '10.12.0'
+    minimumVersionRequired: '3.0.0-beta.0',
+    minimumNodeRequired: '18.0.0'
   },
   init() {
     // Clear content cache
     fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache'))
 
     return this
-  },
-  /**
-   * Upgrade from WIKI.js 1.x - MongoDB database
-   *
-   * @param {Object} opts Options object
-   */
-  async upgradeFromMongo (opts) {
-    WIKI.logger.info('Upgrading from MongoDB...')
-
-    let mongo = require('mongodb').MongoClient
-    let parsedMongoConStr = cfgHelper.parseConfigValue(opts.mongoCnStr)
-
-    return new Promise((resolve, reject) => {
-      // Connect to MongoDB
-
-      mongo.connect(parsedMongoConStr, {
-        autoReconnect: false,
-        reconnectTries: 2,
-        reconnectInterval: 1000,
-        connectTimeoutMS: 5000,
-        socketTimeoutMS: 5000
-      }, async (err, db) => {
-        try {
-          if (err !== null) { throw err }
-
-          let users = db.collection('users')
-
-          // Check if users table is populated
-          let userCount = await users.count()
-          if (userCount < 2) {
-            throw new Error('MongoDB Upgrade: Users table is empty!')
-          }
-
-          // Import all users
-          let userData = await users.find({
-            email: {
-              $not: 'guest'
-            }
-          }).toArray()
-          await WIKI.models.User.bulkCreate(_.map(userData, usr => {
-            return {
-              email: usr.email,
-              name: usr.name || 'Imported User',
-              password: usr.password || '',
-              provider: usr.provider || 'local',
-              providerId: usr.providerId || '',
-              role: 'user',
-              createdAt: usr.createdAt
-            }
-          }))
-
-          resolve(true)
-        } catch (errc) {
-          reject(errc)
-        }
-        db.close()
-      })
-    })
   }
 }

+ 2 - 2
server/index.js

@@ -8,8 +8,8 @@ const { nanoid } = require('nanoid')
 const { DateTime } = require('luxon')
 const semver = require('semver')
 
-if (!semver.satisfies(process.version, '>=16')) {
-  console.error('ERROR: Node.js 16.x or later required!')
+if (!semver.satisfies(process.version, '>=18')) {
+  console.error('ERROR: Node.js 18.x or later required!')
   process.exit(1)
 }
 

+ 22 - 22
ux/package.json

@@ -11,8 +11,8 @@
     "lint": "eslint --ext .js,.vue ./"
   },
   "dependencies": {
-    "@apollo/client": "3.6.1",
-    "@codemirror/autocomplete": "0.20.0",
+    "@apollo/client": "3.6.4",
+    "@codemirror/autocomplete": "0.20.1",
     "@codemirror/basic-setup": "0.20.0",
     "@codemirror/closebrackets": "0.19.2",
     "@codemirror/commands": "0.20.0",
@@ -25,15 +25,15 @@
     "@codemirror/lang-html": "0.20.0",
     "@codemirror/lang-javascript": "0.20.0",
     "@codemirror/lang-json": "0.20.0",
-    "@codemirror/lang-markdown": "0.20.0",
+    "@codemirror/lang-markdown": "0.20.1",
     "@codemirror/matchbrackets": "0.19.4",
     "@codemirror/search": "0.20.1",
     "@codemirror/state": "0.20.0",
     "@codemirror/tooltip": "0.19.16",
-    "@codemirror/view": "0.20.3",
+    "@codemirror/view": "0.20.6",
     "@lezer/common": "0.16.0",
-    "@quasar/extras": "1.13.6",
-    "@tiptap/core": "2.0.0-beta.175",
+    "@quasar/extras": "1.14.0",
+    "@tiptap/core": "2.0.0-beta.176",
     "@tiptap/extension-code-block": "2.0.0-beta.37",
     "@tiptap/extension-code-block-lowlight": "2.0.0-beta.68",
     "@tiptap/extension-color": "2.0.0-beta.9",
@@ -44,9 +44,9 @@
     "@tiptap/extension-highlight": "2.0.0-beta.33",
     "@tiptap/extension-history": "2.0.0-beta.21",
     "@tiptap/extension-image": "2.0.0-beta.27",
-    "@tiptap/extension-mention": "2.0.0-beta.96",
+    "@tiptap/extension-mention": "2.0.0-beta.97",
     "@tiptap/extension-placeholder": "2.0.0-beta.48",
-    "@tiptap/extension-table": "2.0.0-beta.48",
+    "@tiptap/extension-table": "2.0.0-beta.49",
     "@tiptap/extension-table-cell": "2.0.0-beta.20",
     "@tiptap/extension-table-header": "2.0.0-beta.22",
     "@tiptap/extension-table-row": "2.0.0-beta.19",
@@ -55,38 +55,38 @@
     "@tiptap/extension-text-align": "2.0.0-beta.29",
     "@tiptap/extension-text-style": "2.0.0-beta.23",
     "@tiptap/extension-typography": "2.0.0-beta.20",
-    "@tiptap/starter-kit": "2.0.0-beta.184",
+    "@tiptap/starter-kit": "2.0.0-beta.185",
     "@tiptap/vue-3": "2.0.0-beta.91",
-    "@vue/apollo-option": "4.0.0-alpha.16",
+    "@vue/apollo-option": "4.0.0-alpha.17",
     "apollo-upload-client": "17.0.0",
-    "browser-fs-access": "0.29.4",
-    "clipboard": "2.0.10",
+    "browser-fs-access": "0.29.5",
+    "clipboard": "2.0.11",
     "filesize": "8.0.7",
     "filesize-parser": "1.5.0",
-    "graphql": "16.4.0",
+    "graphql": "16.5.0",
     "graphql-tag": "2.12.6",
     "js-cookie": "3.0.1",
     "jwt-decode": "3.1.2",
     "lodash": "4.17.21",
-    "luxon": "2.3.2",
-    "pinia": "2.0.13",
+    "luxon": "2.4.0",
+    "pinia": "2.0.14",
     "pug": "3.0.2",
-    "quasar": "2.6.6",
+    "quasar": "2.7.0",
     "tippy.js": "6.3.7",
     "uuid": "8.3.2",
-    "v-network-graph": "0.5.13",
+    "v-network-graph": "0.5.16",
     "vue": "3.2.31",
-    "vue-i18n": "9.1.9",
-    "vue-router": "4.0.14",
+    "vue-i18n": "9.1.10",
+    "vue-router": "4.0.15",
     "vuedraggable": "4.1.0",
     "zxcvbn": "4.4.2"
   },
   "devDependencies": {
     "@intlify/vite-plugin-vue-i18n": "3.4.0",
-    "@quasar/app-vite": "1.0.0-beta.14",
+    "@quasar/app-vite": "1.0.0",
     "@types/lodash": "4.14.182",
-    "autoprefixer": "10.4.5",
-    "eslint": "8.14.0",
+    "autoprefixer": "10.4.7",
+    "eslint": "8.16.0",
     "eslint-config-standard": "17.0.0",
     "eslint-plugin-import": "2.26.0",
     "eslint-plugin-n": "15.2.0",

+ 90 - 79
ux/src/components/LocaleInstallDialog.vue

@@ -1,18 +1,18 @@
 <template lang="pug">
-q-dialog(ref='dialog', @hide='onDialogHide')
+q-dialog(ref='dialogRef', @hide='onDialogHide')
   q-card(style='min-width: 850px;')
     q-card-section.card-header
       q-icon(name='img:/_assets/icons/fluent-down.svg', left, size='sm')
-      span {{$t(`admin.locale.downloadTitle`)}}
+      span {{t(`admin.locale.downloadTitle`)}}
     q-card-section.q-pa-none
       q-table.no-border-radius(
-        :data='locales'
+        :data='state.locales'
         :columns='headers'
         row-name='code'
         flat
         hide-bottom
         :rows-per-page-options='[0]'
-        :loading='loading > 0'
+        :loading='state.loading > 0'
         )
         template(v-slot:body-cell-code='props')
           q-td(:props='props')
@@ -81,90 +81,101 @@ q-dialog(ref='dialog', @hide='onDialogHide')
       q-space
       q-btn.acrylic-btn(
         flat
-        :label='$t(`common.actions.close`)'
+        :label='t(`common.actions.close`)'
         color='grey'
         padding='xs md'
-        @click='hide'
+        @click='onDialogCancel'
         )
 
-    q-inner-loading(:showing='loading')
+    q-inner-loading(:showing='state.loading > 0')
       q-spinner(color='accent', size='lg')
 </template>
 
-<script>
-// import gql from 'graphql-tag'
-// import cloneDeep from 'lodash/cloneDeep'
-
-export default {
-  emits: ['ok', 'hide'],
-  data () {
-    return {
-      locales: [],
-      loading: 0
-    }
+<script setup>
+import { useI18n } from 'vue-i18n'
+import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { reactive, ref } from 'vue'
+
+import { useAdminStore } from '../stores/admin'
+
+// EMITS
+
+defineEmits([
+  ...useDialogPluginComponent.emits
+])
+
+// QUASAR
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
+const $q = useQuasar()
+
+// STORES
+
+const adminStore = useAdminStore()
+
+// I18N
+
+const { t } = useI18n()
+
+// DATA
+
+const state = reactive({
+  locales: [],
+  loading: 0
+})
+
+const headers = [
+  {
+    label: t('admin.locale.code'),
+    align: 'left',
+    field: 'code',
+    name: 'code',
+    sortable: true,
+    style: 'width: 90px'
+  },
+  {
+    label: t('admin.locale.name'),
+    align: 'left',
+    field: 'name',
+    name: 'name',
+    sortable: true
+  },
+  {
+    label: t('admin.locale.nativeName'),
+    align: 'left',
+    field: 'nativeName',
+    name: 'nativeName',
+    sortable: true
   },
-  computed: {
-    headers () {
-      return [
-        {
-          label: this.$t('admin.locale.code'),
-          align: 'left',
-          field: 'code',
-          name: 'code',
-          sortable: true,
-          style: 'width: 90px'
-        },
-        {
-          label: this.$t('admin.locale.name'),
-          align: 'left',
-          field: 'name',
-          name: 'name',
-          sortable: true
-        },
-        {
-          label: this.$t('admin.locale.nativeName'),
-          align: 'left',
-          field: 'nativeName',
-          name: 'nativeName',
-          sortable: true
-        },
-        {
-          label: this.$t('admin.locale.rtl'),
-          align: 'center',
-          field: 'isRTL',
-          name: 'isRTL',
-          sortable: false,
-          style: 'width: 10px'
-        },
-        {
-          label: this.$t('admin.locale.availability'),
-          align: 'center',
-          field: 'availability',
-          name: 'availability',
-          sortable: false,
-          style: 'width: 120px'
-        },
-        {
-          label: this.$t('admin.locale.download'),
-          align: 'center',
-          field: 'isInstalled',
-          name: 'isInstalled',
-          sortable: false,
-          style: 'width: 100px'
-        }
-      ]
-    }
+  {
+    label: t('admin.locale.rtl'),
+    align: 'center',
+    field: 'isRTL',
+    name: 'isRTL',
+    sortable: false,
+    style: 'width: 10px'
   },
-  methods: {
-    show () {
-      this.$refs.dialog.show()
-    },
-    hide () {
-      this.$refs.dialog.hide()
-    },
-    onDialogHide () {
-      this.$emit('hide')
-    }
+  {
+    label: t('admin.locale.availability'),
+    align: 'center',
+    field: 'availability',
+    name: 'availability',
+    sortable: false,
+    style: 'width: 120px'
+  },
+  {
+    label: t('admin.locale.download'),
+    align: 'center',
+    field: 'isInstalled',
+    name: 'isInstalled',
+    sortable: false,
+    style: 'width: 100px'
   }
+]
+
+// METHODS
+
+async function download (lc) {
+
 }
 </script>

+ 1 - 1
ux/src/pages/AdminEditors.vue

@@ -16,7 +16,7 @@ q-page.admin-flags
         type='a'
         )
       q-btn.q-mr-sm.acrylic-btn(
-        icon='fa-solid fa-rotate'
+        icon='las la-redo-alt'
         flat
         color='secondary'
         :loading='loading > 0'

+ 1 - 1
ux/src/pages/AdminFlags.vue

@@ -11,7 +11,7 @@ q-page.admin-flags
         icon='las la-question-circle'
         flat
         color='grey'
-        href='https://docs.requarks.io/admin/flags'
+        href='https://docs.js.wiki/admin/flags'
         target='_blank'
         type='a'
         )

+ 1 - 7
ux/src/pages/AdminGeneral.vue

@@ -16,7 +16,7 @@ q-page.admin-general
         type='a'
         )
       q-btn.q-mr-sm.acrylic-btn(
-        icon='fa-solid fa-rotate'
+        icon='las la-redo-alt'
         flat
         color='secondary'
         :loading='state.loading > 0'
@@ -385,7 +385,6 @@ import { onMounted, reactive, watch } from 'vue'
 import { useAdminStore } from 'src/stores/admin'
 import { useSiteStore } from 'src/stores/site'
 import { useDataStore } from 'src/stores/data'
-import { useRoute, useRouter } from 'vue-router'
 
 // QUASAR
 
@@ -397,11 +396,6 @@ const adminStore = useAdminStore()
 const siteStore = useSiteStore()
 const dataStore = useDataStore()
 
-// ROUTER
-
-const router = useRouter()
-const route = useRoute()
-
 // I18N
 
 const { t } = useI18n()

+ 226 - 202
ux/src/pages/AdminLocale.vue

@@ -4,15 +4,15 @@ q-page.admin-locale
     .col-auto
       img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-language.svg')
     .col.q-pl-md
-      .text-h5.text-primary.animated.fadeInLeft {{ $t('admin.locale.title') }}
-      .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ $t('admin.locale.subtitle') }}
+      .text-h5.text-primary.animated.fadeInLeft {{ t('admin.locale.title') }}
+      .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.locale.subtitle') }}
     .col-auto.flex
       q-btn.q-mr-md(
         icon='las la-download'
-        :label='$t(`admin.locale.downloadNew`)'
+        :label='t(`admin.locale.downloadNew`)'
         unelevated
         color='primary'
-        :disabled='loading > 0'
+        :disabled='state.loading > 0'
         @click='installNewLocale'
       )
       q-separator.q-mr-md(vertical)
@@ -20,7 +20,7 @@ q-page.admin-locale
         icon='las la-question-circle'
         flat
         color='grey'
-        href='https://docs.requarks.io/admin/locale'
+        href='https://docs.js.wiki/admin/locale'
         target='_blank'
         type='a'
         )
@@ -28,16 +28,16 @@ q-page.admin-locale
         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'
-        :disabled='loading > 0'
+        :disabled='state.loading > 0'
       )
   q-separator(inset)
   .row.q-pa-md.q-col-gutter-md
@@ -47,37 +47,37 @@ q-page.admin-locale
       //- -----------------------
       q-card.shadow-1.q-pb-sm
         q-card-section
-          .text-subtitle1 {{$t('admin.locale.settings')}}
+          .text-subtitle1 {{t('admin.locale.settings')}}
         q-item
           blueprint-icon(icon='translation')
           q-item-section
-            q-item-label {{namespacing ? $t(`admin.locale.base.labelWithNS`) : $t(`admin.locale.base.label`)}}
-            q-item-label(caption) {{$t(`admin.locale.base.hint`)}}
+            q-item-label {{state.namespacing ? t(`admin.locale.base.labelWithNS`) : t(`admin.locale.base.label`)}}
+            q-item-label(caption) {{t(`admin.locale.base.hint`)}}
           q-item-section
             q-select(
               outlined
-              v-model='selectedLocale'
+              v-model='state.selectedLocale'
               :options='installedLocales'
               option-value='code'
               option-label='name'
               emit-value
               map-options
               dense
-              :aria-label='$t(`admin.locale.base.label`)'
+              :aria-label='t(`admin.locale.base.label`)'
               )
         q-separator.q-my-sm(inset)
         q-item(tag='label', v-ripple)
           blueprint-icon(icon='unit')
           q-item-section
-            q-item-label {{$t(`admin.locale.namespaces.label`)}}
-            q-item-label(caption) {{$t(`admin.locale.namespaces.hint`)}}
+            q-item-label {{t(`admin.locale.namespaces.label`)}}
+            q-item-label(caption) {{t(`admin.locale.namespaces.hint`)}}
           q-item-section(avatar)
             q-toggle(
-              v-model='namespacing'
+              v-model='state.namespacing'
               color='primary'
               checked-icon='las la-check'
               unchecked-icon='las la-times'
-              :aria-label='$t(`admin.locale.namespaces.label`)'
+              :aria-label='t(`admin.locale.namespaces.label`)'
               )
         q-item
           q-item-section
@@ -86,21 +86,21 @@ q-page.admin-locale
                 q-card-section.col-auto.q-pr-none
                   q-icon(name='las la-info-circle', size='sm')
                 q-card-section
-                  span {{ $t('admin.locale.namespacingPrefixWarning.title', { langCode: selectedLocale }) }}
-                  .text-caption.text-yellow-1 {{ $t('admin.locale.namespacingPrefixWarning.subtitle') }}
+                  span {{ t('admin.locale.namespacingPrefixWarning.title', { langCode: state.selectedLocale }) }}
+                  .text-caption.text-yellow-1 {{ t('admin.locale.namespacingPrefixWarning.subtitle') }}
 
     .col-5
       //- -----------------------
       //- Namespacing
       //- -----------------------
-      q-card.shadow-1.q-pb-sm(v-if='namespacing')
+      q-card.shadow-1.q-pb-sm(v-if='state.namespacing')
         q-card-section
-          .text-subtitle1 {{$t('admin.locale.activeNamespaces')}}
+          .text-subtitle1 {{t('admin.locale.activeNamespaces')}}
 
         q-item(
           v-for='(lc, idx) of installedLocales'
           :key='lc.code'
-          :tag='lc.code !== selectedLocale ? `label` : null'
+          :tag='lc.code !== state.selectedLocale ? `label` : null'
           )
           blueprint-icon(:text='lc.code')
           q-item-section
@@ -108,8 +108,8 @@ q-page.admin-locale
             q-item-label(caption) {{lc.nativeName}}
           q-item-section(avatar)
             q-toggle(
-              :disable='lc.code === selectedLocale'
-              v-model='namespaces'
+              :disable='lc.code === state.selectedLocale'
+              v-model='state.namespaces'
               :val='lc.code'
               color='primary'
               checked-icon='las la-check'
@@ -121,8 +121,8 @@ q-page.admin-locale
         //- q-item
         //-   blueprint-icon(icon='test-passed')
         //-   q-item-section
-        //-     q-item-label {{$t(`admin.locale.activeNamespaces.label`)}}
-        //-     q-item-label(caption) {{$t(`admin.locale.activeNamespaces.hint`)}}
+        //-     q-item-label {{t(`admin.locale.activeNamespaces.label`)}}
+        //-     q-item-label(caption) {{t(`admin.locale.activeNamespaces.hint`)}}
         //-   q-item-section
         //-     q-select(
         //-       outlined
@@ -136,204 +136,228 @@ q-page.admin-locale
         //-       emit-value
         //-       map-options
         //-       dense
-        //-       :aria-label='$t(`admin.locale.activeNamespaces.label`)'
+        //-       :aria-label='t(`admin.locale.activeNamespaces.label`)'
         //-       )
 
 </template>
 
-<script>
-import { get } from 'vuex-pathify'
+<script setup>
 import gql from 'graphql-tag'
 import filter from 'lodash/filter'
 import _get from 'lodash/get'
 import cloneDeep from 'lodash/cloneDeep'
-import { createMetaMixin } from 'quasar'
 
 import LocaleInstallDialog from '../components/LocaleInstallDialog.vue'
 
-export default {
-  mixins: [
-    createMetaMixin(function () {
-      return {
-        title: this.$t('admin.locale.title')
-      }
-    })
-  ],
-  data () {
-    return {
-      loading: 0,
-      locales: [],
-      selectedLocale: 'en',
-      namespacing: false,
-      namespaces: []
-    }
-  },
-  computed: {
-    currentSiteId: get('admin/currentSiteId', false),
-    installedLocales () {
-      return filter(this.locales, ['isInstalled', true])
-    }
-  },
-  watch: {
-    currentSiteId (newValue) {
-      this.load()
-    },
-    selectedLocale (newValue) {
-      if (!this.namespaces.includes(newValue)) {
-        this.namespaces.push(newValue)
-      }
-    }
-  },
-  mounted () {
-    if (this.currentSiteId) {
-      this.load()
-    }
-  },
-  methods: {
-    installNewLocale () {
-      this.$q.dialog({
-        component: LocaleInstallDialog
-      }).onOk(() => {
-        this.load()
-      })
-    },
-    async load () {
-      this.loading++
-      this.$q.loading.show()
-      const resp = await this.$apollo.query({
-        query: gql`
-          query getLocales ($siteId: UUID!) {
-            locales {
-              availability
-              code
-              createdAt
-              isInstalled
-              installDate
-              isRTL
-              name
-              nativeName
-              updatedAt
-            }
-            siteById(
-              id: $siteId
-            ) {
-              id
-              locale
-              localeNamespacing
-              localeNamespaces
-            }
-          }
-        `,
-        variables: {
-          siteId: this.currentSiteId
-        },
-        fetchPolicy: 'network-only'
-      })
-      this.locales = cloneDeep(resp?.data?.locales)
-      this.selectedLocale = cloneDeep(resp?.data?.siteById?.locale)
-      this.namespacing = cloneDeep(resp?.data?.siteById?.localeNamespacing)
-      this.namespaces = cloneDeep(resp?.data?.siteById?.localeNamespaces)
-      if (!this.namespaces.includes(this.selectedLocale)) {
-        this.namespaces.push(this.selectedLocale)
+import { useI18n } from 'vue-i18n'
+import { useMeta, useQuasar } from 'quasar'
+import { computed, onMounted, reactive, watch } from 'vue'
+
+import { useAdminStore } from 'src/stores/admin'
+import { useSiteStore } from 'src/stores/site'
+import { useDataStore } from 'src/stores/data'
+
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const adminStore = useAdminStore()
+const siteStore = useSiteStore()
+const dataStore = useDataStore()
+
+// I18N
+
+const { t } = useI18n()
+
+// META
+
+useMeta({
+  title: t('admin.locale.title')
+})
+
+// DATA
+
+const state = reactive({
+  loading: 0,
+  locales: [],
+  selectedLocale: 'en',
+  namespacing: false,
+  namespaces: []
+})
+
+// COMPUTED
+
+const installedLocales = computed(() => {
+  return filter(state.locales, ['isInstalled', true])
+})
+
+// WATCHERS
+
+watch(() => adminStore.currentSiteId, (newValue) => {
+  load()
+})
+watch(() => state.selectedLocale, (newValue) => {
+  if (!state.namespaces.includes(newValue)) {
+    state.namespaces.push(newValue)
+  }
+})
+
+// METHODS
+
+function installNewLocale () {
+  $q.dialog({
+    component: LocaleInstallDialog
+  }).onOk(() => {
+    this.load()
+  })
+}
+
+async function load () {
+  state.loading++
+  $q.loading.show()
+  const resp = await APOLLO_CLIENT.query({
+    query: gql`
+      query getLocales ($siteId: UUID!) {
+        locales {
+          availability
+          code
+          createdAt
+          isInstalled
+          installDate
+          isRTL
+          name
+          nativeName
+          updatedAt
+        }
+        siteById(
+          id: $siteId
+        ) {
+          id
+          locale
+          localeNamespacing
+          localeNamespaces
+        }
       }
-      this.$q.loading.hide()
-      this.loading--
+    `,
+    variables: {
+      siteId: adminStore.currentSiteId
     },
-    async download (lc) {
-      lc.isDownloading = true
-      const respRaw = await this.$apollo.mutate({
-        mutation: gql`
-          mutation downloadLocale ($locale: String!) {
-            localization {
-              downloadLocale (locale: $locale) {
-                responseResult {
-                  succeeded
-                  errorCode
-                  slug
-                  message
-                }
-              }
+    fetchPolicy: 'network-only'
+  })
+  state.locales = cloneDeep(resp?.data?.locales)
+  state.selectedLocale = cloneDeep(resp?.data?.siteById?.locale)
+  state.namespacing = cloneDeep(resp?.data?.siteById?.localeNamespacing)
+  state.namespaces = cloneDeep(resp?.data?.siteById?.localeNamespaces)
+  if (!state.namespaces.includes(state.selectedLocale)) {
+    state.namespaces.push(state.selectedLocale)
+  }
+  $q.loading.hide()
+  state.loading--
+}
+
+async function download (lc) {
+  lc.isDownloading = true
+  const respRaw = await APOLLO_CLIENT.mutate({
+    mutation: gql`
+      mutation downloadLocale ($locale: String!) {
+        localization {
+          downloadLocale (locale: $locale) {
+            responseResult {
+              succeeded
+              errorCode
+              slug
+              message
             }
           }
-        `,
-        variables: {
-          locale: lc.code
         }
-      })
-      const resp = _get(respRaw, 'data.localization.downloadLocale.responseResult', {})
-      if (resp.succeeded) {
-        lc.isDownloading = false
-        lc.isInstalled = true
-        lc.updatedAt = new Date().toISOString()
-        lc.installDate = lc.updatedAt
-        this.$store.commit('showNotification', {
-          message: `Locale ${lc.name} has been installed successfully.`,
-          style: 'success',
-          icon: 'get_app'
-        })
-      } else {
-        this.$q.notify({
-          type: 'negative',
-          message: resp.message
-        })
       }
-      this.isDownloading = false
-    },
-    async save () {
-      this.loading = true
-      const respRaw = await this.$apollo.mutate({
-        mutation: gql`
-          mutation saveLocaleSettings (
-            $locale: String!
-            $autoUpdate: Boolean!
-            $namespacing: Boolean!
-            $namespaces: [String]!
+    `,
+    variables: {
+      locale: lc.code
+    }
+  })
+  const resp = _get(respRaw, 'data.localization.downloadLocale.responseResult', {})
+  if (resp.succeeded) {
+    lc.isDownloading = false
+    lc.isInstalled = true
+    lc.updatedAt = new Date().toISOString()
+    lc.installDate = lc.updatedAt
+    $q.notify({
+      message: `Locale ${lc.name} has been installed successfully.`,
+      type: 'positive'
+    })
+  } else {
+    $q.notify({
+      type: 'negative',
+      message: resp.message
+    })
+  }
+  state.isDownloading = false
+}
+
+async function save () {
+  state.loading = true
+  const respRaw = await APOLLO_CLIENT.mutate({
+    mutation: gql`
+      mutation saveLocaleSettings (
+        $locale: String!
+        $autoUpdate: Boolean!
+        $namespacing: Boolean!
+        $namespaces: [String]!
+        ) {
+        localization {
+          updateLocale(
+            locale: $locale
+            autoUpdate: $autoUpdate
+            namespacing: $namespacing
+            namespaces: $namespaces
             ) {
-            localization {
-              updateLocale(
-                locale: $locale
-                autoUpdate: $autoUpdate
-                namespacing: $namespacing
-                namespaces: $namespaces
-                ) {
-                responseResult {
-                  succeeded
-                  errorCode
-                  slug
-                  message
-                }
-              }
+            responseResult {
+              succeeded
+              errorCode
+              slug
+              message
             }
           }
-        `,
-        variables: {
-          locale: this.selectedLocale,
-          autoUpdate: this.autoUpdate,
-          namespacing: this.namespacing,
-          namespaces: this.namespaces
         }
-      })
-      const resp = _get(respRaw, 'data.localization.updateLocale.responseResult', {})
-      if (resp.succeeded) {
-        // Change UI language
-        this.$i18n.locale = this.selectedLocale
-
-        this.$q.notify({
-          type: 'positive',
-          message: 'Locale settings updated successfully.'
-        })
-
-        setTimeout(() => {
-          window.location.reload(true)
-        }, 1000)
-      } else {
-        this.$q.notify({
-          type: 'negative',
-          message: resp.message
-        })
       }
-      this.loading = false
+    `,
+    variables: {
+      locale: state.selectedLocale,
+      autoUpdate: state.autoUpdate,
+      namespacing: state.namespacing,
+      namespaces: state.namespaces
     }
+  })
+  const resp = _get(respRaw, 'data.localization.updateLocale.responseResult', {})
+  if (resp.succeeded) {
+    // Change UI language
+    this.$i18n.locale = state.selectedLocale
+
+    $q.notify({
+      type: 'positive',
+      message: 'Locale settings updated successfully.'
+    })
+
+    setTimeout(() => {
+      window.location.reload(true)
+    }, 1000)
+  } else {
+    $q.notify({
+      type: 'negative',
+      message: resp.message
+    })
   }
+  state.loading = false
 }
+
+// MOUNTED
+
+onMounted(() => {
+  if (adminStore.currentSiteId) {
+    load()
+  }
+})
 </script>

+ 150 - 130
ux/src/pages/AdminLogin.vue

@@ -4,8 +4,8 @@ q-page.admin-login
     .col-auto
       img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-bunch-of-keys.svg')
     .col.q-pl-md
-      .text-h5.text-primary.animated.fadeInLeft {{ $t('admin.login.title') }}
-      .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ $t('admin.login.subtitle') }}
+      .text-h5.text-primary.animated.fadeInLeft {{ t('admin.login.title') }}
+      .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.login.subtitle') }}
     .col-auto
       q-btn.q-mr-sm.acrylic-btn(
         icon='las la-question-circle'
@@ -19,16 +19,16 @@ q-page.admin-login
         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'
-        :disabled='loading > 0'
+        :disabled='state.loading > 0'
       )
   q-separator(inset)
   .row.q-pa-md.q-col-gutter-md
@@ -38,12 +38,12 @@ q-page.admin-login
       //- -----------------------
       q-card.shadow-1.q-pb-sm
         q-card-section
-          .text-subtitle1 {{$t('admin.login.experience')}}
+          .text-subtitle1 {{t('admin.login.experience')}}
         q-item(tag='label', v-ripple)
-          blueprint-icon(icon='full-image', indicator, :indicator-text='$t(`admin.extensions.requiresSharp`)')
+          blueprint-icon(icon='full-image', indicator, :indicator-text='t(`admin.extensions.requiresSharp`)')
           q-item-section
-            q-item-label {{$t(`admin.login.background`)}}
-            q-item-label(caption) {{$t(`admin.login.backgroundHint`)}}
+            q-item-label {{t(`admin.login.background`)}}
+            q-item-label(caption) {{t(`admin.login.backgroundHint`)}}
           q-item-section.col-auto
             q-btn(
               label='Upload'
@@ -56,80 +56,80 @@ q-page.admin-login
         q-item(tag='label', v-ripple)
           blueprint-icon(icon='close-pane')
           q-item-section
-            q-item-label {{$t(`admin.login.bypassScreen`)}}
-            q-item-label(caption) {{$t(`admin.login.bypassScreenHint`)}}
+            q-item-label {{t(`admin.login.bypassScreen`)}}
+            q-item-label(caption) {{t(`admin.login.bypassScreenHint`)}}
           q-item-section(avatar)
             q-toggle(
-              v-model='config.authAutoLogin'
+              v-model='state.config.authAutoLogin'
               color='primary'
               checked-icon='las la-check'
               unchecked-icon='las la-times'
-              :aria-label='$t(`admin.login.bypassScreen`)'
+              :aria-label='t(`admin.login.bypassScreen`)'
               )
         q-separator.q-my-sm(inset)
         q-item(tag='label', v-ripple)
           blueprint-icon(icon='no-access')
           q-item-section
-            q-item-label {{$t(`admin.login.bypassUnauthorized`)}}
-            q-item-label(caption) {{$t(`admin.login.bypassUnauthorizedHint`)}}
+            q-item-label {{t(`admin.login.bypassUnauthorized`)}}
+            q-item-label(caption) {{t(`admin.login.bypassUnauthorizedHint`)}}
           q-item-section(avatar)
             q-toggle(
-              v-model='config.authBypassUnauthorized'
+              v-model='state.config.authBypassUnauthorized'
               color='primary'
               checked-icon='las la-check'
               unchecked-icon='las la-times'
-              :aria-label='$t(`admin.login.bypassUnauthorized`)'
+              :aria-label='t(`admin.login.bypassUnauthorized`)'
               )
         q-separator.q-my-sm(inset)
         q-item
           blueprint-icon(icon='double-right')
           q-item-section
-            q-item-label {{$t(`admin.login.loginRedirect`)}}
-            q-item-label(caption) {{$t(`admin.login.loginRedirectHint`)}}
+            q-item-label {{t(`admin.login.loginRedirect`)}}
+            q-item-label(caption) {{t(`admin.login.loginRedirectHint`)}}
           q-item-section
             q-input(
               outlined
-              v-model='config.loginRedirect'
+              v-model='state.config.loginRedirect'
               dense
               :rules=`[
-                val => invalidCharsRegex.test(val) || $t('admin.login.loginRedirectInvalidChars')
+                val => state.invalidCharsRegex.test(val) || t('admin.login.loginRedirectInvalidChars')
               ]`
               hide-bottom-space
-              :aria-label='$t(`admin.login.loginRedirect`)'
+              :aria-label='t(`admin.login.loginRedirect`)'
               )
         q-separator.q-my-sm(inset)
         q-item
           blueprint-icon(icon='chevron-right')
           q-item-section
-            q-item-label {{$t(`admin.login.welcomeRedirect`)}}
-            q-item-label(caption) {{$t(`admin.login.welcomeRedirectHint`)}}
+            q-item-label {{t(`admin.login.welcomeRedirect`)}}
+            q-item-label(caption) {{t(`admin.login.welcomeRedirectHint`)}}
           q-item-section
             q-input(
               outlined
-              v-model='config.welcomeRedirect'
+              v-model='state.config.welcomeRedirect'
               dense
               :rules=`[
-                val => invalidCharsRegex.test(val) || $t('admin.login.welcomeRedirectInvalidChars')
+                val => state.invalidCharsRegex.test(val) || t('admin.login.welcomeRedirectInvalidChars')
               ]`
               hide-bottom-space
-              :aria-label='$t(`admin.login.welcomeRedirect`)'
+              :aria-label='t(`admin.login.welcomeRedirect`)'
               )
         q-separator.q-my-sm(inset)
         q-item
           blueprint-icon(icon='exit')
           q-item-section
-            q-item-label {{$t(`admin.login.logoutRedirect`)}}
-            q-item-label(caption) {{$t(`admin.login.logoutRedirectHint`)}}
+            q-item-label {{t(`admin.login.logoutRedirect`)}}
+            q-item-label(caption) {{t(`admin.login.logoutRedirectHint`)}}
           q-item-section
             q-input(
               outlined
-              v-model='config.logoutRedirect'
+              v-model='state.config.logoutRedirect'
               dense
               :rules=`[
-                val => invalidCharsRegex.test(val) || $t('admin.login.logoutRedirectInvalidChars')
+                val => state.invalidCharsRegex.test(val) || t('admin.login.logoutRedirectInvalidChars')
               ]`
               hide-bottom-space
-              :aria-label='$t(`admin.login.logoutRedirect`)'
+              :aria-label='t(`admin.login.logoutRedirect`)'
               )
 
     .col-12.col-lg-6
@@ -138,11 +138,11 @@ q-page.admin-login
       //- -----------------------
       q-card.shadow-1.q-pb-sm
         q-card-section
-          .text-subtitle1 {{$t('admin.login.providers')}}
+          .text-subtitle1 {{t('admin.login.providers')}}
         q-card-section.admin-login-providers.q-pt-none
           draggable(
             class='q-list rounded-borders'
-            :list='providers'
+            :list='state.providers'
             :animation='150'
             handle='.handle'
             @end='dragStarted = false'
@@ -171,117 +171,137 @@ q-page.admin-login
               q-card-section.items-center(horizontal)
                 q-card-section.col-auto.q-pr-none
                   q-icon(name='las la-info-circle', size='sm')
-                q-card-section.text-caption {{ $t('admin.login.providersVisbleWarning') }}
+                q-card-section.text-caption {{ t('admin.login.providersVisbleWarning') }}
 
 </template>
 
-<script>
+<script setup>
 import { get } from 'vuex-pathify'
 import cloneDeep from 'lodash/cloneDeep'
 import gql from 'graphql-tag'
 import draggable from 'vuedraggable'
-import { createMetaMixin } from 'quasar'
 
-export default {
-  mixins: [
-    createMetaMixin(function () {
-      return {
-        title: this.$t('admin.login.title')
-      }
-    })
-  ],
-  components: {
-    draggable
-  },
-  data () {
-    return {
-      invalidCharsRegex: /^[^<>"]+$/,
-      loading: 0,
-      config: {
-        authAutoLogin: false,
-        authHideLocal: false,
-        authBypassUnauthorized: false,
-        loginRedirect: '/',
-        welcomeRedirect: '/',
-        logoutRedirect: '/'
-      },
-      providers: [
-        { id: 'local', label: 'Local Authentication', provider: 'Username-Password', icon: 'database', isActive: true },
-        { id: 'google', label: 'Google', provider: 'Google', icon: 'google', isActive: true },
-        { id: 'slack', label: 'Slack', provider: 'Slack', icon: 'slack', isActive: false }
-      ]
-    }
-  },
-  computed: {
-    currentSiteId: get('admin/currentSiteId', false)
+import { useI18n } from 'vue-i18n'
+import { useMeta, useQuasar } from 'quasar'
+import { computed, onMounted, reactive, watch } from 'vue'
+
+import { useAdminStore } from 'src/stores/admin'
+
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const adminStore = useAdminStore()
+
+// I18N
+
+const { t } = useI18n()
+
+// META
+
+useMeta({
+  title: t('admin.login.title')
+})
+
+// DATA
+
+const state = reactive({
+  invalidCharsRegex: /^[^<>"]+$/,
+  loading: 0,
+  config: {
+    authAutoLogin: false,
+    authHideLocal: false,
+    authBypassUnauthorized: false,
+    loginRedirect: '/',
+    welcomeRedirect: '/',
+    logoutRedirect: '/'
   },
-  methods: {
-    async load () {
-      this.loading++
-      this.$q.loading.show()
-      // const resp = await this.$apollo.query({
-      //   query: gql`
-      //     query getSite (
-      //       $id: UUID!
-      //     ) {
-      //       siteById(
-      //         id: $id
-      //       ) {
-      //         id
-      //       }
-      //     }
-      //   `,
-      //   variables: {
-      //     id: this.currentSiteId
-      //   },
-      //   fetchPolicy: 'network-only'
-      // })
-      // this.config = cloneDeep(resp?.data?.siteById)
-      this.$q.loading.hide()
-      this.loading--
-    },
-    async save () {
-      try {
-        await this.$apollo.mutate({
-          mutation: gql`
-            mutation saveLoginSettings (
-              $authAutoLogin: Boolean
-              $authEnforce2FA: Boolean
+  providers: [
+    { id: 'local', label: 'Local Authentication', provider: 'Username-Password', icon: 'database', isActive: true },
+    { id: 'google', label: 'Google', provider: 'Google', icon: 'google', isActive: true },
+    { id: 'slack', label: 'Slack', provider: 'Slack', icon: 'slack', isActive: false }
+  ]
+})
+
+// METHODS
+
+async function load () {
+  state.loading++
+  $q.loading.show()
+  // const resp = await APOLLO_CLIENT.query({
+  //   query: gql`
+  //     query getSite (
+  //       $id: UUID!
+  //     ) {
+  //       siteById(
+  //         id: $id
+  //       ) {
+  //         id
+  //       }
+  //     }
+  //   `,
+  //   variables: {
+  //     id: adminStore.currentSiteId
+  //   },
+  //   fetchPolicy: 'network-only'
+  // })
+  // this.config = cloneDeep(resp?.data?.siteById)
+  $q.loading.hide()
+  state.loading--
+}
+
+async function save () {
+  try {
+    await APOLLO_CLIENT.mutate({
+      mutation: gql`
+        mutation saveLoginSettings (
+          $authAutoLogin: Boolean
+          $authEnforce2FA: Boolean
+        ) {
+          site {
+            updateConfig(
+              authAutoLogin: $authAutoLogin,
+              authEnforce2FA: $authEnforce2FA
             ) {
-              site {
-                updateConfig(
-                  authAutoLogin: $authAutoLogin,
-                  authEnforce2FA: $authEnforce2FA
-                ) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
+              responseResult {
+                succeeded
+                errorCode
+                slug
+                message
               }
             }
-          `,
-          variables: {
-            authAutoLogin: this.config.authAutoLogin ?? false,
-            authEnforce2FA: this.config.authEnforce2FA ?? false
-          },
-          watchLoading (isLoading) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update')
           }
-        })
-        this.$store.commit('showNotification', {
-          style: 'success',
-          message: 'Configuration saved successfully.',
-          icon: 'check'
-        })
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
+        }
+      `,
+      variables: {
+        authAutoLogin: state.config.authAutoLogin ?? false,
+        authEnforce2FA: state.config.authEnforce2FA ?? false
+      },
+      watchLoading (isLoading) {
+        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update')
       }
-    }
+    })
+    $q.notify({
+      type: 'positive',
+      message: 'Configuration saved successfully.'
+    })
+  } catch (err) {
+    $q.notify({
+      type: 'negative',
+      message: err.message
+    })
   }
 }
+
+// MOUNTED
+
+onMounted(() => {
+  if (adminStore.currentSiteId) {
+    load()
+  }
+})
 </script>
 
 <style lang='scss'>

+ 2 - 2
ux/src/pages/AdminSites.vue

@@ -16,7 +16,7 @@ q-page.admin-locale
         target='_blank'
         )
       q-btn.q-mr-sm.acrylic-btn(
-        icon='fa-solid fa-rotate'
+        icon='las la-redo-alt'
         flat
         color='secondary'
         @click='refresh'
@@ -100,7 +100,7 @@ q-page.admin-locale
 <script setup>
 import { useMeta, useQuasar } from 'quasar'
 import { useI18n } from 'vue-i18n'
-import { defineAsyncComponent, nextTick, onMounted, reactive, ref, watch } from 'vue'
+import { nextTick, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
 
 import { useAdminStore } from '../stores/admin'

+ 753 - 695
ux/src/pages/AdminStorage.vue

@@ -4,22 +4,22 @@ q-page.admin-storage
     .col-auto
       img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-ssd.svg')
     .col.q-pl-md
-      .text-h5.text-primary.animated.fadeInLeft {{ $t('admin.storage.title') }}
-      .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ $t('admin.storage.subtitle') }}
+      .text-h5.text-primary.animated.fadeInLeft {{ t('admin.storage.title') }}
+      .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.storage.subtitle') }}
     .col-auto.flex
       q-spinner-tail.q-mr-md(
-        v-show='loading > 0'
+        v-show='state.loading > 0'
         color='accent'
         size='sm'
       )
       q-btn-toggle.q-mr-md(
-        v-model='displayMode'
+        v-model='state.displayMode'
         push
         no-caps
         toggle-color='black'
         :options=`[
-          { label: $t('admin.storage.targets'), value: 'targets' },
-          { label: $t('admin.storage.deliveryPaths'), value: 'delivery' }
+          { label: t('admin.storage.targets'), value: 'targets' },
+          { label: t('admin.storage.deliveryPaths'), value: 'delivery' }
         ]`
       )
       q-separator.q-mr-md(vertical)
@@ -33,11 +33,11 @@ q-page.admin-storage
         )
       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)
 
@@ -45,7 +45,7 @@ q-page.admin-storage
   //- TARGETS
   //- ==========================================
 
-  .row.q-pa-md.q-col-gutter-md(v-if='displayMode === `targets`')
+  .row.q-pa-md.q-col-gutter-md(v-if='state.displayMode === `targets`')
     .col-auto
       q-card.rounded-borders.bg-dark
         q-list(
@@ -54,11 +54,11 @@ q-page.admin-storage
           dark
           )
           q-item(
-            v-for='tgt of targets'
+            v-for='tgt of state.targets'
             :key='tgt.key'
             active-class='bg-primary text-white'
-            :active='selectedTarget === tgt.id'
-            :to='`/_admin/` + currentSiteId + `/storage/` + tgt.id'
+            :active='state.selectedTarget === tgt.id'
+            :to='`/_admin/` + adminStore.currentSiteId + `/storage/` + tgt.id'
             clickable
             )
             q-item-section(side)
@@ -70,76 +70,76 @@ q-page.admin-storage
               q-item-label(caption, :class='getTargetSubtitleColor(tgt)') {{getTargetSubtitle(tgt)}}
             q-item-section(side)
               q-spinner-rings(:color='tgt.isEnabled ? `positive` : `negative`', size='sm')
-    .col(v-if='target')
+    .col(v-if='state.target')
       //- -----------------------
       //- Content Types
       //- -----------------------
       q-card.shadow-1.q-pb-sm
         q-card-section
-          .text-subtitle1 {{$t('admin.storage.contentTypes')}}
-          .text-body2.text-grey {{ $t('admin.storage.contentTypesHint') }}
+          .text-subtitle1 {{t('admin.storage.contentTypes')}}
+          .text-body2.text-grey {{ t('admin.storage.contentTypesHint') }}
         q-item(tag='label')
           q-item-section(avatar)
             q-checkbox(
-              v-model='target.contentTypes.activeTypes'
-              :color='target.module === `db` ? `grey` : `primary`'
+              v-model='state.target.contentTypes.activeTypes'
+              :color='state.target.module === `db` ? `grey` : `primary`'
               val='pages'
-              :aria-label='$t(`admin.storage.contentTypePages`)'
-              :disable='target.module === `db`'
+              :aria-label='t(`admin.storage.contentTypePages`)'
+              :disable='state.target.module === `db`'
               )
           q-item-section
-            q-item-label {{$t(`admin.storage.contentTypePages`)}}
-            q-item-label(caption) {{$t(`admin.storage.contentTypePagesHint`)}}
+            q-item-label {{t(`admin.storage.contentTypePages`)}}
+            q-item-label(caption) {{t(`admin.storage.contentTypePagesHint`)}}
         q-item(tag='label')
           q-item-section(avatar)
             q-checkbox(
-              v-model='target.contentTypes.activeTypes'
+              v-model='state.target.contentTypes.activeTypes'
               color='primary'
               val='images'
-              :aria-label='$t(`admin.storage.contentTypeImages`)'
+              :aria-label='t(`admin.storage.contentTypeImages`)'
               )
           q-item-section
-            q-item-label {{$t(`admin.storage.contentTypeImages`)}}
-            q-item-label(caption) {{$t(`admin.storage.contentTypeImagesHint`)}}
+            q-item-label {{t(`admin.storage.contentTypeImages`)}}
+            q-item-label(caption) {{t(`admin.storage.contentTypeImagesHint`)}}
         q-item(tag='label')
           q-item-section(avatar)
             q-checkbox(
-              v-model='target.contentTypes.activeTypes'
+              v-model='state.target.contentTypes.activeTypes'
               color='primary'
               val='documents'
-              :aria-label='$t(`admin.storage.contentTypeDocuments`)'
+              :aria-label='t(`admin.storage.contentTypeDocuments`)'
               )
           q-item-section
-            q-item-label {{$t(`admin.storage.contentTypeDocuments`)}}
-            q-item-label(caption) {{$t(`admin.storage.contentTypeDocumentsHint`)}}
+            q-item-label {{t(`admin.storage.contentTypeDocuments`)}}
+            q-item-label(caption) {{t(`admin.storage.contentTypeDocumentsHint`)}}
         q-item(tag='label')
           q-item-section(avatar)
             q-checkbox(
-              v-model='target.contentTypes.activeTypes'
+              v-model='state.target.contentTypes.activeTypes'
               color='primary'
               val='others'
-              :aria-label='$t(`admin.storage.contentTypeOthers`)'
+              :aria-label='t(`admin.storage.contentTypeOthers`)'
               )
           q-item-section
-            q-item-label {{$t(`admin.storage.contentTypeOthers`)}}
-            q-item-label(caption) {{$t(`admin.storage.contentTypeOthersHint`)}}
+            q-item-label {{t(`admin.storage.contentTypeOthers`)}}
+            q-item-label(caption) {{t(`admin.storage.contentTypeOthersHint`)}}
         q-item(tag='label')
           q-item-section(avatar)
             q-checkbox(
-              v-model='target.contentTypes.activeTypes'
+              v-model='state.target.contentTypes.activeTypes'
               color='primary'
               val='large'
-              :aria-label='$t(`admin.storage.contentTypeLargeFiles`)'
+              :aria-label='t(`admin.storage.contentTypeLargeFiles`)'
               )
           q-item-section
-            q-item-label {{$t(`admin.storage.contentTypeLargeFiles`)}}
-            q-item-label(caption) {{$t(`admin.storage.contentTypeLargeFilesHint`)}}
-            q-item-label.text-deep-orange(v-if='target.module === `db`', caption) {{$t(`admin.storage.contentTypeLargeFilesDBWarn`)}}
+            q-item-label {{t(`admin.storage.contentTypeLargeFiles`)}}
+            q-item-label(caption) {{t(`admin.storage.contentTypeLargeFilesHint`)}}
+            q-item-label.text-deep-orange(v-if='state.target.module === `db`', caption) {{t(`admin.storage.contentTypeLargeFilesDBWarn`)}}
           q-item-section(side)
             q-input(
               outlined
-              :label='$t(`admin.storage.contentTypeLargeFilesThreshold`)'
-              v-model='target.contentTypes.largeThreshold'
+              :label='t(`admin.storage.contentTypeLargeFilesThreshold`)'
+              v-model='state.target.contentTypes.largeThreshold'
               style='min-width: 150px;'
               dense
             )
@@ -149,41 +149,41 @@ q-page.admin-storage
       //- -----------------------
       q-card.shadow-1.q-pb-sm.q-mt-md
         q-card-section
-          .text-subtitle1 {{$t('admin.storage.assetDelivery')}}
-          .text-body2.text-grey {{ $t('admin.storage.assetDeliveryHint') }}
-        q-item(:tag='target.assetDelivery.isStreamingSupported ? `label` : null')
+          .text-subtitle1 {{t('admin.storage.assetDelivery')}}
+          .text-body2.text-grey {{ t('admin.storage.assetDeliveryHint') }}
+        q-item(:tag='state.target.assetDelivery.isStreamingSupported ? `label` : null')
           q-item-section(avatar)
             q-checkbox(
-              v-model='target.assetDelivery.streaming'
-              :color='target.module === `db` || !target.assetDelivery.isStreamingSupported ? `grey` : `primary`'
-              :aria-label='$t(`admin.storage.contentTypePages`)'
-              :disable='target.module === `db` || !target.assetDelivery.isStreamingSupported'
+              v-model='state.target.assetDelivery.streaming'
+              :color='state.target.module === `db` || !state.target.assetDelivery.isStreamingSupported ? `grey` : `primary`'
+              :aria-label='t(`admin.storage.contentTypePages`)'
+              :disable='state.target.module === `db` || !state.target.assetDelivery.isStreamingSupported'
               )
           q-item-section
-            q-item-label {{$t(`admin.storage.assetStreaming`)}}
-            q-item-label(caption) {{$t(`admin.storage.assetStreamingHint`)}}
-            q-item-label.text-deep-orange(v-if='!target.assetDelivery.isStreamingSupported', caption) {{$t(`admin.storage.assetStreamingNotSupported`)}}
-        q-item(:tag='target.assetDelivery.isDirectAccessSupported ? `label` : null')
+            q-item-label {{t(`admin.storage.assetStreaming`)}}
+            q-item-label(caption) {{t(`admin.storage.assetStreamingHint`)}}
+            q-item-label.text-deep-orange(v-if='!state.target.assetDelivery.isStreamingSupported', caption) {{t(`admin.storage.assetStreamingNotSupported`)}}
+        q-item(:tag='state.target.assetDelivery.isDirectAccessSupported ? `label` : null')
           q-item-section(avatar)
             q-checkbox(
-              v-model='target.assetDelivery.directAccess'
-              :color='!target.assetDelivery.isDirectAccessSupported ? `grey` : `primary`'
-              :aria-label='$t(`admin.storage.contentTypePages`)'
-              :disable='!target.assetDelivery.isDirectAccessSupported'
+              v-model='state.target.assetDelivery.directAccess'
+              :color='!state.target.assetDelivery.isDirectAccessSupported ? `grey` : `primary`'
+              :aria-label='t(`admin.storage.contentTypePages`)'
+              :disable='!state.target.assetDelivery.isDirectAccessSupported'
               )
           q-item-section
-            q-item-label {{$t(`admin.storage.assetDirectAccess`)}}
-            q-item-label(caption) {{$t(`admin.storage.assetDirectAccessHint`)}}
-            q-item-label.text-deep-orange(v-if='!target.assetDelivery.isDirectAccessSupported', caption) {{$t(`admin.storage.assetDirectAccessNotSupported`)}}
+            q-item-label {{t(`admin.storage.assetDirectAccess`)}}
+            q-item-label(caption) {{t(`admin.storage.assetDirectAccessHint`)}}
+            q-item-label.text-deep-orange(v-if='!state.target.assetDelivery.isDirectAccessSupported', caption) {{t(`admin.storage.assetDirectAccessNotSupported`)}}
 
       //- -----------------------
       //- Setup
       //- -----------------------
-      q-card.shadow-1.q-pb-sm.q-mt-md(v-if='target.setup && target.setup.handler && target.setup.state !== `configured`')
+      q-card.shadow-1.q-pb-sm.q-mt-md(v-if='state.target.setup && state.target.setup.handler && state.target.setup.state !== `configured`')
         q-card-section
-          .text-subtitle1 {{$t('admin.storage.setup')}}
-          .text-body2.text-grey {{ $t('admin.storage.setupHint') }}
-        template(v-if='target.setup.handler === `github` && target.setup.state === `notconfigured`')
+          .text-subtitle1 {{t('admin.storage.setup')}}
+          .text-body2.text-grey {{ t('admin.storage.setupHint') }}
+        template(v-if='state.target.setup.handler === `github` && state.target.setup.state === `notconfigured`')
           q-item
             blueprint-icon(icon='test-account')
             q-item-section
@@ -191,42 +191,42 @@ q-page.admin-storage
               q-item-label(caption) Whether to use an organization or personal GitHub account during setup.
             q-item-section.col-auto
               q-btn-toggle(
-                v-model='target.setup.values.accountType'
+                v-model='state.target.setup.values.accountType'
                 push
                 glossy
                 no-caps
                 toggle-color='primary'
                 :options=`[
-                  { label: $t('admin.storage.githubAccTypeOrg'), value: 'org' },
-                  { label: $t('admin.storage.githubAccTypePersonal'), value: 'personal' }
+                  { label: t('admin.storage.githubAccTypeOrg'), value: 'org' },
+                  { label: t('admin.storage.githubAccTypePersonal'), value: 'personal' }
                 ]`
               )
           q-separator.q-my-sm(inset)
-          template(v-if='target.setup.values.accountType === `org`')
+          template(v-if='state.target.setup.values.accountType === `org`')
             q-item
               blueprint-icon(icon='github')
               q-item-section
-                q-item-label {{ $t('admin.storage.githubOrg') }}
-                q-item-label(caption) {{ $t('admin.storage.githubOrgHint') }}
+                q-item-label {{ t('admin.storage.githubOrg') }}
+                q-item-label(caption) {{ t('admin.storage.githubOrgHint') }}
               q-item-section
                 q-input(
                   outlined
-                  v-model='target.setup.values.org'
+                  v-model='state.target.setup.values.org'
                   dense
-                  :aria-label='$t(`admin.storage.githubOrg`)'
+                  :aria-label='t(`admin.storage.githubOrg`)'
                   )
             q-separator.q-my-sm(inset)
           q-item
             blueprint-icon(icon='dns')
             q-item-section
-              q-item-label {{ $t('admin.storage.githubPublicUrl') }}
-              q-item-label(caption) {{ $t('admin.storage.githubPublicUrlHint') }}
+              q-item-label {{ t('admin.storage.githubPublicUrl') }}
+              q-item-label(caption) {{ t('admin.storage.githubPublicUrlHint') }}
             q-item-section
               q-input(
                 outlined
-                v-model='target.setup.values.publicUrl'
+                v-model='state.target.setup.values.publicUrl'
                 dense
-                :aria-label='$t(`admin.storage.githubPublicUrl`)'
+                :aria-label='t(`admin.storage.githubPublicUrl`)'
                 )
           q-card-section.q-pt-sm.text-right
             form(
@@ -242,37 +242,37 @@ q-page.admin-storage
               q-btn(
                 unelevated
                 icon='las la-angle-double-right'
-                :label='$t(`admin.storage.startSetup`)'
+                :label='t(`admin.storage.startSetup`)'
                 color='secondary'
                 @click='setupGitHub'
                 :loading='setupCfg.loading'
               )
-        template(v-else-if='target.setup.handler === `github` && target.setup.state === `pendinginstall`')
+        template(v-else-if='state.target.setup.handler === `github` && state.target.setup.state === `pendinginstall`')
           q-card-section.q-py-none
             q-banner(
               rounded
               :class='$q.dark.isActive ? `bg-teal-9 text-white` : `bg-teal-1 text-teal-9`'
-              ) {{$t('admin.storage.githubFinish')}}
+              ) {{t('admin.storage.githubFinish')}}
           q-card-section.q-pt-sm.text-right
             q-btn.q-mr-sm(
               unelevated
               icon='las la-times-circle'
-              :label='$t(`admin.storage.cancelSetup`)'
+              :label='t(`admin.storage.cancelSetup`)'
               color='negative'
               @click='setupDestroy'
             )
             q-btn(
               unelevated
               icon='las la-angle-double-right'
-              :label='$t(`admin.storage.finishSetup`)'
+              :label='t(`admin.storage.finishSetup`)'
               color='secondary'
               @click='setupGitHubStep(`verify`)'
               :loading='setupCfg.loading'
             )
-      q-card.shadow-1.q-pb-sm.q-mt-md(v-if='target.setup && target.setup.handler && target.setup.state === `configured`')
+      q-card.shadow-1.q-pb-sm.q-mt-md(v-if='state.target.setup && state.target.setup.handler && state.target.setup.state === `configured`')
         q-card-section
-          .text-subtitle1 {{$t('admin.storage.setup')}}
-          .text-body2.text-grey {{ $t('admin.storage.setupConfiguredHint') }}
+          .text-subtitle1 {{t('admin.storage.setup')}}
+          .text-body2.text-grey {{ t('admin.storage.setupConfiguredHint') }}
         q-item
           blueprint-icon.self-start(icon='matches', :hue-rotate='140')
           q-item-section
@@ -285,7 +285,7 @@ q-page.admin-storage
               icon='las la-arrow-circle-right'
               color='negative'
               @click='setupDestroy'
-              :label='$t(`admin.storage.uninstall`)'
+              :label='t(`admin.storage.uninstall`)'
             )
 
       //- -----------------------
@@ -293,14 +293,14 @@ q-page.admin-storage
       //- -----------------------
       q-card.shadow-1.q-pb-sm.q-mt-md
         q-card-section
-          .text-subtitle1 {{$t('admin.storage.config')}}
+          .text-subtitle1 {{t('admin.storage.config')}}
           q-banner.q-mt-md(
-            v-if='!target.config || Object.keys(target.config).length < 1'
+            v-if='!state.target.config || Object.keys(state.target.config).length < 1'
             rounded
             :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-2 text-grey-7`'
-            ) {{$t('admin.storage.noConfigOption')}}
+            ) {{t('admin.storage.noConfigOption')}}
         template(
-          v-for='(cfg, cfgKey, idx) in target.config'
+          v-for='(cfg, cfgKey, idx) in state.target.config'
           )
           template(
             v-if='configIfCheck(cfg.if)'
@@ -317,7 +317,7 @@ q-page.admin-storage
                   color='primary'
                   checked-icon='las la-check'
                   unchecked-icon='las la-times'
-                  :aria-label='$t(`admin.general.allowComments`)'
+                  :aria-label='t(`admin.general.allowComments`)'
                   :disable='cfg.readOnly'
                   )
             q-item(v-else)
@@ -364,34 +364,34 @@ q-page.admin-storage
       //- -----------------------
       //- Sync
       //- -----------------------
-      q-card.shadow-1.q-pb-sm.q-mt-md(v-if='target.sync && Object.keys(target.sync).length > 0')
+      q-card.shadow-1.q-pb-sm.q-mt-md(v-if='state.target.sync && Object.keys(state.target.sync).length > 0')
         q-card-section
-          .text-subtitle1 {{$t('admin.storage.sync')}}
+          .text-subtitle1 {{t('admin.storage.sync')}}
           q-banner.q-mt-md(
             rounded
             :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-2 text-grey-7`'
-            ) {{$t('admin.storage.noSyncModes')}}
+            ) {{t('admin.storage.noSyncModes')}}
 
       //- -----------------------
       //- Actions
       //- -----------------------
       q-card.shadow-1.q-pb-sm.q-mt-md
         q-card-section
-          .text-subtitle1 {{$t('admin.storage.actions')}}
+          .text-subtitle1 {{t('admin.storage.actions')}}
           q-banner.q-mt-md(
-            v-if='!target.actions || target.actions.length < 1'
+            v-if='!state.target.actions || state.target.actions.length < 1'
             rounded
             :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-2 text-grey-7`'
-            ) {{$t('admin.storage.noActions')}}
+            ) {{t('admin.storage.noActions')}}
           q-banner.q-mt-md(
-            v-else-if='!target.isEnabled'
+            v-else-if='!state.target.isEnabled'
             rounded
             :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-2 text-grey-7`'
-            ) {{$t('admin.storage.actionsInactiveWarn')}}
+            ) {{t('admin.storage.actionsInactiveWarn')}}
 
         template(
-          v-if='target.isEnabled'
-          v-for='(act, idx) in target.actions'
+          v-if='state.target.isEnabled'
+          v-for='(act, idx) in state.target.actions'
           )
           q-separator.q-my-sm(inset, v-if='idx > 0')
           q-item
@@ -406,32 +406,32 @@ q-page.admin-storage
                 icon='las la-arrow-circle-right'
                 color='primary'
                 @click=''
-                :label='$t(`common.actions.proceed`)'
+                :label='t(`common.actions.proceed`)'
               )
 
-    .col-auto(v-if='target')
+    .col-auto(v-if='state.target')
       //- -----------------------
       //- Infobox
       //- -----------------------
       q-card.rounded-borders.q-pb-md(style='width: 350px;')
         q-card-section
-          .text-subtitle1 {{target.title}}
+          .text-subtitle1 {{state.target.title}}
           q-img.q-mt-sm.rounded-borders(
             :src='target.banner'
             fit='cover'
             no-spinner
           )
-          .text-body2.q-mt-md {{target.description}}
+          .text-body2.q-mt-md {{state.target.description}}
         q-separator.q-mb-sm(inset)
         q-item
           q-item-section
-            q-item-label.text-grey {{$t(`admin.storage.vendor`)}}
-            q-item-label {{target.vendor}}
+            q-item-label.text-grey {{t(`admin.storage.vendor`)}}
+            q-item-label {{state.target.vendor}}
         q-separator.q-my-sm(inset)
         q-item
           q-item-section
-            q-item-label.text-grey {{$t(`admin.storage.vendorWebsite`)}}
-            q-item-label: a(:href='target.website', target='_blank', rel='noreferrer') {{target.website}}
+            q-item-label.text-grey {{t(`admin.storage.vendorWebsite`)}}
+            q-item-label: a(:href='state.target.website', target='_blank', rel='noreferrer') {{state.target.website}}
 
       //- -----------------------
       //- Status
@@ -439,25 +439,25 @@ q-page.admin-storage
       q-card.rounded-borders.q-pb-md.q-mt-md(style='width: 350px;')
         q-card-section
           .text-subtitle1 Status
-        template(v-if='target.module !== `db` && !(target.setup && target.setup.handler && target.setup.state !== `configured`)')
+        template(v-if='state.target.module !== `db` && !(state.target.setup && state.target.setup.handler && state.target.setup.state !== `configured`)')
           q-item(tag='label')
             q-item-section
-              q-item-label {{$t(`admin.storage.enabled`)}}
-              q-item-label(caption) {{$t(`admin.storage.enabledHint`)}}
-              q-item-label.text-deep-orange(v-if='target.module === `db`', caption) {{$t(`admin.storage.enabledForced`)}}
+              q-item-label {{t(`admin.storage.enabled`)}}
+              q-item-label(caption) {{t(`admin.storage.enabledHint`)}}
+              q-item-label.text-deep-orange(v-if='state.target.module === `db`', caption) {{t(`admin.storage.enabledForced`)}}
             q-item-section(avatar)
               q-toggle(
-                v-model='target.isEnabled'
-                :disable='target.module === `db` || (target.setup && target.setup.handler && target.setup.state !== `configured`)'
+                v-model='state.target.isEnabled'
+                :disable='state.target.module === `db` || (state.target.setup && state.target.setup.handler && state.target.setup.state !== `configured`)'
                 color='primary'
                 checked-icon='las la-check'
                 unchecked-icon='las la-times'
-                :aria-label='$t(`admin.general.allowSearch`)'
+                :aria-label='t(`admin.general.allowSearch`)'
                 )
           q-separator.q-my-sm(inset)
         q-item
           q-item-section
-            q-item-label.text-grey {{$t(`admin.storage.currentState`)}}
+            q-item-label.text-grey {{t(`admin.storage.currentState`)}}
             q-item-label.text-positive No issues detected.
 
       //- -----------------------
@@ -465,50 +465,50 @@ q-page.admin-storage
       //- -----------------------
       q-card.rounded-borders.q-pb-md.q-mt-md(style='width: 350px;')
         q-card-section
-          .text-subtitle1 {{$t(`admin.storage.versioning`)}}
-          .text-body2.text-grey {{$t(`admin.storage.versioningHint`)}}
-        q-item(:tag='target.versioning.isSupported ? `label` : null')
+          .text-subtitle1 {{t(`admin.storage.versioning`)}}
+          .text-body2.text-grey {{t(`admin.storage.versioningHint`)}}
+        q-item(:tag='state.target.versioning.isSupported ? `label` : null')
           q-item-section
-            q-item-label {{$t(`admin.storage.useVersioning`)}}
-            q-item-label(caption) {{$t(`admin.storage.useVersioningHint`)}}
-            q-item-label.text-deep-orange(v-if='!target.versioning.isSupported', caption) {{$t(`admin.storage.versioningNotSupported`)}}
-            q-item-label.text-deep-orange(v-if='target.versioning.isForceEnabled', caption) {{$t(`admin.storage.versioningForceEnabled`)}}
+            q-item-label {{t(`admin.storage.useVersioning`)}}
+            q-item-label(caption) {{t(`admin.storage.useVersioningHint`)}}
+            q-item-label.text-deep-orange(v-if='!state.target.versioning.isSupported', caption) {{t(`admin.storage.versioningNotSupported`)}}
+            q-item-label.text-deep-orange(v-if='state.target.versioning.isForceEnabled', caption) {{t(`admin.storage.versioningForceEnabled`)}}
           q-item-section(avatar)
             q-toggle(
-              v-model='target.versioning.enabled'
-              :disable='!target.versioning.isSupported || target.versioning.isForceEnabled'
+              v-model='state.target.versioning.enabled'
+              :disable='!state.target.versioning.isSupported || state.target.versioning.isForceEnabled'
               color='primary'
               checked-icon='las la-check'
               unchecked-icon='las la-times'
-              :aria-label='$t(`admin.storage.useVersioning`)'
+              :aria-label='t(`admin.storage.useVersioning`)'
               )
 
   //- ==========================================
   //- DELIVERY PATHS
   //- ==========================================
 
-  .row.q-pa-md.q-col-gutter-md(v-if='displayMode === `delivery`')
+  .row.q-pa-md.q-col-gutter-md(v-if='state.displayMode === `delivery`')
     .col
       q-card.rounded-borders
         q-card-section.flex.items-center
-          .text-caption.q-mr-sm {{ $t('admin.storage.deliveryPathsLegend') }}
+          .text-caption.q-mr-sm {{ t('admin.storage.deliveryPathsLegend') }}
           q-chip(square, dense, color='blue-1', text-color='blue-8')
             q-avatar(icon='las la-ellipsis-h', color='blue', text-color='white')
-            span.text-caption.q-px-sm {{ $t('admin.storage.deliveryPathsUserRequest') }}
+            span.text-caption.q-px-sm {{ t('admin.storage.deliveryPathsUserRequest') }}
           q-chip(square, dense, color='teal-1', text-color='teal-8')
             q-avatar(icon='las la-ellipsis-h', color='positive', text-color='white')
-            span.text-caption.q-px-sm {{ $t('admin.storage.deliveryPathsPushToOrigin') }}
+            span.text-caption.q-px-sm {{ t('admin.storage.deliveryPathsPushToOrigin') }}
           q-chip(square, dense, color='red-1', text-color='red-8')
             q-avatar(icon='las la-minus', color='negative', text-color='white')
-            span.text-caption.q-px-sm {{ $t('admin.storage.missingOrigin') }}
+            span.text-caption.q-px-sm {{ t('admin.storage.missingOrigin') }}
         q-separator
         v-network-graph(
           :zoom-level='2'
-          :configs='deliveryConfig'
-          :nodes='deliveryNodes'
-          :edges='deliveryEdges'
-          :paths='deliveryPaths'
-          :layouts='deliveryLayouts'
+          :configs='state.deliveryConfig'
+          :nodes='state.deliveryNodes'
+          :edges='state.deliveryEdges'
+          :paths='state.deliveryPaths'
+          :layouts='state.deliveryLayouts'
           style='height: 600px;'
           )
           template(#override-node='{ nodeId, scale, config, ...slotProps }')
@@ -522,57 +522,57 @@ q-page.admin-storage
               v-bind='slotProps'
               )
             image(
-              v-if='deliveryNodes[nodeId].icon && deliveryNodes[nodeId].icon.endsWith(`.svg`)'
+              v-if='state.deliveryNodes[nodeId].icon && state.deliveryNodes[nodeId].icon.endsWith(`.svg`)'
               :x='(-config.radius + 5) * scale'
               :y='(-config.radius + 5) * scale'
               :width='(config.radius - 5) * scale * 2'
               :height='(config.radius - 5) * scale * 2'
-              :xlink:href='deliveryNodes[nodeId].icon'
+              :xlink:href='state.deliveryNodes[nodeId].icon'
             )
             text(
-              v-if='deliveryNodes[nodeId].icon && deliveryNodes[nodeId].iconText'
-              :class='deliveryNodes[nodeId].icon'
+              v-if='state.deliveryNodes[nodeId].icon && state.deliveryNodes[nodeId].iconText'
+              :class='state.deliveryNodes[nodeId].icon'
               :font-size='22 * scale'
               fill='#ffffff'
               text-anchor='middle'
               dominant-baseline='central'
-              v-html='deliveryNodes[nodeId].iconText'
+              v-html='state.deliveryNodes[nodeId].iconText'
             )
 
-  //-             .overline.my-5 {{$t('admin.storage.syncDirection')}}
-  //-             .body-2.ml-3 {{$t('admin.storage.syncDirectionSubtitle')}}
+  //-             .overline.my-5 {{t('admin.storage.syncDirection')}}
+  //-             .body-2.ml-3 {{t('admin.storage.syncDirectionSubtitle')}}
   //-             .pr-3.pt-3
   //-               v-radio-group.ml-3.py-0(v-model='target.mode')
   //-                 v-radio(
-  //-                   :label='$t(`admin.storage.syncDirBi`)'
+  //-                   :label='t(`admin.storage.syncDirBi`)'
   //-                   color='primary'
   //-                   value='sync'
   //-                   :disabled='target.supportedModes.indexOf(`sync`) < 0'
   //-                 )
   //-                 v-radio(
-  //-                   :label='$t(`admin.storage.syncDirPush`)'
+  //-                   :label='t(`admin.storage.syncDirPush`)'
   //-                   color='primary'
   //-                   value='push'
   //-                   :disabled='target.supportedModes.indexOf(`push`) < 0'
   //-                 )
   //-                 v-radio(
-  //-                   :label='$t(`admin.storage.syncDirPull`)'
+  //-                   :label='t(`admin.storage.syncDirPull`)'
   //-                   color='primary'
   //-                   value='pull'
   //-                   :disabled='target.supportedModes.indexOf(`pull`) < 0'
   //-                 )
   //-             .body-2.ml-3
-  //-               strong {{$t('admin.storage.syncDirBi')}} #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`sync`) < 0') {{$t('admin.storage.unsupported')}}]
-  //-               .pb-3 {{$t('admin.storage.syncDirBiHint')}}
-  //-               strong {{$t('admin.storage.syncDirPush')}} #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`push`) < 0') {{$t('admin.storage.unsupported')}}]
-  //-               .pb-3 {{$t('admin.storage.syncDirPushHint')}}
-  //-               strong {{$t('admin.storage.syncDirPull')}} #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`pull`) < 0') {{$t('admin.storage.unsupported')}}]
-  //-               .pb-3 {{$t('admin.storage.syncDirPullHint')}}
+  //-               strong {{t('admin.storage.syncDirBi')}} #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`sync`) < 0') {{t('admin.storage.unsupported')}}]
+  //-               .pb-3 {{t('admin.storage.syncDirBiHint')}}
+  //-               strong {{t('admin.storage.syncDirPush')}} #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`push`) < 0') {{t('admin.storage.unsupported')}}]
+  //-               .pb-3 {{t('admin.storage.syncDirPushHint')}}
+  //-               strong {{t('admin.storage.syncDirPull')}} #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`pull`) < 0') {{t('admin.storage.unsupported')}}]
+  //-               .pb-3 {{t('admin.storage.syncDirPullHint')}}
 
   //-             template(v-if='target.hasSchedule')
   //-               v-divider.mt-3
-  //-               .overline.my-5 {{$t('admin.storage.syncSchedule')}}
-  //-               .body-2.ml-3 {{$t('admin.storage.syncScheduleHint')}}
+  //-               .overline.my-5 {{t('admin.storage.syncSchedule')}}
+  //-               .body-2.ml-3 {{t('admin.storage.syncScheduleHint')}}
   //-               .pa-3
   //-                 duration-picker(v-model='target.syncInterval')
   //-                 i18next.caption.mt-3(path='admin.storage.syncScheduleCurrent', tag='div')
@@ -582,604 +582,662 @@ q-page.admin-storage
 
 </template>
 
-<script>
+<script setup>
 import find from 'lodash/find'
 import cloneDeep from 'lodash/cloneDeep'
 import gql from 'graphql-tag'
-import { get } from 'vuex-pathify'
 import transform from 'lodash/transform'
-import * as vNG from 'v-network-graph'
+import * as VNetworkGraph from 'v-network-graph'
+
+import { useI18n } from 'vue-i18n'
+import { useMeta, useQuasar } from 'quasar'
+import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+
+import { useAdminStore } from 'src/stores/admin'
+import { useSiteStore } from 'src/stores/site'
+import { useDataStore } from 'src/stores/data'
 
 import GithubSetupInstallDialog from '../components/GithubSetupInstallDialog.vue'
 
-export default {
-  data () {
-    return {
-      loading: 0,
-      displayMode: 'targets',
-      runningAction: false,
-      runningActionHandler: '',
-      selectedTarget: '',
-      desiredTarget: '',
-      target: null,
-      targets: [],
-      setupCfg: {
-        action: '',
-        manifest: '',
-        loading: false
-      },
-      deliveryNodes: {},
-      deliveryEdges: {},
-      deliveryLayouts: {
-        nodes: {}
-      },
-      deliveryPaths: [],
-      deliveryConfig: vNG.defineConfigs({
-        view: {
-          layoutHandler: new vNG.GridLayout({ grid: 15 }),
-          fit: true,
-          mouseWheelZoomEnabled: false,
-          grid: {
-            visible: true,
-            interval: 2.5,
-            thickIncrements: 0
-          }
-        },
-        node: {
-          draggable: false,
-          selectable: true,
-          normal: {
-            type: 'rect',
-            color: node => node.color || '#1976D2',
-            borderRadius: node => node.borderRadius || 5
-          },
-          label: {
-            margin: 8
-          }
-        },
-        edge: {
-          normal: {
-            width: 3,
-            dasharray: edge => edge.animate === false ? 20 : 3,
-            animate: edge => !(edge.animate === false),
-            animationSpeed: edge => edge.animationSpeed || 50,
-            color: edge => edge.color || '#1976D2'
-          },
-          type: 'straight',
-          gap: 7,
-          margin: 4,
-          marker: {
-            source: {
-              type: 'none'
-            },
-            target: {
-              type: 'none'
-            }
-          }
-        },
-        path: {
-          visible: true,
-          end: 'edgeOfNode',
-          margin: 4,
-          path: {
-            width: 7,
-            color: p => p.color,
-            linecap: 'square'
-          }
-        }
-      })
-    }
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const adminStore = useAdminStore()
+const siteStore = useSiteStore()
+const dataStore = useDataStore()
+
+// ROUTER
+
+const router = useRouter()
+const route = useRoute()
+
+// I18N
+
+const { t } = useI18n()
+
+// META
+
+useMeta({
+  title: t('admin.storage.title')
+})
+
+// DATA
+
+const state = reactive({
+  loading: 0,
+  displayMode: 'targets',
+  runningAction: false,
+  runningActionHandler: '',
+  selectedTarget: '',
+  desiredTarget: '',
+  target: null,
+  targets: [],
+  setupCfg: {
+    action: '',
+    manifest: '',
+    loading: false
   },
-  computed: {
-    currentSiteId: get('admin/currentSiteId', false)
+  deliveryNodes: {},
+  deliveryEdges: {},
+  deliveryLayouts: {
+    nodes: {}
   },
-  watch: {
-    async currentSiteId (newValue) {
-      await this.load()
-      this.$nextTick(() => {
-        this.$router.replace(`/_admin/${newValue}/storage/${this.selectedTarget}`)
-      })
-    },
-    displayMode (newValue) {
-      if (newValue === 'delivery') {
-        this.generateGraph()
+  deliveryPaths: [],
+  deliveryConfig: VNetworkGraph.defineConfigs({
+    view: {
+      layoutHandler: new VNetworkGraph.GridLayout({ grid: 15 }),
+      fit: true,
+      mouseWheelZoomEnabled: false,
+      grid: {
+        visible: true,
+        interval: 2.5,
+        thickIncrements: 0
       }
     },
-    selectedTarget (newValue) {
-      this.target = find(this.targets, ['id', newValue]) || null
+    node: {
+      draggable: false,
+      selectable: true,
+      normal: {
+        type: 'rect',
+        color: node => node.color || '#1976D2',
+        borderRadius: node => node.borderRadius || 5
+      },
+      label: {
+        margin: 8
+      }
     },
-    targets (newValue) {
-      if (newValue && newValue.length > 0) {
-        if (this.desiredTarget) {
-          this.selectedTarget = this.desiredTarget
-          this.desiredTarget = ''
-        } else {
-          this.selectedTarget = find(this.targets, ['module', 'db'])?.id || null
-          if (!this.$route.params.id) {
-            this.$router.replace(`/_admin/${this.currentSiteId}/storage/${this.selectedTarget}`)
-          }
+    edge: {
+      normal: {
+        width: 3,
+        dasharray: edge => edge.animate === false ? 20 : 3,
+        animate: edge => !(edge.animate === false),
+        animationSpeed: edge => edge.animationSpeed || 50,
+        color: edge => edge.color || '#1976D2'
+      },
+      type: 'straight',
+      gap: 7,
+      margin: 4,
+      marker: {
+        source: {
+          type: 'none'
+        },
+        target: {
+          type: 'none'
         }
-        this.handleSetupCallback()
       }
     },
-    $route (to, from) {
-      if (!to.params.id) {
-        return
-      }
-      if (this.targets.length < 1) {
-        this.desiredTarget = to.params.id
-      } else {
-        this.selectedTarget = to.params.id
+    path: {
+      visible: true,
+      end: 'edgeOfNode',
+      margin: 4,
+      path: {
+        width: 7,
+        color: p => p.color,
+        linecap: 'square'
       }
     }
-  },
-  mounted () {
-    if (!this.selectedTarget && this.$route.params.id) {
-      if (this.targets.length < 1) {
-        this.desiredTarget = this.$route.params.id
-      } else {
-        this.selectedTarget = this.$route.params.id
+  })
+})
+
+// REFS
+
+const githubSetupForm = ref(null)
+
+// WATCHERS
+
+watch(() => adminStore.currentSiteId, async (newValue) => {
+  await load()
+  nextTick(() => {
+    router.replace(`/_admin/${newValue}/storage/${state.selectedTarget}`)
+  })
+})
+watch(() => state.displayMode, (newValue) => {
+  if (newValue === 'delivery') {
+    generateGraph()
+  }
+})
+watch(() => state.selectedTarget, (newValue) => {
+  state.target = find(state.targets, ['id', newValue]) || null
+})
+watch(() => state.targets, (newValue) => {
+  if (newValue && newValue.length > 0) {
+    if (state.desiredTarget) {
+      state.selectedTarget = state.desiredTarget
+      state.desiredTarget = ''
+    } else {
+      state.selectedTarget = find(state.targets, ['module', 'db'])?.id || null
+      if (!route.params.id) {
+        router.replace(`/_admin/${adminStore.currentSiteId}/storage/${state.selectedTarget}`)
       }
     }
-    if (this.currentSiteId) {
-      this.load()
-    }
-    this.handleSetupCallback()
-  },
-  methods: {
-    async load () {
-      this.loading++
-      this.$q.loading.show()
-      const resp = await this.$apollo.query({
-        query: gql`
-          query getStorageTargets (
-            $siteId: UUID!
+    handleSetupCallback()
+  }
+})
+watch(() => route, (to, from) => {
+  if (!to.params.id) {
+    return
+  }
+  if (state.targets.length < 1) {
+    state.desiredTarget = to.params.id
+  } else {
+    state.selectedTarget = to.params.id
+  }
+})
+
+// METHODS
+
+async function load () {
+  state.loading++
+  $q.loading.show()
+  try {
+    const resp = await APOLLO_CLIENT.query({
+      query: gql`
+        query getStorageTargets (
+          $siteId: UUID!
+        ) {
+        storageTargets (
+          siteId: $siteId
+        ) {
+          id
+          isEnabled
+          module
+          title
+          description
+          icon
+          banner
+          vendor
+          website
+          contentTypes
+          assetDelivery
+          versioning
+          sync
+          status
+          setup
+          config
+          actions
+        }
+      }`,
+      variables: {
+        siteId: adminStore.currentSiteId
+      },
+      fetchPolicy: 'network-only'
+    })
+    state.targets = cloneDeep(resp?.data?.storageTargets)
+  } catch (err) {
+    $q.notify({
+      type: 'negative',
+      message: 'Failed to load storage configuration.',
+      caption: err.message,
+      timeout: 20000
+    })
+  }
+  $q.loading.hide()
+  state.loading--
+}
+
+function configIfCheck (ifs) {
+  if (!ifs || ifs.length < 1) { return true }
+  return ifs.every(s => state.target.config[s.key]?.value === s.eq)
+}
+
+async function refresh () {
+  await load()
+  $q.notify({
+    type: 'positive',
+    message: 'List of storage targets has been refreshed.'
+  })
+}
+
+async function save ({ silent }) {
+  let saveSuccess = false
+  if (!silent) { $q.loading.show() }
+  try {
+    const resp = await APOLLO_CLIENT.mutate({
+      mutation: gql`
+        mutation (
+          $siteId: UUID!
+          $targets: [StorageTargetInput]!
           ) {
-          storageTargets (
+          updateStorageTargets(
             siteId: $siteId
+            targets: $targets
           ) {
-            id
-            isEnabled
-            module
-            title
-            description
-            icon
-            banner
-            vendor
-            website
-            contentTypes
-            assetDelivery
-            versioning
-            sync
-            status
-            setup
-            config
-            actions
-          }
-        }`,
-        variables: {
-          siteId: this.currentSiteId
-        },
-        fetchPolicy: 'network-only'
-      })
-      this.targets = cloneDeep(resp?.data?.storageTargets)
-      this.$q.loading.hide()
-      this.loading--
-    },
-    configIfCheck (ifs) {
-      if (!ifs || ifs.length < 1) { return true }
-      return ifs.every(s => this.target.config[s.key]?.value === s.eq)
-    },
-    async refresh () {
-      await this.$apollo.queries.targets.refetch()
-      this.$store.commit('showNotification', {
-        message: 'List of storage targets has been refreshed.',
-        style: 'success',
-        icon: 'cached'
-      })
-    },
-    async save ({ silent }) {
-      let saveSuccess = false
-      if (!silent) { this.$q.loading.show() }
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation (
-              $siteId: UUID!
-              $targets: [StorageTargetInput]!
-              ) {
-              updateStorageTargets(
-                siteId: $siteId
-                targets: $targets
-              ) {
-                status {
-                  succeeded
-                  message
-                }
-              }
+            status {
+              succeeded
+              message
             }
-          `,
-          variables: {
-            siteId: this.currentSiteId,
-            targets: this.targets.map(tgt => ({
-              id: tgt.id,
-              module: tgt.module,
-              isEnabled: tgt.isEnabled,
-              contentTypes: tgt.contentTypes.activeTypes,
-              largeThreshold: tgt.contentTypes.largeThreshold,
-              assetDeliveryFileStreaming: tgt.assetDelivery.streaming,
-              assetDeliveryDirectAccess: tgt.assetDelivery.directAccess,
-              useVersioning: tgt.versioning.enabled,
-              config: transform(tgt.config, (r, v, k) => { r[k] = v.value }, {})
-            }))
-          }
-        })
-        if (resp?.data?.updateStorageTargets?.status?.succeeded) {
-          saveSuccess = true
-          if (!silent) {
-            this.$q.notify({
-              type: 'positive',
-              message: this.$t('admin.storage.saveSuccess')
-            })
           }
-        } else {
-          throw new Error(resp?.data?.updateStorageTargets?.status?.message || 'Unexpected error')
         }
-      } catch (err) {
-        this.$q.notify({
-          type: 'negative',
-          message: this.$t('admin.storage.saveFailed'),
-          caption: err.message
-        })
-      }
-      if (!silent) { this.$q.loading.hide() }
-      return saveSuccess
-    },
-    getTargetSubtitle (target) {
-      if (!target.isEnabled) {
-        return this.$t('admin.storage.inactiveTarget')
-      }
-      const hasPages = target.contentTypes?.activeTypes?.includes('pages')
-      const hasAssets = target.contentTypes?.activeTypes?.filter(c => c !== 'pages')?.length > 0
-      if (hasPages && hasAssets) {
-        return this.$t('admin.storage.pagesAndAssets')
-      } else if (hasPages) {
-        return this.$t('admin.storage.pagesOnly')
-      } else if (hasAssets) {
-        return this.$t('admin.storage.assetsOnly')
-      } else {
-        return this.$t('admin.storage.notConfigured')
-      }
-    },
-    getTargetSubtitleColor (target) {
-      if (this.selectedTarget === target.id) {
-        return 'text-blue-2'
-      } else if (target.isEnabled) {
-        return 'text-positive'
-      } else {
-        return 'text-grey-7'
+      `,
+      variables: {
+        siteId: adminStore.currentSiteId,
+        targets: state.targets.map(tgt => ({
+          id: tgt.id,
+          module: tgt.module,
+          isEnabled: tgt.isEnabled,
+          contentTypes: tgt.contentTypes.activeTypes,
+          largeThreshold: tgt.contentTypes.largeThreshold,
+          assetDeliveryFileStreaming: tgt.assetDelivery.streaming,
+          assetDeliveryDirectAccess: tgt.assetDelivery.directAccess,
+          useVersioning: tgt.versioning.enabled,
+          config: transform(tgt.config, (r, v, k) => { r[k] = v.value }, {})
+        }))
       }
-    },
-    getDefaultSchedule (val) {
-      if (!val) { return 'N/A' }
-      return '' // moment.duration(val).format('y [years], M [months], d [days], h [hours], m [minutes]')
-    },
-    async executeAction (targetKey, handler) {
-      this.$store.commit('loadingStart', 'admin-storage-executeaction')
-      this.runningAction = true
-      this.runningActionHandler = handler
-      try {
-        await this.$apollo.mutate({
-          mutation: gql`{}`,
-          variables: {
-            targetKey,
-            handler
-          }
-        })
-        this.$store.commit('showNotification', {
-          message: 'Action completed.',
-          style: 'success',
-          icon: 'check'
+    })
+    if (resp?.data?.updateStorageTargets?.status?.succeeded) {
+      saveSuccess = true
+      if (!silent) {
+        $q.notify({
+          type: 'positive',
+          message: t('admin.storage.saveSuccess')
         })
-      } catch (err) {
-        console.warn(err)
       }
-      this.runningAction = false
-      this.runningActionHandler = ''
-      this.$store.commit('loadingStop', 'admin-storage-executeaction')
-    },
-    async handleSetupCallback () {
-      if (this.targets.length < 1 || !this.selectedTarget) { return }
+    } else {
+      throw new Error(resp?.data?.updateStorageTargets?.status?.message || 'Unexpected error')
+    }
+  } catch (err) {
+    $q.notify({
+      type: 'negative',
+      message: t('admin.storage.saveFailed'),
+      caption: err.message
+    })
+  }
+  if (!silent) { $q.loading.hide() }
+  return saveSuccess
+}
 
-      this.$nextTick(() => {
-        if (this.target?.setup?.handler === 'github' && this.$route.query.code) {
-          this.setupGitHubStep('connect', this.$route.query.code)
-        }
-      })
-    },
-    async setupDestroy () {
-      this.$q.dialog({
-        title: this.$t('admin.storage.destroyConfirm'),
-        message: this.$t('admin.storage.destroyConfirmInfo'),
-        cancel: true,
-        persistent: true
-      }).onOk(async () => {
-        this.$q.loading.show({
-          message: this.$t('admin.storage.destroyingSetup')
-        })
+function getTargetSubtitle (target) {
+  if (!target.isEnabled) {
+    return t('admin.storage.inactiveTarget')
+  }
+  const hasPages = target.contentTypes?.activeTypes?.includes('pages')
+  const hasAssets = target.contentTypes?.activeTypes?.filter(c => c !== 'pages')?.length > 0
+  if (hasPages && hasAssets) {
+    return t('admin.storage.pagesAndAssets')
+  } else if (hasPages) {
+    return t('admin.storage.pagesOnly')
+  } else if (hasAssets) {
+    return t('admin.storage.assetsOnly')
+  } else {
+    return t('admin.storage.notConfigured')
+  }
+}
+
+function getTargetSubtitleColor (target) {
+  if (state.selectedTarget === target.id) {
+    return 'text-blue-2'
+  } else if (target.isEnabled) {
+    return 'text-positive'
+  } else {
+    return 'text-grey-7'
+  }
+}
+
+function getDefaultSchedule (val) {
+  if (!val) { return 'N/A' }
+  return '' // moment.duration(val).format('y [years], M [months], d [days], h [hours], m [minutes]')
+}
+
+async function executeAction (targetKey, handler) {
+  // this.$store.commit('loadingStart', 'admin-storage-executeaction')
+  // this.runningAction = true
+  // this.runningActionHandler = handler
+  // try {
+  //   await this.$apollo.mutate({
+  //     mutation: gql`{}`,
+  //     variables: {
+  //       targetKey,
+  //       handler
+  //     }
+  //   })
+  //   this.$store.commit('showNotification', {
+  //     message: 'Action completed.',
+  //     style: 'success',
+  //     icon: 'check'
+  //   })
+  // } catch (err) {
+  //   console.warn(err)
+  // }
+  // this.runningAction = false
+  // this.runningActionHandler = ''
+  // this.$store.commit('loadingStop', 'admin-storage-executeaction')
+}
 
-        try {
-          const resp = await this.$apollo.mutate({
-            mutation: gql`
-              mutation (
-                $targetId: UUID!
-                ) {
-                destroyStorageTargetSetup(
-                  targetId: $targetId
-                ) {
-                  status {
-                    succeeded
-                    message
-                  }
-                }
+async function handleSetupCallback () {
+  if (state.targets.length < 1 || !state.selectedTarget) { return }
+
+  nextTick(() => {
+    if (state.target?.setup?.handler === 'github' && route.query.code) {
+      setupGitHubStep('connect', route.query.code)
+    }
+  })
+}
+
+async function setupDestroy () {
+  $q.dialog({
+    title: t('admin.storage.destroyConfirm'),
+    message: t('admin.storage.destroyConfirmInfo'),
+    cancel: true,
+    persistent: true
+  }).onOk(async () => {
+    $q.loading.show({
+      message: t('admin.storage.destroyingSetup')
+    })
+
+    try {
+      const resp = await APOLLO_CLIENT.mutate({
+        mutation: gql`
+          mutation (
+            $targetId: UUID!
+            ) {
+            destroyStorageTargetSetup(
+              targetId: $targetId
+            ) {
+              status {
+                succeeded
+                message
               }
-            `,
-            variables: {
-              targetId: this.selectedTarget
             }
-          })
-          if (resp?.data?.destroyStorageTargetSetup?.status?.succeeded) {
-            this.target.setup.state = 'notconfigured'
-            setTimeout(() => {
-              this.$q.loading.hide()
-              this.$q.notify({
-                type: 'positive',
-                message: this.$t('admin.storage.githubSetupDestroySuccess')
-              })
-            }, 2000)
-          } else {
-            throw new Error(resp?.data?.destroyStorageTargetSetup?.status?.message || 'Unexpected error')
           }
-        } catch (err) {
-          this.$q.notify({
-            type: 'negative',
-            message: this.$t('admin.storage.githubSetupDestroyFailed'),
-            caption: err.message
-          })
-          this.$q.loading.hide()
+        `,
+        variables: {
+          targetId: state.selectedTarget
         }
       })
-    },
-    async setupGitHub () {
-      // -> Format values
-      this.target.setup.values.publicUrl = this.target.setup.values.publicUrl.toLowerCase()
-
-      // -> Basic input check
-      if (this.target.setup.values.accountType === 'org' && this.target.setup.values.org.length < 1) {
-        return this.$q.notify({
-          type: 'negative',
-          message: 'Invalid GitHub Organization',
-          caption: 'Enter a valid github organization.'
-        })
-      }
-      if (this.target.setup.values.publicUrl.length < 11 || !/^https?:\/\/.{4,}$/.test(this.target.setup.values.publicUrl)) {
-        return this.$q.notify({
-          type: 'negative',
-          message: 'Invalid Wiki Public URL',
-          caption: 'Enter a valid public URL for your wiki.'
-        })
-      }
-
-      if (this.target.setup.values.publicUrl.endsWith('/')) {
-        this.target.setup.values.publicUrl = this.target.setup.values.publicUrl.slice(0, -1)
-      }
-
-      // -> Generate manifest
-      this.setupCfg.loading = true
-      if (this.target.setup.values.accountType === 'org') {
-        this.setupCfg.action = `https://github.com/organizations/${this.target.setup.values.org}/settings/apps/new`
+      if (resp?.data?.destroyStorageTargetSetup?.status?.succeeded) {
+        state.target.setup.state = 'notconfigured'
+        setTimeout(() => {
+          $q.loading.hide()
+          $q.notify({
+            type: 'positive',
+            message: t('admin.storage.githubSetupDestroySuccess')
+          })
+        }, 2000)
       } else {
-        this.setupCfg.action = 'https://github.com/settings/apps/new'
+        throw new Error(resp?.data?.destroyStorageTargetSetup?.status?.message || 'Unexpected error')
       }
-      this.setupCfg.manifest = JSON.stringify({
-        name: `Wiki.js - ${this.currentSiteId.slice(-12)}`,
-        description: 'Connects your Wiki.js to GitHub repositories and synchronize their contents.',
-        url: this.target.setup.values.publicUrl,
-        hook_attributes: {
-          url: `${this.target.setup.values.publicUrl}/_github/${this.currentSiteId}/events`
-        },
-        redirect_url: `${this.target.setup.values.publicUrl}/_admin/${this.currentSiteId}/storage/${this.target.id}`,
-        callback_urls: [
-          `${this.target.setup.values.publicUrl}/_admin/${this.currentSiteId}/storage/${this.target.id}`
-        ],
-        public: false,
-        default_permissions: {
-          contents: 'write',
-          metadata: 'read',
-          members: 'read'
-        },
-        default_events: [
-          'create',
-          'delete',
-          'push'
-        ]
-      })
-      this.$q.loading.show({
-        message: this.$t('admin.storage.githubPreparingManifest')
+    } catch (err) {
+      $q.notify({
+        type: 'negative',
+        message: t('admin.storage.githubSetupDestroyFailed'),
+        caption: err.message
       })
-      if (await this.save({ silent: true })) {
-        this.$refs.githubSetupForm.submit()
-      } else {
-        this.setupCfg.loading = false
-        this.$q.loading.hide()
-      }
+      $q.loading.hide()
+    }
+  })
+}
+
+async function setupGitHub () {
+  // -> Format values
+  state.target.setup.values.publicUrl = state.target.setup.values.publicUrl.toLowerCase()
+
+  // -> Basic input check
+  if (state.target.setup.values.accountType === 'org' && state.target.setup.values.org.length < 1) {
+    return $q.notify({
+      type: 'negative',
+      message: 'Invalid GitHub Organization',
+      caption: 'Enter a valid github organization.'
+    })
+  }
+  if (state.target.setup.values.publicUrl.length < 11 || !/^https?:\/\/.{4,}$/.test(state.target.setup.values.publicUrl)) {
+    return $q.notify({
+      type: 'negative',
+      message: 'Invalid Wiki Public URL',
+      caption: 'Enter a valid public URL for your wiki.'
+    })
+  }
+
+  if (state.target.setup.values.publicUrl.endsWith('/')) {
+    state.target.setup.values.publicUrl = state.target.setup.values.publicUrl.slice(0, -1)
+  }
+
+  // -> Generate manifest
+  state.setupCfg.loading = true
+  if (state.target.setup.values.accountType === 'org') {
+    state.setupCfg.action = `https://github.com/organizations/${state.target.setup.values.org}/settings/apps/new`
+  } else {
+    state.setupCfg.action = 'https://github.com/settings/apps/new'
+  }
+  state.setupCfg.manifest = JSON.stringify({
+    name: `Wiki.js - ${adminStore.currentSiteId.slice(-12)}`,
+    description: 'Connects your Wiki.js to GitHub repositories and synchronize their contents.',
+    url: state.target.setup.values.publicUrl,
+    hook_attributes: {
+      url: `${state.target.setup.values.publicUrl}/_github/${adminStore.currentSiteId}/events`
     },
-    async setupGitHubStep (step, code) {
-      this.$q.loading.show({
-        message: this.$t('admin.storage.githubVerifying')
-      })
+    redirect_url: `${state.target.setup.values.publicUrl}/_admin/${adminStore.currentSiteId}/storage/${state.target.id}`,
+    callback_urls: [
+      `${state.target.setup.values.publicUrl}/_admin/${adminStore.currentSiteId}/storage/${state.target.id}`
+    ],
+    public: false,
+    default_permissions: {
+      contents: 'write',
+      metadata: 'read',
+      members: 'read'
+    },
+    default_events: [
+      'create',
+      'delete',
+      'push'
+    ]
+  })
+  $q.loading.show({
+    message: t('admin.storage.githubPreparingManifest')
+  })
+  if (await save({ silent: true })) {
+    githubSetupForm.value.submit()
+  } else {
+    state.setupCfg.loading = false
+    $q.loading.hide()
+  }
+}
 
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation (
-              $targetId: UUID!
-              $state: JSON!
-              ) {
-              setupStorageTarget(
-                targetId: $targetId
-                state: $state
-              ) {
-                status {
-                  succeeded
-                  message
-                }
-                state
-              }
-            }
-          `,
-          variables: {
-            targetId: this.selectedTarget,
-            state: {
-              step,
-              ...code && { code }
-            }
-          }
-        })
-        if (resp?.data?.setupStorageTarget?.status?.succeeded) {
-          switch (resp.data.setupStorageTarget.state?.nextStep) {
-            case 'installApp': {
-              this.$router.replace({ query: null })
-              this.$q.loading.hide()
-
-              this.$q.dialog({
-                component: GithubSetupInstallDialog,
-                persistent: true
-              }).onOk(() => {
-                this.$q.loading.show({
-                  message: this.$t('admin.storage.githubRedirecting')
-                })
-                window.location.assign(resp.data.setupStorageTarget.state?.url)
-              }).onCancel(() => {
-                throw new Error('Setup was aborted prematurely.')
-              })
-              break
-            }
-            case 'completed': {
-              this.target.isEnabled = true
-              this.target.setup.state = 'configured'
-              setTimeout(() => {
-                this.$q.loading.hide()
-                this.$q.notify({
-                  type: 'positive',
-                  message: this.$t('admin.storage.githubSetupSuccess')
-                })
-              }, 2000)
-              break
-            }
-            default: {
-              throw new Error('Unknown Setup Step')
+async function setupGitHubStep (step, code) {
+  $q.loading.show({
+    message: t('admin.storage.githubVerifying')
+  })
+
+  try {
+    const resp = await APOLLO_CLIENT.mutate({
+      mutation: gql`
+        mutation (
+          $targetId: UUID!
+          $state: JSON!
+          ) {
+          setupStorageTarget(
+            targetId: $targetId
+            state: $state
+          ) {
+            status {
+              succeeded
+              message
             }
+            state
           }
-        } else {
-          throw new Error(resp?.data?.setupStorageTarget?.status?.message || 'Unexpected error')
         }
-      } catch (err) {
-        this.$q.loading.hide()
-        this.$q.notify({
-          type: 'negative',
-          message: this.$t('admin.storage.githubSetupFailed'),
-          caption: err.message
-        })
-      }
-    },
-    generateGraph () {
-      const types = [
-        { key: 'images', label: this.$t('admin.storage.contentTypeImages'), icon: 'las', iconText: '&#xf1c5;' },
-        { key: 'documents', label: this.$t('admin.storage.contentTypeDocuments'), icon: 'las', iconText: '&#xf1c1;' },
-        { key: 'others', label: this.$t('admin.storage.contentTypeOthers'), icon: 'las', iconText: '&#xf15b;' },
-        { key: 'large', label: this.$t('admin.storage.contentTypeLargeFiles'), icon: 'las', iconText: '&#xf1c6;' }
-      ]
-
-      // -> Create PagesNodes
-
-      this.deliveryNodes = {
-        user: { name: this.$t('admin.storage.deliveryPathsUser'), borderRadius: 16, icon: '/_assets/icons/fluent-account.svg' },
-        pages: { name: this.$t('admin.storage.contentTypePages'), color: '#3f51b5', icon: 'las', iconText: '&#xf15c;' },
-        pages_wiki: { name: 'Wiki.js', icon: '/_assets/logo-wikijs.svg', color: '#161b22' }
-      }
-      this.deliveryEdges = {
-        user_pages: { source: 'user', target: 'pages' },
-        pages_in: { source: 'pages', target: 'pages_wiki' },
-        pages_out: { source: 'pages_wiki', target: 'pages' }
+      `,
+      variables: {
+        targetId: this.selectedTarget,
+        state: {
+          step,
+          ...code && { code }
+        }
       }
-      this.deliveryLayouts.nodes = {
-        user: { x: -30, y: 30 },
-        pages: { x: 0, y: 0 },
-        pages_wiki: { x: 60, y: 0 }
+    })
+    if (resp?.data?.setupStorageTarget?.status?.succeeded) {
+      switch (resp.data.setupStorageTarget.state?.nextStep) {
+        case 'installApp': {
+          router.replace({ query: null })
+          $q.loading.hide()
+
+          $q.dialog({
+            component: GithubSetupInstallDialog,
+            persistent: true
+          }).onOk(() => {
+            $q.loading.show({
+              message: t('admin.storage.githubRedirecting')
+            })
+            window.location.assign(resp.data.setupStorageTarget.state?.url)
+          }).onCancel(() => {
+            throw new Error('Setup was aborted prematurely.')
+          })
+          break
+        }
+        case 'completed': {
+          this.target.isEnabled = true
+          this.target.setup.state = 'configured'
+          setTimeout(() => {
+            $q.loading.hide()
+            $q.notify({
+              type: 'positive',
+              message: t('admin.storage.githubSetupSuccess')
+            })
+          }, 2000)
+          break
+        }
+        default: {
+          throw new Error('Unknown Setup Step')
+        }
       }
-      this.deliveryPaths = []
+    } else {
+      throw new Error(resp?.data?.setupStorageTarget?.status?.message || 'Unexpected error')
+    }
+  } catch (err) {
+    $q.loading.hide()
+    $q.notify({
+      type: 'negative',
+      message: t('admin.storage.githubSetupFailed'),
+      caption: err.message
+    })
+  }
+}
 
-      // -> Create Asset Nodes
+function generateGraph () {
+  const types = [
+    { key: 'images', label: t('admin.storage.contentTypeImages'), icon: 'las', iconText: '&#xf1c5;' },
+    { key: 'documents', label: t('admin.storage.contentTypeDocuments'), icon: 'las', iconText: '&#xf1c1;' },
+    { key: 'others', label: t('admin.storage.contentTypeOthers'), icon: 'las', iconText: '&#xf15b;' },
+    { key: 'large', label: t('admin.storage.contentTypeLargeFiles'), icon: 'las', iconText: '&#xf1c6;' }
+  ]
 
-      for (const [i, t] of types.entries()) {
-        this.deliveryNodes[t.key] = { name: t.label, color: '#3f51b5', icon: t.icon, iconText: t.iconText }
-        this.deliveryEdges[`user_${t.key}`] = { source: 'user', target: t.key }
-        this.deliveryLayouts.nodes[t.key] = { x: 0, y: (i + 1) * 15 }
+  // -> Create PagesNodes
 
-        // -> Find target with direct access
-        const dt = find(this.targets, tgt => {
-          return tgt.module !== 'db' && tgt.contentTypes.activeTypes.includes(t.key) && tgt.isEnabled && tgt.assetDelivery.isDirectAccessSupported && tgt.assetDelivery.directAccess
-        })
+  state.deliveryNodes = {
+    user: { name: t('admin.storage.deliveryPathsUser'), borderRadius: 16, icon: '/_assets/icons/fluent-account.svg' },
+    pages: { name: t('admin.storage.contentTypePages'), color: '#3f51b5', icon: 'las', iconText: '&#xf15c;' },
+    pages_wiki: { name: 'Wiki.js', icon: '/_assets/logo-wikijs.svg', color: '#161b22' }
+  }
+  state.deliveryEdges = {
+    user_pages: { source: 'user', target: 'pages' },
+    pages_in: { source: 'pages', target: 'pages_wiki' },
+    pages_out: { source: 'pages_wiki', target: 'pages' }
+  }
+  state.deliveryLayouts.nodes = {
+    user: { x: -30, y: 30 },
+    pages: { x: 0, y: 0 },
+    pages_wiki: { x: 60, y: 0 }
+  }
+  state.deliveryPaths = []
 
-        if (dt) {
-          this.deliveryNodes[`${t.key}_${dt.module}`] = { name: dt.title, icon: dt.icon }
-          this.deliveryNodes[`${t.key}_wiki`] = { name: 'Wiki.js', icon: '/_assets/logo-wikijs.svg', color: '#161b22' }
-          this.deliveryLayouts.nodes[`${t.key}_${dt.module}`] = { x: 60, y: (i + 1) * 15 }
-          this.deliveryLayouts.nodes[`${t.key}_wiki`] = { x: 120, y: (i + 1) * 15 }
-          this.deliveryEdges[`${t.key}_${dt.module}_in`] = { source: t.key, target: `${t.key}_${dt.module}` }
-          this.deliveryEdges[`${t.key}_${dt.module}_out`] = { source: `${t.key}_${dt.module}`, target: t.key }
-          this.deliveryEdges[`${t.key}_${dt.module}_wiki`] = { source: `${t.key}_wiki`, target: `${t.key}_${dt.module}`, color: '#02c39a', animationSpeed: 25 }
-          continue
-        }
+  // -> Create Asset Nodes
 
-        // -> Find target with streaming
+  for (const [i, tp] of types.entries()) {
+    state.deliveryNodes[tp.key] = { name: tp.label, color: '#3f51b5', icon: tp.icon, iconText: tp.iconText }
+    state.deliveryEdges[`user_${tp.key}`] = { source: 'user', target: t.key }
+    state.deliveryLayouts.nodes[tp.key] = { x: 0, y: (i + 1) * 15 }
 
-        const st = find(this.targets, tgt => {
-          return tgt.module !== 'db' && tgt.contentTypes.activeTypes.includes(t.key) && tgt.isEnabled && tgt.assetDelivery.isStreamingSupported && tgt.assetDelivery.streaming
-        })
+    // -> Find target with direct access
+    const dt = find(state.targets, tgt => {
+      return tgt.module !== 'db' && tgt.contentTypes.activeTypes.includes(tp.key) && tgt.isEnabled && tgt.assetDelivery.isDirectAccessSupported && tgt.assetDelivery.directAccess
+    })
 
-        if (st) {
-          this.deliveryNodes[`${t.key}_${st.module}`] = { name: st.title, icon: st.icon }
-          this.deliveryNodes[`${t.key}_wiki`] = { name: 'Wiki.js', icon: '/_assets/logo-wikijs.svg', color: '#161b22' }
-          this.deliveryLayouts.nodes[`${t.key}_${st.module}`] = { x: 120, y: (i + 1) * 15 }
-          this.deliveryLayouts.nodes[`${t.key}_wiki`] = { x: 60, y: (i + 1) * 15 }
-          this.deliveryEdges[`${t.key}_wiki_in`] = { source: t.key, target: `${t.key}_wiki` }
-          this.deliveryEdges[`${t.key}_wiki_out`] = { source: `${t.key}_wiki`, target: t.key }
-          this.deliveryEdges[`${t.key}_${st.module}_out`] = { source: `${t.key}_${st.module}`, target: `${t.key}_wiki` }
-          this.deliveryEdges[`${t.key}_${st.module}_in`] = { source: `${t.key}_wiki`, target: `${t.key}_${st.module}` }
-          this.deliveryEdges[`${t.key}_${st.module}_wiki`] = { source: `${t.key}_wiki`, target: `${t.key}_${st.module}`, color: '#02c39a', animationSpeed: 25 }
-          continue
-        }
+    if (dt) {
+      state.deliveryNodes[`${tp.key}_${dt.module}`] = { name: dt.title, icon: dt.icon }
+      state.deliveryNodes[`${tp.key}_wiki`] = { name: 'Wiki.js', icon: '/_assets/logo-wikijs.svg', color: '#161b22' }
+      state.deliveryLayouts.nodes[`${tp.key}_${dt.module}`] = { x: 60, y: (i + 1) * 15 }
+      state.deliveryLayouts.nodes[`${tp.key}_wiki`] = { x: 120, y: (i + 1) * 15 }
+      state.deliveryEdges[`${tp.key}_${dt.module}_in`] = { source: tp.key, target: `${tp.key}_${dt.module}` }
+      state.deliveryEdges[`${tp.key}_${dt.module}_out`] = { source: `${tp.key}_${dt.module}`, target: tp.key }
+      state.deliveryEdges[`${tp.key}_${dt.module}_wiki`] = { source: `${tp.key}_wiki`, target: `${tp.key}_${dt.module}`, color: '#02c39a', animationSpeed: 25 }
+      continue
+    }
 
-        // -> Check DB fallback
-
-        const dbt = find(this.targets, ['module', 'db'])
-        if (dbt.contentTypes.activeTypes.includes(t.key)) {
-          this.deliveryNodes[`${t.key}_wiki`] = { name: 'Wiki.js', icon: '/_assets/logo-wikijs.svg', color: '#161b22' }
-          this.deliveryLayouts.nodes[`${t.key}_wiki`] = { x: 60, y: (i + 1) * 15 }
-          this.deliveryEdges[`${t.key}_db_in`] = { source: t.key, target: `${t.key}_wiki` }
-          this.deliveryEdges[`${t.key}_db_out`] = { source: `${t.key}_wiki`, target: t.key }
-        } else {
-          this.deliveryNodes[`${t.key}_wiki`] = { name: this.$t('admin.storage.missingOrigin'), color: '#f03a47', icon: 'las', iconText: '&#xf071;' }
-          this.deliveryLayouts.nodes[`${t.key}_wiki`] = { x: 60, y: (i + 1) * 15 }
-          this.deliveryEdges[`${t.key}_db_in`] = { source: t.key, target: `${t.key}_wiki`, color: '#f03a47', animate: false }
-          this.deliveryPaths.push({ edges: [`${t.key}_db_in`], color: '#f03a4755' })
-        }
-      }
+    // -> Find target with streaming
+
+    const st = find(state.targets, tgt => {
+      return tgt.module !== 'db' && tgt.contentTypes.activeTypes.includes(tp.key) && tgt.isEnabled && tgt.assetDelivery.isStreamingSupported && tgt.assetDelivery.streaming
+    })
+
+    if (st) {
+      state.deliveryNodes[`${tp.key}_${st.module}`] = { name: st.title, icon: st.icon }
+      state.deliveryNodes[`${tp.key}_wiki`] = { name: 'Wiki.js', icon: '/_assets/logo-wikijs.svg', color: '#161b22' }
+      state.deliveryLayouts.nodes[`${tp.key}_${st.module}`] = { x: 120, y: (i + 1) * 15 }
+      state.deliveryLayouts.nodes[`${tp.key}_wiki`] = { x: 60, y: (i + 1) * 15 }
+      state.deliveryEdges[`${tp.key}_wiki_in`] = { source: tp.key, target: `${tp.key}_wiki` }
+      state.deliveryEdges[`${tp.key}_wiki_out`] = { source: `${tp.key}_wiki`, target: tp.key }
+      state.deliveryEdges[`${tp.key}_${st.module}_out`] = { source: `${tp.key}_${st.module}`, target: `${tp.key}_wiki` }
+      state.deliveryEdges[`${tp.key}_${st.module}_in`] = { source: `${tp.key}_wiki`, target: `${tp.key}_${st.module}` }
+      state.deliveryEdges[`${tp.key}_${st.module}_wiki`] = { source: `${tp.key}_wiki`, target: `${tp.key}_${st.module}`, color: '#02c39a', animationSpeed: 25 }
+      continue
+    }
+
+    // -> Check DB fallback
+
+    const dbt = find(state.targets, ['module', 'db'])
+    if (dbt.contentTypes.activeTypes.includes(tp.key)) {
+      state.deliveryNodes[`${tp.key}_wiki`] = { name: 'Wiki.js', icon: '/_assets/logo-wikijs.svg', color: '#161b22' }
+      state.deliveryLayouts.nodes[`${tp.key}_wiki`] = { x: 60, y: (i + 1) * 15 }
+      state.deliveryEdges[`${tp.key}_db_in`] = { source: tp.key, target: `${tp.key}_wiki` }
+      state.deliveryEdges[`${tp.key}_db_out`] = { source: `${tp.key}_wiki`, target: tp.key }
+    } else {
+      state.deliveryNodes[`${tp.key}_wiki`] = { name: t('admin.storage.missingOrigin'), color: '#f03a47', icon: 'las', iconText: '&#xf071;' }
+      state.deliveryLayouts.nodes[`${tp.key}_wiki`] = { x: 60, y: (i + 1) * 15 }
+      state.deliveryEdges[`${tp.key}_db_in`] = { source: tp.key, target: `${tp.key}_wiki`, color: '#f03a47', animate: false }
+      state.deliveryPaths.push({ edges: [`${tp.key}_db_in`], color: '#f03a4755' })
     }
   }
 }
+
+// MOUNTED
+
+onMounted(() => {
+  if (!state.selectedTarget && route.params.id) {
+    if (state.targets.length < 1) {
+      state.desiredTarget = route.params.id
+    } else {
+      state.selectedTarget = route.params.id
+    }
+  }
+  if (adminStore.currentSiteId) {
+    load()
+  }
+  handleSetupCallback()
+})
+
 </script>
 
 <style lang='scss' scoped>

+ 1 - 17
ux/src/pages/AdminSystem.vue

@@ -16,7 +16,7 @@ q-page.admin-system
         type='a'
         )
       q-btn.q-mr-sm.acrylic-btn(
-        icon='fa-solid fa-rotate'
+        icon='las la-redo-alt'
         flat
         color='secondary'
         :loading='state.loading > 0'
@@ -234,26 +234,10 @@ import { useMeta, useQuasar } from 'quasar'
 import { computed, onMounted, reactive, ref, watch } from 'vue'
 import ClipboardJS from 'clipboard'
 
-import { useAdminStore } from 'src/stores/admin'
-import { useSiteStore } from 'src/stores/site'
-import { useDataStore } from 'src/stores/data'
-import { useRoute, useRouter } from 'vue-router'
-
 // QUASAR
 
 const $q = useQuasar()
 
-// STORES
-
-const adminStore = useAdminStore()
-const siteStore = useSiteStore()
-const dataStore = useDataStore()
-
-// ROUTER
-
-const router = useRouter()
-const route = useRoute()
-
 // I18N
 
 const { t } = useI18n()

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

@@ -30,20 +30,20 @@ const routes = [
       { path: '', redirect: '/_admin/dashboard' },
       { path: 'dashboard', component: () => import('../pages/AdminDashboard.vue') },
       { path: 'sites', component: () => import('../pages/AdminSites.vue') },
-      // // -> Site
+      // -> Site
       { path: ':siteid/general', component: () => import('../pages/AdminGeneral.vue') },
       { path: ':siteid/editors', component: () => import('../pages/AdminEditors.vue') },
-      // { path: ':siteid/locale', component: () => import('../pages/AdminLocale.vue') },
-      // { path: ':siteid/login', component: () => import('../pages/AdminLogin.vue') },
+      { 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/storage/:id?', component: () => import('../pages/AdminStorage.vue') },
       // { path: ':siteid/rendering', component: () => import('../pages/AdminRendering.vue') },
       // { path: ':siteid/theme', component: () => import('../pages/AdminTheme.vue') },
-      // // -> Users
+      // -> 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
+      // -> System
       // { path: 'api', component: () => import('../pages/AdminApi.vue') },
       // { path: 'extensions', component: () => import('../pages/AdminExtensions.vue') },
       // { path: 'mail', component: () => import('../pages/AdminMail.vue') },

+ 273 - 204
ux/yarn.lock

@@ -5,9 +5,9 @@ __metadata:
   version: 6
   cacheKey: 8
 
-"@apollo/client@npm:3.6.1":
-  version: 3.6.1
-  resolution: "@apollo/client@npm:3.6.1"
+"@apollo/client@npm:3.6.4":
+  version: 3.6.4
+  resolution: "@apollo/client@npm:3.6.4"
   dependencies:
     "@graphql-typed-document-node/core": ^3.1.1
     "@wry/context": ^0.6.0
@@ -18,10 +18,9 @@ __metadata:
     optimism: ^0.16.1
     prop-types: ^15.7.2
     symbol-observable: ^4.0.0
-    ts-invariant: ^0.10.0
+    ts-invariant: ^0.10.3
     tslib: ^2.3.0
-    use-sync-external-store: ^1.0.0
-    zen-observable-ts: ^1.2.0
+    zen-observable-ts: ^1.2.5
   peerDependencies:
     graphql: ^14.0.0 || ^15.0.0 || ^16.0.0
     graphql-ws: ^5.5.5
@@ -34,7 +33,7 @@ __metadata:
       optional: true
     subscriptions-transport-ws:
       optional: true
-  checksum: 608ee7a46e02cbf8212b4e8825cf58e96bb45c035f82308aff898c94f5415db50fe718eb5637ab880982f46fa96a5e08b77c510d3ef60853171ccdeb7c2a957e
+  checksum: 87afb1fc0669ac60b439e19d327c1d7dcf80ecf5cd8ff86a8361cfadd5a6b7261ed4165907ce88316cd36d8764aeb1e121fa1db7bf328410472517634ca1c58f
   languageName: node
   linkType: hard
 
@@ -74,7 +73,19 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@codemirror/autocomplete@npm:0.20.0, @codemirror/autocomplete@npm:^0.20.0":
+"@codemirror/autocomplete@npm:0.20.1":
+  version: 0.20.1
+  resolution: "@codemirror/autocomplete@npm:0.20.1"
+  dependencies:
+    "@codemirror/language": ^0.20.0
+    "@codemirror/state": ^0.20.0
+    "@codemirror/view": ^0.20.0
+    "@lezer/common": ^0.16.0
+  checksum: 44db56c288a26d33734fc0462ccd74cd86fa584d3a3f63ab979f967c2089243fd66cfda1ee71a04fcc06f532835eb51706101756f1e3d51a7d799cbc27dbece6
+  languageName: node
+  linkType: hard
+
+"@codemirror/autocomplete@npm:^0.20.0":
   version: 0.20.0
   resolution: "@codemirror/autocomplete@npm:0.20.0"
   dependencies:
@@ -236,9 +247,9 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@codemirror/lang-markdown@npm:0.20.0":
-  version: 0.20.0
-  resolution: "@codemirror/lang-markdown@npm:0.20.0"
+"@codemirror/lang-markdown@npm:0.20.1":
+  version: 0.20.1
+  resolution: "@codemirror/lang-markdown@npm:0.20.1"
   dependencies:
     "@codemirror/lang-html": ^0.20.0
     "@codemirror/language": ^0.20.0
@@ -246,7 +257,7 @@ __metadata:
     "@codemirror/view": ^0.20.0
     "@lezer/common": ^0.16.0
     "@lezer/markdown": ^0.16.0
-  checksum: 719d4e4e9701bfeaa1193af036140029bbb7f7dcadac0813b0a3aa8a951ee1cd928421f0800c8186bedc134e5d827cc94943caa7a2f0719abb9fa199f7032eca
+  checksum: cc8ba82f8d5c1e279ad5d64d096c476327b9b83e5f9dda70c63258fcf21d97be3d66ada90d32af7f4fdd6f6bea51bf493e247a5875f0d05b8a34c34b701bcab3
   languageName: node
   linkType: hard
 
@@ -352,14 +363,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@codemirror/view@npm:0.20.3, @codemirror/view@npm:^0.20.0, @codemirror/view@npm:^0.20.2":
-  version: 0.20.3
-  resolution: "@codemirror/view@npm:0.20.3"
+"@codemirror/view@npm:0.20.6":
+  version: 0.20.6
+  resolution: "@codemirror/view@npm:0.20.6"
   dependencies:
     "@codemirror/state": ^0.20.0
     style-mod: ^4.0.0
     w3c-keyname: ^2.2.4
-  checksum: 8dc90cf77170d627e3e0bfd607d643bc4fe8c356a1ae8c0e3fc0877f0ba84c81fecdd41f5641acaf123a9e3da900d9f285a17ced349677f4eaec3167c77363a4
+  checksum: 5f78f219c3fbe29950b35b87641ab6ce70ed86379c5dcd6f52f6712b9b58ec620c617e4ce707beac01cc9614c458e641b9523ca80c3f9885bbec75f5509a56a1
   languageName: node
   linkType: hard
 
@@ -376,6 +387,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@codemirror/view@npm:^0.20.0, @codemirror/view@npm:^0.20.2":
+  version: 0.20.3
+  resolution: "@codemirror/view@npm:0.20.3"
+  dependencies:
+    "@codemirror/state": ^0.20.0
+    style-mod: ^4.0.0
+    w3c-keyname: ^2.2.4
+  checksum: 8dc90cf77170d627e3e0bfd607d643bc4fe8c356a1ae8c0e3fc0877f0ba84c81fecdd41f5641acaf123a9e3da900d9f285a17ced349677f4eaec3167c77363a4
+  languageName: node
+  linkType: hard
+
 "@dash14/svg-pan-zoom@npm:^3.6.8":
   version: 3.6.8
   resolution: "@dash14/svg-pan-zoom@npm:3.6.8"
@@ -383,20 +405,20 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@eslint/eslintrc@npm:^1.2.2":
-  version: 1.2.2
-  resolution: "@eslint/eslintrc@npm:1.2.2"
+"@eslint/eslintrc@npm:^1.3.0":
+  version: 1.3.0
+  resolution: "@eslint/eslintrc@npm:1.3.0"
   dependencies:
     ajv: ^6.12.4
     debug: ^4.3.2
-    espree: ^9.3.1
-    globals: ^13.9.0
+    espree: ^9.3.2
+    globals: ^13.15.0
     ignore: ^5.2.0
     import-fresh: ^3.2.1
     js-yaml: ^4.1.0
-    minimatch: ^3.0.4
+    minimatch: ^3.1.2
     strip-json-comments: ^3.1.1
-  checksum: d891036bbffb0efec1462aa4a603ed6e349d546b1632dde7d474ddd15c2a8b6895671b25293f1d3ba10ff629c24a3649ad049373fe695a0e44b612537088563c
+  checksum: a1e734ad31a8b5328dce9f479f185fd4fc83dd7f06c538e1fa457fd8226b89602a55cc6458cd52b29573b01cdfaf42331be8cfc1fec732570086b591f4ed6515
   languageName: node
   linkType: hard
 
@@ -452,30 +474,41 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@intlify/core-base@npm:9.1.9":
-  version: 9.1.9
-  resolution: "@intlify/core-base@npm:9.1.9"
+"@intlify/core-base@npm:9.1.10":
+  version: 9.1.10
+  resolution: "@intlify/core-base@npm:9.1.10"
   dependencies:
-    "@intlify/devtools-if": 9.1.9
-    "@intlify/message-compiler": 9.1.9
-    "@intlify/message-resolver": 9.1.9
-    "@intlify/runtime": 9.1.9
-    "@intlify/shared": 9.1.9
-    "@intlify/vue-devtools": 9.1.9
-  checksum: d148dab44ce97f19c1e507b21e82dc4a67034c36be438f0517c5871fc0e25f0b82287492e2e62b542069a7cb04702fe533804a464d2e022f404754e091934791
+    "@intlify/devtools-if": 9.1.10
+    "@intlify/message-compiler": 9.1.10
+    "@intlify/message-resolver": 9.1.10
+    "@intlify/runtime": 9.1.10
+    "@intlify/shared": 9.1.10
+    "@intlify/vue-devtools": 9.1.10
+  checksum: 5e4407753be014c4c428be546ce3487ad92f6d14b4ab9fb2324a3e67a9bf2d9a3c907300fd3eab076c0c8eeb836e9b7d26c81bf972d38f10f04aec3702e76570
   languageName: node
   linkType: hard
 
-"@intlify/devtools-if@npm:9.1.9":
-  version: 9.1.9
-  resolution: "@intlify/devtools-if@npm:9.1.9"
+"@intlify/devtools-if@npm:9.1.10":
+  version: 9.1.10
+  resolution: "@intlify/devtools-if@npm:9.1.10"
   dependencies:
-    "@intlify/shared": 9.1.9
-  checksum: 91ad5aa22fb559b386df9c0c35d8d025778401f45f17390d247fc081086a77ce9d8050092294164ab334196dc2165a53855215c773ce6fa93874ec0f6f488469
+    "@intlify/shared": 9.1.10
+  checksum: f09cb81d016e30e8cecbc18ae345529ed541c3ab1844e634ab2cad900d72e860a8d0459e0257833f36ba212899f5e4db75f0e2ad9eacb751d7b62fb1204ba470
+  languageName: node
+  linkType: hard
+
+"@intlify/message-compiler@npm:9.1.10":
+  version: 9.1.10
+  resolution: "@intlify/message-compiler@npm:9.1.10"
+  dependencies:
+    "@intlify/message-resolver": 9.1.10
+    "@intlify/shared": 9.1.10
+    source-map: 0.6.1
+  checksum: bd62f9212ca4ab0134976208b7c8bde40e1f1f662468e05aefd798f63330c6f341e816d5ce2f04d37d08f55d40278f08a0c67315c23e99c92078e1a02db177d4
   languageName: node
   linkType: hard
 
-"@intlify/message-compiler@npm:9.1.9, @intlify/message-compiler@npm:^9.1.0":
+"@intlify/message-compiler@npm:^9.1.0":
   version: 9.1.9
   resolution: "@intlify/message-compiler@npm:9.1.9"
   dependencies:
@@ -486,6 +519,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@intlify/message-resolver@npm:9.1.10":
+  version: 9.1.10
+  resolution: "@intlify/message-resolver@npm:9.1.10"
+  checksum: dcd9c66dcedb18f54fd7a086801fb3883ff6e898bf70429ecd898d487a883a64a4d5338e8ff306eb710c951c5166936e9fecd40e8e67855195ac560d32043a80
+  languageName: node
+  linkType: hard
+
 "@intlify/message-resolver@npm:9.1.9":
   version: 9.1.9
   resolution: "@intlify/message-resolver@npm:9.1.9"
@@ -493,14 +533,21 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@intlify/runtime@npm:9.1.9":
-  version: 9.1.9
-  resolution: "@intlify/runtime@npm:9.1.9"
+"@intlify/runtime@npm:9.1.10":
+  version: 9.1.10
+  resolution: "@intlify/runtime@npm:9.1.10"
   dependencies:
-    "@intlify/message-compiler": 9.1.9
-    "@intlify/message-resolver": 9.1.9
-    "@intlify/shared": 9.1.9
-  checksum: 2a754867c37e6473ec3b01f0a88c94d2a950f7c66057038ee53e0bb8eb8e5518fff9e2719160597dbe15243cc3f4d135e5ccbba49b1076fafff132455f27e6ba
+    "@intlify/message-compiler": 9.1.10
+    "@intlify/message-resolver": 9.1.10
+    "@intlify/shared": 9.1.10
+  checksum: 166989a2e432fa4ee65d856fe791ef68e87d59ac8bb17c044c91c857ae3c19721d96da1450fc6a3bbdffd250cc56694a5f7d901401cc0b71714ca932d0da56f1
+  languageName: node
+  linkType: hard
+
+"@intlify/shared@npm:9.1.10":
+  version: 9.1.10
+  resolution: "@intlify/shared@npm:9.1.10"
+  checksum: b4305cb755fdd031834d312712ac2d27d9e6e94318a6dae36aa0740f1e30bbd3c6fd3164eec7ddd62ae1b0f691402c25609cf3ffc6f2e2b7664023f69af74255
   languageName: node
   linkType: hard
 
@@ -534,14 +581,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@intlify/vue-devtools@npm:9.1.9":
-  version: 9.1.9
-  resolution: "@intlify/vue-devtools@npm:9.1.9"
+"@intlify/vue-devtools@npm:9.1.10":
+  version: 9.1.10
+  resolution: "@intlify/vue-devtools@npm:9.1.10"
   dependencies:
-    "@intlify/message-resolver": 9.1.9
-    "@intlify/runtime": 9.1.9
-    "@intlify/shared": 9.1.9
-  checksum: 7ce1e515a1ae87d649cc6b1e8d0b357b885048144ab7bfbd38c650df14c5931aeab908b32ca2b6aea98c89e7b65399b49c40ec74ca384d701c2f0d3cd90e8d5f
+    "@intlify/message-resolver": 9.1.10
+    "@intlify/runtime": 9.1.10
+    "@intlify/shared": 9.1.10
+  checksum: 41af5d9a578439b05b0fd6a89321de6549119c37f92ed026988590685a5581cbe5c409853c7cfacb0dab7b5d22a9020af5ce95d353a663a4b53d1905f7a805ae
   languageName: node
   linkType: hard
 
@@ -697,9 +744,9 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@quasar/app-vite@npm:1.0.0-beta.14":
-  version: 1.0.0-beta.14
-  resolution: "@quasar/app-vite@npm:1.0.0-beta.14"
+"@quasar/app-vite@npm:1.0.0":
+  version: 1.0.0
+  resolution: "@quasar/app-vite@npm:1.0.0"
   dependencies:
     "@quasar/fastclick": 1.1.5
     "@quasar/vite-plugin": ^1.0.9
@@ -748,14 +795,14 @@ __metadata:
       optional: true
   bin:
     quasar: bin/quasar
-  checksum: d5718d04016fe14ed23c8e691935d2757692c7504d2b0eb4fe50748363c056c8ba435fd31931a1ceaecf9684047e04d88fae27b77d59f2c3f57bcdf73064dcde
+  checksum: e5306e58f371acfb8b91f57b9f4936ab9eedf4acce0ba0b828966445d6523a4fa6a4162fcf44e79e307a7e52077aa420ad9d6bfb7310e806bba969ff844b6f4f
   languageName: node
   linkType: hard
 
-"@quasar/extras@npm:1.13.6":
-  version: 1.13.6
-  resolution: "@quasar/extras@npm:1.13.6"
-  checksum: fa7a470488e5463daa8941c6b91525b4f1e110544ef7872605e945a053d9c24bea5fd948189827b354eee4251cb34cc4a9e0e703b6569016c570ed68c803b7a6
+"@quasar/extras@npm:1.14.0":
+  version: 1.14.0
+  resolution: "@quasar/extras@npm:1.14.0"
+  checksum: 60eacf3995172a11c59af6266461f665219488e33e85ab99fb293365fdd95e3dda5427a5ab548b7a5058ee0e3f5d4a6045ad8feeb99e1b0ad9b37f1cbd8d2100
   languageName: node
   linkType: hard
 
@@ -787,9 +834,9 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@tiptap/core@npm:2.0.0-beta.175, @tiptap/core@npm:^2.0.0-beta.175":
-  version: 2.0.0-beta.175
-  resolution: "@tiptap/core@npm:2.0.0-beta.175"
+"@tiptap/core@npm:2.0.0-beta.176, @tiptap/core@npm:^2.0.0-beta.176":
+  version: 2.0.0-beta.176
+  resolution: "@tiptap/core@npm:2.0.0-beta.176"
   dependencies:
     "@types/prosemirror-commands": ^1.0.4
     "@types/prosemirror-keymap": ^1.0.4
@@ -805,7 +852,7 @@ __metadata:
     prosemirror-state: ^1.3.4
     prosemirror-transform: ^1.3.3
     prosemirror-view: ^1.23.6
-  checksum: 2ede43076d8c275e68a1bf471e6e338f383e447e5d413aaea01ee14531351d02addb2bbfd8e49358da426f4bdd0d8f7812c6f5ebb0c6467e3232889fb0e20783
+  checksum: b7a8a277de6732c5c94fcfb19e8aa8c4d38cdddf456c5fd1e002d2b80f6120e4fc219190b7f47391a870c1371ee35f9530b22bd24394fd64a3fa059eac85923d
   languageName: node
   linkType: hard
 
@@ -1028,16 +1075,16 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@tiptap/extension-mention@npm:2.0.0-beta.96":
-  version: 2.0.0-beta.96
-  resolution: "@tiptap/extension-mention@npm:2.0.0-beta.96"
+"@tiptap/extension-mention@npm:2.0.0-beta.97":
+  version: 2.0.0-beta.97
+  resolution: "@tiptap/extension-mention@npm:2.0.0-beta.97"
   dependencies:
-    "@tiptap/suggestion": ^2.0.0-beta.91
+    "@tiptap/suggestion": ^2.0.0-beta.92
     prosemirror-model: ^1.16.1
     prosemirror-state: ^1.3.4
   peerDependencies:
     "@tiptap/core": ^2.0.0-beta.1
-  checksum: 39c196dcdf6d948708dd88522d213fa3ec5108acbb6075425a61b1f27d11edf19d4bf634cac562cefbfcccd9fb3b16a252381e9518e4a44c4b9f060eeeaba3a9
+  checksum: 25d53fa1a98f8a2c5c39cedb1dc155ec5b2d1a74e16a15eafb812c2f1ab405ec735df901f6877ecaec28f454f91df002896e68eb58953c21ff965a956f4fb318
   languageName: node
   linkType: hard
 
@@ -1108,15 +1155,19 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@tiptap/extension-table@npm:2.0.0-beta.48":
-  version: 2.0.0-beta.48
-  resolution: "@tiptap/extension-table@npm:2.0.0-beta.48"
+"@tiptap/extension-table@npm:2.0.0-beta.49":
+  version: 2.0.0-beta.49
+  resolution: "@tiptap/extension-table@npm:2.0.0-beta.49"
   dependencies:
+    "@types/prosemirror-model": ^1.16.0
+    "@types/prosemirror-state": ^1.2.8
+    prosemirror-model: ^1.16.1
+    prosemirror-state: ^1.3.4
     prosemirror-tables: ^1.1.1
     prosemirror-view: ^1.23.6
   peerDependencies:
     "@tiptap/core": ^2.0.0-beta.1
-  checksum: a4cb51916c11cbd1ca9e16ab08b5974037efd42e3849f66c954d815dcc4d6198dbae76ecad898d2b7847761345ebb0170bac5e575a05d9305e3551dccc12f91f
+  checksum: e72f1397fad4e8549d38957e39f2a6b260137561218eb60b76e86ff4672cffcabaabbd5edb064f0ed337094738c488091bcd5b070683d54c0b8f3a0ff93f2a91
   languageName: node
   linkType: hard
 
@@ -1174,11 +1225,11 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@tiptap/starter-kit@npm:2.0.0-beta.184":
-  version: 2.0.0-beta.184
-  resolution: "@tiptap/starter-kit@npm:2.0.0-beta.184"
+"@tiptap/starter-kit@npm:2.0.0-beta.185":
+  version: 2.0.0-beta.185
+  resolution: "@tiptap/starter-kit@npm:2.0.0-beta.185"
   dependencies:
-    "@tiptap/core": ^2.0.0-beta.175
+    "@tiptap/core": ^2.0.0-beta.176
     "@tiptap/extension-blockquote": ^2.0.0-beta.26
     "@tiptap/extension-bold": ^2.0.0-beta.26
     "@tiptap/extension-bullet-list": ^2.0.0-beta.26
@@ -1197,20 +1248,20 @@ __metadata:
     "@tiptap/extension-paragraph": ^2.0.0-beta.23
     "@tiptap/extension-strike": ^2.0.0-beta.27
     "@tiptap/extension-text": ^2.0.0-beta.15
-  checksum: f53e965f1dfe09d71a92963304f0cc4ed9c9cc960752b28268347492213169fbc800dc6a6c155db999ef500a738988edb1de3f7962e74981b32743f95336b460
+  checksum: 8685b4ecd84ad6aab22f157b4af1de44a59386c42133f7cb52bd1ba039901c4b19099f4d54d334077a9fa2aa9d5013576499c8886cd1d9739dddc3898bcd23f3
   languageName: node
   linkType: hard
 
-"@tiptap/suggestion@npm:^2.0.0-beta.91":
-  version: 2.0.0-beta.91
-  resolution: "@tiptap/suggestion@npm:2.0.0-beta.91"
+"@tiptap/suggestion@npm:^2.0.0-beta.92":
+  version: 2.0.0-beta.92
+  resolution: "@tiptap/suggestion@npm:2.0.0-beta.92"
   dependencies:
     prosemirror-model: ^1.16.1
     prosemirror-state: ^1.3.4
     prosemirror-view: ^1.23.6
   peerDependencies:
     "@tiptap/core": ^2.0.0-beta.1
-  checksum: cc6919a2241632a248e9b31326b8ed84f4285847aa28b42a9005687a893f201ec980190900f64f2de45794bbbe140a9fa8c269b4c81d2b839a11c20745d8c312
+  checksum: 043d38411596beda120c4c2d4ddf0864ce5f657a97f6a8062445c28860e146611599bd4668c8c269c8782792b48daa0ea00f6cea69b85b6d2ee5ff407c146060
   languageName: node
   linkType: hard
 
@@ -1464,15 +1515,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@vue/apollo-option@npm:4.0.0-alpha.16":
-  version: 4.0.0-alpha.16
-  resolution: "@vue/apollo-option@npm:4.0.0-alpha.16"
+"@vue/apollo-option@npm:4.0.0-alpha.17":
+  version: 4.0.0-alpha.17
+  resolution: "@vue/apollo-option@npm:4.0.0-alpha.17"
   dependencies:
     throttle-debounce: ^3.0.1
   peerDependencies:
     "@apollo/client": ^3.2.1
     vue: ^3.1.0
-  checksum: e80c3b1f3cf36b9b313ad499b14c7e9b96f8802d5168ab150274d013ae03f8e01f40e13636131d06d507431c90e8dbded0fbd8005d02b8eda8bd5de2263e6e2c
+  checksum: f1d1152131a1bfa36497c7bc42fd7c1ca2848a0a98943990b4943ef80d528ce7713be41e6943dbf7755726f413b350731fdb868cece69dee5d37377346b7fe01
   languageName: node
   linkType: hard
 
@@ -1639,7 +1690,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"acorn-jsx@npm:^5.2.0, acorn-jsx@npm:^5.3.1":
+"acorn-jsx@npm:^5.2.0, acorn-jsx@npm:^5.3.1, acorn-jsx@npm:^5.3.2":
   version: 5.3.2
   resolution: "acorn-jsx@npm:5.3.2"
   peerDependencies:
@@ -1666,6 +1717,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"acorn@npm:^8.7.1":
+  version: 8.7.1
+  resolution: "acorn@npm:8.7.1"
+  bin:
+    acorn: bin/acorn
+  checksum: aca0aabf98826717920ac2583fdcad0a6fbe4e583fdb6e843af2594e907455aeafe30b1e14f1757cd83ce1776773cf8296ffc3a4acf13f0bd3dfebcf1db6ae80
+  languageName: node
+  linkType: hard
+
 "agent-base@npm:6, agent-base@npm:^6.0.2":
   version: 6.0.2
   resolution: "agent-base@npm:6.0.2"
@@ -1883,12 +1943,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"autoprefixer@npm:10.4.5":
-  version: 10.4.5
-  resolution: "autoprefixer@npm:10.4.5"
+"autoprefixer@npm:10.4.7":
+  version: 10.4.7
+  resolution: "autoprefixer@npm:10.4.7"
   dependencies:
-    browserslist: ^4.20.2
-    caniuse-lite: ^1.0.30001332
+    browserslist: ^4.20.3
+    caniuse-lite: ^1.0.30001335
     fraction.js: ^4.2.0
     normalize-range: ^0.1.2
     picocolors: ^1.0.0
@@ -1897,7 +1957,7 @@ __metadata:
     postcss: ^8.1.0
   bin:
     autoprefixer: bin/autoprefixer
-  checksum: 6c638d8f515a9c37776f9d6b1263e497768d9e8d8ad3b45d901513ba0f26c74df73f4afb84efd3253ab3d1c57803b4e2d97ad9b8919ef1955e373fcf24661213
+  checksum: 0e55d0d19806c672ec0c79cc23c27cf77e90edf2600670735266ba33ec5294458f404baaa2f7cd4cfe359cf7a97b3c86f01886bdbdc129a4f2f76ca5977a91af
   languageName: node
   linkType: hard
 
@@ -1995,25 +2055,25 @@ __metadata:
   languageName: node
   linkType: hard
 
-"browser-fs-access@npm:0.29.4":
-  version: 0.29.4
-  resolution: "browser-fs-access@npm:0.29.4"
-  checksum: d78c2421630ccb6d4a15fc8bad823ee3a08d4f5d1e17d23a222f57aad4f44050e88ea103b211612423f430056adbdb9dacf04e16eb68e1f08cf4920a4bee9b6f
+"browser-fs-access@npm:0.29.5":
+  version: 0.29.5
+  resolution: "browser-fs-access@npm:0.29.5"
+  checksum: 394fef8a32542e738ccf09c5c7c5bf903bd87279101e0ab0c05e924dcdb872bfc562854c4e213f2628526719a128fa6ba0b3cb8d1c3de18ed80b233df35efd8b
   languageName: node
   linkType: hard
 
-"browserslist@npm:^4.20.2":
-  version: 4.20.2
-  resolution: "browserslist@npm:4.20.2"
+"browserslist@npm:^4.20.3":
+  version: 4.20.3
+  resolution: "browserslist@npm:4.20.3"
   dependencies:
-    caniuse-lite: ^1.0.30001317
-    electron-to-chromium: ^1.4.84
+    caniuse-lite: ^1.0.30001332
+    electron-to-chromium: ^1.4.118
     escalade: ^3.1.1
-    node-releases: ^2.0.2
+    node-releases: ^2.0.3
     picocolors: ^1.0.0
   bin:
     browserslist: cli.js
-  checksum: 18e09beeae32e69fea45fc3642240fb63027b1460d90e24da86377177dca3d82c80f8fa44469d95109e3962f08eb2a23e03037bd5e1f1ec38e4866e2a8572435
+  checksum: 1e4b719ac2ca0fe235218a606e8b8ef16b8809e0973b924158c39fbc435a0b0fe43437ea52dd6ef5ad2efcb83fcb07431244e472270177814217f7c563651f7d
   languageName: node
   linkType: hard
 
@@ -2110,13 +2170,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"caniuse-lite@npm:^1.0.30001317":
-  version: 1.0.30001325
-  resolution: "caniuse-lite@npm:1.0.30001325"
-  checksum: 383a86a513381e3927a30b578ac8616ce388af79dc5dced22e18fffaef17c0bed0e324eadba1b13a6c15b3ec39128fbcfbb097992d3aca206feef5a539c4639f
-  languageName: node
-  linkType: hard
-
 "caniuse-lite@npm:^1.0.30001332":
   version: 1.0.30001334
   resolution: "caniuse-lite@npm:1.0.30001334"
@@ -2124,6 +2177,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"caniuse-lite@npm:^1.0.30001335":
+  version: 1.0.30001341
+  resolution: "caniuse-lite@npm:1.0.30001341"
+  checksum: 7262b093fb0bf49dbc5328418f5ce4e3dbb0b13e39c015f986ba1807634c123ac214efc94df7d095a336f57f86852b4b63ee61838f18dcc3a4a35f87b390c8f5
+  languageName: node
+  linkType: hard
+
 "chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.1":
   version: 4.1.2
   resolution: "chalk@npm:4.1.2"
@@ -2222,14 +2282,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"clipboard@npm:2.0.10":
-  version: 2.0.10
-  resolution: "clipboard@npm:2.0.10"
+"clipboard@npm:2.0.11":
+  version: 2.0.11
+  resolution: "clipboard@npm:2.0.11"
   dependencies:
     good-listener: ^1.2.2
     select: ^1.1.2
     tiny-emitter: ^2.0.0
-  checksum: 401ae9f27c9cb8f03f7a33e1a06ad8f1208a75dc36d037cc6542708c8e4a43974c4e4186d9154e0161541934bb2073ee2e078db80e5c7df3f0544aad2508262c
+  checksum: 413055a6038e43898e0e895216b58ed54fbf386f091cb00188875ef35b186cefbd258acdf4cb4b0ac87cbc00de936f41b45dde9fe1fd1a57f7babb28363b8748
   languageName: node
   linkType: hard
 
@@ -2614,10 +2674,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"electron-to-chromium@npm:^1.4.84":
-  version: 1.4.106
-  resolution: "electron-to-chromium@npm:1.4.106"
-  checksum: 79eae050a775f6f674a24d4541d54cdb1c35e956d6e112ee9ec8d752fa9bcd94739e5f86c58d8e04f85199cf720146aee301b2e397932ad5c8d8e8cffe65a2ee
+"electron-to-chromium@npm:^1.4.118":
+  version: 1.4.137
+  resolution: "electron-to-chromium@npm:1.4.137"
+  checksum: 639d7b94906efafcf363519c3698eecc44be46755a6a5cdc9088954329978866cc93fbd57e08b97290599b68d5226243d21de9fa50be416b8a5d3fa8fd42c3a0
   languageName: node
   linkType: hard
 
@@ -3326,11 +3386,11 @@ __metadata:
   languageName: node
   linkType: hard
 
-"eslint@npm:8.14.0":
-  version: 8.14.0
-  resolution: "eslint@npm:8.14.0"
+"eslint@npm:8.16.0":
+  version: 8.16.0
+  resolution: "eslint@npm:8.16.0"
   dependencies:
-    "@eslint/eslintrc": ^1.2.2
+    "@eslint/eslintrc": ^1.3.0
     "@humanwhocodes/config-array": ^0.9.2
     ajv: ^6.10.0
     chalk: ^4.0.0
@@ -3341,14 +3401,14 @@ __metadata:
     eslint-scope: ^7.1.1
     eslint-utils: ^3.0.0
     eslint-visitor-keys: ^3.3.0
-    espree: ^9.3.1
+    espree: ^9.3.2
     esquery: ^1.4.0
     esutils: ^2.0.2
     fast-deep-equal: ^3.1.3
     file-entry-cache: ^6.0.1
     functional-red-black-tree: ^1.0.1
     glob-parent: ^6.0.1
-    globals: ^13.6.0
+    globals: ^13.15.0
     ignore: ^5.2.0
     import-fresh: ^3.0.0
     imurmurhash: ^0.1.4
@@ -3357,7 +3417,7 @@ __metadata:
     json-stable-stringify-without-jsonify: ^1.0.1
     levn: ^0.4.1
     lodash.merge: ^4.6.2
-    minimatch: ^3.0.4
+    minimatch: ^3.1.2
     natural-compare: ^1.4.0
     optionator: ^0.9.1
     regexpp: ^3.2.0
@@ -3367,7 +3427,7 @@ __metadata:
     v8-compile-cache: ^2.0.3
   bin:
     eslint: bin/eslint.js
-  checksum: 87d2e3e5eb93216d4ab36006e7b8c0bfad02f40b0a0f193f1d42754512cd3a9d8244152f1c69df5db2e135b3c4f1c10d0ed2f0881fe8a8c01af55465968174c1
+  checksum: 654a0200b49dc07280673fee13cdfb04326466790e031dfa9660b69fba3b1cf766a51504328f9de56bd18e6b5eb7578985cf29dc7f016c5ec851220ff9db95eb
   languageName: node
   linkType: hard
 
@@ -3382,7 +3442,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"espree@npm:^9.0.0, espree@npm:^9.3.1":
+"espree@npm:^9.0.0":
   version: 9.3.1
   resolution: "espree@npm:9.3.1"
   dependencies:
@@ -3393,6 +3453,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"espree@npm:^9.3.2":
+  version: 9.3.2
+  resolution: "espree@npm:9.3.2"
+  dependencies:
+    acorn: ^8.7.1
+    acorn-jsx: ^5.3.2
+    eslint-visitor-keys: ^3.3.0
+  checksum: 9a790d6779847051e87f70d720a0f6981899a722419e80c92ab6dee01e1ab83b8ce52d11b4dc96c2c490182efb5a4c138b8b0d569205bfe1cd4629e658e58c30
+  languageName: node
+  linkType: hard
+
 "esquery@npm:^1.4.0":
   version: 1.4.0
   resolution: "esquery@npm:1.4.0"
@@ -3814,12 +3885,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"globals@npm:^13.6.0, globals@npm:^13.9.0":
-  version: 13.13.0
-  resolution: "globals@npm:13.13.0"
+"globals@npm:^13.15.0":
+  version: 13.15.0
+  resolution: "globals@npm:13.15.0"
   dependencies:
     type-fest: ^0.20.2
-  checksum: c55ea8fd3afecb72567bac41605577e19e68476993dfb0ca4c49b86075af5f0ae3f0f5502525f69010f7c5ea5db6a1c540a80a4f80ebdfb2f686d87b0f05d7e9
+  checksum: 383ade0873b2ab29ce6d143466c203ed960491575bc97406395e5c8434026fb02472ab2dfff5bc16689b8460269b18fda1047975295cd0183904385c51258bae
   languageName: node
   linkType: hard
 
@@ -3850,10 +3921,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"graphql@npm:16.4.0":
-  version: 16.4.0
-  resolution: "graphql@npm:16.4.0"
-  checksum: 8cac2c466891b6c6ace2fa3807f93815dea53cbec372998c139e796a321a6882c2324cc605e5f9aa45356890239aa9e731a04d8c59ec524db82bd18fb48aafee
+"graphql@npm:16.5.0":
+  version: 16.5.0
+  resolution: "graphql@npm:16.5.0"
+  checksum: a82a926d085818934d04fdf303a269af170e79de943678bd2726370a96194f9454ade9d6d76c2de69afbd7b9f0b4f8061619baecbbddbe82125860e675ac219e
   languageName: node
   linkType: hard
 
@@ -4658,10 +4729,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"luxon@npm:2.3.2":
-  version: 2.3.2
-  resolution: "luxon@npm:2.3.2"
-  checksum: ba4f9daa56d03771c2ddf00e3bf996ec7937badfdc2f85f5b58d63c75bb511369b3316f54ab9ce5ab42bfc9118513971f6599128529b68620b876a4b7c16570b
+"luxon@npm:2.4.0":
+  version: 2.4.0
+  resolution: "luxon@npm:2.4.0"
+  checksum: 6071028d65cc3d3bdab5e6a3995dd97411c92bb3b3163a02deb7b7014318ad3a6fd750ae77131c42141b717c14aef8880d3130f265281d500ef2365f7265b3f3
   languageName: node
   linkType: hard
 
@@ -4968,10 +5039,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"node-releases@npm:^2.0.2":
-  version: 2.0.2
-  resolution: "node-releases@npm:2.0.2"
-  checksum: da858bf86b4d512842379749f5a5e4196ddab05ba18ffcf29f05bf460beceaca927f070f4430bb5046efec18941ddbc85e4c5fdbb83afc28a38dd6069a2f255e
+"node-releases@npm:^2.0.3":
+  version: 2.0.4
+  resolution: "node-releases@npm:2.0.4"
+  checksum: b32d6c2032c7b169ae3938b416fc50f123f5bd577d54a79b2ae201febf27b22846b01c803dd35ac8689afe840f8ba4e5f7154723db629b80f359836b6707b92f
   languageName: node
   linkType: hard
 
@@ -5285,9 +5356,9 @@ __metadata:
   languageName: node
   linkType: hard
 
-"pinia@npm:2.0.13":
-  version: 2.0.13
-  resolution: "pinia@npm:2.0.13"
+"pinia@npm:2.0.14":
+  version: 2.0.14
+  resolution: "pinia@npm:2.0.14"
   dependencies:
     "@vue/devtools-api": ^6.1.4
     vue-demi: "*"
@@ -5300,7 +5371,7 @@ __metadata:
       optional: true
     typescript:
       optional: true
-  checksum: e9f2be12996c4778409ff6757b6472948b493924aaba07dd5f35a3222166c4093a746a894f076a702436f3449363f2f535ce4f5347f332167518247105b2af3e
+  checksum: d07ed55b53e92da0971c3fcc0a1bd520b26cd50d266f46c8bab24fed87788461782a2c75088cf97c79810e7a4ceb28381f5d77daf280883ad3340c41962d0934
   languageName: node
   linkType: hard
 
@@ -5662,13 +5733,20 @@ __metadata:
   languageName: node
   linkType: hard
 
-"quasar@npm:*, quasar@npm:2.6.6":
+"quasar@npm:*":
   version: 2.6.6
   resolution: "quasar@npm:2.6.6"
   checksum: 0ef62a7e916f88cea06d3e5320cec9a0eddebd829891b7bc0ee11a4ec47653b1ae08d414241c046a8c65657e9f12cf2ead4fc214f6dc43d57816535f5c216160
   languageName: node
   linkType: hard
 
+"quasar@npm:2.7.0":
+  version: 2.7.0
+  resolution: "quasar@npm:2.7.0"
+  checksum: 7b78c4fe8be56494c6286b300c755b146a315fc7561db4a2ff1ac9f9bae6f098aa90f21f2bb63108d110bc0917e2f20e09155d8de73699ceda001d8b98d1fde3
+  languageName: node
+  linkType: hard
+
 "queue-microtask@npm:^1.2.2":
   version: 1.2.3
   resolution: "queue-microtask@npm:1.2.3"
@@ -6407,12 +6485,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"ts-invariant@npm:^0.10.0":
-  version: 0.10.1
-  resolution: "ts-invariant@npm:0.10.1"
+"ts-invariant@npm:^0.10.3":
+  version: 0.10.3
+  resolution: "ts-invariant@npm:0.10.3"
   dependencies:
     tslib: ^2.1.0
-  checksum: 668ffffcd8738e0bc83a1522be1c16131f3312a6a49a211de19505155848414fa31df814471111a3d8031a23c8fbf2de34cf14f3b5c928df87e9f527dbe0cf71
+  checksum: bb07d56fe4aae69d8860e0301dfdee2d375281159054bc24bf1e49e513fb0835bf7f70a11351344d213a79199c5e695f37ebbf5a447188a377ce0cd81d91ddb5
   languageName: node
   linkType: hard
 
@@ -6537,15 +6615,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"use-sync-external-store@npm:^1.0.0":
-  version: 1.1.0
-  resolution: "use-sync-external-store@npm:1.1.0"
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0 || ^18.0.0
-  checksum: 8993a0b642f91d7fcdbb02b7b3ac984bd3af4769686f38291fe7fcfe73dfb73d6c64d20dfb7e5e7fbf5a6da8f5392d6f8e5b00c243a04975595946e82c02b883
-  languageName: node
-  linkType: hard
-
 "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1":
   version: 1.0.2
   resolution: "util-deprecate@npm:1.0.2"
@@ -6573,8 +6642,8 @@ __metadata:
   version: 0.0.0-use.local
   resolution: "ux@workspace:."
   dependencies:
-    "@apollo/client": 3.6.1
-    "@codemirror/autocomplete": 0.20.0
+    "@apollo/client": 3.6.4
+    "@codemirror/autocomplete": 0.20.1
     "@codemirror/basic-setup": 0.20.0
     "@codemirror/closebrackets": 0.19.2
     "@codemirror/commands": 0.20.0
@@ -6587,17 +6656,17 @@ __metadata:
     "@codemirror/lang-html": 0.20.0
     "@codemirror/lang-javascript": 0.20.0
     "@codemirror/lang-json": 0.20.0
-    "@codemirror/lang-markdown": 0.20.0
+    "@codemirror/lang-markdown": 0.20.1
     "@codemirror/matchbrackets": 0.19.4
     "@codemirror/search": 0.20.1
     "@codemirror/state": 0.20.0
     "@codemirror/tooltip": 0.19.16
-    "@codemirror/view": 0.20.3
+    "@codemirror/view": 0.20.6
     "@intlify/vite-plugin-vue-i18n": 3.4.0
     "@lezer/common": 0.16.0
-    "@quasar/app-vite": 1.0.0-beta.14
-    "@quasar/extras": 1.13.6
-    "@tiptap/core": 2.0.0-beta.175
+    "@quasar/app-vite": 1.0.0
+    "@quasar/extras": 1.14.0
+    "@tiptap/core": 2.0.0-beta.176
     "@tiptap/extension-code-block": 2.0.0-beta.37
     "@tiptap/extension-code-block-lowlight": 2.0.0-beta.68
     "@tiptap/extension-color": 2.0.0-beta.9
@@ -6608,9 +6677,9 @@ __metadata:
     "@tiptap/extension-highlight": 2.0.0-beta.33
     "@tiptap/extension-history": 2.0.0-beta.21
     "@tiptap/extension-image": 2.0.0-beta.27
-    "@tiptap/extension-mention": 2.0.0-beta.96
+    "@tiptap/extension-mention": 2.0.0-beta.97
     "@tiptap/extension-placeholder": 2.0.0-beta.48
-    "@tiptap/extension-table": 2.0.0-beta.48
+    "@tiptap/extension-table": 2.0.0-beta.49
     "@tiptap/extension-table-cell": 2.0.0-beta.20
     "@tiptap/extension-table-header": 2.0.0-beta.22
     "@tiptap/extension-table-row": 2.0.0-beta.19
@@ -6619,15 +6688,15 @@ __metadata:
     "@tiptap/extension-text-align": 2.0.0-beta.29
     "@tiptap/extension-text-style": 2.0.0-beta.23
     "@tiptap/extension-typography": 2.0.0-beta.20
-    "@tiptap/starter-kit": 2.0.0-beta.184
+    "@tiptap/starter-kit": 2.0.0-beta.185
     "@tiptap/vue-3": 2.0.0-beta.91
     "@types/lodash": 4.14.182
-    "@vue/apollo-option": 4.0.0-alpha.16
+    "@vue/apollo-option": 4.0.0-alpha.17
     apollo-upload-client: 17.0.0
-    autoprefixer: 10.4.5
-    browser-fs-access: 0.29.4
-    clipboard: 2.0.10
-    eslint: 8.14.0
+    autoprefixer: 10.4.7
+    browser-fs-access: 0.29.5
+    clipboard: 2.0.11
+    eslint: 8.16.0
     eslint-config-standard: 17.0.0
     eslint-plugin-import: 2.26.0
     eslint-plugin-n: 15.2.0
@@ -6635,36 +6704,36 @@ __metadata:
     eslint-plugin-vue: 8.7.1
     filesize: 8.0.7
     filesize-parser: 1.5.0
-    graphql: 16.4.0
+    graphql: 16.5.0
     graphql-tag: 2.12.6
     js-cookie: 3.0.1
     jwt-decode: 3.1.2
     lodash: 4.17.21
-    luxon: 2.3.2
-    pinia: 2.0.13
+    luxon: 2.4.0
+    pinia: 2.0.14
     pug: 3.0.2
-    quasar: 2.6.6
+    quasar: 2.7.0
     tippy.js: 6.3.7
     uuid: 8.3.2
-    v-network-graph: 0.5.13
+    v-network-graph: 0.5.16
     vue: 3.2.31
-    vue-i18n: 9.1.9
-    vue-router: 4.0.14
+    vue-i18n: 9.1.10
+    vue-router: 4.0.15
     vuedraggable: 4.1.0
     zxcvbn: 4.4.2
   languageName: unknown
   linkType: soft
 
-"v-network-graph@npm:0.5.13":
-  version: 0.5.13
-  resolution: "v-network-graph@npm:0.5.13"
+"v-network-graph@npm:0.5.16":
+  version: 0.5.16
+  resolution: "v-network-graph@npm:0.5.16"
   dependencies:
     "@dash14/svg-pan-zoom": ^3.6.8
     mitt: ^3.0.0
   peerDependencies:
     d3-force: ^3.0.0
     vue: ^3.2.31
-  checksum: 3d778c5c8478551f3fe0d46684add94a05c68ebb2142ab6362308726ecdad710387f264886b561a916991d44dadeeabde7a4cb69c587ab20b8e0ee1453310bd0
+  checksum: 8a59f4af41fef885c46c817c2784582612fe9776e862c074ad69f80a15ccb079424009fe06a1eb3149a9f571c44af6d4a657531004eda030887d70eadf8ab343
   languageName: node
   linkType: hard
 
@@ -6780,28 +6849,28 @@ __metadata:
   languageName: node
   linkType: hard
 
-"vue-i18n@npm:9.1.9":
-  version: 9.1.9
-  resolution: "vue-i18n@npm:9.1.9"
+"vue-i18n@npm:9.1.10":
+  version: 9.1.10
+  resolution: "vue-i18n@npm:9.1.10"
   dependencies:
-    "@intlify/core-base": 9.1.9
-    "@intlify/shared": 9.1.9
-    "@intlify/vue-devtools": 9.1.9
+    "@intlify/core-base": 9.1.10
+    "@intlify/shared": 9.1.10
+    "@intlify/vue-devtools": 9.1.10
     "@vue/devtools-api": ^6.0.0-beta.7
   peerDependencies:
     vue: ^3.0.0
-  checksum: 9d091cc3c1f08c789c3c21760613338732c6a89b798b725e610ad78ab01054942ae0ea184a644deb28c59c752fcab5a76438272da91441254dd0a1499606e255
+  checksum: ae58af1d45aa39334066606228d7daae0376631c8462bdb22d588021745211ff1098e5a46c46720ea9cd33632fcd8d7601f32effd2fd603e75955919c7aa8024
   languageName: node
   linkType: hard
 
-"vue-router@npm:4.0.14":
-  version: 4.0.14
-  resolution: "vue-router@npm:4.0.14"
+"vue-router@npm:4.0.15":
+  version: 4.0.15
+  resolution: "vue-router@npm:4.0.15"
   dependencies:
     "@vue/devtools-api": ^6.0.0
   peerDependencies:
     vue: ^3.2.0
-  checksum: 694f6a85f82c11321ecbbb181db9b1a63fe99f781f33feab56c6ed1924370553b9d4325a9bdc1fbd3bfacb4cea12ef37cee77bc91974c1c6314e07fe8193941b
+  checksum: 9fcfcd05db32b565059af8e70499e5f7c9f81d555aa9d95f2e8ef306fba941a288985037874e184e6212e8c49d509ad61d12e6c4bd94f3e4fbf578934293ec51
   languageName: node
   linkType: hard
 
@@ -6986,12 +7055,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"zen-observable-ts@npm:^1.2.0":
-  version: 1.2.3
-  resolution: "zen-observable-ts@npm:1.2.3"
+"zen-observable-ts@npm:^1.2.5":
+  version: 1.2.5
+  resolution: "zen-observable-ts@npm:1.2.5"
   dependencies:
     zen-observable: 0.8.15
-  checksum: 0548b555c67671f1240fb416755d2c27abf095b74a9e25c1abf23b2e15de40e6b076c678a162021358fe62914864eb9f0a57cd65e203d66c4988a08b220e6172
+  checksum: 3b707b7a0239a9bc40f73ba71b27733a689a957c1f364fabb9fa9cbd7d04b7c2faf0d517bf17004e3ed3f4330ac613e84c0d32313e450ddaa046f3350af44541
   languageName: node
   linkType: hard