summaryrefslogtreecommitdiffstats
path: root/apps/files
diff options
context:
space:
mode:
authorJohn Molakvoæ <skjnldsv@protonmail.com>2023-03-22 11:45:59 +0100
committerJohn Molakvoæ <skjnldsv@protonmail.com>2023-04-06 14:49:31 +0200
commitf330813ff01e321a9e39822b183c49805bff16a5 (patch)
tree2f0349af40c82be1e0132dc3040047e90e63dfba /apps/files
parent10010fc532a02958804667e1cb3acee8e9556394 (diff)
downloadnextcloud-server-f330813ff01e321a9e39822b183c49805bff16a5.tar.gz
nextcloud-server-f330813ff01e321a9e39822b183c49805bff16a5.zip
feat(files): custom columns
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
Diffstat (limited to 'apps/files')
-rw-r--r--apps/files/js/app.js2
-rw-r--r--apps/files/src/components/FileEntry.vue171
-rw-r--r--apps/files/src/components/FilesListHeader.vue17
-rw-r--r--apps/files/src/components/FilesListVirtual.vue24
-rw-r--r--apps/files/src/main.js4
-rw-r--r--apps/files/src/mixins/fileslist-row.scss8
-rw-r--r--apps/files/src/services/Navigation.ts24
-rw-r--r--apps/files/src/views/FilesList.vue28
-rw-r--r--apps/files/src/views/Navigation.vue2
9 files changed, 188 insertions, 92 deletions
diff --git a/apps/files/js/app.js b/apps/files/js/app.js
index 75967ef5753..36afd9a80b7 100644
--- a/apps/files/js/app.js
+++ b/apps/files/js/app.js
@@ -313,7 +313,7 @@
view: 'files'
}, params);
- var lastId = this.navigation.active;
+ var lastId = this.getActiveView();
if (!this.navigation.views.find(view => view.id === params.view)) {
params.view = 'files';
}
diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue
index 84990a5ba39..65fdf4b4c38 100644
--- a/apps/files/src/components/FileEntry.vue
+++ b/apps/files/src/components/FileEntry.vue
@@ -19,56 +19,7 @@
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
-<template>
- <Fragment>
- <td class="files-list__row-checkbox">
- <NcCheckboxRadioSwitch :aria-label="t('files', 'Select the row for {displayName}', { displayName })"
- :checked.sync="selectedFiles"
- :value="fileid.toString()"
- name="selectedFiles" />
- </td>
-
- <!-- Icon or preview -->
- <td class="files-list__row-icon">
- <FolderIcon v-if="source.type === 'folder'" />
-
- <!-- Decorative image, should not be aria documented -->
- <span v-else-if="previewUrl && !backgroundFailed"
- ref="previewImg"
- class="files-list__row-icon-preview"
- :style="{ backgroundImage }" />
-
- <span v-else-if="mimeUrl"
- class="files-list__row-icon-preview files-list__row-icon-preview--mime"
- :style="{ backgroundImage: mimeUrl }" />
-
- <FileIcon v-else />
- </td>
-
- <!-- Link to file and -->
- <td class="files-list__row-name">
- <a v-bind="linkTo">
- {{ displayName }}
- </a>
- </td>
-
- <!-- Actions -->
- <td class="files-list__row-actions">
- <NcActions>
- <NcActionButton>
- {{ t('files', 'Rename') }}
- <Pencil slot="icon" />
- </NcActionButton>
- <NcActionButton>
- {{ t('files', 'Delete') }}
- <TrashCan slot="icon" />
- </NcActionButton>
- </NcActions>
- </td>
- </Fragment>
-</template>
-
-<script lang="ts">
+<script lang='ts'>
import { Folder, File } from '@nextcloud/files'
import { Fragment } from 'vue-fragment'
import { join } from 'path'
@@ -134,6 +85,15 @@ export default Vue.extend({
},
computed: {
+ /** @return {Navigation} */
+ currentView() {
+ return this.$navigation.active
+ },
+
+ columns() {
+ return this.currentView?.columns || []
+ },
+
dir() {
// Remove any trailing slash but leave root slash
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
@@ -279,13 +239,120 @@ export default Vue.extend({
t: translate,
},
+
+ /**
+ * While a bit more complex, this component is pretty straightforward.
+ * For performance reasons, we're using a render function instead of a template.
+ */
+ render(createElement) {
+ // Checkbox
+ const checkbox = createElement('td', {
+ staticClass: 'files-list__row-checkbox',
+ }, [createElement('NcCheckboxRadioSwitch', {
+ attrs: {
+ 'aria-label': this.t('files', 'Select the row for {displayName}', {
+ displayName: this.displayName,
+ }),
+ checked: this.selectedFiles,
+ value: this.fileid.toString(),
+ name: 'selectedFiles',
+ },
+ on: {
+ 'update:checked': ($event) => {
+ this.selectedFiles = $event
+ },
+ },
+ })])
+
+ // Icon
+ const iconContent = () => {
+ // Folder icon
+ if (this.source.type === 'folder') {
+ return createElement('FolderIcon')
+ }
+ // Render cached preview or fallback to mime icon if defined
+ const renderPreview = this.previewUrl && !this.backgroundFailed
+ if (renderPreview || this.mimeUrl) {
+ return createElement('span', {
+ ref: 'previewImg',
+ class: {
+ 'files-list__row-icon-preview': true,
+ 'files-list__row-icon-preview--mime': !renderPreview,
+ },
+ style: {
+ backgroundImage: renderPreview
+ ? this.backgroundImage
+ : this.mimeUrl,
+ },
+ })
+ }
+ // Empty file icon
+ return createElement('FileIcon')
+ }
+ const icon = createElement('td', {
+ staticClass: 'files-list__row-icon',
+ }, [iconContent()])
+
+ // Name
+ const name = createElement('td', {
+ staticClass: 'files-list__row-name',
+ }, [
+ createElement(this.linkTo?.is || 'a', {
+ attrs: this.linkTo,
+ }, this.displayName),
+ ])
+
+ // Actions
+ const actions = createElement('td', {
+ staticClass: 'files-list__row-actions',
+ }, [createElement('NcActions', [
+ createElement('NcActionButton', [
+ this.t('files', 'Rename'),
+ createElement('Pencil', {
+ slot: 'icon',
+ }),
+ ]),
+ createElement('NcActionButton', [
+ this.t('files', 'Delete'),
+ createElement('TrashCan', {
+ slot: 'icon',
+ }),
+ ]),
+ ])])
+
+ // Columns
+ const columns = this.columns.map(column => {
+ const td = document.createElement('td')
+ column.render(td, this.source)
+ return createElement('td', {
+ class: {
+ [`files-list__row-${this.currentView?.id}-${column.id}`]: true,
+ 'files-list__row-column--custom': true,
+ },
+ key: column.id,
+ domProps: {
+ innerHTML: td.innerHTML,
+ },
+ }, '123')
+ })
+
+ console.debug(columns, this.displayName)
+
+ return createElement('Fragment', [
+ checkbox,
+ icon,
+ name,
+ actions,
+ ...columns,
+ ])
+ },
})
</script>
-<style scoped lang="scss">
+<style scoped lang='scss'>
@import '../mixins/fileslist-row.scss';
-.files-list__row-icon-preview:not([style*="background"]) {
+.files-list__row-icon-preview:not([style*='background']) {
background: linear-gradient(110deg, var(--color-loading-dark) 0%, var(--color-loading-dark) 25%, var(--color-loading-light) 50%, var(--color-loading-dark) 75%, var(--color-loading-dark) 100%);
background-size: 400%;
animation: preview-gradient-slide 1s ease infinite;
diff --git a/apps/files/src/components/FilesListHeader.vue b/apps/files/src/components/FilesListHeader.vue
index b09feae04f2..81b56331f9c 100644
--- a/apps/files/src/components/FilesListHeader.vue
+++ b/apps/files/src/components/FilesListHeader.vue
@@ -40,6 +40,13 @@
<!-- Actions -->
<th class="files-list__row-actions" />
+
+ <!-- Custom views columns -->
+ <th v-for="column in columns"
+ :key="column.id"
+ :class="`files-list__row-column--custom files-list__row-${currentView.id}-${column.id}`">
+ {{ column.title }}
+ </th>
</tr>
</template>
@@ -56,6 +63,7 @@ import { useFilesStore } from '../store/files'
import { useSelectionStore } from '../store/selection'
import { useSortingStore } from '../store/sorting'
import logger from '../logger.js'
+import Navigation from '../services/Navigation'
export default Vue.extend({
name: 'FilesListHeader',
@@ -87,6 +95,15 @@ export default Vue.extend({
computed: {
...mapState(useSortingStore, ['defaultFileSorting', 'defaultFileSortingDirection']),
+ /** @return {Navigation} */
+ currentView() {
+ return this.$navigation.active
+ },
+
+ columns() {
+ return this.currentView?.columns || []
+ },
+
dir() {
// Remove any trailing slash but leave root slash
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue
index 62a4e0e42eb..569f5dd09ce 100644
--- a/apps/files/src/components/FilesListVirtual.vue
+++ b/apps/files/src/components/FilesListVirtual.vue
@@ -20,7 +20,27 @@
-
-->
<template>
- <RecycleScroller ref="recycleScroller"
+ <VirtualList v-if="false"
+ class="files-list"
+ :data-component="FileEntry"
+ :data-key="getFileId"
+ :data-sources="nodes"
+ :estimate-size="55"
+ :table-mode="true"
+ item-class="files-list__row"
+ wrap-class="files-list__body">
+ <template #before>
+ <caption v-show="false" class="files-list__caption">
+ {{ summary }}
+ </caption>
+ </template>
+
+ <template #header>
+ <FilesListHeader :nodes="nodes" />
+ </template>
+ </VirtualList>
+
+ <RecycleScroller v-else ref="recycleScroller"
class="files-list"
key-field="source"
:items="nodes"
@@ -50,6 +70,7 @@
<script lang="ts">
import { Folder, File } from '@nextcloud/files'
import { RecycleScroller } from 'vue-virtual-scroller'
+import VirtualList from 'vue-virtual-scroll-list'
import { translate, translatePlural } from '@nextcloud/l10n'
import Vue from 'vue'
@@ -63,6 +84,7 @@ export default Vue.extend({
RecycleScroller,
FileEntry,
FilesListHeader,
+ VirtualList,
},
props: {
diff --git a/apps/files/src/main.js b/apps/files/src/main.js
index 48b981359ed..1339c3c68ee 100644
--- a/apps/files/src/main.js
+++ b/apps/files/src/main.js
@@ -23,6 +23,7 @@ window.OCP.Files = window.OCP.Files ?? {}
// Init Navigation Service
const Navigation = new NavigationService()
Object.assign(window.OCP.Files, { Navigation })
+Vue.prototype.$navigation = Navigation
// Init Files App Settings Service
const Settings = new SettingsService()
@@ -48,9 +49,6 @@ const pinia = createPinia()
const ListView = Vue.extend(FilesListView)
const FilesList = new ListView({
name: 'FilesListRoot',
- propsData: {
- Navigation,
- },
router,
pinia,
})
diff --git a/apps/files/src/mixins/fileslist-row.scss b/apps/files/src/mixins/fileslist-row.scss
index 1315a5724f2..6c3da968b70 100644
--- a/apps/files/src/mixins/fileslist-row.scss
+++ b/apps/files/src/mixins/fileslist-row.scss
@@ -89,3 +89,11 @@ td, th {
flex: 1 1 100%;
justify-content: left;
}
+
+.files-list__row-column--custom {
+ overflow: hidden;
+ flex: 1 1 calc(var(--row-height) * 3);
+ width: auto;
+ min-width: var(--row-height);
+ justify-content: normal;
+}
diff --git a/apps/files/src/services/Navigation.ts b/apps/files/src/services/Navigation.ts
index 01b6e701c72..adcc391b920 100644
--- a/apps/files/src/services/Navigation.ts
+++ b/apps/files/src/services/Navigation.ts
@@ -35,14 +35,10 @@ export interface Column {
id: string
/** Translated column title */
title: string
- /**
- * Property key from Node main or additional attributes.
- * Will be used if no custom sort function is provided.
- * Sorting will be done by localCompare
- */
- property: string
- /** Special function used to sort Nodes between them */
- sortFunction?: (nodeA: Node, nodeB: Node) => number;
+ /** The content of the cell to render */
+ render: (mount: HTMLTableCellElement, node: Node) => void
+ /** Function used to sort Nodes between them */
+ sort?: (nodeA: Node, nodeB: Node) => number
/** Custom summary of the column to display at the end of the list.
Will not be displayed if nothing is provided */
summary?: (node: Node[]) => string
@@ -61,7 +57,7 @@ export interface Navigation {
* You _must_ also return the current directory
* information alongside with its content.
*/
- getContents: (path: string) => Promise<ContentsWithRoot[]>
+ getContents: (path: string) => Promise<ContentsWithRoot>
/** The view icon as an inline svg */
icon: string
/** The view order */
@@ -208,19 +204,19 @@ const isValidNavigation = function(view: Navigation): boolean {
*/
const isValidColumn = function(column: Column): boolean {
if (!column.id || typeof column.id !== 'string') {
- throw new Error('Column id is required')
+ throw new Error('A column id is required')
}
if (!column.title || typeof column.title !== 'string') {
- throw new Error('Column title is required')
+ throw new Error('A column title is required')
}
- if (!column.property || typeof column.property !== 'string') {
- throw new Error('Column property is required')
+ if (!column.render || typeof column.render !== 'function') {
+ throw new Error('A render function is required')
}
// Optional properties
- if (column.sortFunction && typeof column.sortFunction !== 'function') {
+ if (column.sort && typeof column.sort !== 'function') {
throw new Error('Column sortFunction must be a function')
}
diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue
index e261b375862..d09d3c619f2 100644
--- a/apps/files/src/views/FilesList.vue
+++ b/apps/files/src/views/FilesList.vue
@@ -94,14 +94,6 @@ export default Vue.extend({
TrashCan,
},
- props: {
- // eslint-disable-next-line vue/prop-name-casing
- Navigation: {
- type: Navigation,
- required: true,
- },
- },
-
setup() {
const pathsStore = usePathsStore()
const filesStore = useFilesStore()
@@ -123,18 +115,10 @@ export default Vue.extend({
},
computed: {
- currentViewId() {
- return this.$route.params.view || 'files'
- },
-
/** @return {Navigation} */
currentView() {
- return this.views.find(view => view.id === this.currentViewId)
- },
-
- /** @return {Navigation[]} */
- views() {
- return this.Navigation.views
+ return this.$navigation.active
+ || this.$navigation.views.find(view => view.id === 'files')
},
/**
@@ -151,10 +135,14 @@ export default Vue.extend({
* @return {Folder|undefined}
*/
currentFolder() {
+ if (!this.currentView?.id) {
+ return
+ }
+
if (this.dir === '/') {
- return this.filesStore.getRoot(this.currentViewId)
+ return this.filesStore.getRoot(this.currentView.id)
}
- const fileId = this.pathsStore.getPath(this.currentViewId, this.dir)
+ const fileId = this.pathsStore.getPath(this.currentView.id, this.dir)
return this.filesStore.getNode(fileId)
},
diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue
index 9a2e82d1bc6..f7b089b61c1 100644
--- a/apps/files/src/views/Navigation.vue
+++ b/apps/files/src/views/Navigation.vue
@@ -166,7 +166,7 @@ export default {
return
}
- this.Navigation.setActive(view.id)
+ this.Navigation.setActive(view)
logger.debug('Navigation changed', { id: view.id, view })
// debugger