Unverified Commit 5efa0abe authored by NGPixel's avatar NGPixel

feat: file manager improvements

parent 7fde587a
......@@ -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
......
......@@ -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,
......
......@@ -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",
......
......@@ -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) {
......
<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">
q-menu.translucent-menu(
auto-close
anchor='bottom middle'
anchor='bottom left'
self='top left'
)
q-list(padding, style='min-width: 200px;')
......
......@@ -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()
}
......
......@@ -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>
......
......@@ -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
......
......@@ -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
// -----------------------
......
......@@ -23,7 +23,8 @@ export const useEditorStore = defineStore('editor', {
lastChangeTimestamp: null,
editors: {},
configIsLoaded: false,
reasonForChange: ''
reasonForChange: '',
ignoreRouteChange: false
}),
getters: {
hasPendingChanges: (state) => {
......
......@@ -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()
......
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