Browse Source

feat(admin): migrate groups page to vue 3 composable

Nicolas Giard 3 years ago
parent
commit
7e344fc6fa
4 changed files with 214 additions and 155 deletions
  1. 1 1
      server/db/migrations/3.0.0.js
  2. 43 26
      server/graph/schemas/group.graphql
  3. 169 127
      ux/src/pages/AdminGroups.vue
  4. 1 1
      ux/src/router/routes.js

+ 1 - 1
server/db/migrations/3.0.0.js

@@ -341,7 +341,7 @@ exports.up = async knex => {
   // -> GENERATE IDS
   // -> GENERATE IDS
 
 
   const groupAdminId = uuid()
   const groupAdminId = uuid()
-  const groupGuestId = '10000000-0000-4000-0000-000000000001'
+  const groupGuestId = '10000000-0000-4000-8000-000000000001'
   const siteId = uuid()
   const siteId = uuid()
   const authModuleId = uuid()
   const authModuleId = uuid()
   const userAdminId = uuid()
   const userAdminId = uuid()

+ 43 - 26
server/graph/schemas/group.graphql

@@ -9,7 +9,7 @@ extend type Query {
   ): [Group]
   ): [Group]
 
 
   groupById(
   groupById(
-    id: Int!
+    id: UUID!
   ): Group
   ): Group
 }
 }
 
 
@@ -19,22 +19,25 @@ extend type Mutation {
   ): GroupResponse
   ): GroupResponse
 
 
   updateGroup(
   updateGroup(
-    id: Int!
-    patch: GroupUpdateInput!
+    id: UUID!
+    name: String!
+    redirectOnLogin: String!
+    permissions: [String]!
+    rules: [GroupRuleInput]!
   ): DefaultResponse
   ): DefaultResponse
 
 
   deleteGroup(
   deleteGroup(
-    id: Int!
+    id: UUID!
   ): DefaultResponse
   ): DefaultResponse
 
 
   assignUserToGroup(
   assignUserToGroup(
-    groupId: Int!
-    userId: Int!
+    groupId: UUID!
+    userId: UUID!
   ): DefaultResponse
   ): DefaultResponse
 
 
   unassignUserFromGroup(
   unassignUserFromGroup(
-    groupId: Int!
-    userId: Int!
+    groupId: UUID!
+    userId: UUID!
   ): DefaultResponse
   ): DefaultResponse
 }
 }
 
 
@@ -48,46 +51,60 @@ type GroupResponse {
 }
 }
 
 
 type Group {
 type Group {
-  id: Int
+  id: UUID
   name: String
   name: String
   isSystem: Boolean
   isSystem: Boolean
   redirectOnLogin: String
   redirectOnLogin: String
+  redirectOnFirstLogin: String
+  redirectOnLogout: String
   permissions: [String]
   permissions: [String]
-  pageRules: [PageRule]
-  users: [UserMinimal]
+  rules: [GroupRule]
+  users(
+    page: Int
+    pageSize: Int
+    orderBy: UserOrderBy
+    orderByDirection: OrderByDirection
+    # Filter by name / email
+    filter: String
+    ): [UserMinimal]
+  userCount: Int
   createdAt: Date
   createdAt: Date
   updatedAt: Date
   updatedAt: Date
 }
 }
 
 
-type PageRule {
-  id: String
-  deny: Boolean
-  match: PageRuleMatch
+type GroupRule {
+  id: UUID
+  name: String
+  mode: GroupRuleMode
+  match: GroupRuleMatch
   roles: [String]
   roles: [String]
   path: String
   path: String
   locales: [String]
   locales: [String]
+  sites: [UUID]
 }
 }
 
 
-input GroupUpdateInput {
+input GroupRuleInput {
+  id: UUID!
   name: String!
   name: String!
-  redirectOnLogin: String!
-  permissions: [String]!
-  pageRules: [PageRuleInput]!
-}
-
-input PageRuleInput {
-  id: String!
-  deny: Boolean!
-  match: PageRuleMatch!
+  mode: GroupRuleMode!
+  match: GroupRuleMatch!
   roles: [String]!
   roles: [String]!
   path: String!
   path: String!
   locales: [String]!
   locales: [String]!
+  sites: [UUID]
+}
+
+enum GroupRuleMode {
+  ALLOW
+  DENY
+  FORCEALLOW
 }
 }
 
 
-enum PageRuleMatch {
+enum GroupRuleMatch {
   START
   START
   EXACT
   EXACT
   END
   END
   REGEX
   REGEX
   TAG
   TAG
+  TAGALL
 }
 }

+ 169 - 127
ux/src/pages/AdminGroups.vue

@@ -4,12 +4,12 @@ q-page.admin-groups
     .col-auto
     .col-auto
       img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-people.svg')
       img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-people.svg')
     .col.q-pl-md
     .col.q-pl-md
-      .text-h5.text-primary.animated.fadeInLeft {{ $t('admin.groups.title') }}
-      .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ $t('admin.groups.subtitle') }}
+      .text-h5.text-primary.animated.fadeInLeft {{ t('admin.groups.title') }}
+      .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.groups.subtitle') }}
     .col-auto.flex.items-center
     .col-auto.flex.items-center
       q-input.denser.q-mr-sm(
       q-input.denser.q-mr-sm(
         outlined
         outlined
-        v-model='search'
+        v-model='state.search'
         dense
         dense
         :class='$q.dark.isActive ? `bg-dark` : `bg-white`'
         :class='$q.dark.isActive ? `bg-dark` : `bg-white`'
         )
         )
@@ -28,12 +28,12 @@ q-page.admin-groups
         flat
         flat
         color='secondary'
         color='secondary'
         @click='load'
         @click='load'
-        :loading='loading > 0'
+        :loading='state.loading > 0'
         )
         )
       q-btn(
       q-btn(
         unelevated
         unelevated
         icon='las la-plus'
         icon='las la-plus'
-        :label='$t(`admin.groups.create`)'
+        :label='t(`admin.groups.create`)'
         color='primary'
         color='primary'
         @click='createGroup'
         @click='createGroup'
         )
         )
@@ -42,15 +42,15 @@ q-page.admin-groups
     .col-12
     .col-12
       q-card.shadow-1
       q-card.shadow-1
         q-table(
         q-table(
-          :rows='groups'
+          :rows='state.groups'
           :columns='headers'
           :columns='headers'
           row-key='id'
           row-key='id'
           flat
           flat
           hide-header
           hide-header
           hide-bottom
           hide-bottom
           :rows-per-page-options='[0]'
           :rows-per-page-options='[0]'
-          :loading='loading > 0'
-          :filter='search'
+          :loading='state.loading > 0'
+          :filter='state.search'
           )
           )
           template(v-slot:body-cell-id='props')
           template(v-slot:body-cell-id='props')
             q-td(:props='props')
             q-td(:props='props')
@@ -71,7 +71,7 @@ q-page.admin-groups
                 :color='$q.dark.isActive ? `dark-6` : `grey-2`'
                 :color='$q.dark.isActive ? `dark-6` : `grey-2`'
                 :text-color='$q.dark.isActive ? `white` : `grey-8`'
                 :text-color='$q.dark.isActive ? `white` : `grey-8`'
                 dense
                 dense
-              ) {{$t('admin.groups.usersCount', { count: props.value })}}
+              ) {{t('admin.groups.usersCount', { count: props.value })}}
           template(v-slot:body-cell-edit='props')
           template(v-slot:body-cell-edit='props')
             q-td(:props='props')
             q-td(:props='props')
               q-btn.acrylic-btn.q-mr-sm(
               q-btn.acrylic-btn.q-mr-sm(
@@ -79,7 +79,7 @@ q-page.admin-groups
                 :to='`/_admin/groups/` + props.row.id'
                 :to='`/_admin/groups/` + props.row.id'
                 icon='las la-pen'
                 icon='las la-pen'
                 color='indigo'
                 color='indigo'
-                :label='$t(`common.actions.edit`)'
+                :label='t(`common.actions.edit`)'
                 no-caps
                 no-caps
                 )
                 )
               q-btn.acrylic-btn(
               q-btn.acrylic-btn(
@@ -91,136 +91,178 @@ q-page.admin-groups
                 )
                 )
 </template>
 </template>
 
 
-<script>
+<script setup>
 import gql from 'graphql-tag'
 import gql from 'graphql-tag'
 import cloneDeep from 'lodash/cloneDeep'
 import cloneDeep from 'lodash/cloneDeep'
-import { createMetaMixin } from 'quasar'
-import { sync } from 'vuex-pathify'
+import { useI18n } from 'vue-i18n'
+import { useMeta, useQuasar } from 'quasar'
+import { computed, onBeforeUnmount, onMounted, reactive, watch } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+
+import { useAdminStore } from 'src/stores/admin'
 
 
 import GroupCreateDialog from '../components/GroupCreateDialog.vue'
 import GroupCreateDialog from '../components/GroupCreateDialog.vue'
 import GroupDeleteDialog from '../components/GroupDeleteDialog.vue'
 import GroupDeleteDialog from '../components/GroupDeleteDialog.vue'
 
 
-export default {
-  mixins: [
-    createMetaMixin(function () {
-      return {
-        title: this.$t('admin.groups.title')
-      }
-    })
-  ],
-  data () {
-    return {
-      groups: [],
-      loading: 0,
-      search: ''
-    }
-  },
-  computed: {
-    overlay: sync('admin/overlay', false),
-    headers () {
-      return [
-        {
-          align: 'center',
-          field: 'id',
-          name: 'id',
-          sortable: false,
-          style: 'width: 20px'
-        },
-        {
-          label: this.$t('common.field.name'),
-          align: 'left',
-          field: 'name',
-          name: 'name',
-          sortable: true
-        },
-        {
-          label: this.$t('admin.groups.userCount'),
-          align: 'center',
-          field: 'userCount',
-          name: 'usercount',
-          sortable: false,
-          style: 'width: 150px'
-        },
-        {
-          label: '',
-          align: 'right',
-          field: 'edit',
-          name: 'edit',
-          sortable: false,
-          style: 'width: 250px'
-        }
-      ]
-    }
-  },
-  watch: {
-    overlay (newValue, oldValue) {
-      if (newValue === '' && oldValue === 'GroupEditOverlay') {
-        this.$router.push('/_admin/groups')
-        this.load()
-      }
-    },
-    $route: 'checkOverlay'
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const adminStore = useAdminStore()
+
+// ROUTER
+
+const router = useRouter()
+const route = useRoute()
+
+// I18N
+
+const { t } = useI18n()
+
+// META
+
+useMeta({
+  title: t('admin.groups.title')
+})
+
+// DATA
+
+const state = reactive({
+  groups: [],
+  loading: 0,
+  search: ''
+})
+
+const headers = [
+  {
+    align: 'center',
+    field: 'id',
+    name: 'id',
+    sortable: false,
+    style: 'width: 20px'
   },
   },
-  mounted () {
-    this.checkOverlay()
-    this.load()
+  {
+    label: t('common.field.name'),
+    align: 'left',
+    field: 'name',
+    name: 'name',
+    sortable: true
   },
   },
-  beforeUnmount () {
-    this.overlay = ''
+  {
+    label: t('admin.groups.userCount'),
+    align: 'center',
+    field: 'userCount',
+    name: 'usercount',
+    sortable: false,
+    style: 'width: 150px'
   },
   },
-  methods: {
-    async load () {
-      this.loading++
-      this.$q.loading.show()
-      const resp = await this.$apollo.query({
-        query: gql`
-          query getGroups {
-            groups {
-              id
-              name
-              isSystem
-              userCount
-              createdAt
-              updatedAt
-            }
+  {
+    label: '',
+    align: 'right',
+    field: 'edit',
+    name: 'edit',
+    sortable: false,
+    style: 'width: 250px'
+  }
+]
+
+watch(() => adminStore.overlay, (newValue, oldValue) => {
+  if (newValue === '' && oldValue === 'GroupEditOverlay') {
+    router.push('/_admin/groups')
+    load()
+  }
+})
+
+watch(() => route, () => {
+  checkOverlay()
+})
+
+// METHODS
+
+async function load () {
+  state.loading++
+  $q.loading.show()
+  try {
+    const resp = await APOLLO_CLIENT.query({
+      query: gql`
+        query getGroups {
+          groups {
+            id
+            name
+            isSystem
+            userCount
+            createdAt
+            updatedAt
           }
           }
-        `,
-        fetchPolicy: 'network-only'
-      })
-      this.groups = cloneDeep(resp?.data?.groups)
-      this.$q.loading.hide()
-      this.loading--
-    },
-    checkOverlay () {
-      if (this.$route.params && this.$route.params.id) {
-        this.$store.set('admin/overlayOpts', { id: this.$route.params.id })
-        this.$store.set('admin/overlay', 'GroupEditOverlay')
-      } else {
-        this.$store.set('admin/overlay', '')
-      }
-    },
-    createGroup () {
-      this.$q.dialog({
-        component: GroupCreateDialog
-      }).onOk(() => {
-        this.load()
-      })
-    },
-    editGroup (gr) {
-      this.$router.push(`/_admin/groups/${gr.id}`)
-    },
-    deleteGroup (gr) {
-      this.$q.dialog({
-        component: GroupDeleteDialog,
-        componentProps: {
-          group: gr
         }
         }
-      }).onOk(() => {
-        this.load()
-      })
-    }
+      `,
+      fetchPolicy: 'network-only'
+    })
+    state.groups = cloneDeep(resp?.data?.groups)
+  } catch (err) {
+    $q.notify({
+      type: 'negative',
+      message: 'Failed to load groups.',
+      caption: err.message
+    })
   }
   }
+  $q.loading.hide()
+  state.loading--
+}
+
+function checkOverlay () {
+  if (route.params && route.params.id) {
+    adminStore.$patch({
+      overlayOpts: { id: route.params.id },
+      overlay: 'GroupEditOverlay'
+    })
+  } else {
+    adminStore.$patch({
+      overlay: ''
+    })
+  }
+}
+
+function createGroup () {
+  $q.dialog({
+    component: GroupCreateDialog
+  }).onOk(() => {
+    load()
+  })
+}
+
+function editGroup (gr) {
+  router.push(`/_admin/groups/${gr.id}`)
+}
+
+function deleteGroup (gr) {
+  $q.dialog({
+    component: GroupDeleteDialog,
+    componentProps: {
+      group: gr
+    }
+  }).onOk(() => {
+    load()
+  })
 }
 }
+
+// MOUNTED
+
+onMounted(() => {
+  checkOverlay()
+  load()
+})
+
+// BEFORE UNMOUNT
+
+onBeforeUnmount(() => {
+  adminStore.$patch({
+    overlay: ''
+  })
+})
+
 </script>
 </script>
 
 
 <style lang='scss'>
 <style lang='scss'>

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

@@ -41,7 +41,7 @@ const routes = [
       // { path: ':siteid/theme', component: () => import('../pages/AdminTheme.vue') },
       // { path: ':siteid/theme', component: () => import('../pages/AdminTheme.vue') },
       // -> Users
       // -> Users
       // { path: 'auth', component: () => import('../pages/AdminAuth.vue') },
       // { path: 'auth', component: () => import('../pages/AdminAuth.vue') },
-      // { path: 'groups/:id?/:section?', component: () => import('../pages/AdminGroups.vue') },
+      { path: 'groups/:id?/:section?', component: () => import('../pages/AdminGroups.vue') },
       // { path: 'users/:id?/:section?', component: () => import('../pages/AdminUsers.vue') },
       // { path: 'users/:id?/:section?', component: () => import('../pages/AdminUsers.vue') },
       // -> System
       // -> System
       // { path: 'api', component: () => import('../pages/AdminApi.vue') },
       // { path: 'api', component: () => import('../pages/AdminApi.vue') },