Переглянути джерело

refactor: admin sites + create site dialog to vue3 comp api

NGPixel 3 роки тому
батько
коміт
dfde2e10aa

+ 2 - 1
dev.code-workspace

@@ -13,6 +13,7 @@
 		"i18n-ally.localesPaths": [
 			"src/i18n",
 			"src/i18n/locales"
-		]
+		],
+		"i18n-ally.keystyle": "nested"
 	}
 }

+ 93 - 79
ux/src/components/SiteCreateDialog.vue

@@ -1,24 +1,24 @@
 <template lang="pug">
-q-dialog(ref='dialog', @hide='onDialogHide')
+q-dialog(ref='dialogRef', @hide='onDialogHide')
   q-card(style='min-width: 450px;')
     q-card-section.card-header
       q-icon(name='img:/_assets/icons/fluent-plus-plus.svg', left, size='sm')
-      span {{$t(`admin.sites.new`)}}
+      span {{t(`admin.sites.new`)}}
     q-form.q-py-sm(ref='createSiteForm')
       q-item
         blueprint-icon(icon='home')
         q-item-section
           q-input(
             outlined
-            v-model='siteName'
+            v-model='state.siteName'
             dense
             :rules=`[
-              val => val.length > 0 || $t('admin.sites.nameMissing'),
-              val => /^[^<>"]+$/.test(val) || $t('admin.sites.nameInvalidChars')
+              val => val.length > 0 || t('admin.sites.nameMissing'),
+              val => /^[^<>"]+$/.test(val) || t('admin.sites.nameInvalidChars')
             ]`
             hide-bottom-space
-            :label='$t(`common.field.name`)'
-            :aria-label='$t(`common.field.name`)'
+            :label='t(`common.field.name`)'
+            :aria-label='t(`common.field.name`)'
             lazy-rules='ondemand'
             autofocus
             )
@@ -27,107 +27,121 @@ q-dialog(ref='dialog', @hide='onDialogHide')
         q-item-section
           q-input(
             outlined
-            v-model='siteHostname'
+            v-model='state.siteHostname'
             dense
             :rules=`[
-              val => val.length > 0 || $t('admin.sites.hostnameMissing'),
-              val => /^(\\*)|([a-z0-9\-.:]+)$/.test(val) || $t('admin.sites.hostnameInvalidChars')
+              val => val.length > 0 || t('admin.sites.hostnameMissing'),
+              val => /^(\\*)|([a-z0-9\-.:]+)$/.test(val) || t('admin.sites.hostnameInvalidChars')
             ]`
-            :hint='$t(`admin.sites.hostnameHint`)'
+            :hint='t(`admin.sites.hostnameHint`)'
             hide-bottom-space
-            :label='$t(`admin.sites.hostname`)'
-            :aria-label='$t(`admin.sites.hostname`)'
+            :label='t(`admin.sites.hostname`)'
+            :aria-label='t(`admin.sites.hostname`)'
             lazy-rules='ondemand'
             )
     q-card-actions.card-actions
       q-space
       q-btn.acrylic-btn(
         flat
-        :label='$t(`common.actions.cancel`)'
+        :label='t(`common.actions.cancel`)'
         color='grey'
         padding='xs md'
-        @click='hide'
+        @click='onDialogCancel'
         )
       q-btn(
         unelevated
-        :label='$t(`common.actions.create`)'
+        :label='t(`common.actions.create`)'
         color='primary'
         padding='xs md'
         @click='create'
-        :loading='isLoading'
+        :loading='state.isLoading'
         )
 </template>
 
-<script>
+<script setup>
 import gql from 'graphql-tag'
+import { useI18n } from 'vue-i18n'
+import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { reactive, ref } from 'vue'
 
-export default {
-  emits: ['ok', 'hide'],
-  data () {
-    return {
-      siteName: '',
-      siteHostname: 'wiki.example.com',
-      isLoading: false
+import { useAdminStore } from '../stores/admin'
+
+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({
+  siteName: '',
+  siteHostname: 'wiki.example.com',
+  isLoading: false
+})
+
+// REFS
+
+const createSiteForm = ref(null)
+
+// METHODS
+
+async function create () {
+  state.isLoading = true
+  try {
+    const isFormValid = await createSiteForm.value.validate(true)
+    if (!isFormValid) {
+      throw new Error(t('admin.sites.createInvalidData'))
     }
-  },
-  methods: {
-    show () {
-      this.$refs.dialog.show()
-    },
-    hide () {
-      this.$refs.dialog.hide()
-    },
-    onDialogHide () {
-      this.$emit('hide')
-    },
-    async create () {
-      this.isLoading = true
-      try {
-        const isFormValid = await this.$refs.createSiteForm.validate(true)
-        if (!isFormValid) {
-          throw new Error(this.$t('admin.sites.createInvalidData'))
-        }
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation createSite (
-              $hostname: String!
-              $title: String!
-              ) {
-              createSite(
-                hostname: $hostname
-                title: $title
-                ) {
-                status {
-                  succeeded
-                  message
-                }
-              }
+    const resp = await APOLLO_CLIENT.mutate({
+      mutation: gql`
+        mutation createSite (
+          $hostname: String!
+          $title: String!
+          ) {
+          createSite(
+            hostname: $hostname
+            title: $title
+            ) {
+            status {
+              succeeded
+              message
             }
-          `,
-          variables: {
-            hostname: this.siteHostname,
-            title: this.siteName
           }
-        })
-        if (resp?.data?.createSite?.status?.succeeded) {
-          this.$q.notify({
-            type: 'positive',
-            message: this.$t('admin.sites.createSuccess')
-          })
-          await this.$store.dispatch('admin/fetchSites')
-          this.$emit('ok')
-          this.hide()
-        } else {
-          throw new Error(resp?.data?.createSite?.status?.message || 'An unexpected error occured.')
         }
-      } catch (err) {
-        this.$q.notify({
-          type: 'negative',
-          message: err.message
-        })
+      `,
+      variables: {
+        hostname: state.siteHostname,
+        title: state.siteName
       }
-      this.isLoading = false
+    })
+    if (resp?.data?.createSite?.status?.succeeded) {
+      $q.notify({
+        type: 'positive',
+        message: t('admin.sites.createSuccess')
+      })
+      await adminStore.fetchSites()
+      onDialogOK()
+    } else {
+      throw new Error(resp?.data?.createSite?.status?.message || 'An unexpected error occured.')
     }
+  } catch (err) {
+    $q.notify({
+      type: 'negative',
+      message: err.message
+    })
   }
+  state.isLoading = false
 }
 </script>

+ 39 - 0
ux/src/css/app.scss

@@ -138,6 +138,45 @@ body.desktop .acrylic-btn {
   }
 }
 
+// ------------------------------------------------------------------
+// DIALOGS
+// ------------------------------------------------------------------
+
+.card-header {
+  display: flex;
+  align-items: center;
+  font-weight: 500;
+  font-size: .9rem;
+  background-color: $dark-3;
+  background-image: radial-gradient(at bottom right, $dark-3, $dark-5);
+  color: #FFF;
+
+  @at-root .body--light & {
+    border-bottom: 1px solid $dark-3;
+    box-shadow: 0 1px 0 0 $dark-6;
+  }
+  @at-root .body--dark & {
+    border-bottom: 1px solid #000;
+    box-shadow: 0 1px 0 0 lighten($dark-3, 2%);
+  }
+}
+
+.card-actions {
+  @at-root .body--light & {
+    background-color: #FAFAFA;
+    background-image: linear-gradient(to bottom, #FCFCFC, #F0F0F0);
+    color: $dark-3;
+    border-top: 1px solid #EEE;
+    box-shadow: inset 0 1px 0 0 #FFF;
+  }
+  @at-root .body--dark & {
+    background-color: $dark-3;
+    background-image: radial-gradient(at top left, $dark-3, $dark-5);
+    border-top: 1px solid #000;
+    box-shadow: 0 -1px 0 0 lighten($dark-3, 2%);
+  }
+}
+
 // ------------------------------------------------------------------
 // IMPORTS
 // ------------------------------------------------------------------

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

@@ -167,8 +167,7 @@ q-layout.admin(view='hHh Lpr lff')
           q-item-section {{ t('admin.dev.flags.title') }}
   q-page-container.admin-container
     router-view(v-slot='{ Component }')
-      transition(name='fade')
-        component(:is='Component')
+      component(:is='Component')
   q-dialog.admin-overlay(
     v-model='overlayIsShown'
     persistent

+ 85 - 78
ux/src/pages/AdminSites.vue

@@ -4,8 +4,8 @@ q-page.admin-locale
     .col-auto
       img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-change-theme.svg')
     .col.q-pl-md
-      .text-h5.text-primary.animated.fadeInLeft {{ $t('admin.sites.title') }}
-      .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ $t('admin.sites.subtitle') }}
+      .text-h5.text-primary.animated.fadeInLeft {{ t('admin.sites.title') }}
+      .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.sites.subtitle') }}
     .col-auto
       q-btn.q-mr-sm.acrylic-btn(
         icon='las la-question-circle'
@@ -16,7 +16,7 @@ q-page.admin-locale
         target='_blank'
         )
       q-btn.q-mr-sm.acrylic-btn(
-        icon='las la-redo-alt'
+        icon='fa-solid fa-rotate'
         flat
         color='secondary'
         @click='refresh'
@@ -24,7 +24,7 @@ q-page.admin-locale
       q-btn(
         unelevated
         icon='las la-plus'
-        :label='$t(`admin.sites.new`)'
+        :label='t(`admin.sites.new`)'
         color='primary'
         @click='createSite'
         )
@@ -34,7 +34,7 @@ q-page.admin-locale
       q-card.shadow-1
         q-list(separator)
           q-item(
-            v-for='site of sites'
+            v-for='site of adminStore.sites'
             :key='site.id'
             )
             q-item-section(side)
@@ -75,8 +75,8 @@ q-page.admin-locale
                 color='primary'
                 checked-icon='las la-check'
                 unchecked-icon='las la-times'
-                :label='$t(`admin.sites.isActive`)'
-                :aria-label='$t(`admin.sites.isActive`)'
+                :label='t(`admin.sites.isActive`)'
+                :aria-label='t(`admin.sites.isActive`)'
                 @update:model-value ='(val) => { toggleSiteState(site, val) }'
                 )
             q-separator.q-ml-md(vertical)
@@ -86,7 +86,7 @@ q-page.admin-locale
                 @click='editSite(site)'
                 icon='las la-pen'
                 color='indigo'
-                :label='$t(`common.actions.edit`)'
+                :label='t(`common.actions.edit`)'
                 no-caps
                 )
               q-btn.acrylic-btn(
@@ -97,82 +97,89 @@ q-page.admin-locale
                 )
 </template>
 
-<script>
-import { get } from 'vuex-pathify'
-import { copyToClipboard, createMetaMixin } from 'quasar'
+<script setup>
+import { useMeta, useQuasar } from 'quasar'
+import { useI18n } from 'vue-i18n'
+import { defineAsyncComponent, nextTick, onMounted, reactive, ref, watch } from 'vue'
+import { useRouter } from 'vue-router'
+
+import { useAdminStore } from '../stores/admin'
+
+// COMPONENTS
 
 import SiteActivateDialog from '../components/SiteActivateDialog.vue'
 import SiteCreateDialog from '../components/SiteCreateDialog.vue'
 import SiteDeleteDialog from '../components/SiteDeleteDialog.vue'
 
-export default {
-  mixins: [
-    createMetaMixin(function () {
-      return {
-        title: this.$t('admin.sites.title')
-      }
-    })
-  ],
-  data () {
-    return {
-      loading: false
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const adminStore = useAdminStore()
+
+// ROUTER
+
+const router = useRouter()
+
+// I18N
+
+const { t } = useI18n()
+
+// META
+
+useMeta({
+  title: t('admin.sites.title')
+})
+
+// DATA
+
+const loading = ref(false)
+
+// METHODS
+
+async function refresh () {
+  await adminStore.fetchSites()
+  $q.notify({
+    type: 'positive',
+    message: t('admin.sites.refreshSuccess')
+  })
+}
+function createSite () {
+  $q.dialog({
+    component: SiteCreateDialog
+  })
+}
+function editSite (st) {
+  adminStore.$patch({
+    currentSiteId: st.id
+  })
+  nextTick(() => {
+    router.push(`/_admin/${st.id}/general`)
+  })
+}
+function toggleSiteState (st, newState) {
+  $q.dialog({
+    component: SiteActivateDialog,
+    componentProps: {
+      site: st,
+      value: newState
     }
-  },
-  computed: {
-    sites: get('admin/sites', false)
-  },
-  methods: {
-    copyID (uid) {
-      copyToClipboard(uid).then(() => {
-        this.$q.notify({
-          type: 'positive',
-          message: this.$t('common.clipboard.uuidSuccess')
-        })
-      }).catch(() => {
-        this.$q.notify({
-          type: 'negative',
-          message: this.$t('common.clipboard.uuidFailure')
-        })
-      })
-    },
-    async refresh () {
-      await this.$store.dispatch('admin/fetchSites')
-      this.$q.notify({
-        type: 'positive',
-        message: this.$t('admin.sites.refreshSuccess')
-      })
-    },
-    createSite () {
-      this.$q.dialog({
-        component: SiteCreateDialog
-      })
-    },
-    editSite (st) {
-      this.$store.set('admin/currentSiteId', st.id)
-      this.$nextTick(() => {
-        this.$router.push(`/_admin/${st.id}/general`)
-      })
-    },
-    toggleSiteState (st, newState) {
-      this.$q.dialog({
-        component: SiteActivateDialog,
-        componentProps: {
-          site: st,
-          value: newState
-        }
-      })
-    },
-    deleteSite (st) {
-      this.$q.dialog({
-        component: SiteDeleteDialog,
-        componentProps: {
-          site: st
-        }
-      })
+  })
+}
+function deleteSite (st) {
+  $q.dialog({
+    component: SiteDeleteDialog,
+    componentProps: {
+      site: st
     }
-  },
-  mounted () {
-    this.$store.dispatch('admin/fetchSites')
-  }
+  })
 }
+
+// MOUNTED
+
+onMounted(async () => {
+  await adminStore.fetchSites()
+})
 </script>