aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorskjnldsv <skjnldsv@protonmail.com>2024-12-13 12:00:28 +0100
committerskjnldsv <skjnldsv@protonmail.com>2024-12-17 09:59:56 +0100
commite7001022c75b3a818356378bb53bbfe5129a10fe (patch)
tree5cb23bd1fb1ff92f6ee99aed94629525de42ab45 /apps
parentf16d0478084ca17551a0bc3242e48b633ff23a24 (diff)
downloadnextcloud-server-e7001022c75b3a818356378bb53bbfe5129a10fe.tar.gz
nextcloud-server-e7001022c75b3a818356378bb53bbfe5129a10fe.zip
feat(files): add opendetails param and file list up/down keyboard shortcut
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
Diffstat (limited to 'apps')
-rw-r--r--apps/files/src/actions/sidebarAction.spec.ts4
-rw-r--r--apps/files/src/actions/sidebarAction.ts9
-rw-r--r--apps/files/src/components/FilesListVirtual.vue172
-rw-r--r--apps/files/src/composables/useRouteParameters.ts8
-rw-r--r--apps/files/src/store/active.ts77
-rw-r--r--apps/files/src/store/dragging.ts3
-rw-r--r--apps/files/src/types.ts10
7 files changed, 234 insertions, 49 deletions
diff --git a/apps/files/src/actions/sidebarAction.spec.ts b/apps/files/src/actions/sidebarAction.spec.ts
index 1f1e81dbeaf..75ed8c97b47 100644
--- a/apps/files/src/actions/sidebarAction.spec.ts
+++ b/apps/files/src/actions/sidebarAction.spec.ts
@@ -130,7 +130,7 @@ describe('Open sidebar action exec tests', () => {
expect(goToRouteMock).toBeCalledWith(
null,
{ view: view.id, fileid: '1' },
- { dir: '/' },
+ { dir: '/', opendetails: 'true' },
true,
)
})
@@ -159,7 +159,7 @@ describe('Open sidebar action exec tests', () => {
expect(goToRouteMock).toBeCalledWith(
null,
{ view: view.id, fileid: '1' },
- { dir: '/' },
+ { dir: '/', opendetails: 'true' },
true,
)
})
diff --git a/apps/files/src/actions/sidebarAction.ts b/apps/files/src/actions/sidebarAction.ts
index a951de1db97..0b8ad91741e 100644
--- a/apps/files/src/actions/sidebarAction.ts
+++ b/apps/files/src/actions/sidebarAction.ts
@@ -44,6 +44,11 @@ export const action = new FileAction({
async exec(node: Node, view: View, dir: string) {
try {
+ // If the sidebar is already open for the current file, do nothing
+ if (window.OCA.Files.Sidebar.file === node.path) {
+ logger.debug('Sidebar already open for this file', { node })
+ return null
+ }
// Open sidebar and set active tab to sharing by default
window.OCA.Files.Sidebar.setActiveTab('sharing')
@@ -51,10 +56,10 @@ export const action = new FileAction({
await window.OCA.Files.Sidebar.open(node.path)
// Silently update current fileid
- window.OCP.Files.Router.goToRoute(
+ window.OCP?.Files?.Router?.goToRoute(
null,
{ view: view.id, fileid: String(node.fileid) },
- { ...window.OCP.Files.Router.query, dir },
+ { ...window.OCP.Files.Router.query, dir, opendetails: 'true' },
true,
)
diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue
index 81c4c5ac666..6df059f6143 100644
--- a/apps/files/src/components/FilesListVirtual.vue
+++ b/apps/files/src/components/FilesListVirtual.vue
@@ -12,7 +12,6 @@
isMtimeAvailable,
isSizeAvailable,
nodes,
- fileListWidth,
}"
:scroll-to-index="scrollToIndex"
:caption="caption">
@@ -58,32 +57,34 @@
</template>
<script lang="ts">
-import type { Node as NcNode } from '@nextcloud/files'
import type { ComponentPublicInstance, PropType } from 'vue'
+import type { Node as NcNode } from '@nextcloud/files'
import type { UserConfig } from '../types'
+import { defineComponent } from 'vue'
import { getFileListHeaders, Folder, Permission, View, getFileActions, FileType } from '@nextcloud/files'
import { showError } from '@nextcloud/dialogs'
-import { translate as t } from '@nextcloud/l10n'
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
-import { defineComponent } from 'vue'
+import { translate as t } from '@nextcloud/l10n'
+import { useHotKey } from '@nextcloud/vue/dist/Composables/useHotKey.js'
import { action as sidebarAction } from '../actions/sidebarAction.ts'
+import { getSummaryFor } from '../utils/fileUtils'
+import { useActiveStore } from '../store/active.ts'
import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
-import { getSummaryFor } from '../utils/fileUtils'
import { useSelectionStore } from '../store/selection.js'
import { useUserConfigStore } from '../store/userconfig.ts'
import FileEntry from './FileEntry.vue'
import FileEntryGrid from './FileEntryGrid.vue'
+import FileListFilters from './FileListFilters.vue'
import FilesListHeader from './FilesListHeader.vue'
import FilesListTableFooter from './FilesListTableFooter.vue'
import FilesListTableHeader from './FilesListTableHeader.vue'
-import VirtualList from './VirtualList.vue'
-import logger from '../logger.ts'
import FilesListTableHeaderActions from './FilesListTableHeaderActions.vue'
-import FileListFilters from './FileListFilters.vue'
+import logger from '../logger.ts'
+import VirtualList from './VirtualList.vue'
export default defineComponent({
name: 'FilesListVirtual',
@@ -113,18 +114,24 @@ export default defineComponent({
},
setup() {
- const userConfigStore = useUserConfigStore()
+ const activeStore = useActiveStore()
const selectionStore = useSelectionStore()
+ const userConfigStore = useUserConfigStore()
+
const fileListWidth = useFileListWidth()
- const { fileId, openFile } = useRouteParameters()
+ const { fileId, openDetails, openFile } = useRouteParameters()
return {
fileId,
fileListWidth,
+ openDetails,
openFile,
- userConfigStore,
+ activeStore,
selectionStore,
+ userConfigStore,
+
+ t,
}
},
@@ -215,12 +222,20 @@ export default defineComponent({
handler() {
// wait for scrolling and updating the actions to settle
this.$nextTick(() => {
- if (this.fileId) {
- if (this.openFile) {
- this.handleOpenFile(this.fileId)
- } else {
- this.unselectFile()
- }
+ if (this.fileId && this.openFile) {
+ this.handleOpenFile(this.fileId)
+ }
+ })
+ },
+ immediate: true,
+ },
+
+ openDetails: {
+ handler() {
+ // wait for scrolling and updating the actions to settle
+ this.$nextTick(() => {
+ if (this.fileId && this.openDetails) {
+ this.openSidebarForFile(this.fileId)
}
})
},
@@ -228,39 +243,39 @@ export default defineComponent({
},
},
+ created() {
+ useHotKey('Escape', this.unselectFile, {
+ stop: true,
+ prevent: true,
+ })
+
+ useHotKey(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'], this.onKeyDown, {
+ stop: true,
+ prevent: true,
+ })
+ },
+
mounted() {
// Add events on parent to cover both the table and DragAndDrop notice
const mainContent = window.document.querySelector('main.app-content') as HTMLElement
mainContent.addEventListener('dragover', this.onDragOver)
-
- subscribe('files:sidebar:closed', this.unselectFile)
-
- // If the file list is mounted with a fileId specified
- // then we need to open the sidebar initially
- if (this.fileId) {
- this.openSidebarForFile(this.fileId)
- }
+ subscribe('files:sidebar:closed', this.onSidebarClosed)
},
beforeDestroy() {
const mainContent = window.document.querySelector('main.app-content') as HTMLElement
mainContent.removeEventListener('dragover', this.onDragOver)
-
- unsubscribe('files:sidebar:closed', this.unselectFile)
+ unsubscribe('files:sidebar:closed', this.onSidebarClosed)
},
methods: {
- // Open the file sidebar if we have the room for it
- // but don't open the sidebar for the current folder
openSidebarForFile(fileId) {
- if (document.documentElement.clientWidth > 1024 && this.currentFolder.fileid !== fileId) {
- // Open the sidebar for the given URL fileid
- // iif we just loaded the app.
- const node = this.nodes.find(n => n.fileid === fileId) as NcNode
- if (node && sidebarAction?.enabled?.([node], this.currentView)) {
- logger.debug('Opening sidebar on file ' + node.path, { node })
- sidebarAction.exec(node, this.currentView, this.currentFolder.path)
- }
+ // Open the sidebar for the given URL fileid
+ // iif we just loaded the app.
+ const node = this.nodes.find(n => n.fileid === fileId) as NcNode
+ if (node && sidebarAction?.enabled?.([node], this.currentView)) {
+ logger.debug('Opening sidebar on file ' + node.path, { node })
+ sidebarAction.exec(node, this.currentView, this.currentFolder.path)
}
},
@@ -273,19 +288,39 @@ export default defineComponent({
const index = this.nodes.findIndex(node => node.fileid === fileId)
if (warn && index === -1 && fileId !== this.currentFolder.fileid) {
- showError(this.t('files', 'File not found'))
+ showError(t('files', 'File not found'))
}
+
this.scrollToIndex = Math.max(0, index)
}
},
+ /**
+ * Unselect the current file and clear open parameters from the URL
+ */
unselectFile() {
- // If the Sidebar is closed and if openFile is false, remove the file id from the URL
- if (!this.openFile && OCA.Files.Sidebar.file === '') {
+ const query = { ...this.$route.query }
+ delete query.openfile
+ delete query.opendetails
+
+ this.activeStore.clearActiveNode()
+ window.OCP.Files.Router.goToRoute(
+ null,
+ { ...this.$route.params, fileid: String(this.currentFolder.fileid ?? '') },
+ query,
+ true,
+ )
+ },
+
+ // When sidebar is closed, we remove the openDetails parameter from the URL
+ onSidebarClosed() {
+ if (this.openDetails) {
+ const query = { ...this.$route.query }
+ delete query.opendetails
window.OCP.Files.Router.goToRoute(
null,
- { ...this.$route.params, fileid: String(this.currentFolder.fileid ?? '') },
- this.$route.query,
+ this.$route.params,
+ query,
)
}
},
@@ -348,7 +383,58 @@ export default defineComponent({
}
},
- t,
+ onKeyDown(event: KeyboardEvent) {
+ // Up and down arrow keys
+ if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
+ const columnCount = this.$refs.table?.columnCount ?? 1
+ const index = this.nodes.findIndex(node => node.fileid === this.fileId) ?? 0
+ const nextIndex = event.key === 'ArrowUp' ? index - columnCount : index + columnCount
+ if (nextIndex < 0 || nextIndex >= this.nodes.length) {
+ return
+ }
+
+ const nextNode = this.nodes[nextIndex]
+
+ if (nextNode && nextNode?.fileid) {
+ this.setActiveNode(nextNode)
+ }
+ }
+
+ // if grid mode, left and right arrow keys
+ if (this.userConfig.grid_view && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) {
+ const index = this.nodes.findIndex(node => node.fileid === this.fileId) ?? 0
+ const nextIndex = event.key === 'ArrowLeft' ? index - 1 : index + 1
+ if (nextIndex < 0 || nextIndex >= this.nodes.length) {
+ return
+ }
+
+ const nextNode = this.nodes[nextIndex]
+
+ if (nextNode && nextNode?.fileid) {
+ this.setActiveNode(nextNode)
+ }
+ }
+ },
+
+ setActiveNode(node: NcNode & { fileid: number }) {
+ logger.debug('Navigating to file ' + node.path, { node, fileid: node.fileid })
+ this.scrollToFile(node.fileid)
+
+ // Remove openfile and opendetails from the URL
+ const query = { ...this.$route.query }
+ delete query.openfile
+ delete query.opendetails
+
+ this.activeStore.setActiveNode(node)
+
+ // Silent update of the URL
+ window.OCP.Files.Router.goToRoute(
+ null,
+ { ...this.$route.params, fileid: String(node.fileid) },
+ query,
+ true,
+ )
+ },
},
})
</script>
diff --git a/apps/files/src/composables/useRouteParameters.ts b/apps/files/src/composables/useRouteParameters.ts
index abf14614fb7..dbb8ca7f081 100644
--- a/apps/files/src/composables/useRouteParameters.ts
+++ b/apps/files/src/composables/useRouteParameters.ts
@@ -37,6 +37,11 @@ export function useRouteParameters() {
() => 'openfile' in route.query && (typeof route.query.openfile !== 'string' || route.query.openfile.toLocaleLowerCase() !== 'false'),
)
+ const openDetails = computed<boolean>(
+ // if `opendetails` is set it is considered truthy, but allow to explicitly set it to 'false'
+ () => 'opendetails' in route.query && (typeof route.query.opendetails !== 'string' || route.query.opendetails.toLocaleLowerCase() !== 'false'),
+ )
+
return {
/** Path of currently open directory */
directory,
@@ -46,5 +51,8 @@ export function useRouteParameters() {
/** Should the active node should be opened (`openFile` route param) */
openFile,
+
+ /** Should the details sidebar be shown (`openDetails` route param) */
+ openDetails,
}
}
diff --git a/apps/files/src/store/active.ts b/apps/files/src/store/active.ts
new file mode 100644
index 00000000000..2efb823b232
--- /dev/null
+++ b/apps/files/src/store/active.ts
@@ -0,0 +1,77 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import type { ActiveStore } from '../types.ts'
+import type { FileAction, Node, View } from '@nextcloud/files'
+
+import { defineStore } from 'pinia'
+import { getNavigation } from '@nextcloud/files'
+import { subscribe } from '@nextcloud/event-bus'
+
+import logger from '../logger.ts'
+import type { set } from 'lodash'
+
+export const useActiveStore = function(...args) {
+ const store = defineStore('active', {
+ state: () => ({
+ _initialized: false,
+ activeNode: null,
+ activeView: null,
+ activeAction: null,
+ } as ActiveStore),
+
+ actions: {
+ setActiveNode(node: Node) {
+ if (!node) {
+ throw new Error('Use clearActiveNode to clear the active node')
+ }
+ logger.debug('Setting active node', { node })
+ this.activeNode = node
+ },
+
+ clearActiveNode() {
+ this.activeNode = null
+ },
+
+ onDeletedNode(node: Node) {
+ if (this.activeNode && this.activeNode.source === node.source) {
+ this.clearActiveNode()
+ }
+ },
+
+ setActiveAction(action: FileAction) {
+ this.activeAction = action
+ },
+
+ clearActiveAction() {
+ this.activeAction = null
+ },
+
+ onChangedView(view: View|null = null) {
+ logger.debug('Setting active view', { view })
+ this.activeView = view
+ this.clearActiveNode()
+ },
+ },
+ })
+
+ const activeStore = store(...args)
+ const navigation = getNavigation()
+
+ // Make sure we only register the listeners once
+ if (!activeStore._initialized) {
+ subscribe('files:node:deleted', activeStore.onDeletedNode)
+
+ activeStore._initialized = true
+ activeStore.onChangedView(navigation.active)
+
+ // Or you can react to changes of the current active view
+ navigation.addEventListener('updateActive', (event) => {
+ activeStore.onChangedView(event.detail)
+ })
+ }
+
+ return activeStore
+}
diff --git a/apps/files/src/store/dragging.ts b/apps/files/src/store/dragging.ts
index 667c6fe67a7..f5c20095cca 100644
--- a/apps/files/src/store/dragging.ts
+++ b/apps/files/src/store/dragging.ts
@@ -2,9 +2,10 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
+import type { DragAndDropStore, FileSource } from '../types'
+
import { defineStore } from 'pinia'
import Vue from 'vue'
-import type { DragAndDropStore, FileSource } from '../types'
export const useDragAndDropStore = defineStore('dragging', {
state: () => ({
diff --git a/apps/files/src/types.ts b/apps/files/src/types.ts
index 39f0ed0865f..673cb06e182 100644
--- a/apps/files/src/types.ts
+++ b/apps/files/src/types.ts
@@ -2,7 +2,7 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import type { Folder, Node } from '@nextcloud/files'
+import type { FileAction, Folder, Node, View } from '@nextcloud/files'
import type { Upload } from '@nextcloud/upload'
// Global definitions
@@ -95,6 +95,14 @@ export interface DragAndDropStore {
dragging: FileSource[]
}
+// Active node store
+export interface ActiveStore {
+ _initialized: boolean
+ activeNode: Node|null
+ activeView: View|null
+ activeAction: FileAction|null
+}
+
export interface TemplateFile {
app: string
label: string