diff options
author | Andy Scherzinger <info@andy-scherzinger.de> | 2024-07-11 14:25:19 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-07-11 14:25:19 +0200 |
commit | 52718fe2f59b220d3a46c422a6f4d65e06bec998 (patch) | |
tree | b2827cd0d5eb54decc234d13e50d485c746c1600 /apps | |
parent | 7096ef29f154bb537e0df7f13953daebaffaf1cb (diff) | |
parent | 5565ac6186dc91bbded709c2ceb6fa87b9e4fc2b (diff) | |
download | nextcloud-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')
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' |