feat: file manager tree view component

pull/6078/head
Nicolas Giard 2 years ago
parent 4e34151d15
commit 0cbeec37d6
No known key found for this signature in database
GPG Key ID: 85061B8F9D55B7C8

@ -434,6 +434,22 @@ exports.up = async knex => {
guestUserId: userGuestId
}
},
{
key: 'icons',
value: {
fa: {
isActive: true,
config: {
version: 6,
license: 'free',
token: ''
}
},
la: {
isActive: true
}
}
},
{
key: 'mail',
value: {

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><linearGradient id="BC9FGaQYSWC9vfPQ436~Wa" x1="3.584" x2="29.287" y1="26.5" y2="26.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#a1aab3"/><stop offset="1" stop-color="#8f979e"/></linearGradient><path fill="url(#BC9FGaQYSWC9vfPQ436~Wa)" d="M29,24v-2H13v-9h-2v26c0,0.552,0.448,1,1,1h17v-2H13V24H29z"/><linearGradient id="BC9FGaQYSWC9vfPQ436~Wb" x1="5" x2="20" y1="11.5" y2="11.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#eba600"/><stop offset="1" stop-color="#c28200"/></linearGradient><path fill="url(#BC9FGaQYSWC9vfPQ436~Wb)" d="M18.5,7H12l-0.707-0.707C11.105,6.105,10.851,6,10.586,6H6C5.448,6,5,6.448,5,7v8.5 C5,16.325,5.675,17,6.5,17h12c0.825,0,1.5-0.675,1.5-1.5v-7C20,7.675,19.325,7,18.5,7z"/><linearGradient id="BC9FGaQYSWC9vfPQ436~Wc" x1="11.505" x2="13.359" y1="6.814" y2="16.489" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd869"/><stop offset="1" stop-color="#fec52b"/></linearGradient><path fill="url(#BC9FGaQYSWC9vfPQ436~Wc)" d="M18.5,7L12,6.978l-0.801,0.752C11.013,7.903,10.769,8,10.514,8H6C5.448,8,5,8.448,5,9v6.5 C5,16.325,5.675,17,6.5,17h12c0.825,0,1.5-0.675,1.5-1.5v-7C20,7.7,19.3,7,18.5,7z"/><linearGradient id="BC9FGaQYSWC9vfPQ436~Wd" x1="29" x2="44" y1="22.5" y2="22.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#eba600"/><stop offset="1" stop-color="#c28200"/></linearGradient><path fill="url(#BC9FGaQYSWC9vfPQ436~Wd)" d="M42.5,18H36l-0.707-0.707C35.105,17.105,34.851,17,34.586,17H30c-0.552,0-1,0.448-1,1v8.5 c0,0.825,0.675,1.5,1.5,1.5h12c0.825,0,1.5-0.675,1.5-1.5v-7C44,18.675,43.325,18,42.5,18z"/><linearGradient id="BC9FGaQYSWC9vfPQ436~We" x1="35.505" x2="37.359" y1="17.814" y2="27.489" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd869"/><stop offset="1" stop-color="#fec52b"/></linearGradient><path fill="url(#BC9FGaQYSWC9vfPQ436~We)" d="M42.5,18L36,17.978l-0.801,0.752C35.013,18.903,34.769,19,34.514,19H30c-0.552,0-1,0.448-1,1 v6.5c0,0.825,0.675,1.5,1.5,1.5h12c0.825,0,1.5-0.675,1.5-1.5v-7C44,18.7,43.3,18,42.5,18z"/><linearGradient id="BC9FGaQYSWC9vfPQ436~Wf" x1="29" x2="44" y1="38.5" y2="38.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#eba600"/><stop offset="1" stop-color="#c28200"/></linearGradient><path fill="url(#BC9FGaQYSWC9vfPQ436~Wf)" d="M42.5,34H36l-0.707-0.707C35.105,33.105,34.851,33,34.586,33H30c-0.552,0-1,0.448-1,1v8.5 c0,0.825,0.675,1.5,1.5,1.5h12c0.825,0,1.5-0.675,1.5-1.5v-7C44,34.675,43.325,34,42.5,34z"/><linearGradient id="BC9FGaQYSWC9vfPQ436~Wg" x1="35.505" x2="37.359" y1="33.814" y2="43.489" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd869"/><stop offset="1" stop-color="#fec52b"/></linearGradient><path fill="url(#BC9FGaQYSWC9vfPQ436~Wg)" d="M42.5,34L36,33.978l-0.801,0.752C35.013,34.903,34.769,35,34.514,35H30c-0.552,0-1,0.448-1,1 v6.5c0,0.825,0.675,1.5,1.5,1.5h12c0.825,0,1.5-0.675,1.5-1.5v-7C44,34.7,43.3,34,42.5,34z"/></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><linearGradient id="73hvxuIu2KoRU1i7qzd8qa" x1="23" x2="23" y1="548.505" y2="556.288" gradientTransform="translate(0 -541.78)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#eba600"/><stop offset="1" stop-color="#c28200"/></linearGradient><path fill="url(#73hvxuIu2KoRU1i7qzd8qa)" d="M22.414,10.414l-2.536-2.536C19.316,7.316,18.553,7,17.757,7H5C3.895,7,3,7.895,3,9v8l2,22h38 V13l-18.569-2h-0.603C23.298,11,22.789,10.789,22.414,10.414z"/><linearGradient id="73hvxuIu2KoRU1i7qzd8qb" x1="24" x2="24" y1="552.634" y2="582.763" gradientTransform="translate(0 -541.78)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd869"/><stop offset="1" stop-color="#fec52b"/></linearGradient><path fill="url(#73hvxuIu2KoRU1i7qzd8qb)" d="M20.586,14.414l3.268-3.268C23.947,11.053,24.074,11,24.207,11H28l17,17v11 c0,1.105-0.895,2-2,2H5c-1.105,0-2-0.895-2-2V15.5C3,15.224,3.224,15,3.5,15h15.672C19.702,15,20.211,14.789,20.586,14.414z"/><path d="M45,22.99v6.98C44.66,29.99,44.33,30,44,30c-4.78,0-9.3-1.88-12.72-5.28 c-3.62-3.64-5.53-8.63-5.25-13.72h6.98v0.04c-0.11,3.03,0.99,6.03,3.02,8.23C38.2,21.64,41.29,23,44.5,23c0.16,0,0.3,0,0.44-0.01H45 z" opacity=".05"/><path d="M45,23.49v5.98c-0.34,0.02-0.67,0.03-1,0.03c-4.65,0-9.04-1.82-12.36-5.14 c-3.53-3.53-5.39-8.4-5.11-13.36h5.98v0.02c-0.12,3.16,1.03,6.29,3.15,8.59c2.27,2.47,5.49,3.89,8.84,3.89h0.03 c0.14,0,0.29,0,0.44-0.01H45z" opacity=".07"/><linearGradient id="73hvxuIu2KoRU1i7qzd8qc" x1="27" x2="45" y1="27.89" y2="27.89" gradientTransform="matrix(1 0 0 -1 0 47.89)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#7819a2"/><stop offset="1" stop-color="#771aa9"/></linearGradient><path fill="url(#73hvxuIu2KoRU1i7qzd8qc)" d="M31.99,24.01c-3.298-3.309-5.254-7.936-4.961-13.01h4.981c-0.12,3.29,1.05,6.53,3.28,8.95 C37.65,22.52,41.01,24,44.5,24c0.16,0,0.33,0,0.5-0.01v4.981C39.926,29.264,35.299,27.308,31.99,24.01z"/><path fill="#a238c2" d="M31,11.5c0-0.17,0-0.34,0.01-0.5h5.01c-0.13,2.3,0.67,4.56,2.21,6.25C39.84,19,42.12,20,44.5,20 c0.17,0,0.33,0,0.5-0.02v5.01C44.84,25,44.67,25,44.5,25C37.06,25,31,18.94,31,11.5z"/><path fill="#ba54d9" d="M35,11.5c0-0.17,0-0.33,0.01-0.5h5.08c0.2,1.26,0.8,2.38,1.67,3.24c0.86,0.87,1.98,1.47,3.24,1.67 v5.08C44.83,21,44.67,21,44.5,21C39.26,21,35,16.74,35,11.5z"/><path fill="#c767e5" d="M39.021,11.008c0.003-0.003,0.005-0.005,0.009-0.008H43c1.105,0,2,0.893,2,1.998v3.944 c-0.004,0.036-0.004,0.036-0.008,0.037C41.565,17.276,38.724,14.436,39.021,11.008z"/></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><linearGradient id="xGIh33lbYX9pWIYWeZsuka" x1="24" x2="24" y1="6.955" y2="23.167" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#eba600"/><stop offset="1" stop-color="#c28200"/></linearGradient><path fill="url(#xGIh33lbYX9pWIYWeZsuka)" d="M24.414,10.414l-2.536-2.536C21.316,7.316,20.553,7,19.757,7H5C3.895,7,3,7.895,3,9v30 c0,1.105,0.895,2,2,2h38c1.105,0,2-0.895,2-2V13c0-1.105-0.895-2-2-2H25.828C25.298,11,24.789,10.789,24.414,10.414z"/><linearGradient id="xGIh33lbYX9pWIYWeZsukb" x1="24.066" x2="24.066" y1="19.228" y2="33.821" gradientTransform="matrix(-1 0 0 1 48 0)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd869"/><stop offset="1" stop-color="#fec52b"/></linearGradient><path fill="url(#xGIh33lbYX9pWIYWeZsukb)" d="M24,23l3.854-3.854C27.947,19.053,28.074,19,28.207,19H44.81c1.176,0,2.098,1.01,1.992,2.181 l-1.636,18C45.072,40.211,44.208,41,43.174,41H4.79c-1.019,0-1.875-0.766-1.988-1.779L1.062,23.555C1.029,23.259,1.261,23,1.559,23 H24z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -1,5 +1,5 @@
<template lang="pug">
q-layout(view='hHh lpR fFf', container)
q-layout(view='hHh lpR lFr', container)
q-header.card-header
q-toolbar(dark)
q-icon(name='img:/_assets/icons/fluent-folder.svg', left, size='md')
@ -25,58 +25,11 @@ q-layout(view='hHh lpR fFf', container)
)
q-toolbar(dark)
q-space
q-btn.q-mr-sm(
flat
dense
color='blue-4'
:aria-label='t(`common.actions.upload`)'
icon='las la-plus-circle'
@click=''
)
q-tooltip(anchor='bottom middle', self='top middle') {{t(`common.actions.upload`)}}
q-btn(
flat
dense
color='positive'
:aria-label='t(`common.actions.upload`)'
icon='las la-cloud-upload-alt'
@click=''
)
q-tooltip(anchor='bottom middle', self='top middle') {{t(`common.actions.upload`)}}
q-separator.q-mx-sm(vertical, dark, inset)
q-btn.q-mr-sm(
flat
dense
color='blue-grey-4'
:aria-label='t(`common.actions.upload`)'
icon='las la-check-square'
@click=''
)
q-tooltip(anchor='bottom middle', self='top middle') {{t(`common.actions.upload`)}}
q-btn.q-mr-sm(
flat
dense
color='blue-grey-4'
:aria-label='t(`common.actions.upload`)'
icon='las la-list'
@click=''
)
q-tooltip(anchor='bottom middle', self='top middle') {{t(`common.actions.upload`)}}
q-btn(
flat
dense
color='blue-grey-4'
:aria-label='t(`common.actions.refresh`)'
icon='las la-redo-alt'
@click=''
:loading='state.loading > 0'
)
q-tooltip(anchor='bottom middle', self='top middle') {{t(`common.actions.refresh`)}}
q-separator.q-mx-sm(vertical, dark, inset)
q-btn(
flat
dense
color='white'
:label='t(`common.actions.close`)'
:aria-label='t(`common.actions.close`)'
icon='las la-times'
@click='close'
@ -84,16 +37,10 @@ q-layout(view='hHh lpR fFf', container)
q-tooltip(anchor='bottom middle', self='top middle') {{t(`common.actions.close`)}}
q-drawer.bg-blue-grey-1(:model-value='true', :width='350')
.q-px-md.q-pb-sm
q-tree.fileman-toc(
:nodes='state.tree'
icon='las la-caret-right'
node-key='key'
dense
accordion
no-connectors
v-model:expanded='state.treeExpanded'
v-model:selected='state.treeSelected'
@click='openFolder'
tree(
:nodes='state.treeNodes'
:roots='state.treeRoots'
v-model:selected='state.currentFolderId'
)
q-drawer.bg-grey-1(:model-value='true', :width='350', side='right')
.q-pa-md
@ -105,8 +52,40 @@ q-layout(view='hHh lpR fFf', container)
)
q-page-container
q-page.bg-white
q-bar.bg-blue-grey-1
small.text-caption.text-grey-7 / foo / bar
q-toolbar.bg-grey-1
q-space
q-btn.q-mr-sm(
flat
dense
no-caps
color='grey'
:aria-label='t(`common.actions.refresh`)'
icon='las la-redo-alt'
@click=''
)
q-tooltip(anchor='bottom middle', self='top middle') {{t(`common.actions.refresh`)}}
q-separator.q-mr-sm(inset, vertical)
q-btn.q-mr-sm(
flat
dense
no-caps
color='blue'
:label='t(`common.actions.new`)'
:aria-label='t(`common.actions.new`)'
icon='las la-plus-circle'
@click=''
)
new-menu(hide-asset-btn)
q-btn(
flat
dense
no-caps
color='positive'
:label='t(`common.actions.upload`)'
:aria-label='t(`common.actions.upload`)'
icon='las la-cloud-upload-alt'
@click=''
)
q-list.fileman-filelist
q-item(clickable)
q-item-section(avatar)
@ -141,12 +120,18 @@ q-layout(view='hHh lpR fFf', container)
q-item-section(side)
.text-caption 2022/01/01
q-footer
q-bar.bg-blue-grey-1
small.text-caption.text-grey-7 / foo / bar
</template>
<script setup>
import { useI18n } from 'vue-i18n'
import { reactive } from 'vue'
import NewMenu from './PageNewMenu.vue'
import Tree from './TreeNav.vue'
import { usePageStore } from 'src/stores/page'
import { useSiteStore } from 'src/stores/site'
@ -164,33 +149,31 @@ const { t } = useI18n()
const state = reactive({
loading: 0,
search: '',
tree: [
{
key: 'root',
label: 'Root',
children: [
{
key: '1',
label: 'guides',
icon: 'las la-folder',
children: [
{
key: '3',
label: 'offline',
icon: 'las la-folder'
}
]
},
{
key: '2',
label: 'administration',
icon: 'las la-folder'
}
]
currentFolderId: 'boop',
treeNodes: {
beep: {
text: 'Beep',
children: ['foo', 'bar']
},
foo: {
text: 'Foo'
},
bar: {
text: 'Bar',
children: ['boop']
},
boop: {
text: 'Boop'
},
bop: {
text: 'Bop',
children: ['bap']
},
bap: {
text: 'Bap'
}
],
treeExpanded: ['root'],
treeSelected: []
},
treeRoots: ['beep', 'bop']
})
// METHODS
@ -215,11 +198,5 @@ function openFolder (node, noder) {
border-radius: 8px;
}
}
&-toc {
&.q-tree--dense .q-tree__node {
padding-bottom: 5px;
}
}
}
</style>

@ -23,10 +23,11 @@ q-menu.translucent-menu(
q-item(clickable, @click='create(`redirect`)')
blueprint-icon(icon='advance')
q-item-section.q-pr-sm New Redirection
q-separator.q-my-sm(inset)
q-item(clickable, @click='openFileManager')
blueprint-icon(icon='add-image')
q-item-section.q-pr-sm Upload Media Asset
template(v-if='props.hideAssetBtn === false')
q-separator.q-my-sm(inset)
q-item(clickable, @click='openFileManager')
blueprint-icon(icon='add-image')
q-item-section.q-pr-sm Upload Media Asset
</template>
<script setup>
@ -36,6 +37,15 @@ import { useQuasar } from 'quasar'
import { usePageStore } from 'src/stores/page'
import { useSiteStore } from 'src/stores/site'
// PROPS
const props = defineProps({
hideAssetBtn: {
type: Boolean,
default: false
}
})
// QUASAR
const $q = useQuasar()

@ -0,0 +1,83 @@
<template lang="pug">
ul.treeview-level
//- ROOT NODE
li.treeview-node(v-if='!props.parentId')
.treeview-label(@click='setRoot', :class='{ "active": !selection }')
q-icon(name='img:/_assets/icons/fluent-ftp.svg', size='sm')
em.text-purple root
q-menu(
touch-position
context-menu
auto-close
transition-show='jump-down'
transition-hide='jump-up'
)
q-card.q-pa-sm
q-list(dense, style='min-width: 150px;')
q-item(clickable)
q-item-section(side)
q-icon(name='las la-plus-circle', color='primary')
q-item-section New Folder
//- NORMAL NODES
tree-node(
v-for='node of level'
:key='node.id'
:node='node'
:depth='props.depth'
:parent-id='props.parentId'
)
</template>
<script setup>
import { computed, inject } from 'vue'
import TreeNode from './TreeNode.vue'
// PROPS
const props = defineProps({
depth: {
required: true,
type: Number
},
parentId: {
type: String,
default: null
}
})
// INJECT
const roots = inject('roots', [])
const nodes = inject('nodes')
const selection = inject('selection')
// COMPUTED
const level = computed(() => {
const items = []
if (!props.parentId) {
for (const root of roots) {
items.push({
id: root,
...nodes[root]
})
}
} else {
for (const node of nodes[props.parentId].children) {
items.push({
id: node,
...nodes[node]
})
}
}
return items
})
// METHODS
function setRoot () {
selection.value = null
}
</script>

@ -1,21 +1,135 @@
<template lang="pug">
.treenav
.treeview
tree-level(
:depth='0'
:parent-id='null'
)
</template>
<script setup>
import { computed, onMounted, provide, reactive } from 'vue'
import { findKey } from 'lodash-es'
import TreeLevel from './TreeLevel.vue'
// PROPS
defineProps({
const props = defineProps({
nodes: {
type: Object,
default: () => ({})
},
roots: {
type: Array,
default: () => []
},
selected: {
type: String,
default: null
}
})
// EMITS
const emits = defineEmits(['selected'])
const emit = defineEmits(['update:selected'])
// DATA
const state = reactive({
opened: {}
})
// COMPOUTED
const selection = computed({
get () {
return props.selected
},
set (val) {
emit('update:selected', val)
}
})
// METHODS
// PROVIDE
provide('roots', props.roots)
provide('nodes', props.nodes)
provide('opened', state.opened)
provide('selection', selection)
// MOUNTED
onMounted(() => {
if (props.selected) {
let foundRoot = false
let currentId = props.selected
while (!foundRoot) {
const parentId = findKey(props.nodes, n => n.children?.includes(currentId))
if (parentId) {
state.opened[parentId] = true
currentId = parentId
} else {
foundRoot = true
}
}
state.opened[props.selected] = true
}
})
</script>
<style lang="scss">
.treeview {
&-level {
list-style: none;
padding-left: 19px;
}
> .treeview-level {
padding-left: 0;
> .treeview-node {
border-left: none;
> .treeview-label {
border-radius: 5px;
}
}
}
&-node {
display: block;
border-left: 2px solid rgba(0,0,0,.05);
}
&-label {
padding: 4px 8px;
border-radius: 0 5px 5px 0;
cursor: pointer;
display: flex;
align-items: center;
transition: background-color .4s ease;
&:hover, &:focus, &.active {
background-color: rgba(0,0,0,.05);
}
> .q-icon {
margin-right: 5px;
}
}
// Animations
&-enter-active, &-leave-active {
transition: all 0.2s ease;
}
&-enter-from, &-leave-to {
transform: translateY(-10px);
opacity: 0;
}
}
</style>

@ -0,0 +1,107 @@
<template lang="pug">
li.treeview-node
//- NODE
.treeview-label(@click='toggleNode', :class='{ "active": isActive }')
q-icon(:name='icon', size='sm')
span {{node.text}}
//- RIGHT-CLICK MENU
q-menu(
touch-position
context-menu
auto-close
transition-show='jump-down'
transition-hide='jump-up'
@before-show='state.isContextMenuShown = true'
@before-hide='state.isContextMenuShown = false'
)
q-card.q-pa-sm
q-list(dense, style='min-width: 150px;')
q-item(clickable)
q-item-section(side)
q-icon(name='las la-plus-circle', color='primary')
q-item-section New Folder
q-item(clickable)
q-item-section(side)
q-icon(name='las la-redo', color='teal')
q-item-section Rename...
q-item(clickable)
q-item-section(side)
q-icon(name='las la-arrow-right', color='teal')
q-item-section Move to...
q-item(clickable)
q-item-section(side)
q-icon(name='las la-trash-alt', color='negative')
q-item-section.text-negative Delete
//- SUB-LEVEL
transition(name='treeview')
tree-level(
v-if='hasChildren && isOpened'
:parent-id='props.node.id'
:depth='props.depth + 1'
)
</template>
<script setup>
import { computed, inject, reactive } from 'vue'
import TreeLevel from './TreeLevel.vue'
// PROPS
const props = defineProps({
depth: {
type: Number,
default: 0
},
node: {
required: true,
type: Object
},
parentId: {
type: String,
default: null
}
})
// INJECT
const opened = inject('opened')
const selection = inject('selection')
// DATA
const state = reactive({
isContextMenuShown: false
})
// COMPUTED
const icon = computed(() => {
if (props.node.icon) {
return props.node.icon
}
return hasChildren.value && isOpened.value ? 'img:/_assets/icons/fluent-opened-folder.svg' : 'img:/_assets/icons/fluent-folder.svg'
})
const hasChildren = computed(() => {
return props.node.children?.length > 0
})
const isOpened = computed(() => {
return opened[props.node.id]
})
const isActive = computed(() => {
return state.isContextMenuShown || selection.value === props.node.id
})
// METHODS
function toggleNode () {
selection.value = props.node.id
if (selection.value !== props.node.id && opened[props.node.id]) {
return
}
opened[props.node.id] = !(opened[props.node.id] === true)
}
</script>

@ -44,7 +44,7 @@ q-page.admin-icons
.text-caption.text-red-1 {{ t('admin.icons.warnHint') }}
q-list(separator)
q-item(v-for='pack of combinedPacks', :key='pack.key')
blueprint-icon(icon='small-icons', :hueRotate='140')
blueprint-icon(icon='small-icons', :hueRotate='30')
q-item-section
q-item-label: strong {{pack.label}}
q-item-label(caption, v-if='pack.isMandatory')

Loading…
Cancel
Save