Unverified Commit 5efa0abe authored by NGPixel's avatar NGPixel

feat: file manager improvements

parent 7fde587a
...@@ -28,7 +28,7 @@ services: ...@@ -28,7 +28,7 @@ services:
# (Adding the "ports" property to this file will not forward from a Codespace.) # (Adding the "ports" property to this file will not forward from a Codespace.)
db: db:
image: postgres:latest image: postgres:16beta1
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- postgres-data:/var/lib/postgresql/data - postgres-data:/var/lib/postgresql/data
......
...@@ -66,11 +66,6 @@ export default { ...@@ -66,11 +66,6 @@ export default {
if (args.includeAncestors) { if (args.includeAncestors) {
const parentPathParts = parentPath.split('.') const parentPathParts = parentPath.split('.')
for (let i = 0; i <= parentPathParts.length; i++) { 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({ builder.orWhere({
folderPath: encodeFolderPath(_.dropRight(parentPathParts, i).join('.')), folderPath: encodeFolderPath(_.dropRight(parentPathParts, i).join('.')),
fileName: _.nth(parentPathParts, i * -1), fileName: _.nth(parentPathParts, i * -1),
...@@ -110,7 +105,7 @@ export default { ...@@ -110,7 +105,7 @@ export default {
updatedAt: item.updatedAt, updatedAt: item.updatedAt,
...(item.type === 'folder') && { ...(item.type === 'folder') && {
childrenCount: item.meta?.children || 0, childrenCount: item.meta?.children || 0,
isAncestor: item.folderPath.length < parentPath.length || (parentPath !== '' && item.folderPath === parentPath) isAncestor: item.folderPath.length < parentPath.length
}, },
...(item.type === 'asset') && { ...(item.type === 'asset') && {
fileSize: item.meta?.fileSize || 0, fileSize: item.meta?.fileSize || 0,
......
...@@ -1517,6 +1517,7 @@ ...@@ -1517,6 +1517,7 @@
"editor.pageRel.title": "Add Page Relation", "editor.pageRel.title": "Add Page Relation",
"editor.pageRel.titleEdit": "Edit Page Relation", "editor.pageRel.titleEdit": "Edit Page Relation",
"editor.pageScripts.title": "Page Scripts", "editor.pageScripts.title": "Page Scripts",
"editor.props.alias": "Alias",
"editor.props.allowComments": "Allow Comments", "editor.props.allowComments": "Allow Comments",
"editor.props.allowCommentsHint": "Enable commenting abilities on this page.", "editor.props.allowCommentsHint": "Enable commenting abilities on this page.",
"editor.props.allowContributions": "Allow Contributions", "editor.props.allowContributions": "Allow Contributions",
...@@ -1642,6 +1643,7 @@ ...@@ -1642,6 +1643,7 @@
"fileman.rarFileType": "RAR Archive", "fileman.rarFileType": "RAR Archive",
"fileman.renameFolderInvalidData": "One or more fields are invalid.", "fileman.renameFolderInvalidData": "One or more fields are invalid.",
"fileman.renameFolderSuccess": "Folder renamed successfully.", "fileman.renameFolderSuccess": "Folder renamed successfully.",
"fileman.searchFolder": "Search folder...",
"fileman.svgFileType": "Scalable Vector Graphic", "fileman.svgFileType": "Scalable Vector Graphic",
"fileman.tarFileType": "TAR Archive", "fileman.tarFileType": "TAR Archive",
"fileman.tgzFileType": "Gzipped TAR Archive", "fileman.tgzFileType": "Gzipped TAR Archive",
......
...@@ -8,9 +8,11 @@ q-layout.fileman(view='hHh lpR lFr', container) ...@@ -8,9 +8,11 @@ q-layout.fileman(view='hHh lpR lFr', container)
q-btn.q-mr-sm.acrylic-btn( q-btn.q-mr-sm.acrylic-btn(
flat flat
color='white' color='white'
label='EN' :label='commonStore.locale'
:aria-label='commonStore.locale'
style='height: 40px;' style='height: 40px;'
) )
locale-selector-menu
q-input( q-input(
dark dark
v-model='state.search' v-model='state.search'
...@@ -18,7 +20,7 @@ q-layout.fileman(view='hHh lpR lFr', container) ...@@ -18,7 +20,7 @@ q-layout.fileman(view='hHh lpR lFr', container)
dense dense
ref='searchField' ref='searchField'
style='width: 100%;' style='width: 100%;'
label='Search folder...' :label='t(`fileman.searchFolder`)'
:debounce='500' :debounce='500'
) )
template(v-slot:prepend) template(v-slot:prepend)
...@@ -197,6 +199,8 @@ q-layout.fileman(view='hHh lpR lFr', container) ...@@ -197,6 +199,8 @@ q-layout.fileman(view='hHh lpR lFr', container)
:hide-asset-btn='true' :hide-asset-btn='true'
:show-new-folder='true' :show-new-folder='true'
@new-folder='() => newFolder(state.currentFolderId)' @new-folder='() => newFolder(state.currentFolderId)'
@new-page='() => close()'
:base-path='folderPath'
) )
q-btn( q-btn(
flat flat
...@@ -252,11 +256,11 @@ q-layout.fileman(view='hHh lpR lFr', container) ...@@ -252,11 +256,11 @@ q-layout.fileman(view='hHh lpR lFr', container)
) )
q-card.q-pa-sm q-card.q-pa-sm
q-list(dense, style='min-width: 150px;') 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-item-section(side)
q-icon(name='las la-plus-circle', color='primary') q-icon(name='las la-plus-circle', color='primary')
q-item-section {{ t(`common.actions.insert`) }} 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-item-section(side)
q-icon(name='las la-edit', color='orange') q-icon(name='las la-edit', color='orange')
q-item-section {{ t(`common.actions.edit`) }} q-item-section {{ t(`common.actions.edit`) }}
...@@ -277,7 +281,7 @@ q-layout.fileman(view='hHh lpR lFr', container) ...@@ -277,7 +281,7 @@ q-layout.fileman(view='hHh lpR lFr', container)
q-item-section(side) q-item-section(side)
q-icon(name='las la-clipboard', color='primary') q-icon(name='las la-clipboard', color='primary')
q-item-section {{ t(`common.actions.copyURL`) }} 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-item-section(side)
q-icon(name='las la-download', color='primary') q-icon(name='las la-download', color='primary')
q-item-section {{ t(`common.actions.download`) }} q-item-section {{ t(`common.actions.download`) }}
...@@ -326,12 +330,14 @@ import Tree from './TreeNav.vue' ...@@ -326,12 +330,14 @@ import Tree from './TreeNav.vue'
import fileTypes from '../helpers/fileTypes' import fileTypes from '../helpers/fileTypes'
import { useCommonStore } from 'src/stores/common'
import { usePageStore } from 'src/stores/page' import { usePageStore } from 'src/stores/page'
import { useSiteStore } from 'src/stores/site' import { useSiteStore } from 'src/stores/site'
import FolderCreateDialog from 'src/components/FolderCreateDialog.vue' import FolderCreateDialog from 'src/components/FolderCreateDialog.vue'
import FolderDeleteDialog from 'src/components/FolderDeleteDialog.vue' import FolderDeleteDialog from 'src/components/FolderDeleteDialog.vue'
import FolderRenameDialog from 'src/components/FolderRenameDialog.vue' import FolderRenameDialog from 'src/components/FolderRenameDialog.vue'
import LocaleSelectorMenu from 'src/components/LocaleSelectorMenu.vue'
// QUASAR // QUASAR
...@@ -339,6 +345,7 @@ const $q = useQuasar() ...@@ -339,6 +345,7 @@ const $q = useQuasar()
// STORES // STORES
const commonStore = useCommonStore()
const pageStore = usePageStore() const pageStore = usePageStore()
const siteStore = useSiteStore() const siteStore = useSiteStore()
...@@ -941,6 +948,15 @@ async function copyItemURL (item) { ...@@ -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) { function renameItem (item) {
console.info(item) console.info(item)
switch (item.type) { switch (item.type) {
......
<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>
<template lang="pug"> <template lang="pug">
q-menu.translucent-menu( q-menu.translucent-menu(
auto-close auto-close
anchor='bottom middle' anchor='bottom left'
self='top left' self='top left'
) )
q-list(padding, style='min-width: 200px;') q-list(padding, style='min-width: 200px;')
......
...@@ -88,12 +88,16 @@ const props = defineProps({ ...@@ -88,12 +88,16 @@ const props = defineProps({
showNewFolder: { showNewFolder: {
type: Boolean, type: Boolean,
default: false default: false
},
basePath: {
type: String,
default: null
} }
}) })
// EMITS // EMITS
const emit = defineEmits(['newFolder']) const emit = defineEmits(['newFolder', 'newPage'])
// QUASAR // QUASAR
...@@ -114,7 +118,8 @@ const { t } = useI18n() ...@@ -114,7 +118,8 @@ const { t } = useI18n()
async function create (editor) { async function create (editor) {
$q.loading.show() $q.loading.show()
await pageStore.pageCreate({ editor }) emit('newPage')
await pageStore.pageCreate({ editor, basePath: props.basePath })
$q.loading.hide() $q.loading.hide()
} }
......
...@@ -114,7 +114,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide') ...@@ -114,7 +114,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { computed, onMounted, reactive } from 'vue' import { computed, onMounted, reactive } from 'vue'
import { useDialogPluginComponent, useQuasar } from 'quasar' import { useDialogPluginComponent, useQuasar } from 'quasar'
import { cloneDeep, find } from 'lodash-es' import { cloneDeep, find, last } from 'lodash-es'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import fileTypes from '../helpers/fileTypes' import fileTypes from '../helpers/fileTypes'
...@@ -124,6 +124,7 @@ import Tree from 'src/components/TreeNav.vue' ...@@ -124,6 +124,7 @@ import Tree from 'src/components/TreeNav.vue'
import { usePageStore } from 'src/stores/page' import { usePageStore } from 'src/stores/page'
import { useSiteStore } from 'src/stores/site' import { useSiteStore } from 'src/stores/site'
import { dropRight } from 'lodash'
// PROPS // PROPS
...@@ -351,6 +352,13 @@ function newFolder (parentId) { ...@@ -351,6 +352,13 @@ function newFolder (parentId) {
// MOUNTED // MOUNTED
onMounted(() => { 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) { switch (props.mode) {
case 'pageSave': { case 'pageSave': {
state.typesToFetch = ['folder', 'page'] state.typesToFetch = ['folder', 'page']
...@@ -358,12 +366,12 @@ onMounted(() => { ...@@ -358,12 +366,12 @@ onMounted(() => {
} }
} }
loadTree({ loadTree({
parentPath: props.folderPath, parentPath: fPath,
types: state.typesToFetch, types: state.typesToFetch,
initLoad: true initLoad: true
}) })
state.title = props.itemTitle || '' state.title = props.itemTitle || ''
state.path = props.itemFileName || '' state.path = fName || ''
}) })
</script> </script>
......
...@@ -262,6 +262,12 @@ const lastModified = computed(() => { ...@@ -262,6 +262,12 @@ const lastModified = computed(() => {
// WATCHERS // WATCHERS
watch(() => route.path, async (newValue) => { 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? // -> Enter Create Mode?
if (newValue.startsWith('/_create')) { if (newValue.startsWith('/_create')) {
if (!route.params.editor) { if (!route.params.editor) {
...@@ -272,8 +278,27 @@ watch(() => route.path, async (newValue) => { ...@@ -272,8 +278,27 @@ watch(() => route.path, async (newValue) => {
return router.replace('/') return router.replace('/')
} }
$q.loading.show() $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() $q.loading.hide()
return
} }
// -> Moving to a non-page path? Ignore // -> Moving to a non-page path? Ignore
......
...@@ -86,6 +86,16 @@ const routes = [ ...@@ -86,6 +86,16 @@ const routes = [
{ path: '', component: () => import('../pages/Index.vue') } { 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 // STANDARD PAGE CATCH-ALL
// ----------------------- // -----------------------
......
...@@ -23,7 +23,8 @@ export const useEditorStore = defineStore('editor', { ...@@ -23,7 +23,8 @@ export const useEditorStore = defineStore('editor', {
lastChangeTimestamp: null, lastChangeTimestamp: null,
editors: {}, editors: {},
configIsLoaded: false, configIsLoaded: false,
reasonForChange: '' reasonForChange: '',
ignoreRouteChange: false
}), }),
getters: { getters: {
hasPendingChanges: (state) => { hasPendingChanges: (state) => {
......
...@@ -313,14 +313,30 @@ export const usePageStore = defineStore('page', { ...@@ -313,14 +313,30 @@ export const usePageStore = defineStore('page', {
/** /**
* PAGE - CREATE * PAGE - CREATE
*/ */
async pageCreate ({ editor, locale, path, title = '', description = '', content = '' }) { async pageCreate ({ editor, locale, path, basePath, title = '', description = '', content = '', fromNavigate = false } = {}) {
const editorStore = useEditorStore() const editorStore = useEditorStore()
// -> Load editor config
if (!editorStore.configIsLoaded) { if (!editorStore.configIsLoaded) {
await editorStore.fetchConfigs() 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 // -> Init editor
editorStore.$patch({ editorStore.$patch({
...@@ -333,7 +349,7 @@ export const usePageStore = defineStore('page', { ...@@ -333,7 +349,7 @@ export const usePageStore = defineStore('page', {
// -> Default Page Path // -> Default Page Path
let newPath = path let newPath = path
if (!path && 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' newPath = parentPath ? `${parentPath}/new-page` : 'new-page'
} }
...@@ -353,18 +369,26 @@ export const usePageStore = defineStore('page', { ...@@ -353,18 +369,26 @@ export const usePageStore = defineStore('page', {
render: '', render: '',
mode: 'edit' mode: 'edit'
}) })
if (noDefaultPath) {
this.router.push(`/_create/${editor}`)
}
}, },
/** /**
* PAGE - EDIT * PAGE - EDIT
*/ */
async pageEdit () { async pageEdit ({ path, id, fromNavigate = false } = {}) {
const editorStore = useEditorStore() 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) { if (!editorStore.configIsLoaded) {
await editorStore.fetchConfigs() await editorStore.fetchConfigs()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment