refactor(files): move hotkey handling to composable

This is a composable - not a service, because it is using the
`useHotKey` composable. At this moment it works, but in general
its only safe to put composables into `setup`-context.

This makes it future prove.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
pull/53437/head
Ferdinand Thiessen 2025-06-11 13:43:26 +07:00
parent db8dd9f7f6
commit 3a5769e8f9
4 changed files with 79 additions and 50 deletions

@ -12,11 +12,10 @@
<script lang="ts">
import { isPublicShare } from '@nextcloud/sharing/public'
import { defineComponent } from 'vue'
import NcContent from '@nextcloud/vue/components/NcContent'
import Navigation from './views/Navigation.vue'
import FilesList from './views/FilesList.vue'
import { useHotKeys } from './composables/useHotKeys'
export default defineComponent({
name: 'FilesApp',
@ -28,6 +27,9 @@ export default defineComponent({
},
setup() {
// Register global hotkeys
useHotKeys()
const isPublic = isPublicShare()
return {

@ -1,10 +1,14 @@
/**
/*
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Location } from 'vue-router'
import { File, Folder, Permission, View } from '@nextcloud/files'
import { describe, it, vi, expect, beforeEach, beforeAll, afterEach } from 'vitest'
import { nextTick } from 'vue'
import { enableAutoDestroy, mount } from '@vue/test-utils'
import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest'
import { defineComponent, nextTick } from 'vue'
import axios from '@nextcloud/axios'
import { getPinia } from '../store/index.ts'
@ -15,38 +19,64 @@ import { action as deleteAction } from '../actions/deleteAction.ts'
import { action as favoriteAction } from '../actions/favoriteAction.ts'
import { action as renameAction } from '../actions/renameAction.ts'
import { action as sidebarAction } from '../actions/sidebarAction.ts'
import { registerHotkeys } from './HotKeysService.ts'
import { useHotKeys } from './useHotKeys.ts'
import { useUserConfigStore } from '../store/userconfig.ts'
// this is the mocked current route
const route = vi.hoisted(() => ({
name: 'test',
params: {
fileId: 123,
},
query: {
openFile: 'false',
dir: '/parent/dir',
},
}))
// mocked router
const router = vi.hoisted(() => ({
push: vi.fn<(route: Location) => void>(),
}))
vi.mock('../actions/sidebarAction.ts', { spy: true })
vi.mock('../actions/deleteAction.ts', { spy: true })
vi.mock('../actions/favoriteAction.ts', { spy: true })
vi.mock('../actions/renameAction.ts', { spy: true })
vi.mock('vue-router/composables', () => ({
useRoute: vi.fn(() => route),
useRouter: vi.fn(() => router),
}))
let file: File
const view = {
id: 'files',
name: 'Files',
} as View
vi.mock('../actions/sidebarAction.ts', { spy: true })
vi.mock('../actions/deleteAction.ts', { spy: true })
vi.mock('../actions/favoriteAction.ts', { spy: true })
vi.mock('../actions/renameAction.ts', { spy: true })
const TestComponent = defineComponent({
name: 'test',
setup() {
useHotKeys()
},
template: '<div />',
})
describe('HotKeysService testing', () => {
const activeStore = useActiveStore(getPinia())
const goToRouteMock = vi.fn()
let initialState: HTMLInputElement
enableAutoDestroy(afterEach)
afterEach(() => {
document.body.removeChild(initialState)
})
beforeAll(() => {
registerHotkeys()
})
beforeEach(() => {
// Make sure the router is reset before each test
goToRouteMock.mockClear()
router.push.mockClear()
// Make sure the file is reset before each test
file = new File({
@ -66,9 +96,6 @@ describe('HotKeysService testing', () => {
activeStore.activeNode = file
window.OCA = { Files: { Sidebar: { open: () => {}, setActiveTab: () => {} } } }
// We only mock what needed, we do not need Files.Router.goTo or Files.Navigation
window.OCP = { Files: { Router: { goToRoute: goToRouteMock, params: {}, query: {} } } }
initialState = document.createElement('input')
initialState.setAttribute('type', 'hidden')
initialState.setAttribute('id', 'initial-state-files_trashbin-config')
@ -76,6 +103,8 @@ describe('HotKeysService testing', () => {
allow_delete: true,
})))
document.body.appendChild(initialState)
mount(TestComponent)
})
it('Pressing d should open the sidebar once', () => {
@ -135,13 +164,11 @@ describe('HotKeysService testing', () => {
})
it('Pressing alt+up should go to parent directory', () => {
expect(goToRouteMock).toHaveBeenCalledTimes(0)
window.OCP.Files.Router.query = { dir: '/foo/bar' }
expect(router.push).toHaveBeenCalledTimes(0)
dispatchEvent({ key: 'ArrowUp', code: 'ArrowUp', altKey: true })
expect(goToRouteMock).toHaveBeenCalledOnce()
expect(goToRouteMock.mock.calls[0][2].dir).toBe('/foo')
expect(router.push).toHaveBeenCalledOnce()
expect(router.push.mock.calls[0][0].query?.dir).toBe('/parent')
})
it('Pressing v should toggle grid view', async () => {

@ -4,13 +4,15 @@
*/
import { useHotKey } from '@nextcloud/vue/composables/useHotKey'
import { dirname } from 'path'
import { useRoute, useRouter } from 'vue-router/composables'
import { action as deleteAction } from '../actions/deleteAction.ts'
import { action as favoriteAction } from '../actions/favoriteAction.ts'
import { action as renameAction } from '../actions/renameAction.ts'
import { action as sidebarAction } from '../actions/sidebarAction.ts'
import { executeAction } from '../utils/actionUtils.ts'
import { useUserConfigStore } from '../store/userconfig.ts'
import { useRouteParameters } from './useRouteParameters.ts'
import { executeAction } from '../utils/actionUtils.ts'
import logger from '../logger.ts'
/**
@ -18,7 +20,12 @@ import logger from '../logger.ts'
* As much as possible, we try to have all the hotkeys in one place.
* Please make sure to add tests for the hotkeys after adding a new one.
*/
export const registerHotkeys = function() {
export function useHotKeys(): void {
const userConfigStore = useUserConfigStore()
const { directory } = useRouteParameters()
const router = useRouter()
const route = useRoute()
// d opens the sidebar
useHotKey('d', () => executeAction(sidebarAction), {
stop: true,
@ -57,26 +64,23 @@ export const registerHotkeys = function() {
})
logger.debug('Hotkeys registered')
}
const goToParentDir = function() {
const params = window.OCP.Files.Router?.params || {}
const query = window.OCP.Files.Router?.query || {}
const currentDir = (query?.dir || '/') as string
const parentDir = dirname(currentDir)
/**
* Use the router to go to the parent directory
*/
function goToParentDir() {
const dir = dirname(directory.value)
logger.debug('Navigating to parent directory', { parentDir })
window.OCP.Files.Router.goToRoute(
null,
{ ...params },
{ ...query, dir: parentDir },
)
}
logger.debug('Navigating to parent directory', { dir })
router.push({ params: { ...route.params }, query: { ...route.query, dir } })
}
const toggleGridView = function() {
const userConfigStore = useUserConfigStore()
const value = userConfigStore?.userConfig?.grid_view
logger.debug('Toggling grid view', { old: value, new: !value })
userConfigStore.update('grid_view', !value)
/**
* Toggle the grid view
*/
function toggleGridView() {
const value = userConfigStore.userConfig.grid_view
logger.debug('Toggling grid view', { old: value, new: !value })
userConfigStore.update('grid_view', !value)
}
}

@ -8,7 +8,6 @@ import { PiniaVuePlugin } from 'pinia'
import Vue from 'vue'
import { getPinia } from './store/index.ts'
import { registerHotkeys } from './services/HotKeysService.ts'
import FilesApp from './FilesApp.vue'
import router from './router/router'
import RouterService from './services/RouterService'
@ -40,9 +39,6 @@ if (!window.OCP.Files.Router) {
// Init Pinia store
Vue.use(PiniaVuePlugin)
// Init HotKeys AFTER pinia is set up
registerHotkeys()
// Init Files App Settings Service
const Settings = new SettingsService()
Object.assign(window.OCA.Files, { Settings })