aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src/components/FilesListVirtual.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/src/components/FilesListVirtual.vue')
-rw-r--r--apps/files/src/components/FilesListVirtual.vue255
1 files changed, 168 insertions, 87 deletions
diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue
index 2442dd98190..47b8ef19b19 100644
--- a/apps/files/src/components/FilesListVirtual.vue
+++ b/apps/files/src/components/FilesListVirtual.vue
@@ -9,6 +9,7 @@
:data-sources="nodes"
:grid-mode="userConfig.grid_view"
:extra-props="{
+ isMimeAvailable,
isMtimeAvailable,
isSizeAvailable,
nodes,
@@ -20,14 +21,16 @@
</template>
<template v-if="!isNoneSelected" #header-overlay>
- <span class="files-list__selected">{{ t('files', '{count} selected', { count: selectedNodes.length }) }}</span>
+ <span class="files-list__selected">
+ {{ n('files', '{count} selected', '{count} selected', selectedNodes.length, { count: selectedNodes.length }) }}
+ </span>
<FilesListTableHeaderActions :current-view="currentView"
:selected-nodes="selectedNodes" />
</template>
<template #before>
<!-- Headers -->
- <FilesListHeader v-for="header in sortedHeaders"
+ <FilesListHeader v-for="header in headers"
:key="header.id"
:current-folder="currentFolder"
:current-view="currentView"
@@ -39,15 +42,22 @@
<!-- Table header and sort buttons -->
<FilesListTableHeader ref="thead"
:files-list-width="fileListWidth"
+ :is-mime-available="isMimeAvailable"
:is-mtime-available="isMtimeAvailable"
:is-size-available="isSizeAvailable"
:nodes="nodes" />
</template>
+ <!-- Body replacement if no files are available -->
+ <template #empty>
+ <slot name="empty" />
+ </template>
+
<!-- Tfoot-->
<template #footer>
<FilesListTableFooter :current-view="currentView"
:files-list-width="fileListWidth"
+ :is-mime-available="isMimeAvailable"
:is-mtime-available="isMtimeAvailable"
:is-size-available="isSizeAvailable"
:nodes="nodes"
@@ -57,24 +67,25 @@
</template>
<script lang="ts">
-import type { ComponentPublicInstance, PropType } from 'vue'
-import type { Node as NcNode } from '@nextcloud/files'
import type { UserConfig } from '../types'
+import type { Node as NcNode } from '@nextcloud/files'
+import type { ComponentPublicInstance, PropType } from 'vue'
-import { defineComponent } from 'vue'
-import { getFileListHeaders, Folder, Permission, View, getFileActions, FileType } from '@nextcloud/files'
+import { Folder, Permission, View, getFileActions, FileType } from '@nextcloud/files'
import { showError } from '@nextcloud/dialogs'
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
-import { translate as t } from '@nextcloud/l10n'
-import { useHotKey } from '@nextcloud/vue/dist/Composables/useHotKey.js'
+import { n, t } from '@nextcloud/l10n'
+import { useHotKey } from '@nextcloud/vue/composables/useHotKey'
+import { defineComponent } from 'vue'
import { action as sidebarAction } from '../actions/sidebarAction.ts'
-import { getSummaryFor } from '../utils/fileUtils'
import { useActiveStore } from '../store/active.ts'
+import { useFileListHeaders } from '../composables/useFileListHeaders.ts'
import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import { useSelectionStore } from '../store/selection.js'
import { useUserConfigStore } from '../store/userconfig.ts'
+import logger from '../logger.ts'
import FileEntry from './FileEntry.vue'
import FileEntryGrid from './FileEntryGrid.vue'
@@ -83,7 +94,6 @@ import FilesListHeader from './FilesListHeader.vue'
import FilesListTableFooter from './FilesListTableFooter.vue'
import FilesListTableHeader from './FilesListTableHeader.vue'
import FilesListTableHeaderActions from './FilesListTableHeaderActions.vue'
-import logger from '../logger.ts'
import VirtualList from './VirtualList.vue'
export default defineComponent({
@@ -111,6 +121,10 @@ export default defineComponent({
type: Array as PropType<NcNode[]>,
required: true,
},
+ summary: {
+ type: String,
+ required: true,
+ },
},
setup() {
@@ -124,6 +138,7 @@ export default defineComponent({
return {
fileId,
fileListWidth,
+ headers: useFileListHeaders(),
openDetails,
openFile,
@@ -131,6 +146,7 @@ export default defineComponent({
selectionStore,
userConfigStore,
+ n,
t,
}
},
@@ -139,9 +155,7 @@ export default defineComponent({
return {
FileEntry,
FileEntryGrid,
- headers: getFileListHeaders(),
scrollToIndex: 0,
- openFileId: null as number|null,
}
},
@@ -150,10 +164,16 @@ export default defineComponent({
return this.userConfigStore.userConfig
},
- summary() {
- return getSummaryFor(this.nodes)
+ isMimeAvailable() {
+ if (!this.userConfig.show_mime_column) {
+ return false
+ }
+ // Hide mime column on narrow screens
+ if (this.fileListWidth < 1024) {
+ return false
+ }
+ return this.nodes.some(node => node.mime !== undefined || node.mime !== 'application/octet-stream')
},
-
isMtimeAvailable() {
// Hide mtime column on narrow screens
if (this.fileListWidth < 768) {
@@ -169,14 +189,6 @@ export default defineComponent({
return this.nodes.some(node => node.size !== undefined)
},
- sortedHeaders() {
- if (!this.currentFolder || !this.currentView) {
- return []
- }
-
- return [...this.headers].sort((a, b) => a.order - b.order)
- },
-
cantUpload() {
return this.currentFolder && (this.currentFolder.permissions & Permission.CREATE) === 0
},
@@ -188,7 +200,7 @@ export default defineComponent({
caption() {
const defaultCaption = t('files', 'List of files and folders.')
const viewCaption = this.currentView.caption || defaultCaption
- const cantUploadCaption = this.cantUpload ? t('files', 'You don’t have permission to upload or create files here.') : null
+ const cantUploadCaption = this.cantUpload ? t('files', 'You do not have permission to upload or create files here.') : null
const quotaExceededCaption = this.isQuotaExceeded ? t('files', 'You have used your space quota and cannot upload files anymore.') : null
const sortableCaption = t('files', 'Column headers with buttons are sortable.')
const virtualListNote = t('files', 'This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list.')
@@ -208,38 +220,26 @@ export default defineComponent({
isNoneSelected() {
return this.selectedNodes.length === 0
},
+
+ isEmpty() {
+ return this.nodes.length === 0
+ },
},
watch: {
- fileId: {
- handler(fileId) {
- this.scrollToFile(fileId, false)
- },
- immediate: true,
+ // If nodes gets populated and we have a fileId,
+ // an openFile or openDetails, we fire the appropriate actions.
+ isEmpty() {
+ this.handleOpenQueries()
},
-
- openFile: {
- handler() {
- // wait for scrolling and updating the actions to settle
- this.$nextTick(() => {
- if (this.fileId && this.openFile) {
- this.handleOpenFile(this.fileId)
- }
- })
- },
- immediate: true,
+ fileId() {
+ this.handleOpenQueries()
},
-
- openDetails: {
- handler() {
- // wait for scrolling and updating the actions to settle
- this.$nextTick(() => {
- if (this.fileId && this.openDetails) {
- this.openSidebarForFile(this.fileId)
- }
- })
- },
- immediate: true,
+ openFile() {
+ this.handleOpenQueries()
+ },
+ openDetails() {
+ this.handleOpenQueries()
},
},
@@ -269,6 +269,33 @@ export default defineComponent({
},
methods: {
+ handleOpenQueries() {
+ // If the list is empty, or we don't have a fileId,
+ // there's nothing to be done.
+ if (this.isEmpty || !this.fileId) {
+ return
+ }
+
+ logger.debug('FilesListVirtual: checking for requested fileId, openFile or openDetails', {
+ nodes: this.nodes,
+ fileId: this.fileId,
+ openFile: this.openFile,
+ openDetails: this.openDetails,
+ })
+
+ if (this.openFile) {
+ this.handleOpenFile(this.fileId)
+ }
+
+ if (this.openDetails) {
+ this.openSidebarForFile(this.fileId)
+ }
+
+ if (this.fileId) {
+ this.scrollToFile(this.fileId, false)
+ }
+ },
+
openSidebarForFile(fileId) {
// Open the sidebar for the given URL fileid
// iif we just loaded the app.
@@ -276,7 +303,9 @@ export default defineComponent({
if (node && sidebarAction?.enabled?.([node], this.currentView)) {
logger.debug('Opening sidebar on file ' + node.path, { node })
sidebarAction.exec(node, this.currentView, this.currentFolder.path)
+ return
}
+ logger.warn(`Failed to open sidebar on file ${fileId}, file isn't cached yet !`, { fileId, node })
},
scrollToFile(fileId: number|null, warn = true) {
@@ -292,6 +321,7 @@ export default defineComponent({
}
this.scrollToIndex = Math.max(0, index)
+ logger.debug('Scrolling to file ' + fileId, { fileId, index })
}
},
@@ -303,7 +333,7 @@ export default defineComponent({
delete query.openfile
delete query.opendetails
- this.activeStore.clearActiveNode()
+ this.activeStore.activeNode = undefined
window.OCP.Files.Router.goToRoute(
null,
{ ...this.$route.params, fileid: String(this.currentFolder.fileid ?? '') },
@@ -329,30 +359,40 @@ export default defineComponent({
* Handle opening a file (e.g. by ?openfile=true)
* @param fileId File to open
*/
- handleOpenFile(fileId: number|null) {
- if (fileId === null) {
- return
- }
-
+ async handleOpenFile(fileId: number) {
const node = this.nodes.find(n => n.fileid === fileId) as NcNode
- if (node === undefined || node.type === FileType.Folder) {
+ if (node === undefined) {
return
}
- logger.debug('Opening file ' + node.path, { node })
- this.openFileId = fileId
- const defaultAction = getFileActions()
- // Get only default actions (visible and hidden)
- .filter(action => !!action?.default)
- // Find actions that are either always enabled or enabled for the current node
- .filter((action) => !action.enabled || action.enabled([node], this.currentView))
- // Sort enabled default actions by order
- .sort((a, b) => (a.order || 0) - (b.order || 0))
- // Get the first one
- .at(0)
- // Some file types do not have a default action (e.g. they can only be downloaded)
- // So if there is an enabled default action, so execute it
- defaultAction?.exec(node, this.currentView, this.currentFolder.path)
+ if (node.type === FileType.File) {
+ const defaultAction = getFileActions()
+ // Get only default actions (visible and hidden)
+ .filter((action) => !!action?.default)
+ // Find actions that are either always enabled or enabled for the current node
+ .filter((action) => !action.enabled || action.enabled([node], this.currentView))
+ .filter((action) => action.id !== 'download')
+ // Sort enabled default actions by order
+ .sort((a, b) => (a.order || 0) - (b.order || 0))
+ // Get the first one
+ .at(0)
+
+ // Some file types do not have a default action (e.g. they can only be downloaded)
+ // So if there is an enabled default action, so execute it
+ if (defaultAction) {
+ logger.debug('Opening file ' + node.path, { node })
+ return await defaultAction.exec(node, this.currentView, this.currentFolder.path)
+ }
+ }
+ // The file is either a folder or has no default action other than downloading
+ // in this case we need to open the details instead and remove the route from the history
+ logger.debug('Ignore `openfile` query and replacing with `opendetails` for ' + node.path, { node })
+ window.OCP.Files.Router.goToRoute(
+ null,
+ this.$route.params,
+ { ...this.$route.query, openfile: undefined, opendetails: '' },
+ true, // silent update of the URL
+ )
},
onDragOver(event: DragEvent) {
@@ -425,7 +465,7 @@ export default defineComponent({
delete query.openfile
delete query.opendetails
- this.activeStore.setActiveNode(node)
+ this.activeStore.activeNode = node
// Silent update of the URL
window.OCP.Files.Router.goToRoute(
@@ -441,15 +481,17 @@ export default defineComponent({
<style scoped lang="scss">
.files-list {
- --row-height: 55px;
+ --row-height: 44px;
--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;
+ --icon-preview-size: 24px;
--fixed-block-start-position: var(--default-clickable-area);
+ display: flex;
+ flex-direction: column;
overflow: auto;
height: 100%;
will-change: scroll-position;
@@ -497,6 +539,13 @@ export default defineComponent({
// Hide the table header below the overlay
margin-block-start: calc(-1 * var(--row-height));
}
+
+ // Visually hide the table when there are no files
+ &--hidden {
+ visibility: hidden;
+ z-index: -1;
+ opacity: 0;
+ }
}
.files-list__filters {
@@ -528,6 +577,7 @@ export default defineComponent({
background-color: var(--color-main-background);
border-block-end: 1px solid var(--color-border);
height: var(--row-height);
+ flex: 0 0 var(--row-height);
}
.files-list__thead,
@@ -536,7 +586,6 @@ export default defineComponent({
flex-direction: column;
width: 100%;
background-color: var(--color-main-background);
-
}
// Table header
@@ -547,6 +596,16 @@ export default defineComponent({
top: var(--fixed-block-start-position);
}
+ // Empty content
+ .files-list__empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ }
+
tr {
position: relative;
display: flex;
@@ -716,8 +775,8 @@ export default defineComponent({
// File and folder overlay
&-overlay {
position: absolute;
- max-height: calc(var(--icon-preview-size) * 0.5);
- max-width: calc(var(--icon-preview-size) * 0.5);
+ max-height: calc(var(--icon-preview-size) * 0.6);
+ max-width: calc(var(--icon-preview-size) * 0.6);
color: var(--color-primary-element-text);
// better alignment with the folder icon
margin-block-start: 2px;
@@ -822,22 +881,28 @@ export default defineComponent({
margin-inline-end: 7px;
}
+ .files-list__row-mime,
.files-list__row-mtime,
.files-list__row-size {
color: var(--color-text-maxcontrast);
}
+
.files-list__row-size {
- width: calc(var(--row-height) * 1.5);
+ width: calc(var(--row-height) * 2);
// Right align content/text
justify-content: flex-end;
}
.files-list__row-mtime {
- width: calc(var(--row-height) * 2);
+ width: calc(var(--row-height) * 2.5);
+ }
+
+ .files-list__row-mime {
+ width: calc(var(--row-height) * 3.5);
}
.files-list__row-column-custom {
- width: calc(var(--row-height) * 2);
+ width: calc(var(--row-height) * 2.5);
}
}
}
@@ -853,12 +918,11 @@ export default defineComponent({
<style lang="scss">
// Grid mode
-tbody.files-list__tbody.files-list__tbody--grid {
- --half-clickable-area: calc(var(--clickable-area) / 2);
+.files-list--grid tbody.files-list__tbody {
--item-padding: 16px;
--icon-preview-size: 166px;
- --name-height: 32px;
- --mtime-height: 16px;
+ --name-height: var(--default-clickable-area);
+ --mtime-height: calc(var(--font-size-small) + var(--default-grid-baseline));
--row-width: calc(var(--icon-preview-size) + var(--item-padding) * 2);
--row-height: calc(var(--icon-preview-size) + var(--name-height) + var(--mtime-height) + var(--item-padding) * 2);
--checkbox-padding: 0px;
@@ -940,15 +1004,32 @@ tbody.files-list__tbody.files-list__tbody--grid {
.files-list__row-mtime {
width: var(--icon-preview-size);
height: var(--mtime-height);
- font-size: calc(var(--default-font-size) - 4px);
+ font-size: var(--font-size-small);
}
.files-list__row-actions {
position: absolute;
- inset-inline-end: calc(var(--half-clickable-area) / 2);
+ inset-inline-end: calc(var(--clickable-area) / 4);
inset-block-end: calc(var(--mtime-height) / 2);
width: var(--clickable-area);
height: var(--clickable-area);
}
}
+
+@media screen and (max-width: 768px) {
+ // there is no mtime
+ .files-list--grid tbody.files-list__tbody {
+ --mtime-height: 0px;
+
+ // so we move the action to the name
+ .files-list__row-actions {
+ inset-block-end: var(--item-padding);
+ }
+
+ // and we need to keep space on the name for the actions
+ .files-list__row-name-text {
+ padding-inline-end: var(--clickable-area) !important;
+ }
+ }
+}
</style>