mirror of
https://github.com/LibreSign/libresign.git
synced 2025-12-17 21:12:16 +01:00
feat: rewrite file list
Signed-off-by: Vitor Mattos <vitor@php.rio>
This commit is contained in:
parent
4e359acd9c
commit
70d463d715
13 changed files with 752 additions and 1 deletions
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -12,6 +12,8 @@
|
||||||
"@fontsource/dancing-script": "^5.1.0",
|
"@fontsource/dancing-script": "^5.1.0",
|
||||||
"@libresign/vue-pdf-editor": "^1.3.7",
|
"@libresign/vue-pdf-editor": "^1.3.7",
|
||||||
"@marionebl/option": "^1.0.8",
|
"@marionebl/option": "^1.0.8",
|
||||||
|
"@mdi/js": "^7.4.47",
|
||||||
|
"@mdi/svg": "^7.4.47",
|
||||||
"@nextcloud/auth": "^2.4.0",
|
"@nextcloud/auth": "^2.4.0",
|
||||||
"@nextcloud/axios": "^2.5.1",
|
"@nextcloud/axios": "^2.5.1",
|
||||||
"@nextcloud/dialogs": "^6.0.1",
|
"@nextcloud/dialogs": "^6.0.1",
|
||||||
|
|
@ -2783,6 +2785,12 @@
|
||||||
"integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==",
|
"integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/@mdi/svg": {
|
||||||
|
"version": "7.4.47",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mdi/svg/-/svg-7.4.47.tgz",
|
||||||
|
"integrity": "sha512-WQ2gDll12T9WD34fdRFgQVgO8bag3gavrAgJ0frN4phlwdJARpE6gO1YvLEMJR0KKgoc+/Ea/A0Pp11I00xBvw==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@nextcloud/auth": {
|
"node_modules/@nextcloud/auth": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-2.4.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@
|
||||||
"@fontsource/dancing-script": "^5.1.0",
|
"@fontsource/dancing-script": "^5.1.0",
|
||||||
"@libresign/vue-pdf-editor": "^1.3.7",
|
"@libresign/vue-pdf-editor": "^1.3.7",
|
||||||
"@marionebl/option": "^1.0.8",
|
"@marionebl/option": "^1.0.8",
|
||||||
|
"@mdi/js": "^7.4.47",
|
||||||
|
"@mdi/svg": "^7.4.47",
|
||||||
"@nextcloud/auth": "^2.4.0",
|
"@nextcloud/auth": "^2.4.0",
|
||||||
"@nextcloud/axios": "^2.5.1",
|
"@nextcloud/axios": "^2.5.1",
|
||||||
"@nextcloud/dialogs": "^6.0.1",
|
"@nextcloud/dialogs": "^6.0.1",
|
||||||
|
|
|
||||||
11
src/helpers/logger.js
Normal file
11
src/helpers/logger.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getLoggerBuilder } from '@nextcloud/logger'
|
||||||
|
|
||||||
|
export default getLoggerBuilder()
|
||||||
|
.setApp('libresign')
|
||||||
|
.detectUser()
|
||||||
|
.build()
|
||||||
|
|
@ -141,6 +141,11 @@ const router = new Router({
|
||||||
name: 'timeline',
|
name: 'timeline',
|
||||||
component: () => import('../views/Timeline/Timeline.vue'),
|
component: () => import('../views/Timeline/Timeline.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/f/filelist/sign',
|
||||||
|
name: 'fileslist',
|
||||||
|
component: () => import('../views/FilesList/FilesList.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/f/request',
|
path: '/f/request',
|
||||||
name: 'requestFiles',
|
name: 'requestFiles',
|
||||||
|
|
|
||||||
27
src/store/filters.js
Normal file
27
src/store/filters.js
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import logger from '../helpers/logger.js'
|
||||||
|
|
||||||
|
export const useFiltersStore = defineStore('filter', {
|
||||||
|
state: () => ({
|
||||||
|
chips: {},
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
activeChips(state) {
|
||||||
|
return Object.values(state.chips).flat()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
onFilterUpdateChips(event) {
|
||||||
|
this.chips = { ...this.chips, [event.id]: [...event.detail] }
|
||||||
|
|
||||||
|
logger.debug('File list filter chips updated', { chips: event.detail })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
58
src/views/FilesList/FileListFilter/FileListFilter.vue
Normal file
58
src/views/FilesList/FileListFilter/FileListFilter.vue
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<template>
|
||||||
|
<NcActions force-menu
|
||||||
|
:type="isActive ? 'secondary' : 'tertiary'"
|
||||||
|
:menu-name="filterName">
|
||||||
|
<template #icon>
|
||||||
|
<slot name="icon" />
|
||||||
|
</template>
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<template v-if="isActive">
|
||||||
|
<NcActionSeparator />
|
||||||
|
<NcActionButton class="files-list-filter__clear-button"
|
||||||
|
close-after-click
|
||||||
|
@click="$emit('reset-filter')">
|
||||||
|
{{ t('files', 'Clear filter') }}
|
||||||
|
</NcActionButton>
|
||||||
|
</template>
|
||||||
|
</NcActions>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
|
||||||
|
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
|
||||||
|
import NcActionSeparator from '@nextcloud/vue/dist/Components/NcActionSeparator.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FileListFilter',
|
||||||
|
components: {
|
||||||
|
NcActions,
|
||||||
|
NcActionButton,
|
||||||
|
NcActionSeparator,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
filterName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.files-list-filter__clear-button :deep(.action-button__text) {
|
||||||
|
color: var(--color-error-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.button-vue) {
|
||||||
|
font-weight: normal !important;
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-weight: normal !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
128
src/views/FilesList/FileListFilter/FileListFilterModified.vue
Normal file
128
src/views/FilesList/FileListFilter/FileListFilterModified.vue
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
<template>
|
||||||
|
<FileListFilter :is-active="isActive"
|
||||||
|
:filter-name="t('libresign', 'Modified')"
|
||||||
|
@reset-filter="resetFilter">
|
||||||
|
<template #icon>
|
||||||
|
<NcIconSvgWrapper :path="mdiCalendarRange" />
|
||||||
|
</template>
|
||||||
|
<NcActionButton v-for="preset of timePresets"
|
||||||
|
:key="preset.id"
|
||||||
|
type="radio"
|
||||||
|
close-after-click
|
||||||
|
:model-value.sync="selectedOption"
|
||||||
|
:value="preset.id">
|
||||||
|
{{ preset.label }}
|
||||||
|
</NcActionButton>
|
||||||
|
</FileListFilter>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mdiCalendarRange } from '@mdi/js'
|
||||||
|
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
|
||||||
|
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
|
||||||
|
import calendarSvg from '@mdi/svg/svg/calendar.svg?raw'
|
||||||
|
import FileListFilter from './FileListFilter.vue'
|
||||||
|
import { useFiltersStore } from '../../../store/filters.js'
|
||||||
|
|
||||||
|
const startOfToday = () => (new Date()).setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FileListFilterModified',
|
||||||
|
components: {
|
||||||
|
FileListFilter,
|
||||||
|
NcActionButton,
|
||||||
|
NcIconSvgWrapper,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const filtersStore = useFiltersStore()
|
||||||
|
return {
|
||||||
|
// icons used in template
|
||||||
|
mdiCalendarRange,
|
||||||
|
filtersStore,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedOption: null,
|
||||||
|
timeRangeEnd: null,
|
||||||
|
timeRangeStart: null,
|
||||||
|
timePresets: [
|
||||||
|
{
|
||||||
|
id: 'today',
|
||||||
|
label: t('libresign', 'Today'),
|
||||||
|
filter: (time) => time > startOfToday(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'last-7',
|
||||||
|
label: t('libresign', 'Last 7 days'),
|
||||||
|
filter: (time) => time > (startOfToday() - (7 * 24 * 60 * 60 * 1000)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'last-30',
|
||||||
|
label: t('libresign', 'Last 30 days'),
|
||||||
|
filter: (time) => time > (startOfToday() - (30 * 24 * 60 * 60 * 1000)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'this-year',
|
||||||
|
label: t('libresign', 'This year ({year})', { year: (new Date()).getFullYear() }),
|
||||||
|
filter: (time) => time > (new Date(startOfToday())).setMonth(0, 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'last-year',
|
||||||
|
label: t('libresign', 'Last year ({year})', { year: (new Date()).getFullYear() - 1 }),
|
||||||
|
filter: (time) => (time > (new Date(startOfToday())).setFullYear((new Date()).getFullYear() - 1, 0, 1)) && (time < (new Date(startOfToday())).setMonth(0, 1)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isActive() {
|
||||||
|
return this.selectedOption !== null
|
||||||
|
},
|
||||||
|
currentPreset() {
|
||||||
|
return this.timePresets.find(({ id }) => id === this.selectedOption) ?? null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
selectedOption() {
|
||||||
|
if (this.selectedOption === null) {
|
||||||
|
this.selectedOption = null
|
||||||
|
this.setPreset()
|
||||||
|
} else {
|
||||||
|
this.setPreset(this.currentPreset)
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setPreset(preset) {
|
||||||
|
const chips = []
|
||||||
|
if (preset) {
|
||||||
|
chips.push({
|
||||||
|
icon: calendarSvg,
|
||||||
|
text: preset.label,
|
||||||
|
onclick: () => this.setPreset(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.resetFilter()
|
||||||
|
}
|
||||||
|
this.filtersStore.onFilterUpdateChips({ detail: chips, id: 'modified' })
|
||||||
|
},
|
||||||
|
resetFilter() {
|
||||||
|
if (this.selectedOption !== null) {
|
||||||
|
this.selectedOption = null
|
||||||
|
this.timeRangeEnd = null
|
||||||
|
this.timeRangeStart = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.files-list-filter-time {
|
||||||
|
&__clear-button :deep(.action-button__text) {
|
||||||
|
color: var(--color-error-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
136
src/views/FilesList/FileListFilter/FileListFilterStatus.vue
Normal file
136
src/views/FilesList/FileListFilter/FileListFilterStatus.vue
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
<template>
|
||||||
|
<FileListFilter class="file-list-filter-status"
|
||||||
|
:is-active="isActive"
|
||||||
|
:filter-name="t('libresign', 'Status')"
|
||||||
|
@reset-filter="resetFilter">
|
||||||
|
<template #icon>
|
||||||
|
<NcIconSvgWrapper :path="mdiListStatus" />
|
||||||
|
</template>
|
||||||
|
<NcActionButton v-for="status of statusPresets"
|
||||||
|
:key="status.id"
|
||||||
|
type="checkbox"
|
||||||
|
:model-value="selectedOptions.includes(status)"
|
||||||
|
@click="toggleOption(status)">
|
||||||
|
<template #icon>
|
||||||
|
<NcIconSvgWrapper :svg="status.icon" />
|
||||||
|
</template>
|
||||||
|
{{ status.label }}
|
||||||
|
</NcActionButton>
|
||||||
|
</FileListFilter>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mdiListStatus } from '@mdi/js'
|
||||||
|
|
||||||
|
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
|
||||||
|
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
|
||||||
|
import svgFile from '@mdi/svg/svg/file.svg?raw'
|
||||||
|
import svgSignature from '@mdi/svg/svg/signature.svg?raw'
|
||||||
|
import svgFractionOneHalf from '@mdi/svg/svg/fraction-one-half.svg?raw'
|
||||||
|
import svgSignatureFreehand from '@mdi/svg/svg/signature-freehand.svg?raw'
|
||||||
|
import svgDelete from '@mdi/svg/svg/delete.svg?raw'
|
||||||
|
import FileListFilter from './FileListFilter.vue'
|
||||||
|
import { useFiltersStore } from '../../../store/filters.js'
|
||||||
|
|
||||||
|
const colorize = (svg, color) => {
|
||||||
|
return svg.replace('<path ', `<path fill="${color}" `)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FileListFilterStatus',
|
||||||
|
components: {
|
||||||
|
FileListFilter,
|
||||||
|
NcActionButton,
|
||||||
|
NcIconSvgWrapper,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const filtersStore = useFiltersStore()
|
||||||
|
return {
|
||||||
|
mdiListStatus,
|
||||||
|
filtersStore,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedOptions: [],
|
||||||
|
statusPresets: [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
icon: colorize(svgFile, '#E0E0E0'),
|
||||||
|
label: t('libresign', 'draft'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
icon: colorize(svgSignature, '#B2E0B2'),
|
||||||
|
label: t('libresign', 'available for signature'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
icon: colorize(svgFractionOneHalf, '#F0E68C'),
|
||||||
|
label: t('libresign', 'partially signed'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
icon: colorize(svgSignatureFreehand, '#A0C4FF'),
|
||||||
|
label: t('libresign', 'signed'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
icon: colorize(svgDelete, '#FFB2B2'),
|
||||||
|
label: t('libresign', 'deleted'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isActive() {
|
||||||
|
return this.selectedOptions.length > 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
selectedOptions(newValue, oldValue) {
|
||||||
|
if (newValue.length === 0) {
|
||||||
|
this.setPreset()
|
||||||
|
} else {
|
||||||
|
this.setPreset(newValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setPreset(presets) {
|
||||||
|
const chips = []
|
||||||
|
if (presets && presets.length > 0) {
|
||||||
|
for (const preset of presets) {
|
||||||
|
chips.push({
|
||||||
|
icon: preset.icon,
|
||||||
|
text: preset.label,
|
||||||
|
onclick: () => this.setPreset(presets.filter(({ id }) => id !== preset.id)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.resetFilter()
|
||||||
|
}
|
||||||
|
this.filtersStore.onFilterUpdateChips({ detail: chips, id: 'status' })
|
||||||
|
},
|
||||||
|
resetFilter() {
|
||||||
|
if (this.selectedOptions.length > 0) {
|
||||||
|
this.selectedOptions = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleOption(option) {
|
||||||
|
const idx = this.selectedOptions.indexOf(option)
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.selectedOptions.splice(idx, 1)
|
||||||
|
} else {
|
||||||
|
this.selectedOptions.push(option)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.file-list-filter-status {
|
||||||
|
max-width: 220px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
74
src/views/FilesList/FileListFilters.vue
Normal file
74
src/views/FilesList/FileListFilters.vue
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<template>
|
||||||
|
<div class="file-list-filters">
|
||||||
|
<div class="file-list-filters__filter">
|
||||||
|
<div class="file-list-filters__filter">
|
||||||
|
<FileListFilterModified />
|
||||||
|
<FileListFilterStatus />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul v-if="filtersStore.activeChips.length > 0" class="file-list-filters__active" :aria-label="t('libresign', 'Active filters')">
|
||||||
|
<li v-for="(chip, index) of filtersStore.activeChips" :key="index">
|
||||||
|
<NcChip :aria-label-close="t('libresign', 'Remove filter')"
|
||||||
|
:icon-svg="chip.icon"
|
||||||
|
:text="chip.text"
|
||||||
|
@close="chip.onclick">
|
||||||
|
<template v-if="chip.user" #icon>
|
||||||
|
<NcAvatar disable-menu
|
||||||
|
:show-user-status="false"
|
||||||
|
:size="24"
|
||||||
|
:user="chip.user" />
|
||||||
|
</template>
|
||||||
|
</NcChip>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import NcChip from '@nextcloud/vue/dist/Components/NcChip.js'
|
||||||
|
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
|
||||||
|
import FileListFilterModified from './FileListFilter/FileListFilterModified.vue'
|
||||||
|
import FileListFilterStatus from './FileListFilter/FileListFilterStatus.vue'
|
||||||
|
import { useFiltersStore } from '../../store/filters.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FileListFilters',
|
||||||
|
components: {
|
||||||
|
NcChip,
|
||||||
|
NcAvatar,
|
||||||
|
FileListFilterModified,
|
||||||
|
FileListFilterStatus,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const filtersStore = useFiltersStore()
|
||||||
|
return { filtersStore }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.file-list-filters {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--default-grid-baseline);
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&__filter {
|
||||||
|
display: flex;
|
||||||
|
align-items: start;
|
||||||
|
justify-content: start;
|
||||||
|
gap: calc(var(--default-grid-baseline, 4px) * 2);
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex: 0 1 fit-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__active {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: calc(var(--default-grid-baseline, 4px) * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
232
src/views/FilesList/FilesList.vue
Normal file
232
src/views/FilesList/FilesList.vue
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
<template>
|
||||||
|
<NcAppContent :page-heading="t('libresign', 'Files')">
|
||||||
|
<div class="files-list__header">
|
||||||
|
<NcBreadcrumbs class="files-list__breadcrumbs">
|
||||||
|
<NcBreadcrumb :name="t('libresign', 'Files')"
|
||||||
|
:title="t('libresign', 'Files')"
|
||||||
|
:exact="true"
|
||||||
|
:force-icon-text="true"
|
||||||
|
:to="{ name: 'fileslist' }"
|
||||||
|
:aria-description="t('libresign', 'Files')"
|
||||||
|
:disable-drop="true"
|
||||||
|
@click.native="refresh()">
|
||||||
|
<template #icon>
|
||||||
|
<NcIconSvgWrapper :size="20"
|
||||||
|
:svg="viewIcon" />
|
||||||
|
</template>
|
||||||
|
</NcBreadcrumb>
|
||||||
|
<template #actions>
|
||||||
|
<NcActions :menu-name="t('libresign', 'Request')">
|
||||||
|
<template #icon>
|
||||||
|
<PlusIcon :size="20" />
|
||||||
|
</template>
|
||||||
|
<NcActionButton>
|
||||||
|
<template #icon>
|
||||||
|
<LinkIcon :size="20" />
|
||||||
|
</template>
|
||||||
|
{{ t('libresign', 'Upload from URL') }}
|
||||||
|
</NcActionButton>
|
||||||
|
<NcActionButton>
|
||||||
|
<template #icon>
|
||||||
|
<FolderIcon :size="20" />
|
||||||
|
</template>
|
||||||
|
{{ t('libresign', 'Choose from Files') }}
|
||||||
|
</NcActionButton>
|
||||||
|
<NcActionButton>
|
||||||
|
<template #icon>
|
||||||
|
<NcLoadingIcon v-if="isUploading" :size="20" />
|
||||||
|
<UploadIcon v-else :size="20" />
|
||||||
|
</template>
|
||||||
|
{{ t('libresign', 'Upload') }}
|
||||||
|
</NcActionButton>
|
||||||
|
</NcActions>
|
||||||
|
</template>
|
||||||
|
</NcBreadcrumbs>
|
||||||
|
|
||||||
|
<NcLoadingIcon v-if="isRefreshing" class="files-list__refresh-icon" />
|
||||||
|
|
||||||
|
<NcButton :aria-label="gridViewButtonLabel"
|
||||||
|
:title="gridViewButtonLabel"
|
||||||
|
class="files-list__header-grid-button"
|
||||||
|
type="tertiary"
|
||||||
|
@click="toggleGridView">
|
||||||
|
<template #icon>
|
||||||
|
<ListViewIcon v-if="userConfig.grid_view" />
|
||||||
|
<ViewGridIcon v-else />
|
||||||
|
</template>
|
||||||
|
</NcButton>
|
||||||
|
</div>
|
||||||
|
<NcLoadingIcon v-if="loading && !isRefreshing"
|
||||||
|
class="files-list__loading-icon"
|
||||||
|
:size="38"
|
||||||
|
:name="t('libresign', 'Loading …')" />
|
||||||
|
<NcEmptyContent v-else-if="!loading && isEmptyDir"
|
||||||
|
:name="t('libresign', 'There are no documents')"
|
||||||
|
:description="t('libresign', 'Choose the file to request signatures.')">
|
||||||
|
<template #icon>
|
||||||
|
<FolderIcon />
|
||||||
|
</template>
|
||||||
|
</NcEmptyContent>
|
||||||
|
<FilesListVirtual v-else
|
||||||
|
:nodes="dirContentsSorted" />
|
||||||
|
</NcAppContent>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js'
|
||||||
|
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||||
|
import PlusIcon from 'vue-material-design-icons/Plus.vue'
|
||||||
|
|
||||||
|
import HomeSvg from '@mdi/svg/svg/home.svg?raw'
|
||||||
|
import ListViewIcon from 'vue-material-design-icons/FormatListBulletedSquare.vue'
|
||||||
|
import ViewGridIcon from 'vue-material-design-icons/ViewGrid.vue'
|
||||||
|
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
|
||||||
|
import FolderIcon from 'vue-material-design-icons/Folder.vue'
|
||||||
|
import UploadIcon from 'vue-material-design-icons/Upload.vue'
|
||||||
|
import LinkIcon from 'vue-material-design-icons/Link.vue'
|
||||||
|
import NcBreadcrumb from '@nextcloud/vue/dist/Components/NcBreadcrumb.js'
|
||||||
|
import NcBreadcrumbs from '@nextcloud/vue/dist/Components/NcBreadcrumbs.js'
|
||||||
|
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
|
||||||
|
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
|
||||||
|
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
|
||||||
|
import FilesListVirtual from './FilesListVirtual.vue'
|
||||||
|
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
|
||||||
|
import { useFilesStore } from '../../store/files.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FilesList',
|
||||||
|
components: {
|
||||||
|
NcAppContent,
|
||||||
|
NcButton,
|
||||||
|
PlusIcon,
|
||||||
|
ListViewIcon,
|
||||||
|
ViewGridIcon,
|
||||||
|
NcLoadingIcon,
|
||||||
|
FolderIcon,
|
||||||
|
UploadIcon,
|
||||||
|
LinkIcon,
|
||||||
|
NcBreadcrumb,
|
||||||
|
NcBreadcrumbs,
|
||||||
|
NcActions,
|
||||||
|
NcActionButton,
|
||||||
|
NcIconSvgWrapper,
|
||||||
|
FilesListVirtual,
|
||||||
|
NcEmptyContent,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const filesStore = useFilesStore()
|
||||||
|
return { filesStore }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isUploading: false,
|
||||||
|
loading: false,
|
||||||
|
userConfig: {
|
||||||
|
grid_view: false,
|
||||||
|
},
|
||||||
|
dirContentsFiltered: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
viewIcon() {
|
||||||
|
return HomeSvg
|
||||||
|
},
|
||||||
|
gridViewButtonLabel() {
|
||||||
|
return this.userConfig.grid_view
|
||||||
|
? t('libresign', 'Switch to list view')
|
||||||
|
: t('libresign', 'Switch to grid view')
|
||||||
|
},
|
||||||
|
dirContentsSorted() {
|
||||||
|
if (!this.isEmptyDir) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return this.dirContentsFiltered
|
||||||
|
},
|
||||||
|
isEmptyDir() {
|
||||||
|
return this.filesStore.files.length === 0
|
||||||
|
},
|
||||||
|
isRefreshing() {
|
||||||
|
return !this.isEmptyDir
|
||||||
|
&& this.loading
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
await this.filesStore.getAllFiles()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
refresh() {
|
||||||
|
console.log('Need to implement refresh')
|
||||||
|
},
|
||||||
|
toggleGridView() {
|
||||||
|
this.userConfig.grid_view = !this.userConfig.grid_view
|
||||||
|
console.log('Need to implement toggle')
|
||||||
|
// this.userConfigStore.update('grid_view', !this.userConfig.grid_view)
|
||||||
|
},
|
||||||
|
filterDirContent() {
|
||||||
|
const nodes = this.filesStore.files
|
||||||
|
console.log('Implement filter here')
|
||||||
|
this.dirContentsFiltered = nodes
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.app-content {
|
||||||
|
// Virtual list needs to be full height and is scrollable
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 100%;
|
||||||
|
position: relative !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-list__breadcrumbs {
|
||||||
|
// Take as much space as possible
|
||||||
|
flex: 1 1 100% !important;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin-block: 0;
|
||||||
|
margin-inline: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
:deep() {
|
||||||
|
a {
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--with-progress {
|
||||||
|
flex-direction: column !important;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-list {
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
// Do not grow or shrink (vertically)
|
||||||
|
flex: 0 0;
|
||||||
|
max-width: 100%;
|
||||||
|
// Align with the navigation toggle icon
|
||||||
|
margin-block: var(--app-navigation-padding, 4px);
|
||||||
|
margin-inline: calc(var(--default-clickable-area, 44px) + 2 * var(--app-navigation-padding, 4px)) var(--app-navigation-padding, 4px);
|
||||||
|
|
||||||
|
>* {
|
||||||
|
// Do not grow or shrink (horizontally)
|
||||||
|
flex: 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__refresh-icon {
|
||||||
|
flex: 0 0 var(--default-clickable-area);
|
||||||
|
width: var(--default-clickable-area);
|
||||||
|
height: var(--default-clickable-area);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__loading-icon {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
57
src/views/FilesList/FilesListVirtual.vue
Normal file
57
src/views/FilesList/FilesListVirtual.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<template>
|
||||||
|
<VirtualList>
|
||||||
|
<template #filters>
|
||||||
|
<FileListFilters />
|
||||||
|
</template>
|
||||||
|
</VirtualList>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import VirtualList from './VirtualList.vue'
|
||||||
|
import FileListFilters from './FileListFilters.vue'
|
||||||
|
export default {
|
||||||
|
name: 'FilesListVirtual',
|
||||||
|
components: {
|
||||||
|
VirtualList,
|
||||||
|
FileListFilters,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
nodes: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.files-list {
|
||||||
|
--row-height: 55px;
|
||||||
|
--cell-margin: 14px;
|
||||||
|
|
||||||
|
--checkbox-padding: calc((var(--row-height) - var(--checkbox-size)) / 2);
|
||||||
|
--checkbox-size: 24px;
|
||||||
|
--clickable-area: var(--default-clickable-area);
|
||||||
|
--icon-preview-size: 32px;
|
||||||
|
|
||||||
|
--fixed-block-start-position: var(--default-clickable-area);
|
||||||
|
|
||||||
|
overflow: auto;
|
||||||
|
height: 100%;
|
||||||
|
will-change: scroll-position;
|
||||||
|
& :deep() {
|
||||||
|
.files-list__filters {
|
||||||
|
// Pinned on top when scrolling above table header
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
// ensure there is a background to hide the file list on scroll
|
||||||
|
background-color: var(--color-main-background);
|
||||||
|
z-index: 10;
|
||||||
|
// fixed the size
|
||||||
|
padding-inline: var(--row-height) var(--default-grid-baseline, 4px);
|
||||||
|
height: var(--fixed-block-start-position);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
13
src/views/FilesList/VirtualList.vue
Normal file
13
src/views/FilesList/VirtualList.vue
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<template>
|
||||||
|
<div class="files-list">
|
||||||
|
<div class="files-list__filters">
|
||||||
|
<slot name="filters" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'VirtualList',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -12,4 +12,4 @@
|
||||||
"vueCompilerOptions": {
|
"vueCompilerOptions": {
|
||||||
"target": 2.7,
|
"target": 2.7,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue