aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorAndy Scherzinger <info@andy-scherzinger.de>2024-07-11 14:25:19 +0200
committerGitHub <noreply@github.com>2024-07-11 14:25:19 +0200
commit52718fe2f59b220d3a46c422a6f4d65e06bec998 (patch)
treeb2827cd0d5eb54decc234d13e50d485c746c1600 /apps
parent7096ef29f154bb537e0df7f13953daebaffaf1cb (diff)
parent5565ac6186dc91bbded709c2ceb6fa87b9e4fc2b (diff)
downloadnextcloud-server-52718fe2f59b220d3a46c422a6f4d65e06bec998.tar.gz
nextcloud-server-52718fe2f59b220d3a46c422a6f4d65e06bec998.zip
Merge pull request #46422 from nextcloud/backport/46374/stable28
[stable28] fix: Update Nextcloud libraries
Diffstat (limited to 'apps')
-rw-r--r--apps/files/src/actions/moveOrCopyAction.ts3
-rw-r--r--apps/files/src/actions/openFolderAction.ts2
-rw-r--r--apps/files/src/components/BreadCrumbs.vue22
-rw-r--r--apps/files/src/components/DragAndDropNotice.vue22
-rw-r--r--apps/files/src/components/DragAndDropPreview.vue2
-rw-r--r--apps/files/src/components/FileEntry.vue16
-rw-r--r--apps/files/src/components/FileEntry/FileEntryActions.vue21
-rw-r--r--apps/files/src/components/FileEntry/FileEntryName.vue6
-rw-r--r--apps/files/src/components/FileEntryGrid.vue5
-rw-r--r--apps/files/src/components/FileEntryMixin.ts11
-rw-r--r--apps/files/src/components/FilesListTableHeader.vue20
-rw-r--r--apps/files/src/components/NewNodeDialog.vue2
-rw-r--r--apps/files/src/components/VirtualList.vue2
-rw-r--r--apps/files/src/composables/useNavigation.spec.ts98
-rw-r--r--apps/files/src/composables/useNavigation.ts46
-rw-r--r--apps/files/src/eventbus.d.ts1
-rw-r--r--apps/files/src/utils/davUtils.js9
-rw-r--r--apps/files/src/utils/fileUtils.ts35
-rw-r--r--apps/files/src/views/FilesList.vue97
-rw-r--r--apps/files/src/views/Navigation.cy.ts102
-rw-r--r--apps/files/src/views/Navigation.vue68
-rw-r--r--apps/settings/src/components/AdminSettingsSharingForm.vue3
-rw-r--r--apps/settings/src/components/Users/VirtualList.vue2
-rw-r--r--apps/theming/src/components/admin/ColorPickerField.vue3
24 files changed, 380 insertions, 218 deletions
diff --git a/apps/files/src/actions/moveOrCopyAction.ts b/apps/files/src/actions/moveOrCopyAction.ts
index d969d87efbe..8a8103be4e5 100644
--- a/apps/files/src/actions/moveOrCopyAction.ts
+++ b/apps/files/src/actions/moveOrCopyAction.ts
@@ -30,7 +30,7 @@ import { AxiosError } from 'axios'
import { basename, join } from 'path'
import { emit } from '@nextcloud/event-bus'
import { FilePickerClosed, getFilePickerBuilder, showError } from '@nextcloud/dialogs'
-import { Permission, FileAction, FileType, NodeStatus, davGetClient, davRootPath, davResultToNode, davGetDefaultPropfind } from '@nextcloud/files'
+import { FileAction, FileType, NodeStatus, davGetClient, davRootPath, davResultToNode, davGetDefaultPropfind, getUniqueName } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import { openConflictPicker, hasConflict } from '@nextcloud/upload'
import Vue from 'vue'
@@ -41,7 +41,6 @@ import FolderMoveSvg from '@mdi/svg/svg/folder-move.svg?raw'
import { MoveCopyAction, canCopy, canMove, getQueue } from './moveOrCopyActionUtils'
import { getContents } from '../services/Files'
import logger from '../logger'
-import { getUniqueName } from '../utils/fileUtils'
/**
* Return the action that is possible for the given nodes
diff --git a/apps/files/src/actions/openFolderAction.ts b/apps/files/src/actions/openFolderAction.ts
index 791b328b3ed..f575bdde7e8 100644
--- a/apps/files/src/actions/openFolderAction.ts
+++ b/apps/files/src/actions/openFolderAction.ts
@@ -27,7 +27,7 @@ export const action = new FileAction({
id: 'open-folder',
displayName(files: Node[]) {
// Only works on single node
- const displayName = files[0].attributes.displayName || files[0].basename
+ const displayName = files[0].attributes.displayname || files[0].basename
return t('files', 'Open folder {displayName}', { displayName })
},
iconSvgInline: () => FolderSvg,
diff --git a/apps/files/src/components/BreadCrumbs.vue b/apps/files/src/components/BreadCrumbs.vue
index 9f77b7502ba..df7931a4a5b 100644
--- a/apps/files/src/components/BreadCrumbs.vue
+++ b/apps/files/src/components/BreadCrumbs.vue
@@ -52,6 +52,7 @@
<script lang="ts">
import type { Node } from '@nextcloud/files'
+import type { FileSource } from '../types.ts'
import { basename } from 'path'
import { defineComponent } from 'vue'
@@ -62,6 +63,7 @@ import NcBreadcrumb from '@nextcloud/vue/dist/Components/NcBreadcrumb.js'
import NcBreadcrumbs from '@nextcloud/vue/dist/Components/NcBreadcrumbs.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
+import { useNavigation } from '../composables/useNavigation'
import { onDropInternalFiles, dataTransferToFileTree, onDropExternalFiles } from '../services/DropService'
import { showError } from '@nextcloud/dialogs'
import { useDragAndDropStore } from '../store/dragging.ts'
@@ -71,7 +73,6 @@ import { useSelectionStore } from '../store/selection.ts'
import { useUploaderStore } from '../store/uploader.ts'
import filesListWidthMixin from '../mixins/filesListWidth.ts'
import logger from '../logger'
-import type { FileSource } from '../types.ts'
export default defineComponent({
name: 'BreadCrumbs',
@@ -99,6 +100,7 @@ export default defineComponent({
const pathsStore = usePathsStore()
const selectionStore = useSelectionStore()
const uploaderStore = useUploaderStore()
+ const { currentView } = useNavigation()
return {
draggingStore,
@@ -106,14 +108,12 @@ export default defineComponent({
pathsStore,
selectionStore,
uploaderStore,
+
+ currentView,
}
},
computed: {
- currentView() {
- return this.$navigation.active
- },
-
dirs(): string[] {
const cumulativePath = (acc: string) => (value: string) => (acc += `${value}/`)
// Generate a cumulative path for each path segment: ['/', '/foo', '/foo/bar', ...] etc
@@ -167,17 +167,17 @@ export default defineComponent({
getNodeFromSource(source: FileSource): Node | undefined {
return this.filesStore.getNode(source)
},
- getFileSourceFromPath(path: string): FileSource | undefined {
- return this.pathsStore.getPath(this.currentView!.id, path)
+ getFileSourceFromPath(path: string): FileSource | null {
+ return (this.currentView && this.pathsStore.getPath(this.currentView.id, path)) ?? null
},
getDirDisplayName(path: string): string {
if (path === '/') {
return this.$navigation?.active?.name || t('files', 'Home')
}
- const source: FileSource | undefined = this.getFileSourceFromPath(path)
+ const source: FileSource | null = this.getFileSourceFromPath(path)
const node: Node | undefined = source ? this.getNodeFromSource(source) : undefined
- return node?.attributes?.displayName || basename(path)
+ return node?.attributes?.displayname || basename(path)
},
onClick(to) {
@@ -187,6 +187,10 @@ export default defineComponent({
},
onDragOver(event: DragEvent, path: string) {
+ if (!event.dataTransfer) {
+ return
+ }
+
// Cannot drop on the current directory
if (path === this.dirs[this.dirs.length - 1]) {
event.dataTransfer.dropEffect = 'none'
diff --git a/apps/files/src/components/DragAndDropNotice.vue b/apps/files/src/components/DragAndDropNotice.vue
index c036c86fb64..40f95f54aeb 100644
--- a/apps/files/src/components/DragAndDropNotice.vue
+++ b/apps/files/src/components/DragAndDropNotice.vue
@@ -44,16 +44,18 @@
</template>
<script lang="ts">
-import { defineComponent } from 'vue'
-import { Folder, Permission } from '@nextcloud/files'
+import type { Folder } from '@nextcloud/files'
+import { Permission } from '@nextcloud/files'
import { showError } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import { UploadStatus } from '@nextcloud/upload'
+import { defineComponent, type PropType } from 'vue'
import TrayArrowDownIcon from 'vue-material-design-icons/TrayArrowDown.vue'
-import logger from '../logger.js'
+import { useNavigation } from '../composables/useNavigation'
import { dataTransferToFileTree, onDropExternalFiles } from '../services/DropService'
+import logger from '../logger.js'
export default defineComponent({
name: 'DragAndDropNotice',
@@ -64,11 +66,19 @@ export default defineComponent({
props: {
currentFolder: {
- type: Folder,
+ type: Object as PropType<Folder>,
required: true,
},
},
+ setup() {
+ const { currentView } = useNavigation()
+
+ return {
+ currentView,
+ }
+ },
+
data() {
return {
dragover: false,
@@ -76,10 +86,6 @@ export default defineComponent({
},
computed: {
- currentView() {
- return this.$navigation.active
- },
-
/**
* Check if the current folder has create permissions
*/
diff --git a/apps/files/src/components/DragAndDropPreview.vue b/apps/files/src/components/DragAndDropPreview.vue
index 1284eed2566..dd4e2d036bc 100644
--- a/apps/files/src/components/DragAndDropPreview.vue
+++ b/apps/files/src/components/DragAndDropPreview.vue
@@ -79,7 +79,7 @@ export default Vue.extend({
summary(): string {
if (this.isSingleNode) {
const node = this.nodes[0]
- return node.attributes?.displayName || node.basename
+ return node.attributes?.displayname || node.basename
}
return getSummaryFor(this.nodes)
diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue
index fc14b4e62e3..f1cccf669e5 100644
--- a/apps/files/src/components/FileEntry.vue
+++ b/apps/files/src/components/FileEntry.vue
@@ -103,9 +103,10 @@
<script lang="ts">
import { defineComponent } from 'vue'
-import { Permission, formatFileSize } from '@nextcloud/files'
+import { formatFileSize } from '@nextcloud/files'
import moment from '@nextcloud/moment'
+import { useNavigation } from '../composables/useNavigation'
import { useActionsMenuStore } from '../store/actionsmenu.ts'
import { useDragAndDropStore } from '../store/dragging.ts'
import { useFilesStore } from '../store/files.ts'
@@ -157,12 +158,16 @@ export default defineComponent({
const filesStore = useFilesStore()
const renamingStore = useRenamingStore()
const selectionStore = useSelectionStore()
+ const { currentView } = useNavigation()
+
return {
actionsMenuStore,
draggingStore,
filesStore,
renamingStore,
selectionStore,
+
+ currentView,
}
},
@@ -196,21 +201,22 @@ export default defineComponent({
},
size() {
- const size = parseInt(this.source.size, 10)
- if (typeof size !== 'number' || isNaN(size) || size < 0) {
+ const size = this.source.size
+ if (!size || size < 0) {
return this.t('files', 'Pending')
}
return formatFileSize(size, true)
},
+
sizeOpacity() {
const maxOpacitySize = 10 * 1024 * 1024
- const size = parseInt(this.source.size, 10)
+ const size = this.source.size
if (!size || isNaN(size) || size < 0) {
return {}
}
- const ratio = Math.round(Math.min(100, 100 * Math.pow((this.source.size / maxOpacitySize), 2)))
+ const ratio = Math.round(Math.min(100, 100 * Math.pow((size / maxOpacitySize), 2)))
return {
color: `color-mix(in srgb, var(--color-main-text) ${ratio}%, var(--color-text-maxcontrast))`,
}
diff --git a/apps/files/src/components/FileEntry/FileEntryActions.vue b/apps/files/src/components/FileEntry/FileEntryActions.vue
index 27dba632d4b..c6ee7b9aac7 100644
--- a/apps/files/src/components/FileEntry/FileEntryActions.vue
+++ b/apps/files/src/components/FileEntry/FileEntryActions.vue
@@ -93,13 +93,13 @@
</template>
<script lang="ts">
+import type { PropType, ShallowRef } from 'vue'
import type { FileAction, Node, View } from '@nextcloud/files'
-import type { PropType } from 'vue'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { DefaultType, NodeStatus, getFileActions } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
-import Vue, { defineComponent } from 'vue'
+import { defineComponent } from 'vue'
import ArrowLeftIcon from 'vue-material-design-icons/ArrowLeft.vue'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
@@ -108,6 +108,7 @@ import NcActionSeparator from '@nextcloud/vue/dist/Components/NcActionSeparator.
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
+import { useNavigation } from '../../composables/useNavigation'
import CustomElementRender from '../CustomElementRender.vue'
import logger from '../../logger.js'
@@ -150,6 +151,15 @@ export default defineComponent({
},
},
+ setup() {
+ const { currentView } = useNavigation()
+
+ return {
+ // The file list is guaranteed to be only shown with active view
+ currentView: currentView as ShallowRef<View>,
+ }
+ },
+
data() {
return {
openedSubmenu: null as FileAction | null,
@@ -161,9 +171,6 @@ export default defineComponent({
// Remove any trailing slash but leave root slash
return (this.$route?.query?.dir?.toString() || '/').replace(/^(.+)\/$/, '$1')
},
- currentView(): View {
- return this.$navigation.active as View
- },
isLoading() {
return this.source.status === NodeStatus.LOADING
},
@@ -287,7 +294,7 @@ export default defineComponent({
try {
// Set the loading marker
this.$emit('update:loading', action.id)
- Vue.set(this.source, 'status', NodeStatus.LOADING)
+ this.$set(this.source, 'status', NodeStatus.LOADING)
const success = await action.exec(this.source, this.currentView, this.currentDir)
@@ -307,7 +314,7 @@ export default defineComponent({
} finally {
// Reset the loading marker
this.$emit('update:loading', '')
- Vue.set(this.source, 'status', undefined)
+ this.$set(this.source, 'status', undefined)
// If that was a submenu, we just go back after the action
if (isSubmenu) {
diff --git a/apps/files/src/components/FileEntry/FileEntryName.vue b/apps/files/src/components/FileEntry/FileEntryName.vue
index 91873f053eb..4b387071a4a 100644
--- a/apps/files/src/components/FileEntry/FileEntryName.vue
+++ b/apps/files/src/components/FileEntry/FileEntryName.vue
@@ -54,6 +54,7 @@
</template>
<script lang="ts">
+import type { Node } from '@nextcloud/files'
import type { PropType } from 'vue'
import { emit } from '@nextcloud/event-bus'
@@ -66,6 +67,7 @@ import Vue from 'vue'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
+import { useNavigation } from '../../composables/useNavigation'
import { useRenamingStore } from '../../store/renaming.ts'
import logger from '../../logger.js'
@@ -106,8 +108,12 @@ export default Vue.extend({
},
setup() {
+ const { currentView } = useNavigation()
const renamingStore = useRenamingStore()
+
return {
+ currentView,
+
renamingStore,
}
},
diff --git a/apps/files/src/components/FileEntryGrid.vue b/apps/files/src/components/FileEntryGrid.vue
index c6823ee7cea..b2e98a80d9e 100644
--- a/apps/files/src/components/FileEntryGrid.vue
+++ b/apps/files/src/components/FileEntryGrid.vue
@@ -77,6 +77,7 @@
<script lang="ts">
import { defineComponent } from 'vue'
+import { useNavigation } from '../composables/useNavigation'
import { useActionsMenuStore } from '../store/actionsmenu.ts'
import { useDragAndDropStore } from '../store/dragging.ts'
import { useFilesStore } from '../store/files.ts'
@@ -110,12 +111,16 @@ export default defineComponent({
const filesStore = useFilesStore()
const renamingStore = useRenamingStore()
const selectionStore = useSelectionStore()
+ const { currentView } = useNavigation()
+
return {
actionsMenuStore,
draggingStore,
filesStore,
renamingStore,
selectionStore,
+
+ currentView,
}
},
diff --git a/apps/files/src/components/FileEntryMixin.ts b/apps/files/src/components/FileEntryMixin.ts
index 94d7610bc07..5c5a806b97c 100644
--- a/apps/files/src/components/FileEntryMixin.ts
+++ b/apps/files/src/components/FileEntryMixin.ts
@@ -65,10 +65,6 @@ export default defineComponent({
},
computed: {
- currentView(): View {
- return this.$navigation.active as View
- },
-
currentDir() {
// Remove any trailing slash but leave root slash
return (this.$route?.query?.dir?.toString() || '/').replace(/^(.+)\/$/, '$1')
@@ -88,15 +84,14 @@ export default defineComponent({
},
extension() {
- if (this.source.attributes?.displayName) {
- return extname(this.source.attributes.displayName)
+ if (this.source.attributes?.displayname) {
+ return extname(this.source.attributes.displayname)
}
return this.source.extension || ''
},
displayName() {
const ext = this.extension
- const name = (this.source.attributes.displayName
- || this.source.basename)
+ const name = String(this.source.attributes.displayname || this.source.basename)
// Strip extension from name if defined
return !ext ? name : name.slice(0, 0 - ext.length)
diff --git a/apps/files/src/components/FilesListTableHeader.vue b/apps/files/src/components/FilesListTableHeader.vue
index caa549fa9ba..f7c449ebf24 100644
--- a/apps/files/src/components/FilesListTableHeader.vue
+++ b/apps/files/src/components/FilesListTableHeader.vue
@@ -71,17 +71,21 @@
</template>
<script lang="ts">
+import type { Node } from '@nextcloud/files'
+import type { PropType } from 'vue'
+import type { FileSource } from '../types.ts'
+
import { translate as t } from '@nextcloud/l10n'
+import { defineComponent } from 'vue'
+
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
-import { defineComponent, type PropType } from 'vue'
+import FilesListTableHeaderButton from './FilesListTableHeaderButton.vue'
+import { useNavigation } from '../composables/useNavigation'
import { useFilesStore } from '../store/files.ts'
import { useSelectionStore } from '../store/selection.ts'
-import FilesListTableHeaderButton from './FilesListTableHeaderButton.vue'
import filesSortingMixin from '../mixins/filesSorting.ts'
import logger from '../logger.js'
-import type { Node } from '@nextcloud/files'
-import type { FileSource } from '../types.ts'
export default defineComponent({
name: 'FilesListTableHeader',
@@ -117,17 +121,17 @@ export default defineComponent({
setup() {
const filesStore = useFilesStore()
const selectionStore = useSelectionStore()
+ const { currentView } = useNavigation()
+
return {
filesStore,
selectionStore,
+
+ currentView,
}
},
computed: {
- currentView() {
- return this.$navigation.active
- },
-
columns() {
// Hide columns if the list is too small
if (this.filesListWidth < 512) {
diff --git a/apps/files/src/components/NewNodeDialog.vue b/apps/files/src/components/NewNodeDialog.vue
index 5947334f11b..4087b58c607 100644
--- a/apps/files/src/components/NewNodeDialog.vue
+++ b/apps/files/src/components/NewNodeDialog.vue
@@ -47,7 +47,7 @@ import type { PropType } from 'vue'
import { defineComponent } from 'vue'
import { translate as t } from '@nextcloud/l10n'
-import { getUniqueName } from '../utils/fileUtils'
+import { getUniqueName } from '@nextcloud/files'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
diff --git a/apps/files/src/components/VirtualList.vue b/apps/files/src/components/VirtualList.vue
index b00c24a12a2..320ca4a6ce3 100644
--- a/apps/files/src/components/VirtualList.vue
+++ b/apps/files/src/components/VirtualList.vue
@@ -47,7 +47,7 @@
import type { File, Folder, Node } from '@nextcloud/files'
import type { PropType } from 'vue'
-import { debounce } from 'debounce'
+import debounce from 'debounce'
import Vue from 'vue'
import filesListWidthMixin from '../mixins/filesListWidth.ts'
diff --git a/apps/files/src/composables/useNavigation.spec.ts b/apps/files/src/composables/useNavigation.spec.ts
new file mode 100644
index 00000000000..360e12660f3
--- /dev/null
+++ b/apps/files/src/composables/useNavigation.spec.ts
@@ -0,0 +1,98 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { beforeEach, describe, expect, it, jest } from '@jest/globals'
+import { Navigation, View } from '@nextcloud/files'
+import { mount } from '@vue/test-utils'
+import { defineComponent, nextTick } from 'vue'
+import { useNavigation } from './useNavigation'
+
+import nextcloudFiles from '@nextcloud/files'
+
+// Just a wrapper so we can test the composable
+const TestComponent = defineComponent({
+ template: '<div></div>',
+ setup() {
+ const { currentView, views } = useNavigation()
+ return {
+ currentView,
+ views,
+ }
+ },
+})
+
+describe('Composables: useNavigation', () => {
+ const spy = jest.spyOn(nextcloudFiles, 'getNavigation')
+ let navigation: Navigation
+
+ describe('currentView', () => {
+ beforeEach(() => {
+ navigation = new Navigation()
+ spy.mockImplementation(() => navigation)
+ })
+
+ it('should return null without active navigation', () => {
+ const wrapper = mount(TestComponent)
+ expect((wrapper.vm as unknown as { currentView: View | null}).currentView).toBe(null)
+ })
+
+ it('should return already active navigation', async () => {
+ const view = new View({ getContents: () => Promise.reject(), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 })
+ navigation.register(view)
+ navigation.setActive(view)
+ // Now the navigation is already set it should take the active navigation
+ const wrapper = mount(TestComponent)
+ expect((wrapper.vm as unknown as { currentView: View | null}).currentView).toBe(view)
+ })
+
+ it('should be reactive on updating active navigation', async () => {
+ const view = new View({ getContents: () => Promise.reject(), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 })
+ navigation.register(view)
+ const wrapper = mount(TestComponent)
+
+ // no active navigation
+ expect((wrapper.vm as unknown as { currentView: View | null}).currentView).toBe(null)
+
+ navigation.setActive(view)
+ // Now the navigation is set it should take the active navigation
+ expect((wrapper.vm as unknown as { currentView: View | null}).currentView).toBe(view)
+ })
+ })
+
+ describe('views', () => {
+ beforeEach(() => {
+ navigation = new Navigation()
+ spy.mockImplementation(() => navigation)
+ })
+
+ it('should return empty array without registered views', () => {
+ const wrapper = mount(TestComponent)
+ expect((wrapper.vm as unknown as { views: View[]}).views).toStrictEqual([])
+ })
+
+ it('should return already registered views', () => {
+ const view = new View({ getContents: () => Promise.reject(), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 })
+ // register before mount
+ navigation.register(view)
+ // now mount and check that the view is listed
+ const wrapper = mount(TestComponent)
+ expect((wrapper.vm as unknown as { views: View[]}).views).toStrictEqual([view])
+ })
+
+ it('should be reactive on registering new views', () => {
+ const view = new View({ getContents: () => Promise.reject(), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 })
+ const view2 = new View({ getContents: () => Promise.reject(), icon: '<svg></svg>', id: 'view-2', name: 'My View 2', order: 1 })
+
+ // register before mount
+ navigation.register(view)
+ // now mount and check that the view is listed
+ const wrapper = mount(TestComponent)
+ expect((wrapper.vm as unknown as { views: View[]}).views).toStrictEqual([view])
+
+ // now register view 2 and check it is reactivly added
+ navigation.register(view2)
+ expect((wrapper.vm as unknown as { views: View[]}).views).toStrictEqual([view, view2])
+ })
+ })
+})
diff --git a/apps/files/src/composables/useNavigation.ts b/apps/files/src/composables/useNavigation.ts
new file mode 100644
index 00000000000..f410aec895f
--- /dev/null
+++ b/apps/files/src/composables/useNavigation.ts
@@ -0,0 +1,46 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import type { View } from '@nextcloud/files'
+
+import { getNavigation } from '@nextcloud/files'
+import { onMounted, onUnmounted, shallowRef, type ShallowRef } from 'vue'
+
+/**
+ * Composable to get the currently active files view from the files navigation
+ */
+export function useNavigation() {
+ const navigation = getNavigation()
+ const views: ShallowRef<View[]> = shallowRef(navigation.views)
+ const currentView: ShallowRef<View | null> = shallowRef(navigation.active)
+
+ /**
+ * Event listener to update the `currentView`
+ * @param event The update event
+ */
+ function onUpdateActive(event: CustomEvent<View|null>) {
+ currentView.value = event.detail
+ }
+
+ /**
+ * Event listener to update all registered views
+ */
+ function onUpdateViews() {
+ views.value = navigation.views
+ }
+
+ onMounted(() => {
+ navigation.addEventListener('update', onUpdateViews)
+ navigation.addEventListener('updateActive', onUpdateActive)
+ })
+ onUnmounted(() => {
+ navigation.removeEventListener('update', onUpdateViews)
+ navigation.removeEventListener('updateActive', onUpdateActive)
+ })
+
+ return {
+ currentView,
+ views,
+ }
+}
diff --git a/apps/files/src/eventbus.d.ts b/apps/files/src/eventbus.d.ts
index c6c66a766d0..6a25e463a4d 100644
--- a/apps/files/src/eventbus.d.ts
+++ b/apps/files/src/eventbus.d.ts
@@ -6,6 +6,7 @@ declare module '@nextcloud/event-bus' {
'files:favorites:removed': Node
'files:favorites:added': Node
'files:node:renamed': Node
+ 'nextcloud:unified-search.search': { query: string }
}
}
diff --git a/apps/files/src/utils/davUtils.js b/apps/files/src/utils/davUtils.js
index 22367d09a1a..d86b69eaabd 100644
--- a/apps/files/src/utils/davUtils.js
+++ b/apps/files/src/utils/davUtils.js
@@ -20,17 +20,8 @@
*
*/
-import { generateRemoteUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
-export const getRootPath = function() {
- if (getCurrentUser()) {
- return generateRemoteUrl(`dav/files/${getCurrentUser().uid}`)
- } else {
- return generateRemoteUrl('webdav').replace('/remote.php', '/public.php')
- }
-}
-
export const isPublic = function() {
return !getCurrentUser()
}
diff --git a/apps/files/src/utils/fileUtils.ts b/apps/files/src/utils/fileUtils.ts
index 180bec31004..0d4baa84f20 100644
--- a/apps/files/src/utils/fileUtils.ts
+++ b/apps/files/src/utils/fileUtils.ts
@@ -21,41 +21,6 @@
*/
import { FileType, type Node } from '@nextcloud/files'
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
-import { basename, extname } from 'path'
-
-// TODO: move to @nextcloud/files
-/**
- * Create an unique file name
- * @param name The initial name to use
- * @param otherNames Other names that are already used
- * @param options Optional parameters for tuning the behavior
- * @param options.suffix A function that takes an index and returns a suffix to add to the file name, defaults to '(index)'
- * @param options.ignoreFileExtension Set to true to ignore the file extension when adding the suffix (when getting a unique directory name)
- * @return Either the initial name, if unique, or the name with the suffix so that the name is unique
- */
-export const getUniqueName = (
- name: string,
- otherNames: string[],
- options: {
- suffix?: (i: number) => string,
- ignoreFileExtension?: boolean,
- } = {},
-): string => {
- const opts = {
- suffix: (n: number) => `(${n})`,
- ignoreFileExtension: false,
- ...options,
- }
-
- let newName = name
- let i = 1
- while (otherNames.includes(newName)) {
- const ext = opts.ignoreFileExtension ? '' : extname(name)
- const base = basename(name, ext)
- newName = `${base} ${opts.suffix(i++)}${ext}`
- }
- return newName
-}
export const encodeFilePath = function(path) {
const pathSections = (path.startsWith('/') ? path : `/${path}`).split('/')
diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue
index a9c7dacc1ae..f992ed3cd36 100644
--- a/apps/files/src/views/FilesList.vue
+++ b/apps/files/src/views/FilesList.vue
@@ -34,7 +34,7 @@
type="tertiary"
@click="openSharingSidebar">
<template #icon>
- <LinkIcon v-if="shareButtonType === Type.SHARE_TYPE_LINK" />
+ <LinkIcon v-if="shareButtonType === ShareType.Link" />
<AccountPlusIcon v-else :size="20" />
</template>
</NcButton>
@@ -116,21 +116,23 @@
</template>
<script lang="ts">
-import type { Route } from 'vue-router'
+import type { ContentsWithRoot } from '@nextcloud/files'
import type { Upload } from '@nextcloud/upload'
+import type { CancelablePromise } from 'cancelable-promise'
+import type { ComponentInstance } from 'vue'
+import type { Route } from 'vue-router'
import type { UserConfig } from '../types.ts'
-import type { View, ContentsWithRoot } from '@nextcloud/files'
+import { getCapabilities } from '@nextcloud/capabilities'
+import { showError } from '@nextcloud/dialogs'
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import { Folder, Node, Permission } from '@nextcloud/files'
-import { getCapabilities } from '@nextcloud/capabilities'
+import { loadState } from '@nextcloud/initial-state'
+import { translate as t, translatePlural as n } from '@nextcloud/l10n'
+import { ShareType } from '@nextcloud/sharing'
+import { UploadPicker } from '@nextcloud/upload'
import { join, dirname } from 'path'
import { Parser } from 'xml2js'
-import { showError } from '@nextcloud/dialogs'
-import { translate, translatePlural } from '@nextcloud/l10n'
-import { Type } from '@nextcloud/sharing'
-import { UploadPicker } from '@nextcloud/upload'
-import { loadState } from '@nextcloud/initial-state'
import { defineComponent } from 'vue'
import LinkIcon from 'vue-material-design-icons/Link.vue'
@@ -145,6 +147,7 @@ import AccountPlusIcon from 'vue-material-design-icons/AccountPlus.vue'
import ViewGridIcon from 'vue-material-design-icons/ViewGrid.vue'
import { action as sidebarAction } from '../actions/sidebarAction.ts'
+import { useNavigation } from '../composables/useNavigation.ts'
import { useFilesStore } from '../store/files.ts'
import { usePathsStore } from '../store/paths.ts'
import { useSelectionStore } from '../store/selection.ts'
@@ -194,10 +197,15 @@ export default defineComponent({
const uploaderStore = useUploaderStore()
const userConfigStore = useUserConfigStore()
const viewConfigStore = useViewConfigStore()
+ const { currentView } = useNavigation()
const enableGridView = (loadState('core', 'config', [])['enable_non-accessible_features'] ?? true)
return {
+ currentView,
+ n,
+ t,
+
filesStore,
pathsStore,
selectionStore,
@@ -205,6 +213,8 @@ export default defineComponent({
userConfigStore,
viewConfigStore,
enableGridView,
+
+ ShareType,
}
},
@@ -212,10 +222,9 @@ export default defineComponent({
return {
filterText: '',
loading: true,
- promise: null,
- Type,
+ promise: null as Promise<ContentsWithRoot> | CancelablePromise<ContentsWithRoot> | null,
- _unsubscribeStore: () => {},
+ unsubscribeStoreCallback: () => {},
}
},
@@ -224,10 +233,6 @@ export default defineComponent({
return this.userConfigStore.userConfig
},
- currentView(): View {
- return this.$navigation.active || this.$navigation.views.find((view) => view.id === (this.$route.params?.view ?? 'files'))
- },
-
pageHeading(): string {
return this.currentView?.name ?? this.t('files', 'Files')
},
@@ -280,8 +285,8 @@ export default defineComponent({
...(this.userConfig.sort_folders_first ? [v => v.type !== 'folder'] : []),
// 3: Use sorting mode if NOT basename (to be able to use displayName too)
...(this.sortingMode !== 'basename' ? [v => v[this.sortingMode]] : []),
- // 4: Use displayName if available, fallback to name
- v => v.attributes?.displayName || v.basename,
+ // 4: Use displayname if available, fallback to name
+ v => v.attributes?.displayname || v.basename,
// 5: Finally, use basename if all previous sorting methods failed
v => v.basename,
]
@@ -384,22 +389,22 @@ export default defineComponent({
return this.t('files', 'Share')
}
- if (this.shareButtonType === Type.SHARE_TYPE_LINK) {
+ if (this.shareButtonType === ShareType.Link) {
return this.t('files', 'Shared by link')
}
return this.t('files', 'Shared')
},
- shareButtonType(): Type | null {
+ shareButtonType(): ShareType | null {
if (!this.shareAttributes) {
return null
}
// If all types are links, show the link icon
- if (this.shareAttributes.some(type => type === Type.SHARE_TYPE_LINK)) {
- return Type.SHARE_TYPE_LINK
+ if (this.shareAttributes.some(type => type === ShareType.Link)) {
+ return ShareType.Link
}
- return Type.SHARE_TYPE_USER
+ return ShareType.User
},
gridViewButtonLabel() {
@@ -431,6 +436,18 @@ export default defineComponent({
return isSharingEnabled
&& this.currentFolder && (this.currentFolder.permissions & Permission.SHARE) !== 0
},
+
+ /**
+ * Handle search event from unified search.
+ *
+ * @return {(searchEvent: {query: string}) => void}
+ */
+ onSearch() {
+ return debounce((searchEvent: { query: string }) => {
+ console.debug('Files app handling search event from unified search...', searchEvent)
+ this.filterText = searchEvent.query
+ }, 500)
+ },
},
watch: {
@@ -453,8 +470,9 @@ export default defineComponent({
this.fetchContent()
// Scroll to top, force virtual scroller to re-render
- if (this.$refs?.filesListVirtual?.$el) {
- this.$refs.filesListVirtual.$el.scrollTop = 0
+ const filesListVirtual = this.$refs?.filesListVirtual as ComponentInstance | undefined
+ if (filesListVirtual?.$el) {
+ filesListVirtual.$el.scrollTop = 0
}
},
@@ -470,18 +488,18 @@ export default defineComponent({
subscribe('files:node:deleted', this.onNodeDeleted)
subscribe('files:node:updated', this.onUpdatedNode)
subscribe('nextcloud:unified-search.search', this.onSearch)
- subscribe('nextcloud:unified-search.reset', this.onSearch)
+ subscribe('nextcloud:unified-search.reset', this.resetSearch)
// reload on settings change
- this._unsubscribeStore = this.userConfigStore.$subscribe(() => this.fetchContent(), { deep: true })
+ this.unsubscribeStoreCallback = this.userConfigStore.$subscribe(() => this.fetchContent(), { deep: true })
},
unmounted() {
unsubscribe('files:node:deleted', this.onNodeDeleted)
unsubscribe('files:node:updated', this.onUpdatedNode)
unsubscribe('nextcloud:unified-search.search', this.onSearch)
- unsubscribe('nextcloud:unified-search.reset', this.onSearch)
- this._unsubscribeStore()
+ unsubscribe('nextcloud:unified-search.reset', this.resetSearch)
+ this.unsubscribeStoreCallback()
},
methods: {
@@ -496,7 +514,7 @@ export default defineComponent({
}
// If we have a cancellable promise ongoing, cancel it
- if (typeof this.promise?.cancel === 'function') {
+ if (this.promise && 'cancel' in this.promise) {
this.promise.cancel()
logger.debug('Cancelled previous ongoing fetch')
}
@@ -531,7 +549,7 @@ export default defineComponent({
// Update paths store
const folders = contents.filter(node => node.type === 'folder')
folders.forEach(node => {
- this.pathsStore.addPath({ service: currentView.id, fileid: node.fileid, path: join(dir, node.basename) })
+ this.pathsStore.addPath({ service: currentView.id, source: node.source, path: join(dir, node.basename) })
})
} catch (error) {
logger.error('Error while fetching content', { error })
@@ -642,20 +660,14 @@ export default defineComponent({
this.fetchContent()
}
},
- /**
- * Handle search event from unified search.
- *
- * @param searchEvent is event object.
- */
- onSearch: debounce(function(searchEvent) {
- console.debug('Files app handling search event from unified search...', searchEvent)
- this.filterText = searchEvent.query
- }, 500),
/**
* Reset the search query
*/
resetSearch() {
+ // Reset debounced calls to not set the query again
+ this.onSearch.clear()
+ // Reset filter query
this.filterText = ''
},
@@ -668,14 +680,11 @@ export default defineComponent({
if (window?.OCA?.Files?.Sidebar?.setActiveTab) {
window.OCA.Files.Sidebar.setActiveTab('sharing')
}
- sidebarAction.exec(this.currentFolder, this.currentView, this.currentFolder.path)
+ sidebarAction.exec(this.currentFolder, this.currentView!, this.currentFolder.path)
},
toggleGridView() {
this.userConfigStore.update('grid_view', !this.userConfig.grid_view)
},
-
- t: translate,
- n: translatePlural,
},
})
</script>
diff --git a/apps/files/src/views/Navigation.cy.ts b/apps/files/src/views/Navigation.cy.ts
index 07d9eee80cb..fe7800f32c9 100644
--- a/apps/files/src/views/Navigation.cy.ts
+++ b/apps/files/src/views/Navigation.cy.ts
@@ -1,19 +1,40 @@
-import FolderSvg from '@mdi/svg/svg/folder.svg'
-import ShareSvg from '@mdi/svg/svg/share-variant.svg'
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import type { Navigation } from '@nextcloud/files'
+
import { createTestingPinia } from '@pinia/testing'
+import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
import NavigationView from './Navigation.vue'
-import router from '../router/router'
import { useViewConfigStore } from '../store/viewConfig'
import { Folder, View, getNavigation } from '@nextcloud/files'
import Vue from 'vue'
+import router from '../router/router'
+
+const resetNavigation = () => {
+ const nav = getNavigation()
+ ;[...nav.views].forEach(({ id }) => nav.remove(id))
+ nav.setActive(null)
+}
+
+const createView = (id: string, name: string, parent?: string) => new View({
+ id,
+ name,
+ getContents: async () => ({ folder: {} as Folder, contents: [] }),
+ icon: FolderSvg,
+ order: 1,
+ parent,
+})
describe('Navigation renders', () => {
- delete window._nc_navigation
- const Navigation = getNavigation()
+ let Navigation: Navigation
before(() => {
+ delete window._nc_navigation
+ Navigation = getNavigation()
Vue.prototype.$navigation = Navigation
cy.mockInitialState('files', 'storageStats', {
@@ -40,29 +61,31 @@ describe('Navigation renders', () => {
})
describe('Navigation API', () => {
- delete window._nc_navigation
- const Navigation = getNavigation()
+ let Navigation: Navigation
+
+ before(async () => {
+ delete window._nc_navigation
+ Navigation = getNavigation()
- before(() => {
Vue.prototype.$navigation = Navigation
+ await router.replace({ name: 'filelist', params: { view: 'files' } })
})
+ beforeEach(() => resetNavigation())
+
it('Check API entries rendering', () => {
- Navigation.register(new View({
- id: 'files',
- name: 'Files',
- getContents: async () => ({ folder: {} as Folder, contents: [] }),
- icon: FolderSvg,
- order: 1,
- }))
+ Navigation.register(createView('files', 'Files'))
+ console.warn(Navigation.views)
cy.mount(NavigationView, {
+ router,
global: {
- plugins: [createTestingPinia({
- createSpy: cy.spy,
- })],
+ plugins: [
+ createTestingPinia({
+ createSpy: cy.spy,
+ }),
+ ],
},
- router,
})
cy.get('[data-cy-files-navigation]').should('be.visible')
@@ -72,21 +95,16 @@ describe('Navigation API', () => {
})
it('Adds a new entry and render', () => {
- Navigation.register(new View({
- id: 'sharing',
- name: 'Sharing',
- getContents: async () => ({ folder: {} as Folder, contents: [] }),
- icon: ShareSvg,
- order: 2,
- }))
+ Navigation.register(createView('files', 'Files'))
+ Navigation.register(createView('sharing', 'Sharing'))
cy.mount(NavigationView, {
+ router,
global: {
plugins: [createTestingPinia({
createSpy: cy.spy,
})],
},
- router,
})
cy.get('[data-cy-files-navigation]').should('be.visible')
@@ -96,22 +114,17 @@ describe('Navigation API', () => {
})
it('Adds a new children, render and open menu', () => {
- Navigation.register(new View({
- id: 'sharingin',
- name: 'Shared with me',
- getContents: async () => ({ folder: {} as Folder, contents: [] }),
- parent: 'sharing',
- icon: ShareSvg,
- order: 1,
- }))
+ Navigation.register(createView('files', 'Files'))
+ Navigation.register(createView('sharing', 'Sharing'))
+ Navigation.register(createView('sharingin', 'Shared with me', 'sharing'))
cy.mount(NavigationView, {
+ router,
global: {
plugins: [createTestingPinia({
createSpy: cy.spy,
})],
},
- router,
})
cy.wrap(useViewConfigStore()).as('viewConfigStore')
@@ -139,23 +152,18 @@ describe('Navigation API', () => {
})
it('Throws when adding a duplicate entry', () => {
- expect(() => {
- Navigation.register(new View({
- id: 'files',
- name: 'Files',
- getContents: async () => ({ folder: {} as Folder, contents: [] }),
- icon: FolderSvg,
- order: 1,
- }))
- }).to.throw('View id files is already registered')
+ Navigation.register(createView('files', 'Files'))
+ expect(() => Navigation.register(createView('files', 'Files')))
+ .to.throw('View id files is already registered')
})
})
describe('Quota rendering', () => {
- delete window._nc_navigation
- const Navigation = getNavigation()
+ let Navigation: Navigation
before(() => {
+ delete window._nc_navigation
+ Navigation = getNavigation()
Vue.prototype.$navigation = Navigation
})
diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue
index 38b6fcb7f49..e71e4ececa1 100644
--- a/apps/files/src/views/Navigation.vue
+++ b/apps/files/src/views/Navigation.vue
@@ -62,7 +62,7 @@
:name="t('files', 'Files settings')"
data-cy-files-navigation-settings-button
@click.prevent.stop="openSettings">
- <Cog slot="icon" :size="20" />
+ <IconCog slot="icon" :size="20" />
</NcAppNavigationItem>
</ul>
</template>
@@ -75,24 +75,29 @@
</template>
<script lang="ts">
+import type { View } from '@nextcloud/files'
+
import { emit } from '@nextcloud/event-bus'
-import { translate } from '@nextcloud/l10n'
-import Cog from 'vue-material-design-icons/Cog.vue'
+import { translate as t } from '@nextcloud/l10n'
+import { defineComponent } from 'vue'
+
+import IconCog from 'vue-material-design-icons/Cog.vue'
import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js'
import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
+import NavigationQuota from '../components/NavigationQuota.vue'
+import SettingsModal from './Settings.vue'
+import { useNavigation } from '../composables/useNavigation'
import { useViewConfigStore } from '../store/viewConfig.ts'
import logger from '../logger.js'
-import type { View } from '@nextcloud/files'
-import NavigationQuota from '../components/NavigationQuota.vue'
-import SettingsModal from './Settings.vue'
-export default {
+export default defineComponent({
name: 'Navigation',
components: {
- Cog,
+ IconCog,
+
NavigationQuota,
NcAppNavigation,
NcAppNavigationItem,
@@ -102,7 +107,12 @@ export default {
setup() {
const viewConfigStore = useViewConfigStore()
+ const { currentView, views } = useNavigation()
+
return {
+ currentView,
+ views,
+
viewConfigStore,
}
},
@@ -114,18 +124,13 @@ export default {
},
computed: {
+ /**
+ * The current view ID from the route params
+ */
currentViewId() {
return this.$route?.params?.view || 'files'
},
- currentView(): View {
- return this.views.find(view => view.id === this.currentViewId)!
- },
-
- views(): View[] {
- return this.$navigation.views
- },
-
parentViews(): View[] {
return this.views
// filter child views
@@ -153,24 +158,27 @@ export default {
},
watch: {
- currentView(view, oldView) {
- if (view.id !== oldView?.id) {
- this.$navigation.setActive(view)
- logger.debug('Navigation changed', { id: view.id, view })
-
+ currentViewId(newView, oldView) {
+ if (this.currentViewId !== this.currentView?.id) {
+ // This is guaranteed to be a view because `currentViewId` falls back to the default 'files' view
+ const view = this.views.find(({ id }) => id === this.currentViewId)!
+ // The the new view as active
this.showView(view)
+ logger.debug(`Navigation changed from ${oldView} to ${newView}`, { to: view })
}
},
},
beforeMount() {
- if (this.currentView) {
- logger.debug('Navigation mounted. Showing requested view', { view: this.currentView })
- this.showView(this.currentView)
- }
+ // This is guaranteed to be a view because `currentViewId` falls back to the default 'files' view
+ const view = this.views.find(({ id }) => id === this.currentViewId)!
+ this.showView(view)
+ logger.debug('Navigation mounted. Showing requested view', { view })
},
methods: {
+ t,
+
/**
* Only use exact route matching on routes with child views
* Because if a view does not have children (like the files view) then multiple routes might be matched for it
@@ -181,9 +189,13 @@ export default {
return this.childViews[view.id]?.length > 0
},
+ /**
+ * Set the view as active on the navigation and handle internal state
+ * @param view View to set active
+ */
showView(view: View) {
// Closing any opened sidebar
- window?.OCA?.Files?.Sidebar?.close?.()
+ window.OCA?.Files?.Sidebar?.close?.()
this.$navigation.setActive(view)
emit('files:navigation:changed', view)
},
@@ -237,10 +249,8 @@ export default {
onSettingsClose() {
this.settingsOpened = false
},
-
- t: translate,
},
-}
+})
</script>
<style scoped lang="scss">
diff --git a/apps/settings/src/components/AdminSettingsSharingForm.vue b/apps/settings/src/components/AdminSettingsSharingForm.vue
index 2165303349f..2a652004c8c 100644
--- a/apps/settings/src/components/AdminSettingsSharingForm.vue
+++ b/apps/settings/src/components/AdminSettingsSharingForm.vue
@@ -193,10 +193,11 @@ import {
import { showError, showSuccess } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import { loadState } from '@nextcloud/initial-state'
+import { snakeCase } from 'lodash'
import { defineComponent } from 'vue'
+import debounce from 'debounce'
import SelectSharingPermissions from './SelectSharingPermissions.vue'
-import { snakeCase, debounce } from 'lodash'
interface IShareSettings {
enabled: boolean
diff --git a/apps/settings/src/components/Users/VirtualList.vue b/apps/settings/src/components/Users/VirtualList.vue
index a90f778b48e..f247d417400 100644
--- a/apps/settings/src/components/Users/VirtualList.vue
+++ b/apps/settings/src/components/Users/VirtualList.vue
@@ -52,7 +52,7 @@
<script lang="ts">
import Vue from 'vue'
import { vElementVisibility } from '@vueuse/components'
-import { debounce } from 'debounce'
+import debounce from 'debounce'
import logger from '../../logger.js'
diff --git a/apps/theming/src/components/admin/ColorPickerField.vue b/apps/theming/src/components/admin/ColorPickerField.vue
index fad40408b37..dbd08ed6720 100644
--- a/apps/theming/src/components/admin/ColorPickerField.vue
+++ b/apps/theming/src/components/admin/ColorPickerField.vue
@@ -57,7 +57,8 @@
</template>
<script>
-import { debounce } from 'debounce'
+import debounce from 'debounce'
+
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker.js'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'