aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorskjnldsv <skjnldsv@protonmail.com>2024-12-13 11:51:36 +0100
committerskjnldsv <skjnldsv@protonmail.com>2024-12-13 11:51:36 +0100
commit8e02db6a162a18c51b20fc76ac8a40f7411eb7ff (patch)
treeff1e79f92bd88717af7afc213fc1160daa1ab4fa
parent0862632b2c8f11cd5d3742a7061e2797ec59f230 (diff)
downloadnextcloud-server-feat/files-shortcuts-2.tar.gz
nextcloud-server-feat/files-shortcuts-2.zip
chore(files): add hotkey service testsfeat/files-shortcuts-2
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
-rw-r--r--apps/files/src/actions/sidebarAction.ts2
-rw-r--r--apps/files/src/components/FilesListTableHeader.vue3
-rw-r--r--apps/files/src/main.ts6
-rw-r--r--apps/files/src/services/HotKeysService.spec.ts161
-rw-r--r--apps/files/src/services/HotKeysService.ts45
-rw-r--r--apps/files/src/services/Recent.ts4
-rw-r--r--apps/files/src/store/active.ts13
-rw-r--r--apps/files/src/store/index.ts9
-rw-r--r--apps/files/src/store/userconfig.ts4
-rw-r--r--apps/files/src/utils/actionUtils.ts6
-rw-r--r--apps/files/src/views/FilesList.vue25
-rw-r--r--apps/files/src/views/Settings.vue10
-rw-r--r--apps/systemtags/src/components/SystemTagPicker.vue2
-rw-r--r--apps/systemtags/src/files_actions/inlineSystemTagsAction.ts2
-rw-r--r--apps/systemtags/src/init.ts8
-rw-r--r--apps/systemtags/src/logger.ts2
-rw-r--r--apps/systemtags/src/services/HotKeysService.ts24
-rw-r--r--apps/systemtags/src/services/api.ts2
-rw-r--r--apps/systemtags/src/services/files.ts5
-rw-r--r--apps/systemtags/src/services/logger.ts10
20 files changed, 278 insertions, 65 deletions
diff --git a/apps/files/src/actions/sidebarAction.ts b/apps/files/src/actions/sidebarAction.ts
index 37305578383..0b8ad91741e 100644
--- a/apps/files/src/actions/sidebarAction.ts
+++ b/apps/files/src/actions/sidebarAction.ts
@@ -56,7 +56,7 @@ 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, opendetails: 'true' },
diff --git a/apps/files/src/components/FilesListTableHeader.vue b/apps/files/src/components/FilesListTableHeader.vue
index 783b0813b49..b7818a9a40f 100644
--- a/apps/files/src/components/FilesListTableHeader.vue
+++ b/apps/files/src/components/FilesListTableHeader.vue
@@ -200,6 +200,9 @@ export default defineComponent({
},
resetSelection() {
+ if (this.isNoneSelected) {
+ return
+ }
this.selectionStore.reset()
},
diff --git a/apps/files/src/main.ts b/apps/files/src/main.ts
index 26fa7cdca95..b4755df8eed 100644
--- a/apps/files/src/main.ts
+++ b/apps/files/src/main.ts
@@ -2,12 +2,13 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
+import type { Pinia } from 'pinia'
import { getCSPNonce } from '@nextcloud/auth'
import { getNavigation } from '@nextcloud/files'
import { PiniaVuePlugin } from 'pinia'
import Vue from 'vue'
-import { pinia } from './store/index.ts'
+import { getPinia } from './store/index.ts'
import { registerHotkeys } from './services/HotKeysService.ts'
import FilesApp from './FilesApp.vue'
import router from './router/router'
@@ -23,6 +24,7 @@ declare global {
OCP: Nextcloud.v29.OCP
// eslint-disable-next-line @typescript-eslint/no-explicit-any
OCA: Record<string, any>
+ _nc_files_pinia: Pinia
}
}
@@ -55,5 +57,5 @@ Object.assign(window.OCA.Files.Settings, { Setting: SettingsModel })
const FilesAppVue = Vue.extend(FilesApp)
new FilesAppVue({
router: (window.OCP.Files.Router as RouterService)._router,
- pinia,
+ pinia: getPinia(),
}).$mount('#content')
diff --git a/apps/files/src/services/HotKeysService.spec.ts b/apps/files/src/services/HotKeysService.spec.ts
new file mode 100644
index 00000000000..dfe9f66601b
--- /dev/null
+++ b/apps/files/src/services/HotKeysService.spec.ts
@@ -0,0 +1,161 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { describe, it, vi, expect, beforeEach, beforeAll } from 'vitest'
+import { File, Permission, View } from '@nextcloud/files'
+import axios from '@nextcloud/axios'
+
+import { getPinia } from '../store/index.ts'
+import { useActiveStore } from '../store/active.ts'
+
+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 { useUserConfigStore } from '../store/userconfig.ts'
+import { subscribe } from '@nextcloud/event-bus'
+
+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 })
+
+describe('HotKeysService testing', () => {
+ const activeStore = useActiveStore(getPinia())
+
+ const goToRouteMock = vi.fn()
+
+ beforeAll(() => {
+ registerHotkeys()
+ })
+
+ beforeEach(() => {
+ // Make sure the router is reset before each test
+ goToRouteMock.mockClear()
+
+ // Make sure the file is reset before each test
+ file = new File({
+ id: 1,
+ source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
+ owner: 'admin',
+ mime: 'text/plain',
+ permissions: Permission.ALL,
+ })
+
+ // Setting the view first as it reset the active node
+ activeStore.onChangedView(view)
+ activeStore.setActiveNode(file)
+
+ window.OCA = { Files: { Sidebar: { open: () => {}, setActiveTab: () => {} } } }
+ // @ts-expect-error We only mock what needed, we do not need Files.Router.goTo or Files.Navigation
+ window.OCP = { Files: { Router: { goToRoute: goToRouteMock, params: {}, query: {} } } }
+ })
+
+ it('Pressing d should open the sidebar once', () => {
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'd', code: 'KeyD' }))
+
+ // Modifier keys should not trigger the action
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'd', code: 'KeyD', ctrlKey: true }))
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'd', code: 'KeyD', altKey: true }))
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'd', code: 'KeyD', shiftKey: true }))
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'd', code: 'KeyD', metaKey: true }))
+
+ expect(sidebarAction.enabled).toHaveReturnedWith(true)
+ expect(sidebarAction.exec).toHaveBeenCalledOnce()
+ })
+
+ it('Pressing F2 should rename the file', () => {
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'F2', code: 'F2' }))
+
+ // Modifier keys should not trigger the action
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'F2', code: 'F2', ctrlKey: true }))
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'F2', code: 'F2', altKey: true }))
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'F2', code: 'F2', shiftKey: true }))
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'F2', code: 'F2', metaKey: true }))
+
+ expect(renameAction.enabled).toHaveReturnedWith(true)
+ expect(renameAction.exec).toHaveBeenCalledOnce()
+ })
+
+ it('Pressing s should toggle favorite', () => {
+ vi.spyOn(axios, 'post').mockImplementationOnce(() => Promise.resolve())
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 's', code: 'KeyS' }))
+
+ // Modifier keys should not trigger the action
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 's', code: 'KeyS', ctrlKey: true }))
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 's', code: 'KeyS', altKey: true }))
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 's', code: 'KeyS', shiftKey: true }))
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 's', code: 'KeyS', metaKey: true }))
+
+ expect(favoriteAction.enabled).toHaveReturnedWith(true)
+ expect(favoriteAction.exec).toHaveBeenCalledOnce()
+ })
+
+ it('Pressing Delete should delete the file', async () => {
+ vi.spyOn(deleteAction._action, 'exec').mockResolvedValue(() => true)
+
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', code: 'Delete' }))
+
+ // Modifier keys should not trigger the action
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', code: 'Delete', ctrlKey: true }))
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', code: 'Delete', altKey: true }))
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', code: 'Delete', shiftKey: true }))
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', code: 'Delete', metaKey: true }))
+
+ expect(deleteAction.enabled).toHaveReturnedWith(true)
+ expect(deleteAction.exec).toHaveBeenCalledOnce()
+ })
+
+ it('Pressing alt+up should go to parent directory', () => {
+ expect(goToRouteMock).toHaveBeenCalledTimes(0)
+ window.OCP.Files.Router.query = { dir: '/foo/bar' }
+
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', code: 'ArrowUp', altKey: true }))
+
+ expect(goToRouteMock).toHaveBeenCalledOnce()
+ expect(goToRouteMock.mock.calls[0][2].dir).toBe('/foo')
+ })
+
+ it('Pressing v should toggle grid view', async () => {
+ vi.spyOn(axios, 'put').mockImplementationOnce(() => Promise.resolve())
+
+ const userConfigStore = useUserConfigStore(getPinia())
+ const currentGridConfig = userConfigStore.userConfig.grid_view
+
+ // Wait for the user config to be updated
+ // or timeout after 500ms
+ const waitForUserConfig = () => new Promise((resolve) => {
+ subscribe('files:config:updated', resolve)
+ setTimeout(resolve, 500)
+ })
+
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'v', code: 'KeyV' }))
+ await waitForUserConfig()
+ expect(userConfigStore.userConfig.grid_view).toBe(!currentGridConfig)
+
+ // Modifier keys should not trigger the action
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', code: 'Delete', ctrlKey: true }))
+ await waitForUserConfig()
+ expect(userConfigStore.userConfig.grid_view).toBe(!currentGridConfig)
+
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', code: 'Delete', altKey: true }))
+ await waitForUserConfig()
+ expect(userConfigStore.userConfig.grid_view).toBe(!currentGridConfig)
+
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', code: 'Delete', shiftKey: true }))
+ await waitForUserConfig()
+ expect(userConfigStore.userConfig.grid_view).toBe(!currentGridConfig)
+
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', code: 'Delete', metaKey: true }))
+ await waitForUserConfig()
+ expect(userConfigStore.userConfig.grid_view).toBe(!currentGridConfig)
+ })
+})
diff --git a/apps/files/src/services/HotKeysService.ts b/apps/files/src/services/HotKeysService.ts
index 381fc582441..8318554ea99 100644
--- a/apps/files/src/services/HotKeysService.ts
+++ b/apps/files/src/services/HotKeysService.ts
@@ -2,16 +2,22 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-
+import { dirname } from 'path'
import { useHotKey } from '@nextcloud/vue/dist/Composables/useHotKey.js'
import { action as deleteAction } from '../actions/deleteAction.ts'
-import { action as renameAction } from '../actions/renameAction.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 logger from '../logger.ts'
+/**
+ * This register the hotkeys for the Files app.
+ * 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() {
// d opens the sidebar
useHotKey('d', () => executeAction(sidebarAction), {
@@ -37,5 +43,40 @@ export const registerHotkeys = function() {
prevent: true,
})
+ // alt+up go to parent directory
+ useHotKey('ArrowUp', goToParentDir, {
+ stop: true,
+ prevent: true,
+ alt: true,
+ })
+
+ // v toggle grid view
+ useHotKey('v', toggleGridView, {
+ stop: true,
+ prevent: true,
+ })
+
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)
+
+ logger.debug('Navigating to parent directory', { parentDir })
+ window.OCP.Files.Router.goToRoute(
+ null,
+ { ...params },
+ { ...query, dir: parentDir },
+ )
+}
+
+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)
+}
diff --git a/apps/files/src/services/Recent.ts b/apps/files/src/services/Recent.ts
index 0a953087781..d0ca285b05c 100644
--- a/apps/files/src/services/Recent.ts
+++ b/apps/files/src/services/Recent.ts
@@ -9,7 +9,7 @@ import { getCurrentUser } from '@nextcloud/auth'
import { Folder, Permission, davGetRecentSearch, davRootPath, davRemoteURL, davResultToNode } from '@nextcloud/files'
import { CancelablePromise } from 'cancelable-promise'
import { useUserConfigStore } from '../store/userconfig.ts'
-import { pinia } from '../store/index.ts'
+import { getPinia } from '../store/index.ts'
import { client } from './WebdavClient.ts'
import { getBaseUrl } from '@nextcloud/router'
@@ -32,7 +32,7 @@ const resultToNode = (stat: FileStat) => davResultToNode(stat, davRootPath, getB
* @param path Path to search for recent changes
*/
export const getContents = (path = '/'): CancelablePromise<ContentsWithRoot> => {
- const store = useUserConfigStore(pinia)
+ const store = useUserConfigStore(getPinia())
/**
* Filter function that returns only the visible nodes - or hidden if explicitly configured
diff --git a/apps/files/src/store/active.ts b/apps/files/src/store/active.ts
index 84715e090f3..2efb823b232 100644
--- a/apps/files/src/store/active.ts
+++ b/apps/files/src/store/active.ts
@@ -11,6 +11,7 @@ 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', {
@@ -34,6 +35,12 @@ export const useActiveStore = function(...args) {
this.activeNode = null
},
+ onDeletedNode(node: Node) {
+ if (this.activeNode && this.activeNode.source === node.source) {
+ this.clearActiveNode()
+ }
+ },
+
setActiveAction(action: FileAction) {
this.activeAction = action
},
@@ -42,12 +49,6 @@ export const useActiveStore = function(...args) {
this.activeAction = null
},
- onDeletedNode(node: Node) {
- if (this.activeNode && this.activeNode.source === node.source) {
- this.clearActiveNode()
- }
- },
-
onChangedView(view: View|null = null) {
logger.debug('Setting active view', { view })
this.activeView = view
diff --git a/apps/files/src/store/index.ts b/apps/files/src/store/index.ts
index 00676b3bc8e..3ba667ffd2f 100644
--- a/apps/files/src/store/index.ts
+++ b/apps/files/src/store/index.ts
@@ -5,4 +5,11 @@
import { createPinia } from 'pinia'
-export const pinia = createPinia()
+export const getPinia = () => {
+ if (window._nc_files_pinia) {
+ return window._nc_files_pinia
+ }
+
+ window._nc_files_pinia = createPinia()
+ return window._nc_files_pinia
+}
diff --git a/apps/files/src/store/userconfig.ts b/apps/files/src/store/userconfig.ts
index ffe07a91bab..6fd2ad886bc 100644
--- a/apps/files/src/store/userconfig.ts
+++ b/apps/files/src/store/userconfig.ts
@@ -27,8 +27,6 @@ export const useUserConfigStore = function(...args) {
actions: {
/**
* Update the user config local store
- * @param key
- * @param value
*/
onUpdate(key: string, value: boolean) {
Vue.set(this.userConfig, key, value)
@@ -36,8 +34,6 @@ export const useUserConfigStore = function(...args) {
/**
* Update the user config local store AND on server side
- * @param key
- * @param value
*/
async update(key: string, value: boolean) {
await axios.put(generateUrl('/apps/files/api/v1/config/' + key), {
diff --git a/apps/files/src/utils/actionUtils.ts b/apps/files/src/utils/actionUtils.ts
index 87b8b6f44d3..730a1149229 100644
--- a/apps/files/src/utils/actionUtils.ts
+++ b/apps/files/src/utils/actionUtils.ts
@@ -9,7 +9,7 @@ import { showError, showSuccess } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'
import Vue from 'vue'
-import { pinia } from '../store'
+import { getPinia } from '../store'
import { useActiveStore } from '../store/active'
import logger from '../logger'
@@ -19,8 +19,8 @@ import logger from '../logger'
* @param action The action to execute
*/
export const executeAction = async (action: FileAction) => {
- const activeStore = useActiveStore(pinia)
- const currentDir = (window.OCP.Files.Router?.query?.dir || '/') as string
+ const activeStore = useActiveStore(getPinia())
+ const currentDir = (window?.OCP?.Files?.Router?.query?.dir || '/') as string
const currentNode = activeStore.activeNode
const currentView = activeStore.activeView
diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue
index 6ce385776f2..0f301abe024 100644
--- a/apps/files/src/views/FilesList.vue
+++ b/apps/files/src/views/FilesList.vue
@@ -514,21 +514,6 @@ export default defineComponent({
},
},
- created() {
- // v toggle grid view
- useHotKey('v', this.toggleGridView, {
- stop: true,
- prevent: true,
- })
-
- // alt+up go to parent directory
- useHotKey('ArrowUp', this.goToPreviousDir, {
- stop: true,
- prevent: true,
- alt: true,
- })
- },
-
mounted() {
this.filtersStore.init()
this.fetchContent()
@@ -726,16 +711,6 @@ export default defineComponent({
}
this.dirContentsFiltered = nodes
},
-
- goToPreviousDir() {
- const prevDir = dirname(this.directory)
- logger.debug('Navigating to parent directory', { prevDir })
- window.OCP.Files.Router.goToRoute(
- null,
- { ...this.$route.params },
- { ...this.$route.query, dir: prevDir },
- )
- },
},
})
</script>
diff --git a/apps/files/src/views/Settings.vue b/apps/files/src/views/Settings.vue
index ba1086e700f..e4d786a1e84 100644
--- a/apps/files/src/views/Settings.vue
+++ b/apps/files/src/views/Settings.vue
@@ -90,7 +90,7 @@
<h3>{{ t('files', 'Actions') }}</h3>
<dl>
<div>
- <dt><kbd>A</kbd></dt>
+ <dt><kbd>a</kbd></dt>
<dd class="shortcut-description">
{{ t('files', 'Open the actions menu for a file') }}
</dd>
@@ -108,11 +108,17 @@
</dd>
</div>
<div>
- <dt><kbd>S</kbd></dt>
+ <dt><kbd>s</kbd></dt>
<dd class="shortcut-description">
{{ t('files', 'Favorite or remove a file from favorites') }}
</dd>
</div>
+ <div>
+ <dt><kbd>t</kbd></dt>
+ <dd class="shortcut-description">
+ {{ t('files', 'Manage tags for a file') }}
+ </dd>
+ </div>
</dl>
<h3>{{ t('files', 'Selection') }}</h3>
diff --git a/apps/systemtags/src/components/SystemTagPicker.vue b/apps/systemtags/src/components/SystemTagPicker.vue
index 652ea262409..e17fb1a7bc5 100644
--- a/apps/systemtags/src/components/SystemTagPicker.vue
+++ b/apps/systemtags/src/components/SystemTagPicker.vue
@@ -151,7 +151,7 @@ import TagIcon from 'vue-material-design-icons/Tag.vue'
import { createTag, fetchTag, fetchTags, getTagObjects, setTagObjects, updateTag } from '../services/api'
import { getNodeSystemTags, setNodeSystemTags } from '../utils'
import { elementColor, invertTextColor, isDarkModeEnabled } from '../utils/colorUtils'
-import logger from '../services/logger'
+import logger from '../logger.ts'
const debounceUpdateTag = debounce(updateTag, 500)
const mainBackgroundColor = getComputedStyle(document.body)
diff --git a/apps/systemtags/src/files_actions/inlineSystemTagsAction.ts b/apps/systemtags/src/files_actions/inlineSystemTagsAction.ts
index cc6cb55c632..cab2f61906b 100644
--- a/apps/systemtags/src/files_actions/inlineSystemTagsAction.ts
+++ b/apps/systemtags/src/files_actions/inlineSystemTagsAction.ts
@@ -12,7 +12,7 @@ import '../css/fileEntryInlineSystemTags.scss'
import { elementColor, isDarkModeEnabled } from '../utils/colorUtils'
import { fetchTags } from '../services/api'
import { getNodeSystemTags } from '../utils'
-import logger from '../services/logger'
+import logger from '../logger.ts'
// Init tag cache
const cache: TagWithId[] = []
diff --git a/apps/systemtags/src/init.ts b/apps/systemtags/src/init.ts
index 54ba0d604ee..fdf645e6044 100644
--- a/apps/systemtags/src/init.ts
+++ b/apps/systemtags/src/init.ts
@@ -3,10 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { registerDavProperty, registerFileAction } from '@nextcloud/files'
+import { registerHotkeys } from './services/HotKeysService'
+import { registerSystemTagsView } from './files_views/systemtagsView'
+
import { action as bulkSystemTagsAction } from './files_actions/bulkSystemTagsAction'
import { action as inlineSystemTagsAction } from './files_actions/inlineSystemTagsAction'
import { action as openInFilesAction } from './files_actions/openInFilesAction'
-import { registerSystemTagsView } from './files_views/systemtagsView'
registerDavProperty('nc:system-tags')
registerFileAction(bulkSystemTagsAction)
@@ -14,3 +16,7 @@ registerFileAction(inlineSystemTagsAction)
registerFileAction(openInFilesAction)
registerSystemTagsView()
+
+document.addEventListener('DOMContentLoaded', () => {
+ registerHotkeys()
+})
diff --git a/apps/systemtags/src/logger.ts b/apps/systemtags/src/logger.ts
index fa02888bdb8..5a37e3874ed 100644
--- a/apps/systemtags/src/logger.ts
+++ b/apps/systemtags/src/logger.ts
@@ -5,7 +5,7 @@
import { getLoggerBuilder } from '@nextcloud/logger'
-export const logger = getLoggerBuilder()
+export default getLoggerBuilder()
.setApp('systemtags')
.detectUser()
.build()
diff --git a/apps/systemtags/src/services/HotKeysService.ts b/apps/systemtags/src/services/HotKeysService.ts
new file mode 100644
index 00000000000..3661531fcb5
--- /dev/null
+++ b/apps/systemtags/src/services/HotKeysService.ts
@@ -0,0 +1,24 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { useHotKey } from '@nextcloud/vue/dist/Composables/useHotKey.js'
+
+import { action as manageTagAction } from '../files_actions/bulkSystemTagsAction.ts'
+import { executeAction } from '../../../files/src/utils/actionUtils.ts'
+import logger from '../logger.ts'
+
+/**
+ * This register the hotkeys for the Files app.
+ * 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() {
+ // t opens the tag management dialog
+ useHotKey('t', () => executeAction(manageTagAction), {
+ stop: true,
+ prevent: true,
+ })
+
+ logger.debug('Hotkeys registered')
+}
diff --git a/apps/systemtags/src/services/api.ts b/apps/systemtags/src/services/api.ts
index c0b39fde6bd..ef44fa82dae 100644
--- a/apps/systemtags/src/services/api.ts
+++ b/apps/systemtags/src/services/api.ts
@@ -12,7 +12,7 @@ import { t } from '@nextcloud/l10n'
import { davClient } from './davClient.js'
import { formatTag, parseIdFromLocation, parseTags } from '../utils'
-import { logger } from '../logger.js'
+import logger from '../logger.ts'
import { emit } from '@nextcloud/event-bus'
export const fetchTagsPayload = `<?xml version="1.0"?>
diff --git a/apps/systemtags/src/services/files.ts b/apps/systemtags/src/services/files.ts
index 8759a99d560..d61b7ff4825 100644
--- a/apps/systemtags/src/services/files.ts
+++ b/apps/systemtags/src/services/files.ts
@@ -7,10 +7,11 @@ import type { FileStat, ResponseDataDetailed } from 'webdav'
import type { ServerTagWithId, Tag, TagWithId } from '../types.js'
import { t } from '@nextcloud/l10n'
-import { davClient } from './davClient.js'
+
import { createTag, fetchTagsPayload } from './api.js'
+import { davClient } from './davClient.js'
import { formatTag, parseTags } from '../utils.js'
-import { logger } from '../logger.js'
+import logger from '../logger.ts'
export const fetchTagsForFile = async (fileId: number): Promise<TagWithId[]> => {
const path = '/systemtags-relations/files/' + fileId
diff --git a/apps/systemtags/src/services/logger.ts b/apps/systemtags/src/services/logger.ts
deleted file mode 100644
index 8cce9f5f92e..00000000000
--- a/apps/systemtags/src/services/logger.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-import { getLoggerBuilder } from '@nextcloud/logger'
-
-export default getLoggerBuilder()
- .setApp('systemtags')
- .detectUser()
- .build()