Browse Source

feat: file manager improvements

NGPixel 2 years ago
parent
commit
5efa0abe62

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

@@ -28,7 +28,7 @@ services:
     # (Adding the "ports" property to this file will not forward from a Codespace.)
 
   db:
-    image: postgres:latest
+    image: postgres:16beta1
     restart: unless-stopped
     volumes:
       - postgres-data:/var/lib/postgresql/data

+ 1 - 6
server/graph/resolvers/tree.mjs

@@ -66,11 +66,6 @@ export default {
           if (args.includeAncestors) {
             const parentPathParts = parentPath.split('.')
             for (let i = 0; i <= parentPathParts.length; i++) {
-              console.info({
-                folderPath: encodeFolderPath(_.dropRight(parentPathParts, i).join('.')),
-                fileName: _.nth(parentPathParts, i * -1),
-                type: 'folder'
-              })
               builder.orWhere({
                 folderPath: encodeFolderPath(_.dropRight(parentPathParts, i).join('.')),
                 fileName: _.nth(parentPathParts, i * -1),
@@ -110,7 +105,7 @@ export default {
         updatedAt: item.updatedAt,
         ...(item.type === 'folder') && {
           childrenCount: item.meta?.children || 0,
-          isAncestor: item.folderPath.length < parentPath.length || (parentPath !== '' && item.folderPath === parentPath)
+          isAncestor: item.folderPath.length < parentPath.length
         },
         ...(item.type === 'asset') && {
           fileSize: item.meta?.fileSize || 0,

+ 2 - 0
server/locales/en.json

@@ -1517,6 +1517,7 @@
   "editor.pageRel.title": "Add Page Relation",
   "editor.pageRel.titleEdit": "Edit Page Relation",
   "editor.pageScripts.title": "Page Scripts",
+  "editor.props.alias": "Alias",
   "editor.props.allowComments": "Allow Comments",
   "editor.props.allowCommentsHint": "Enable commenting abilities on this page.",
   "editor.props.allowContributions": "Allow Contributions",
@@ -1642,6 +1643,7 @@
   "fileman.rarFileType": "RAR Archive",
   "fileman.renameFolderInvalidData": "One or more fields are invalid.",
   "fileman.renameFolderSuccess": "Folder renamed successfully.",
+  "fileman.searchFolder": "Search folder...",
   "fileman.svgFileType": "Scalable Vector Graphic",
   "fileman.tarFileType": "TAR Archive",
   "fileman.tgzFileType": "Gzipped TAR Archive",

+ 21 - 5
ux/src/components/FileManager.vue

@@ -8,9 +8,11 @@ q-layout.fileman(view='hHh lpR lFr', container)
       q-btn.q-mr-sm.acrylic-btn(
         flat
         color='white'
-        label='EN'
+        :label='commonStore.locale'
+        :aria-label='commonStore.locale'
         style='height: 40px;'
         )
+        locale-selector-menu
       q-input(
         dark
         v-model='state.search'
@@ -18,7 +20,7 @@ q-layout.fileman(view='hHh lpR lFr', container)
         dense
         ref='searchField'
         style='width: 100%;'
-        label='Search folder...'
+        :label='t(`fileman.searchFolder`)'
         :debounce='500'
         )
         template(v-slot:prepend)
@@ -197,6 +199,8 @@ q-layout.fileman(view='hHh lpR lFr', container)
               :hide-asset-btn='true'
               :show-new-folder='true'
               @new-folder='() => newFolder(state.currentFolderId)'
+              @new-page='() => close()'
+              :base-path='folderPath'
               )
           q-btn(
             flat
@@ -252,11 +256,11 @@ q-layout.fileman(view='hHh lpR lFr', container)
                   )
                   q-card.q-pa-sm
                     q-list(dense, style='min-width: 150px;')
-                      q-item(clickable, v-if='item.type !== `folder`', @click='insertItem(item)')
+                      q-item(clickable, v-if='insertMode && item.type !== `folder`', @click='insertItem(item)')
                         q-item-section(side)
                           q-icon(name='las la-plus-circle', color='primary')
                         q-item-section {{ t(`common.actions.insert`) }}
-                      q-item(clickable, v-if='item.type === `page`')
+                      q-item(clickable, v-if='item.type === `page`', @click='editItem(item)')
                         q-item-section(side)
                           q-icon(name='las la-edit', color='orange')
                         q-item-section {{ t(`common.actions.edit`) }}
@@ -277,7 +281,7 @@ q-layout.fileman(view='hHh lpR lFr', container)
                         q-item-section(side)
                           q-icon(name='las la-clipboard', color='primary')
                         q-item-section {{ t(`common.actions.copyURL`) }}
-                      q-item(clickable, v-if='item.type !== `folder`', @click='')
+                      q-item(clickable, v-if='item.type !== `folder`', @click='downloadItem(item)')
                         q-item-section(side)
                           q-icon(name='las la-download', color='primary')
                         q-item-section {{ t(`common.actions.download`) }}
@@ -326,12 +330,14 @@ import Tree from './TreeNav.vue'
 
 import fileTypes from '../helpers/fileTypes'
 
+import { useCommonStore } from 'src/stores/common'
 import { usePageStore } from 'src/stores/page'
 import { useSiteStore } from 'src/stores/site'
 
 import FolderCreateDialog from 'src/components/FolderCreateDialog.vue'
 import FolderDeleteDialog from 'src/components/FolderDeleteDialog.vue'
 import FolderRenameDialog from 'src/components/FolderRenameDialog.vue'
+import LocaleSelectorMenu from 'src/components/LocaleSelectorMenu.vue'
 
 // QUASAR
 
@@ -339,6 +345,7 @@ const $q = useQuasar()
 
 // STORES
 
+const commonStore = useCommonStore()
 const pageStore = usePageStore()
 const siteStore = useSiteStore()
 
@@ -941,6 +948,15 @@ async function copyItemURL (item) {
   }
 }
 
+async function editItem (item) {
+  router.push(item.folderPath ? `/_edit/${item.folderPath}/${item.fileName}` : `/_edit/${item.fileName}`)
+  close()
+}
+
+function downloadItem (item) {
+
+}
+
 function renameItem (item) {
   console.info(item)
   switch (item.type) {

+ 0 - 181
ux/src/components/LocaleInstallDialog.vue

@@ -1,181 +0,0 @@
-<template lang="pug">
-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`)}}
-    q-card-section.q-pa-none
-      q-table.no-border-radius(
-        :data='state.locales'
-        :columns='headers'
-        row-name='code'
-        flat
-        hide-bottom
-        :rows-per-page-options='[0]'
-        :loading='state.loading > 0'
-        )
-        template(v-slot:body-cell-code='props')
-          q-td(:props='props')
-            q-chip(
-              square
-              color='teal'
-              text-color='white'
-              dense
-              ): span.text-caption {{props.value}}
-        template(v-slot:body-cell-name='props')
-          q-td(:props='props')
-            strong {{props.value}}
-        template(v-slot:body-cell-isRTL='props')
-          q-td(:props='props')
-            q-icon(
-              v-if='props.value'
-              name='las la-check'
-              color='brown'
-              size='xs'
-              )
-        template(v-slot:body-cell-availability='props')
-          q-td(:props='props')
-            q-circular-progress(
-              size='md'
-              show-value
-              :value='props.value'
-              :thickness='0.1'
-              :color='props.value <= 33 ? `negative` : (props.value <= 66) ? `warning` : `positive`'
-            ) {{ props.value }}%
-        template(v-slot:body-cell-isInstalled='props')
-          q-td(:props='props')
-            q-spinner(
-              v-if='props.row.isDownloading'
-              color='primary'
-              size='20px'
-              :thickness='2'
-              )
-            q-btn(
-              v-else-if='props.value && props.row.installDate < props.row.updatedAt'
-              flat
-              round
-              dense
-              @click='download(props.row)'
-              icon='las la-redo-alt'
-              color='accent'
-              )
-            q-btn(
-              v-else-if='props.value'
-              flat
-              round
-              dense
-              @click='download(props.row)'
-              icon='las la-check-circle'
-              color='positive'
-              )
-            q-btn(
-              v-else
-              flat
-              round
-              dense
-              @click='download(props.row)'
-              icon='las la-cloud-download-alt'
-              color='primary'
-              )
-    q-card-actions.card-actions
-      q-space
-      q-btn.acrylic-btn(
-        flat
-        :label='t(`common.actions.close`)'
-        color='grey'
-        padding='xs md'
-        @click='onDialogCancel'
-        )
-
-    q-inner-loading(:showing='state.loading > 0')
-      q-spinner(color='accent', size='lg')
-</template>
-
-<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
-  },
-  {
-    label: t('admin.locale.rtl'),
-    align: 'center',
-    field: 'isRTL',
-    name: 'isRTL',
-    sortable: false,
-    style: 'width: 10px'
-  },
-  {
-    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/components/LocaleSelectorMenu.vue

@@ -1,7 +1,7 @@
 <template lang="pug">
 q-menu.translucent-menu(
   auto-close
-  anchor='bottom middle'
+  anchor='bottom left'
   self='top left'
   )
   q-list(padding, style='min-width: 200px;')

+ 7 - 2
ux/src/components/PageNewMenu.vue

@@ -88,12 +88,16 @@ const props = defineProps({
   showNewFolder: {
     type: Boolean,
     default: false
+  },
+  basePath: {
+    type: String,
+    default: null
   }
 })
 
 // EMITS
 
-const emit = defineEmits(['newFolder'])
+const emit = defineEmits(['newFolder', 'newPage'])
 
 // QUASAR
 
@@ -114,7 +118,8 @@ const { t } = useI18n()
 
 async function create (editor) {
   $q.loading.show()
-  await pageStore.pageCreate({ editor })
+  emit('newPage')
+  await pageStore.pageCreate({ editor, basePath: props.basePath })
   $q.loading.hide()
 }
 

+ 11 - 3
ux/src/components/TreeBrowserDialog.vue

@@ -114,7 +114,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
 import { useI18n } from 'vue-i18n'
 import { computed, onMounted, reactive } from 'vue'
 import { useDialogPluginComponent, useQuasar } from 'quasar'
-import { cloneDeep, find } from 'lodash-es'
+import { cloneDeep, find, last } from 'lodash-es'
 import gql from 'graphql-tag'
 
 import fileTypes from '../helpers/fileTypes'
@@ -124,6 +124,7 @@ import Tree from 'src/components/TreeNav.vue'
 
 import { usePageStore } from 'src/stores/page'
 import { useSiteStore } from 'src/stores/site'
+import { dropRight } from 'lodash'
 
 // PROPS
 
@@ -351,6 +352,13 @@ function newFolder (parentId) {
 // MOUNTED
 
 onMounted(() => {
+  let fPath = props.folderPath
+  let fName = props.itemFileName
+  if (props.itemFileName?.indexOf('/') >= 0) {
+    const fParts = props.itemFileName.split('/')
+    fPath = dropRight(fParts, 1).join('/')
+    fName = last(fParts)
+  }
   switch (props.mode) {
     case 'pageSave': {
       state.typesToFetch = ['folder', 'page']
@@ -358,12 +366,12 @@ onMounted(() => {
     }
   }
   loadTree({
-    parentPath: props.folderPath,
+    parentPath: fPath,
     types: state.typesToFetch,
     initLoad: true
   })
   state.title = props.itemTitle || ''
-  state.path = props.itemFileName || ''
+  state.path = fName || ''
 })
 
 </script>

+ 26 - 1
ux/src/pages/Index.vue

@@ -262,6 +262,12 @@ const lastModified = computed(() => {
 // WATCHERS
 
 watch(() => route.path, async (newValue) => {
+  // -> Ignore route change (e.g. from page create route fix)
+  if (editorStore.ignoreRouteChange) {
+    editorStore.$patch({ ignoreRouteChange: false })
+    return
+  }
+
   // -> Enter Create Mode?
   if (newValue.startsWith('/_create')) {
     if (!route.params.editor) {
@@ -272,8 +278,27 @@ watch(() => route.path, async (newValue) => {
       return router.replace('/')
     }
     $q.loading.show()
-    await pageStore.pageCreate({ editor: route.params.editor })
+    const pageCreateArgs = { editor: route.params.editor, fromNavigate: true }
+    if (route.query.path) {
+      pageCreateArgs.path = route.query.path
+    }
+    if (route.query.locale) {
+      pageCreateArgs.locale = route.query.locale
+    }
+    await pageStore.pageCreate(pageCreateArgs)
+    $q.loading.hide()
+    return
+  }
+
+  // -> Enter Edit Mode?
+  if (newValue.startsWith('/_edit')) {
+    if (!route.params.pagePath) {
+      return router.replace('/')
+    }
+    $q.loading.show()
+    await pageStore.pageEdit({ path: route.params.pagePath, fromNavigate: true })
     $q.loading.hide()
+    return
   }
 
   // -> Moving to a non-page path? Ignore

+ 10 - 0
ux/src/router/routes.js

@@ -86,6 +86,16 @@ const routes = [
       { path: '', component: () => import('../pages/Index.vue') }
     ]
   },
+  // --------------------------------
+  // EDIT
+  // --------------------------------
+  {
+    path: '/_edit/:pagePath?',
+    component: () => import('../layouts/MainLayout.vue'),
+    children: [
+      { path: '', component: () => import('../pages/Index.vue') }
+    ]
+  },
   // -----------------------
   // STANDARD PAGE CATCH-ALL
   // -----------------------

+ 2 - 1
ux/src/stores/editor.js

@@ -23,7 +23,8 @@ export const useEditorStore = defineStore('editor', {
     lastChangeTimestamp: null,
     editors: {},
     configIsLoaded: false,
-    reasonForChange: ''
+    reasonForChange: '',
+    ignoreRouteChange: false
   }),
   getters: {
     hasPendingChanges: (state) => {

+ 33 - 9
ux/src/stores/page.js

@@ -313,14 +313,30 @@ export const usePageStore = defineStore('page', {
     /**
      * PAGE - CREATE
      */
-    async pageCreate ({ editor, locale, path, title = '', description = '', content = '' }) {
+    async pageCreate ({ editor, locale, path, basePath, title = '', description = '', content = '', fromNavigate = false } = {}) {
       const editorStore = useEditorStore()
 
+      // -> Load editor config
       if (!editorStore.configIsLoaded) {
         await editorStore.fetchConfigs()
       }
 
-      const noDefaultPath = Boolean(!path && path !== '')
+      // -> Path normalization
+      if (path?.startsWith('/')) {
+        path = path.substring(1)
+      }
+      if (basePath?.startsWith('/')) {
+        basePath = basePath.substring(1)
+      }
+      if (basePath?.endsWith('/')) {
+        basePath = basePath.substring(0, basePath.length - 1)
+      }
+
+      // -> Redirect if not at /_create path
+      if (!this.router.currentRoute.value.path.startsWith('/_create/') && !fromNavigate) {
+        editorStore.$patch({ ignoreRouteChange: true })
+        this.router.push(`/_create/${editor}`)
+      }
 
       // -> Init editor
       editorStore.$patch({
@@ -333,7 +349,7 @@ export const usePageStore = defineStore('page', {
       // -> Default Page Path
       let newPath = path
       if (!path && path !== '') {
-        const parentPath = dropRight(this.path.split('/'), 1).join('/')
+        const parentPath = basePath || basePath === '' ? basePath : dropRight(this.path.split('/'), 1).join('/')
         newPath = parentPath ? `${parentPath}/new-page` : 'new-page'
       }
 
@@ -353,18 +369,26 @@ export const usePageStore = defineStore('page', {
         render: '',
         mode: 'edit'
       })
-
-      if (noDefaultPath) {
-        this.router.push(`/_create/${editor}`)
-      }
     },
     /**
      * PAGE - EDIT
      */
-    async pageEdit () {
+    async pageEdit ({ path, id, fromNavigate = false } = {}) {
       const editorStore = useEditorStore()
 
-      await this.pageLoad({ id: this.id, withContent: true })
+      const loadArgs = {
+        withContent: true
+      }
+
+      if (id) {
+        loadArgs.id = id
+      } else if (path) {
+        loadArgs.path = path
+      } else {
+        loadArgs.id = this.id
+      }
+
+      await this.pageLoad(loadArgs)
 
       if (!editorStore.configIsLoaded) {
         await editorStore.fetchConfigs()