diff --git a/changelog/unreleased/enhancement-access-sidebar-via-url b/changelog/unreleased/enhancement-access-sidebar-via-url new file mode 100644 index 00000000000..941622a719f --- /dev/null +++ b/changelog/unreleased/enhancement-access-sidebar-via-url @@ -0,0 +1,16 @@ +Enhancement: Access right sidebar panels via URL + +Opening the right sidebar (including its panels) is now possible via URL param. + +For private or internal links it only requires the new `details` param in the URL. For other URLs (e.g. personal space, project space) the `scrollTo` param including the resource id is needed as well. + +The following values can be used for the `details` param: + +* `details` - sidebar open, no specific panel +* `actions` - actions panel +* `sharing` - share panel +* `versions` - versions panel +* `space-share` - members panel (project space only) + +https://github.com/owncloud/web/pull/8021 +https://github.com/owncloud/web/issues/7927 diff --git a/packages/web-app-files/src/components/FilesList/KeyboardActions.vue b/packages/web-app-files/src/components/FilesList/KeyboardActions.vue index 3ae4fb6819f..9ed19259093 100644 --- a/packages/web-app-files/src/components/FilesList/KeyboardActions.vue +++ b/packages/web-app-files/src/components/FilesList/KeyboardActions.vue @@ -7,11 +7,10 @@ import keycode from 'keycode' import { eventBus } from 'web-pkg/src/services/eventBus' import { mapActions, mapState, mapMutations, mapGetters } from 'vuex' import { defineComponent, PropType } from 'vue' -import MixinFilesListScrolling from '../../mixins/filesListScrolling' import { SpaceResource } from 'web-client/src/helpers' +import { useScrollTo } from 'web-app-files/src/composables/scrollTo' export default defineComponent({ - mixins: [MixinFilesListScrolling], props: { paginatedResources: { type: Array, @@ -27,6 +26,9 @@ export default defineComponent({ required: true } }, + setup() { + return { ...useScrollTo() } + }, data: () => { return { selectionCursor: 0 diff --git a/packages/web-app-files/src/components/FilesList/ResourceTable.vue b/packages/web-app-files/src/components/FilesList/ResourceTable.vue index 5902f0f8c44..f15df45cc06 100644 --- a/packages/web-app-files/src/components/FilesList/ResourceTable.vue +++ b/packages/web-app-files/src/components/FilesList/ResourceTable.vue @@ -566,11 +566,11 @@ export default defineComponent({ openSharingSidebar(file) { let panelToOpen if (file.type === 'space') { - panelToOpen = 'space-share-item' + panelToOpen = 'space-share' } else if (file.share?.shareType === ShareTypes.link.value) { - panelToOpen = 'sharing-item#linkShares' + panelToOpen = 'sharing#linkShares' } else { - panelToOpen = 'sharing-item#peopleShares' + panelToOpen = 'sharing#peopleShares' } eventBus.publish(SideBarEventTopics.openWithPanel, panelToOpen) }, diff --git a/packages/web-app-files/src/components/Search/List.vue b/packages/web-app-files/src/components/Search/List.vue index f23c25d5021..5944bab87a3 100644 --- a/packages/web-app-files/src/components/Search/List.vue +++ b/packages/web-app-files/src/components/Search/List.vue @@ -77,11 +77,10 @@ import ContextActions from '../FilesList/ContextActions.vue' import debounce from 'lodash-es/debounce' import { mapMutations, mapGetters, mapActions } from 'vuex' import AppBar from '../AppBar/AppBar.vue' -import { defineComponent } from 'vue' +import { defineComponent, nextTick } from 'vue' import ListInfo from '../FilesList/ListInfo.vue' import Pagination from '../FilesList/Pagination.vue' import MixinFileActions from '../../mixins/fileActions' -import MixinFilesListScrolling from '../../mixins/filesListScrolling' import { searchLimit } from '../../search/sdk/list' import { Resource } from 'web-client' import FilesViewWrapper from '../FilesViewWrapper.vue' @@ -105,7 +104,7 @@ export default defineComponent({ ResourceTable, FilesViewWrapper }, - mixins: [MixinFileActions, MixinFilesListScrolling], + mixins: [MixinFileActions], props: { searchResult: { type: Object, @@ -172,7 +171,7 @@ export default defineComponent({ }, watch: { searchResult: { - handler: function () { + handler: async function () { if (!this.searchResult) { return } @@ -184,6 +183,8 @@ export default defineComponent({ ? this.searchResult.values.map((searchResult) => searchResult.data) : [] }) + await nextTick() + this.scrollToResourceFromRoute(this.paginatedResources) } } }, diff --git a/packages/web-app-files/src/components/SideBar/Details/FileDetails.vue b/packages/web-app-files/src/components/SideBar/Details/FileDetails.vue index 4a4166c6dcb..b53f6de1fed 100644 --- a/packages/web-app-files/src/components/SideBar/Details/FileDetails.vue +++ b/packages/web-app-files/src/components/SideBar/Details/FileDetails.vue @@ -430,7 +430,7 @@ export default defineComponent({ return null }, expandVersionsPanel() { - eventBus.publish(SideBarEventTopics.setActivePanel, 'versions-item') + eventBus.publish(SideBarEventTopics.setActivePanel, 'versions') }, async loadData() { const calls = [] diff --git a/packages/web-app-files/src/components/SideBar/Details/SpaceDetails.vue b/packages/web-app-files/src/components/SideBar/Details/SpaceDetails.vue index 62b059baf09..7b71b6911cc 100644 --- a/packages/web-app-files/src/components/SideBar/Details/SpaceDetails.vue +++ b/packages/web-app-files/src/components/SideBar/Details/SpaceDetails.vue @@ -246,7 +246,7 @@ export default defineComponent({ }, methods: { expandSharesPanel() { - eventBus.publish(SideBarEventTopics.setActivePanel, 'space-share-item') + eventBus.publish(SideBarEventTopics.setActivePanel, 'space-share') } } }) diff --git a/packages/web-app-files/src/components/Spaces/SpaceHeader.vue b/packages/web-app-files/src/components/Spaces/SpaceHeader.vue index 026645f0145..d0a656beb69 100644 --- a/packages/web-app-files/src/components/Spaces/SpaceHeader.vue +++ b/packages/web-app-files/src/components/Spaces/SpaceHeader.vue @@ -250,7 +250,7 @@ export default defineComponent({ const openSideBarSharePanel = () => { store.commit('Files/SET_SELECTED_IDS', []) - eventBus.publish(SideBarEventTopics.openWithPanel, 'space-share-item') + eventBus.publish(SideBarEventTopics.openWithPanel, 'space-share') } return { diff --git a/packages/web-app-files/src/composables/resourcesViewDefaults/useResourcesViewDefaults.ts b/packages/web-app-files/src/composables/resourcesViewDefaults/useResourcesViewDefaults.ts index 5440c45e7a5..7399eccbeb9 100644 --- a/packages/web-app-files/src/composables/resourcesViewDefaults/useResourcesViewDefaults.ts +++ b/packages/web-app-files/src/composables/resourcesViewDefaults/useResourcesViewDefaults.ts @@ -11,6 +11,7 @@ import { Task } from 'vue-concurrency' import { Resource } from 'web-client' import { useSelectedResources, SelectedResourcesResult } from '../selection' import { ReadOnlyRef } from 'web-pkg' +import { ScrollToResult, useScrollTo } from '../scrollTo' interface ResourcesViewDefaultsOptions { loadResourcesTask?: Task @@ -36,7 +37,8 @@ type ResourcesViewDefaultsResult = { sideBarOpen: Ref sideBarActivePanel: Ref -} & SelectedResourcesResult +} & SelectedResourcesResult & + ScrollToResult export const useResourcesViewDefaults = ( options: ResourcesViewDefaultsOptions = {} @@ -85,6 +87,7 @@ export const useResourcesViewDefaults = ( sortBy, sortDir, ...useSelectedResources({ store }), - ...useSideBar() + ...useSideBar(), + ...useScrollTo() } } diff --git a/packages/web-app-files/src/composables/scrollTo/index.ts b/packages/web-app-files/src/composables/scrollTo/index.ts new file mode 100644 index 00000000000..e59d4c066df --- /dev/null +++ b/packages/web-app-files/src/composables/scrollTo/index.ts @@ -0,0 +1 @@ +export * from './useScrollTo' diff --git a/packages/web-app-files/src/composables/scrollTo/useScrollTo.ts b/packages/web-app-files/src/composables/scrollTo/useScrollTo.ts new file mode 100644 index 00000000000..99dda5d4562 --- /dev/null +++ b/packages/web-app-files/src/composables/scrollTo/useScrollTo.ts @@ -0,0 +1,70 @@ +import { computed, unref } from 'vue' +import { Resource } from 'web-client/src' +import { eventBus, useStore } from 'web-pkg/src' +import { queryItemAsString } from 'web-pkg/src/composables/appDefaults' +import { useRouteQuery } from 'web-pkg/src/composables' +import { SideBarEventTopics } from '../sideBar' + +export interface ScrollToResult { + scrollToResource(resource: Resource): void + scrollToResourceFromRoute(resources: Resource[]): void +} + +export const useScrollTo = (): ScrollToResult => { + const store = useStore() + const scrollToQuery = useRouteQuery('scrollTo') + const detailsQuery = useRouteQuery('details') + const scrollTo = computed(() => { + return queryItemAsString(unref(scrollToQuery)) + }) + const details = computed(() => { + return queryItemAsString(unref(detailsQuery)) + }) + + const scrollToResource = (resource) => { + const resourceElement = document.querySelectorAll( + `[data-item-id='${resource.id}']` + )[0] as HTMLElement + + if (!resourceElement) { + return + } + + // bottom reached + if (resourceElement.getBoundingClientRect().bottom > window.innerHeight) { + resourceElement.scrollIntoView(false) + return + } + + const topbarElement = document.getElementsByClassName('files-topbar')[0] as HTMLElement + // topbar height + th height + height of one row = offset needed when scrolling top + const topOffset = topbarElement.offsetHeight + resourceElement.offsetHeight * 2 + + // top reached + if (resourceElement.getBoundingClientRect().top < topOffset) { + const fileListWrapperElement = document.getElementsByClassName('files-view-wrapper')[0] + fileListWrapperElement.scrollBy(0, -resourceElement.offsetHeight) + } + } + + const scrollToResourceFromRoute = (resources: Resource[]) => { + if (!unref(scrollTo) || !resources.length) { + return + } + + const resource = unref(resources).find((r) => r.id === unref(scrollTo)) + if (resource) { + store.commit('Files/SET_FILE_SELECTION', [resource]) + scrollToResource(resource) + + if (unref(details)) { + eventBus.publish(SideBarEventTopics.openWithPanel, unref(details)) + } + } + } + + return { + scrollToResource, + scrollToResourceFromRoute + } +} diff --git a/packages/web-app-files/src/fileSideBars.ts b/packages/web-app-files/src/fileSideBars.ts index 6b11c5beee7..50100a7d4a2 100644 --- a/packages/web-app-files/src/fileSideBars.ts +++ b/packages/web-app-files/src/fileSideBars.ts @@ -41,7 +41,7 @@ const panelGenerators: (({ // We don't have file details in the trashbin, yet. // Only allow `actions` panel on trashbin route for now. ({ rootFolder, highlightedFile }): Panel => ({ - app: 'no-selection-item', + app: 'no-selection', icon: 'questionnaire-line', title: $gettext('Details'), component: NoSelection, @@ -51,7 +51,7 @@ const panelGenerators: (({ } }), ({ router, multipleSelection, rootFolder, highlightedFile }) => ({ - app: 'details-item', + app: 'details', icon: 'questionnaire-line', title: $gettext('Details'), component: FileDetails, @@ -66,7 +66,7 @@ const panelGenerators: (({ } }), ({ multipleSelection, rootFolder, highlightedFile, router }) => ({ - app: 'details-multiple-item', + app: 'details-multiple', icon: 'questionnaire-line', title: $gettext('Details'), component: FileDetailsMultiple, @@ -85,7 +85,7 @@ const panelGenerators: (({ } }), ({ multipleSelection, highlightedFile }) => ({ - app: 'details-space-item', + app: 'details-space', icon: 'questionnaire-line', title: $gettext('Details'), component: SpaceDetails, @@ -95,7 +95,7 @@ const panelGenerators: (({ } }), ({ router, multipleSelection, rootFolder, highlightedFile }) => ({ - app: 'actions-item', + app: 'actions', icon: 'slideshow-3', title: $gettext('Actions'), component: FileActions, @@ -105,7 +105,7 @@ const panelGenerators: (({ } }), ({ multipleSelection, highlightedFile, user }) => ({ - app: 'space-actions-item', + app: 'space-actions', icon: 'slideshow-3', title: $gettext('Actions'), component: SpaceActions, @@ -123,7 +123,7 @@ const panelGenerators: (({ } }), ({ capabilities, router, multipleSelection, rootFolder, highlightedFile }) => ({ - app: 'sharing-item', + app: 'sharing', icon: 'user-add', iconFillType: 'line', title: $gettext('Shares'), @@ -155,7 +155,7 @@ const panelGenerators: (({ } }), ({ multipleSelection, highlightedFile, capabilities }) => ({ - app: 'space-share-item', + app: 'space-share', icon: 'group', title: $gettext('Members'), component: SharesPanel, @@ -173,7 +173,7 @@ const panelGenerators: (({ } }), ({ capabilities, highlightedFile, router, multipleSelection, rootFolder }) => ({ - app: 'versions-item', + app: 'versions', icon: 'git-branch', title: $gettext('Versions'), component: FileVersions, diff --git a/packages/web-app-files/src/helpers/statusIndicators.js b/packages/web-app-files/src/helpers/statusIndicators.js index c18c40b1d5b..31005302de0 100644 --- a/packages/web-app-files/src/helpers/statusIndicators.js +++ b/packages/web-app-files/src/helpers/statusIndicators.js @@ -95,7 +95,7 @@ export const getIndicators = (resource, sharesTree, hasShareJail = false) => { label: $gettext('Show invited people'), visible: isUserShare(resource, sharesTree, hasShareJail), icon: 'group', - target: 'sharing-item', + target: 'sharing', type: isDirectUserShare(resource) ? 'user-direct' : 'user-indirect', handler: (resource, panel) => { eventBus.publish(SideBarEventTopics.openWithPanel, `${panel}#peopleShares`) @@ -107,7 +107,7 @@ export const getIndicators = (resource, sharesTree, hasShareJail = false) => { label: $gettext('Show links'), visible: isLinkShare(resource, sharesTree), icon: 'link', - target: 'sharing-item', + target: 'sharing', type: isDirectLinkShare(resource) ? 'link-direct' : 'link-indirect', handler: (resource, panel) => { eventBus.publish(SideBarEventTopics.openWithPanel, `${panel}#linkShares`) diff --git a/packages/web-app-files/src/mixins/actions/createQuicklink.ts b/packages/web-app-files/src/mixins/actions/createQuicklink.ts index 48d797f5493..0ba0de42bb2 100644 --- a/packages/web-app-files/src/mixins/actions/createQuicklink.ts +++ b/packages/web-app-files/src/mixins/actions/createQuicklink.ts @@ -44,7 +44,7 @@ export default { $gettext: this.$gettext }) - eventBus.publish(SideBarEventTopics.openWithPanel, 'sharing-item#linkShares') + eventBus.publish(SideBarEventTopics.openWithPanel, 'sharing#linkShares') } } } diff --git a/packages/web-app-files/src/mixins/actions/showActions.js b/packages/web-app-files/src/mixins/actions/showActions.js index 88b44812b07..1dfc4f5909f 100644 --- a/packages/web-app-files/src/mixins/actions/showActions.js +++ b/packages/web-app-files/src/mixins/actions/showActions.js @@ -33,11 +33,11 @@ export default { $_showActions_trigger() { // we don't have details in the trashbin, yet. the actions panel is the default // panel at the moment, so we need to use `null` as panel name for trashbins. - // unconditionally return hardcoded `actions-item` once we have a dedicated + // unconditionally return hardcoded `actions` once we have a dedicated // details panel in trashbins. const panelName = isLocationTrashActive(this.$router, 'files-trash-generic') ? null - : 'actions-item' + : 'actions' eventBus.publish(SideBarEventTopics.openWithPanel, panelName) } } diff --git a/packages/web-app-files/src/mixins/actions/showShares.ts b/packages/web-app-files/src/mixins/actions/showShares.ts index 13da5bdf51b..687990dd1e8 100644 --- a/packages/web-app-files/src/mixins/actions/showShares.ts +++ b/packages/web-app-files/src/mixins/actions/showShares.ts @@ -47,7 +47,7 @@ export default { $_showShares_trigger({ resources }) { this.SET_FILE_SELECTION(resources) - eventBus.publish(SideBarEventTopics.openWithPanel, 'sharing-item#peopleShares') + eventBus.publish(SideBarEventTopics.openWithPanel, 'sharing#peopleShares') } } } diff --git a/packages/web-app-files/src/mixins/filesListScrolling.js b/packages/web-app-files/src/mixins/filesListScrolling.js deleted file mode 100644 index d22c1d9dc6c..00000000000 --- a/packages/web-app-files/src/mixins/filesListScrolling.js +++ /dev/null @@ -1,23 +0,0 @@ -export default { - methods: { - scrollToResource(resource) { - const resourceElement = document.querySelectorAll(`[data-item-id='${resource.id}']`)[0] - - // bottom reached - if (resourceElement.getBoundingClientRect().bottom > window.innerHeight) { - resourceElement.scrollIntoView(false) - return - } - - const topbarElement = document.getElementsByClassName('files-topbar')[0] - // topbar height + th height + height of one row = offset needed when scrolling top - const topOffset = topbarElement.offsetHeight + resourceElement.offsetHeight * 2 - - // top reached - if (resourceElement.getBoundingClientRect().top < topOffset) { - const fileListWrapperElement = document.getElementsByClassName('files-view-wrapper')[0] - fileListWrapperElement.scrollBy(0, -resourceElement.offsetHeight) - } - } - } -} diff --git a/packages/web-app-files/src/mixins/spaces/actions/showMembers.js b/packages/web-app-files/src/mixins/spaces/actions/showMembers.js index 19c9898147c..7a99824fd03 100644 --- a/packages/web-app-files/src/mixins/spaces/actions/showMembers.js +++ b/packages/web-app-files/src/mixins/spaces/actions/showMembers.js @@ -27,7 +27,7 @@ export default { $_showMembers_trigger({ resources }) { this.SET_FILE_SELECTION(resources) - eventBus.publish(SideBarEventTopics.openWithPanel, 'space-share-item') + eventBus.publish(SideBarEventTopics.openWithPanel, 'space-share') } } } diff --git a/packages/web-app-files/src/quickActions.js b/packages/web-app-files/src/quickActions.js index ee84a4cb7d8..9be640db44c 100644 --- a/packages/web-app-files/src/quickActions.js +++ b/packages/web-app-files/src/quickActions.js @@ -52,7 +52,7 @@ export default { id: 'collaborators', label: ($gettext) => $gettext('Add people'), icon: 'user-add', - handler: () => eventBus.publish(SideBarEventTopics.openWithPanel, 'sharing-item#peopleShares'), + handler: () => eventBus.publish(SideBarEventTopics.openWithPanel, 'sharing#peopleShares'), displayed: canShare }, quicklink: { @@ -67,12 +67,12 @@ export default { if (passwordEnforced) { return showQuickLinkPasswordModal(ctx, async (password) => { await createQuicklink({ ...ctx, resource: ctx.item, password }) - eventBus.publish(SideBarEventTopics.openWithPanel, 'sharing-item#linkShares') + eventBus.publish(SideBarEventTopics.openWithPanel, 'sharing#linkShares') }) } await createQuicklink({ ...ctx, resource: ctx.item }) - eventBus.publish(SideBarEventTopics.openWithPanel, 'sharing-item#linkShares') + eventBus.publish(SideBarEventTopics.openWithPanel, 'sharing#linkShares') }, displayed: canShare } diff --git a/packages/web-app-files/src/views/Favorites.vue b/packages/web-app-files/src/views/Favorites.vue index 1021fe2fc9b..4d25c2c414b 100644 --- a/packages/web-app-files/src/views/Favorites.vue +++ b/packages/web-app-files/src/views/Favorites.vue @@ -146,8 +146,9 @@ export default defineComponent({ } }, - created() { - this.loadResourcesTask.perform() + async created() { + await this.loadResourcesTask.perform() + this.scrollToResourceFromRoute(this.paginatedResources) }, beforeDestroy() { diff --git a/packages/web-app-files/src/views/shares/SharedViaLink.vue b/packages/web-app-files/src/views/shares/SharedViaLink.vue index eaf2dd84142..ba7b5104108 100644 --- a/packages/web-app-files/src/views/shares/SharedViaLink.vue +++ b/packages/web-app-files/src/views/shares/SharedViaLink.vue @@ -145,8 +145,9 @@ export default defineComponent({ } }, - created() { - this.loadResourcesTask.perform() + async created() { + await this.loadResourcesTask.perform() + this.scrollToResourceFromRoute(this.paginatedResources) }, beforeDestroy() { diff --git a/packages/web-app-files/src/views/shares/SharedWithMe.vue b/packages/web-app-files/src/views/shares/SharedWithMe.vue index 5572c565033..eb1aed4793d 100644 --- a/packages/web-app-files/src/views/shares/SharedWithMe.vue +++ b/packages/web-app-files/src/views/shares/SharedWithMe.vue @@ -95,7 +95,8 @@ export default defineComponent({ selectedResourcesIds, sideBarActivePanel, sideBarOpen, - storeItems + storeItems, + scrollToResourceFromRoute } = useResourcesViewDefaults() // pending shares @@ -176,6 +177,7 @@ export default defineComponent({ sideBarOpen, sideBarActivePanel, selectedShareSpace, + scrollToResourceFromRoute, // view specific pendingHandleSort, @@ -224,8 +226,9 @@ export default defineComponent({ } }, - created() { - this.loadResourcesTask.perform() + async created() { + await this.loadResourcesTask.perform() + this.scrollToResourceFromRoute(this.acceptedItems) } }) diff --git a/packages/web-app-files/src/views/shares/SharedWithOthers.vue b/packages/web-app-files/src/views/shares/SharedWithOthers.vue index 48ef85674dc..75431e9239c 100644 --- a/packages/web-app-files/src/views/shares/SharedWithOthers.vue +++ b/packages/web-app-files/src/views/shares/SharedWithOthers.vue @@ -140,8 +140,9 @@ export default defineComponent({ } }, - created() { - this.loadResourcesTask.perform() + async created() { + await this.loadResourcesTask.perform() + this.scrollToResourceFromRoute(this.paginatedResources) }, beforeDestroy() { diff --git a/packages/web-app-files/src/views/spaces/GenericSpace.vue b/packages/web-app-files/src/views/spaces/GenericSpace.vue index 15cff230beb..c2712faf137 100644 --- a/packages/web-app-files/src/views/spaces/GenericSpace.vue +++ b/packages/web-app-files/src/views/spaces/GenericSpace.vue @@ -100,7 +100,6 @@ import debounce from 'lodash-es/debounce' import MixinAccessibleBreadcrumb from '../../mixins/accessibleBreadcrumb' import MixinFileActions from '../../mixins/fileActions' -import MixinFilesListScrolling from '../../mixins/filesListScrolling' import AppBar from '../../components/AppBar/AppBar.vue' import ContextActions from '../../components/FilesList/ContextActions.vue' @@ -164,7 +163,7 @@ export default defineComponent({ SpaceHeader }, - mixins: [MixinAccessibleBreadcrumb, MixinFileActions, MixinFilesListScrolling], + mixins: [MixinAccessibleBreadcrumb, MixinFileActions], props: { space: { @@ -355,7 +354,7 @@ export default defineComponent({ fileId || this.itemId, options ) - this.scrollToResourceFromRoute() + this.scrollToResourceFromRoute([this.currentFolder, ...this.paginatedResources]) this.refreshFileListHeaderPosition() this.accessibleBreadcrumb_focusAndAnnounceBreadcrumb(sameRoute) }, @@ -407,18 +406,6 @@ export default defineComponent({ }, 250) visibilityObserver.observe(component.$el, { onEnter: debounced, onExit: debounced.cancel }) - }, - scrollToResourceFromRoute() { - const resourceName = this.$route.query.scrollTo - - if (resourceName && this.paginatedResources.length > 0) { - const resource = this.paginatedResources.find((r) => r.name === resourceName) - - if (resource) { - this.selectedResources = [resource] - this.scrollToResource(resource) - } - } } } }) diff --git a/packages/web-app-files/src/views/spaces/GenericTrash.vue b/packages/web-app-files/src/views/spaces/GenericTrash.vue index 92c2e501627..1b5af1269c4 100644 --- a/packages/web-app-files/src/views/spaces/GenericTrash.vue +++ b/packages/web-app-files/src/views/spaces/GenericTrash.vue @@ -192,6 +192,7 @@ export default defineComponent({ async performLoaderTask() { await this.loadResourcesTask.perform(this.space) this.refreshFileListHeaderPosition() + this.scrollToResourceFromRoute(this.paginatedResources) } } }) diff --git a/packages/web-app-files/src/views/spaces/Projects.vue b/packages/web-app-files/src/views/spaces/Projects.vue index aef765cca22..3e9868ed8db 100644 --- a/packages/web-app-files/src/views/spaces/Projects.vue +++ b/packages/web-app-files/src/views/spaces/Projects.vue @@ -33,7 +33,7 @@ >
@@ -151,6 +151,7 @@ import { SideBarEventTopics, useSideBar } from '../../composables/sideBar' import { WebDAV } from 'web-client/src/webdav' import { createLocationSpaces } from '../../router' import { createFileRouteOptions } from 'web-pkg/src/helpers/router' +import { useScrollTo } from 'web-app-files/src/composables/scrollTo' export default defineComponent({ components: { @@ -188,6 +189,7 @@ export default defineComponent({ return { ...useSideBar(), + ...useScrollTo(), spaces, graphClient, loadResourcesTask, @@ -261,8 +263,9 @@ export default defineComponent({ immediate: true } }, - created() { - this.loadResourcesTask.perform() + async created() { + await this.loadResourcesTask.perform() + this.scrollToResourceFromRoute(this.spaces) }, methods: { ...mapActions(['showMessage']), @@ -308,7 +311,7 @@ export default defineComponent({ openSidebarSharePanel(space: SpaceResource) { this.SET_FILE_SELECTION([space]) - eventBus.publish(SideBarEventTopics.openWithPanel, 'space-share-item') + eventBus.publish(SideBarEventTopics.openWithPanel, 'space-share') }, getSpaceLinkProps(space: SpaceResource) { diff --git a/packages/web-app-files/tests/__fixtures__/fileActions.js b/packages/web-app-files/tests/__fixtures__/fileActions.js index d236678ad36..729abee8137 100644 --- a/packages/web-app-files/tests/__fixtures__/fileActions.js +++ b/packages/web-app-files/tests/__fixtures__/fileActions.js @@ -61,12 +61,12 @@ const editors = [ const sideBars = [ { - app: 'details-item', + app: 'details', enabled: jest.fn(), icon: 'information' }, { - app: 'actions-item', + app: 'actions', enabled: jest.fn(), icon: 'information' } diff --git a/packages/web-app-files/tests/mocks/useResourcesViewDefaultsMock.ts b/packages/web-app-files/tests/mocks/useResourcesViewDefaultsMock.ts index d86d79c308f..b675af20efd 100644 --- a/packages/web-app-files/tests/mocks/useResourcesViewDefaultsMock.ts +++ b/packages/web-app-files/tests/mocks/useResourcesViewDefaultsMock.ts @@ -28,6 +28,8 @@ export const useResourcesViewDefaultsMock = ( isResourceInSelection: jest.fn(() => false), sideBarOpen: ref(false), sideBarActivePanel: ref(''), + scrollToResource: jest.fn(), + scrollToResourceFromRoute: jest.fn(), ...options } } diff --git a/packages/web-app-files/tests/unit/composables/composables.setup.ts b/packages/web-app-files/tests/unit/composables/composables.setup.ts index 261ed98bfef..1cec61f1ef3 100644 --- a/packages/web-app-files/tests/unit/composables/composables.setup.ts +++ b/packages/web-app-files/tests/unit/composables/composables.setup.ts @@ -6,13 +6,18 @@ import Vue from 'vue' const localVue = createLocalVue() localVue.use(Vuex) -export const createComposableWrapper = (setup: SetupFunction): Wrapper => +export const createComposableWrapper = ( + setup: SetupFunction, + options = { mocks: undefined, store: undefined } +): Wrapper => mount( defineComponent({ setup, template: `
` }), { - localVue + localVue, + ...(options.mocks && { mocks: options.mocks }), + ...(options.store && { store: options.store }) } ) diff --git a/packages/web-app-files/tests/unit/composables/scrollTo/useScrollTo.spec.ts b/packages/web-app-files/tests/unit/composables/scrollTo/useScrollTo.spec.ts new file mode 100644 index 00000000000..136c0e5da04 --- /dev/null +++ b/packages/web-app-files/tests/unit/composables/scrollTo/useScrollTo.spec.ts @@ -0,0 +1,142 @@ +import { mockDeep } from 'jest-mock-extended' +import { useScrollTo } from 'web-app-files/src/composables/scrollTo' +import { Resource } from 'web-client/src' +import { eventBus } from 'web-pkg/src' +import { defaultComponentMocks } from 'web-test-helpers/src/mocks/defaultComponentMocks' +import { defaultStoreMockOptions } from 'web-test-helpers/src/mocks/store/defaultStoreMockOptions' +import { createComposableWrapper } from '../composables.setup' + +describe('useScrollTo', () => { + it('should be valid', () => { + expect(useScrollTo).toBeDefined() + }) + describe('method "scrollToResource"', () => { + const getHTMLPageObject = () => ({ + getBoundingClientRect: jest.fn(() => ({ bottom: 300, top: 0 })), + scrollIntoView: jest.fn(), + scrollBy: jest.fn(), + offsetHeight: 100 + }) + + it('calls does nothing when no element was found', () => { + const htmlPageObject = getHTMLPageObject() + jest.spyOn(document, 'querySelectorAll').mockImplementation(() => [] as any) + + createComposableWrapper( + () => { + const { scrollToResource } = useScrollTo() + scrollToResource(mockDeep()) + expect(htmlPageObject.scrollIntoView).not.toHaveBeenCalled() + }, + { mocks: defaultComponentMocks(), store: defaultStoreMockOptions } + ) + }) + it('calls "scrollIntoView" when the page bottom is reached', () => { + const htmlPageObject = getHTMLPageObject() + jest.spyOn(document, 'querySelectorAll').mockImplementation(() => [htmlPageObject] as any) + window.innerHeight = 100 + + createComposableWrapper( + () => { + const { scrollToResource } = useScrollTo() + scrollToResource(mockDeep()) + expect(htmlPageObject.scrollIntoView).toHaveBeenCalled() + }, + { mocks: defaultComponentMocks(), store: defaultStoreMockOptions } + ) + }) + it('calls "scrollBy" when the page top is reached', () => { + const htmlPageObject = getHTMLPageObject() + jest.spyOn(document, 'querySelectorAll').mockImplementation(() => [htmlPageObject] as any) + jest + .spyOn(document, 'getElementsByClassName') + .mockImplementation(() => [htmlPageObject] as any) + window.innerHeight = 500 + + createComposableWrapper( + () => { + const { scrollToResource } = useScrollTo() + scrollToResource(mockDeep()) + expect(htmlPageObject.scrollBy).toHaveBeenCalled() + }, + { mocks: defaultComponentMocks(), store: defaultStoreMockOptions } + ) + }) + }) + describe('method "scrollToResourceFromRoute"', () => { + const resourceId = 'someFileId' + + it('does not scroll without the "scrollTo" param', () => { + createComposableWrapper( + () => { + const resource = mockDeep({ id: resourceId }) + const { scrollToResourceFromRoute } = useScrollTo() + const querySelectorAllSpy = jest.spyOn(document, 'querySelectorAll') + scrollToResourceFromRoute([resource]) + expect(querySelectorAllSpy).not.toHaveBeenCalled() + }, + { + mocks: { ...defaultComponentMocks() }, + store: defaultStoreMockOptions + } + ) + }) + it('does not scroll when no resource found', () => { + createComposableWrapper( + () => { + const resource = mockDeep({ id: 'someOtherFileId' }) + const { scrollToResourceFromRoute } = useScrollTo() + const querySelectorAllSpy = jest.spyOn(document, 'querySelectorAll') + scrollToResourceFromRoute([resource]) + expect(querySelectorAllSpy).not.toHaveBeenCalled() + }, + { + mocks: { + ...defaultComponentMocks({ currentRoute: { query: { scrollTo: resourceId } } }) + }, + store: defaultStoreMockOptions + } + ) + }) + it('scrolls to the resource when the "scrollTo" param is given and a resource is found', () => { + createComposableWrapper( + () => { + const resource = mockDeep({ id: resourceId }) + const { scrollToResourceFromRoute } = useScrollTo() + const querySelectorAllSpy = jest.spyOn(document, 'querySelectorAll') + scrollToResourceFromRoute([resource]) + expect(querySelectorAllSpy).toHaveBeenCalled() + }, + { + mocks: { + ...defaultComponentMocks({ + currentRoute: { query: { scrollTo: resourceId } } + }), + $store: { commit: jest.fn() } + }, + store: defaultStoreMockOptions + } + ) + }) + it('opens the sidebar when a resource is found and the "details" param is given', () => { + createComposableWrapper( + () => { + const busStub = jest.spyOn(eventBus, 'publish') + const resource = mockDeep({ id: resourceId }) + const { scrollToResourceFromRoute } = useScrollTo() + scrollToResourceFromRoute([resource]) + expect(busStub).toHaveBeenCalled() + }, + { + mocks: { + ...defaultComponentMocks({ + currentRoute: { query: { scrollTo: resourceId, details: 'details' } } + }), + $store: { commit: jest.fn() } + }, + store: defaultStoreMockOptions + } + ) + }) + }) +}) diff --git a/packages/web-app-files/tests/unit/views/spaces/__snapshots__/Projects.spec.ts.snap b/packages/web-app-files/tests/unit/views/spaces/__snapshots__/Projects.spec.ts.snap index 3eff7e05627..c5983ac0169 100644 --- a/packages/web-app-files/tests/unit/views/spaces/__snapshots__/Projects.spec.ts.snap +++ b/packages/web-app-files/tests/unit/views/spaces/__snapshots__/Projects.spec.ts.snap @@ -8,7 +8,7 @@ exports[`Projects view different files view states lists all available project s
  • -
    +
    @@ -29,7 +29,7 @@ exports[`Projects view different files view states lists all available project s
  • -
    +
    @@ -44,7 +44,7 @@ exports[`Projects view different files view states lists all available project s
  • -
    +
    diff --git a/packages/web-runtime/src/pages/resolvePrivateLink.vue b/packages/web-runtime/src/pages/resolvePrivateLink.vue index 01e6a50911a..0d9545d8a79 100644 --- a/packages/web-runtime/src/pages/resolvePrivateLink.vue +++ b/packages/web-runtime/src/pages/resolvePrivateLink.vue @@ -53,7 +53,8 @@ import { useRouter, queryItemAsString, useCapabilitySpacesEnabled, - useClientService + useClientService, + useRouteQuery } from 'web-pkg/src/composables' import { unref, defineComponent, computed, onMounted, ref, Ref } from 'vue' // import { createLocationSpaces } from 'web-app-files/src/router' @@ -86,6 +87,11 @@ export default defineComponent({ const clientService = useClientService() + const detailsQuery = useRouteQuery('details') + const details = computed(() => { + return queryItemAsString(unref(detailsQuery)) + }) + onMounted(() => { resolvePrivateLinkTask.perform(queryItemAsString(unref(id))) }) @@ -126,12 +132,10 @@ export default defineComponent({ } let fileId - let scrollTo if (unref(resource).type === 'folder') { fileId = unref(resource).fileId } else { fileId = unref(resource).parentFolderId - scrollTo = unref(resource).name path = dirname(path) } @@ -141,7 +145,11 @@ export default defineComponent({ const location: RawLocation = { name: 'files-spaces-generic', params, - query: { ...query, ...(scrollTo && { scrollTo }) } + query: { + ...query, + scrollTo: unref(resource).fileId, + ...(unref(details) && { details: unref(details) }) + } } router.push(location) }) diff --git a/packages/web-runtime/src/pages/resolvePublicLink.vue b/packages/web-runtime/src/pages/resolvePublicLink.vue index 811eb838d94..67d85de661a 100644 --- a/packages/web-runtime/src/pages/resolvePublicLink.vue +++ b/packages/web-runtime/src/pages/resolvePublicLink.vue @@ -103,6 +103,11 @@ export default defineComponent({ ) const isUserContext = useUserContext({ store }) + const detailsQuery = useRouteQuery('details') + const details = computed(() => { + return queryItemAsString(unref(detailsQuery)) + }) + // token info const { loadTokenInfoTask } = useLoadTokenInfo({ clientService, isUserContext }) const tokenInfo = ref(null) @@ -147,7 +152,8 @@ export default defineComponent({ const redirectToPrivateLink = (fileId: string | number) => { return router.push({ name: 'resolvePrivateLink', - params: { fileId: `${fileId}` } + params: { fileId: `${fileId}` }, + ...(unref(details) && { query: { details: unref(details) } }) }) } const resolvePublicLinkTask = useTask(function* (signal, passwordRequired: boolean) { diff --git a/packages/web-test-helpers/src/mocks/store/defaultStoreMockOptions.ts b/packages/web-test-helpers/src/mocks/store/defaultStoreMockOptions.ts index 7bbafea22ba..6976fe3c6e3 100644 --- a/packages/web-test-helpers/src/mocks/store/defaultStoreMockOptions.ts +++ b/packages/web-test-helpers/src/mocks/store/defaultStoreMockOptions.ts @@ -2,6 +2,7 @@ import { filesModuleMockOptions } from './filesModuleMockOptions' import { runtimeModuleMockOptions } from './runtimeModuleMockOptions' export const defaultStoreMockOptions = { + commit: jest.fn(), getters: { capabilities: jest.fn().mockImplementation(() => ({})), configuration: jest diff --git a/tests/acceptance/pageObjects/FilesPageElement/appSideBar.js b/tests/acceptance/pageObjects/FilesPageElement/appSideBar.js index 162c2895077..9f05bf2589d 100644 --- a/tests/acceptance/pageObjects/FilesPageElement/appSideBar.js +++ b/tests/acceptance/pageObjects/FilesPageElement/appSideBar.js @@ -221,7 +221,7 @@ module.exports = { selector: '#oc-file-details-sidebar .details-icon' }, fileInfoIconSmallPreview: { - selector: '#sidebar-panel-%s-item .file_info__icon' + selector: '#sidebar-panel-%s .file_info__icon' }, fileInfoResourceNameAnyType: { selector: `//div[contains(@id, "app-sidebar")]//span[contains(@class, "oc-resource-name") and (@data-test-resource-name=%s or @data-test-resource-path=%s)]`, @@ -253,36 +253,36 @@ module.exports = { }, collaboratorsPanel: { selector: - '//*[@id="sidebar-panel-sharing-item"]//*[contains(@class, "sidebar-panel__body-content")]', + '//*[@id="sidebar-panel-sharing"]//*[contains(@class, "sidebar-panel__body-content")]', locateStrategy: 'xpath' }, collaboratorsPanelMenuItem: { - selector: '//button[@id="sidebar-panel-sharing-item-select"]', + selector: '//button[@id="sidebar-panel-sharing-select"]', locateStrategy: 'xpath' }, linksPanel: { selector: '#oc-files-file-link' }, linksPanelMenuItem: { - selector: '//button[@id="sidebar-panel-links-item-select"]', + selector: '//button[@id="sidebar-panel-links-select"]', locateStrategy: 'xpath' }, actionsPanel: { selector: - '//*[@id="sidebar-panel-actions-item"]//*[contains(@class, "sidebar-panel__body-content")]', + '//*[@id="sidebar-panel-actions"]//*[contains(@class, "sidebar-panel__body-content")]', locateStrategy: 'xpath' }, actionsPanelMenuItem: { - selector: '//button[@id="sidebar-panel-actions-item-select"]', + selector: '//button[@id="sidebar-panel-actions-select"]', locateStrategy: 'xpath' }, detailsPanel: { selector: - '//*[@id="sidebar-panel-details-item"]//*[contains(@class, "sidebar-panel__body-content")]', + '//*[@id="sidebar-panel-details"]//*[contains(@class, "sidebar-panel__body-content")]', locateStrategy: 'xpath' }, detailsPanelMenuItem: { - selector: '//button[@id="sidebar-panel-details-item-select"]', + selector: '//button[@id="sidebar-panel-details-select"]', locateStrategy: 'xpath' }, privateLinkURLCopyButton: { @@ -290,11 +290,11 @@ module.exports = { }, versionsPanel: { selector: - '//*[@id="sidebar-panel-versions-item"]//*[contains(@class, "sidebar-panel__body-content")]', + '//*[@id="sidebar-panel-versions"]//*[contains(@class, "sidebar-panel__body-content")]', locateStrategy: 'xpath' }, versionsPanelMenuItem: { - selector: '//button[@id="sidebar-panel-versions-item-select"]', + selector: '//button[@id="sidebar-panel-versions-select"]', locateStrategy: 'xpath' } } diff --git a/tests/e2e/support/objects/app-files/spaces/actions.ts b/tests/e2e/support/objects/app-files/spaces/actions.ts index be482942d1b..84e5bbf87b9 100644 --- a/tests/e2e/support/objects/app-files/spaces/actions.ts +++ b/tests/e2e/support/objects/app-files/spaces/actions.ts @@ -8,7 +8,7 @@ import { createLink } from '../link/actions' const newSpaceMenuButton = '#new-space-menu-btn' const spaceNameInputField = '.oc-modal input' const actionConfirmButton = '.oc-modal-body-actions-confirm' -const spaceIdSelector = `[data-space-id="%s"]` +const spaceIdSelector = `[data-item-id="%s"]` const spacesRenameOptionSelector = '.oc-files-actions-rename-trigger:visible' const editSpacesSubtitleOptionSelector = '.oc-files-actions-edit-description-trigger:visible' const editQuotaOptionSelector = '.oc-files-actions-edit-quota-trigger:visible' diff --git a/tests/e2e/support/objects/app-files/spaces/utils.ts b/tests/e2e/support/objects/app-files/spaces/utils.ts index 8b52b355426..52c6708b8d4 100644 --- a/tests/e2e/support/objects/app-files/spaces/utils.ts +++ b/tests/e2e/support/objects/app-files/spaces/utils.ts @@ -2,7 +2,7 @@ import { Page } from 'playwright' import util from 'util' const emptySpacesSelector = '//span[@data-msgid="%s"]' -const spaceIdSelector = `[data-space-id="%s"]` +const spaceIdSelector = `[data-item-id="%s"]` export interface searchForSpacesIdsArgs { spaceID: string diff --git a/tests/e2e/support/objects/app-files/utils/sidebar.ts b/tests/e2e/support/objects/app-files/utils/sidebar.ts index 70068b46546..a74e016c301 100644 --- a/tests/e2e/support/objects/app-files/utils/sidebar.ts +++ b/tests/e2e/support/objects/app-files/utils/sidebar.ts @@ -64,8 +64,8 @@ export const openPanel = async ({ page, name }: { page: Page; name: string }): P await backButton.click() await locatorUtils.waitForEvent(currentPanel, 'transitionend') } - const panelSelector = page.locator(`#sidebar-panel-${name}-item-select`) - const nextPanel = page.locator(`#sidebar-panel-${name}-item`) + const panelSelector = page.locator(`#sidebar-panel-${name}-select`) + const nextPanel = page.locator(`#sidebar-panel-${name}`) await panelSelector.click() if (config.ocis) {