From cc0eec875d167bb34583c863473e17c0f533f34d Mon Sep 17 00:00:00 2001 From: Benedikt Kulmann Date: Mon, 10 Oct 2022 14:37:10 +0200 Subject: [PATCH] [full-ci] Resource ids in urls (#7725) Co-authored-by: Jannik Stehle Co-authored-by: Dominik Schmidt Co-authored-by: Jan --- ...nhancement-enable-renaming-received-shares | 7 ++ .../unreleased/enhancement-id-based-routing | 14 +++ docs/getting-started.md | 2 + packages/web-app-draw-io/src/App.vue | 48 ++++---- .../src/components/AppBar/CreateAndUpload.vue | 25 +++-- .../components/FilesList/NotFoundMessage.vue | 20 ++-- .../components/FilesList/ResourceTable.vue | 65 ++++++----- .../src/components/Search/List.vue | 3 +- .../src/components/Search/Preview.vue | 38 ++----- .../components/Shares/SharedWithMeSection.vue | 24 ++-- .../SideBar/Details/FileDetails.vue | 30 ++--- .../components/SideBar/Shares/FileShares.vue | 16 +-- .../SideBar/Shares/Links/DetailsAndEdit.vue | 15 +-- .../composables/upload/useUploadHelpers.ts | 13 ++- .../web-app-files/src/helpers/breadcrumbs.ts | 3 +- .../src/helpers/folderLink/index.ts | 1 + .../src/helpers/folderLink/types.ts | 7 ++ .../src/helpers/resource/actions/upload.ts | 3 +- .../web-app-files/src/helpers/resources.ts | 21 ++-- .../src/mixins/actions/navigate.ts | 66 +++++------ .../src/mixins/actions/rename.ts | 61 +++++++---- .../src/mixins/deleteResources.ts | 20 ++-- .../web-app-files/src/mixins/fileActions.ts | 4 +- .../src/mixins/spaces/actions/deletedFiles.js | 14 +-- .../src/mixins/spaces/actions/navigate.js | 13 +-- packages/web-app-files/src/services/folder.ts | 15 +-- .../src/services/folder/index.ts | 6 +- .../services/folder/legacy/loaderPersonal.ts | 51 --------- .../src/services/folder/loaderPublicFiles.ts | 46 -------- .../src/services/folder/loaderSpace.ts | 103 ++++++++++++++++++ .../src/services/folder/spaces/loaderShare.ts | 75 ------------- .../folder/spaces/loaderSpaceGeneric.ts | 68 ------------ .../src/services/folder/types.ts | 3 + packages/web-app-files/src/store/mutations.ts | 11 -- .../src/views/spaces/DriveRedirect.vue | 13 ++- .../src/views/spaces/DriveResolver.vue | 4 +- .../src/views/spaces/GenericSpace.vue | 78 +++++++++---- .../src/views/spaces/GenericTrash.vue | 5 + .../src/views/spaces/Projects.vue | 16 +-- .../components/AppBar/CreateAndUpload.spec.js | 1 + .../FilesList/NotFoundMessage.spec.ts | 17 +-- .../unit/components/Search/Preview.spec.ts | 3 +- .../tests/unit/helpers/breadcrumbs.spec.js | 6 +- .../tests/unit/mixins/actions/rename.spec.ts | 4 +- .../unit/mixins/spaces/deletedFiles.spec.js | 90 --------------- .../unit/mixins/spaces/deletedFiles.spec.ts | 75 +++++++++++++ .../tests/unit/mixins/spaces/navigate.spec.js | 7 +- packages/web-app-pdf-viewer/src/App.vue | 1 + packages/web-app-preview/src/App.vue | 9 +- packages/web-app-text-editor/src/App.vue | 13 ++- .../web-client/src/helpers/resource/types.ts | 2 +- .../web-client/src/helpers/space/functions.ts | 12 +- .../web-client/src/helpers/space/types.ts | 4 + packages/web-client/src/types.ts | 1 + packages/web-client/src/webdav/getFileInfo.ts | 2 +- packages/web-client/src/webdav/listFiles.ts | 37 +++++-- packages/web-client/src/webdav/moveFiles.ts | 14 ++- .../src/composables/appDefaults/types.ts | 1 + .../composables/appDefaults/useAppDefaults.ts | 3 +- .../appDefaults/useAppFileHandling.ts | 3 +- .../appDefaults/useAppFolderHandling.ts | 20 +++- .../appDefaults/useAppNavigation.ts | 19 +++- .../src/composables/configuration/index.ts | 1 + .../configuration/useConfigurationManager.ts | 5 + .../driveResolver/useDriveResolver.ts | 54 +++++++-- .../composables/router/useFileRouteReplace.ts | 48 ++++++++ packages/web-pkg/src/configuration/manager.ts | 23 +++- packages/web-pkg/src/configuration/types.ts | 8 ++ packages/web-pkg/src/constants/dav.ts | 5 +- packages/web-pkg/src/helpers/router/index.ts | 1 + .../src/helpers/router/routeOptions.ts | 51 +++++++++ .../web-runtime/src/components/UploadInfo.vue | 21 +++- .../src/composables/upload/useUpload.ts | 25 ++++- packages/web-runtime/src/index.ts | 15 ++- .../tests/unit/components/UploadInfo.spec.ts | 10 ++ tests/drone/config-oc10-oauth.json | 3 + tests/drone/config-oc10-openid.json | 3 + tests/drone/config-ocis.json | 3 + .../mocks/store/filesModuleMockOptions.ts | 3 +- 79 files changed, 952 insertions(+), 698 deletions(-) create mode 100644 changelog/unreleased/enhancement-enable-renaming-received-shares create mode 100644 changelog/unreleased/enhancement-id-based-routing create mode 100644 packages/web-app-files/src/helpers/folderLink/index.ts create mode 100644 packages/web-app-files/src/helpers/folderLink/types.ts delete mode 100644 packages/web-app-files/src/services/folder/legacy/loaderPersonal.ts delete mode 100644 packages/web-app-files/src/services/folder/loaderPublicFiles.ts create mode 100644 packages/web-app-files/src/services/folder/loaderSpace.ts delete mode 100644 packages/web-app-files/src/services/folder/spaces/loaderShare.ts delete mode 100644 packages/web-app-files/src/services/folder/spaces/loaderSpaceGeneric.ts create mode 100644 packages/web-app-files/src/services/folder/types.ts delete mode 100644 packages/web-app-files/tests/unit/mixins/spaces/deletedFiles.spec.js create mode 100644 packages/web-app-files/tests/unit/mixins/spaces/deletedFiles.spec.ts create mode 100644 packages/web-pkg/src/composables/configuration/index.ts create mode 100644 packages/web-pkg/src/composables/configuration/useConfigurationManager.ts create mode 100644 packages/web-pkg/src/composables/router/useFileRouteReplace.ts create mode 100644 packages/web-pkg/src/helpers/router/index.ts create mode 100644 packages/web-pkg/src/helpers/router/routeOptions.ts diff --git a/changelog/unreleased/enhancement-enable-renaming-received-shares b/changelog/unreleased/enhancement-enable-renaming-received-shares new file mode 100644 index 00000000000..f995432567f --- /dev/null +++ b/changelog/unreleased/enhancement-enable-renaming-received-shares @@ -0,0 +1,7 @@ +Enhancement: Enable renaming on received shares + +As a receiver the user can rename a share which will only take effect for the respective user +but won't change the name for the sharee or other share receivers. + +https://github.com/owncloud/web/pull/7725 +https://github.com/owncloud/web/issues/6247 diff --git a/changelog/unreleased/enhancement-id-based-routing b/changelog/unreleased/enhancement-id-based-routing new file mode 100644 index 00000000000..328afa35a63 --- /dev/null +++ b/changelog/unreleased/enhancement-id-based-routing @@ -0,0 +1,14 @@ +Enhancement: Id based routing + +We now include fileIds in the URL query to be able to +- resolve files and spaces correctly and +- resolve the correct relative path of a file if it was changed (this might be the case for bookmarks) +The fileIds in the URL can be disabled by setting `options.routing.idBased` to `false` in the `config.json`. + +Note: It's recommended to keep the default of fileIds being used in routing. Otherwise it's not possible +to resolve spaces with name clashes correctly. + +https://github.com/owncloud/web/issues/6247 +https://github.com/owncloud/web/pull/7725 +https://github.com/owncloud/web/issues/7714 +https://github.com/owncloud/web/issues/7715 diff --git a/docs/getting-started.md b/docs/getting-started.md index f5eb8c928f2..5c2512923fe 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -60,6 +60,8 @@ substring of a value of the authenticated user. Examples are `/Shares`, `/{{.Id} - `options.cernFeatures` Enabling this will activate CERN-specific features. Defaults to `false`. - `options.hoverableQuickActions` Set this option to `true` to hide the quick actions (buttons appearing on file rows), and only show them when the user hovers the row with his mouse. Defaults to `false`. +- `option.routing` This accepts an object with the following fields to customize the routing behaviour: + - `options.routing.idBased` Enable or disable fileIds being added to the URL. Defaults to `true` because otherwise e.g. spaces with name clashes can't be resolved correctly. Only disable this if you can guarantee server side that spaces of the same namespace can't have name clashes. ### Sentry diff --git a/packages/web-app-draw-io/src/App.vue b/packages/web-app-draw-io/src/App.vue index 524649f402c..ff9f4085340 100644 --- a/packages/web-app-draw-io/src/App.vue +++ b/packages/web-app-draw-io/src/App.vue @@ -61,10 +61,17 @@ export default defineComponent({ return `${this.config.url}?${query}` } }, + watch: { + currentFileContext: { + handler: function () { + this.load() + }, + immediate: true + } + }, created() { this.filePath = this.currentFileContext.path this.fileExtension = this.filePath.split('.').pop() - this.checkPermissions() window.addEventListener('message', (event) => { if (event.data.length > 0) { const payload = JSON.parse(event.data) @@ -114,8 +121,9 @@ export default defineComponent({ async checkPermissions() { try { const resource = await this.getFileInfo(this.currentFileContext, { - davProperties: [DavProperty.Permissions] + davProperties: [DavProperty.FileId, DavProperty.Permissions] }) + this.replaceInvalidFileRoute(this.currentFileContext, resource) this.isReadOnly = ![DavPermission.Updateable, DavPermission.FileUpdateable].some( (p) => (resource.permissions || '').indexOf(p) > -1 ) @@ -124,24 +132,26 @@ export default defineComponent({ this.errorPopup(error) } }, - load() { - this.getFileContents(this.currentFileContext) - .then((resp) => { - this.currentETag = resp.headers.ETag - this.$refs.drawIoEditor.contentWindow.postMessage( - JSON.stringify({ - action: 'load', - xml: resp.body, - autosave: this.config.autosave - }), - '*' - ) - }) - .catch((error) => { - this.errorPopup(error) - }) + async loadFileContent() { + try { + const response = await this.getFileContents(this.currentFileContext) + this.currentETag = response.headers.ETag + this.$refs.drawIoEditor.contentWindow.postMessage( + JSON.stringify({ + action: 'load', + xml: response.body, + autosave: this.config.autosave + }), + '*' + ) + } catch (error) { + this.errorPopup(error) + } + }, + async load() { + await Promise.all([this.checkPermissions(), this.loadFileContent()]) }, - async importVisio() { + importVisio() { const getDescription = () => this.$gettextInterpolate( this.$gettext('The diagram will open as a new .drawio file: %{file}'), diff --git a/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue b/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue index 9907b31cd20..41d347cd742 100644 --- a/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue +++ b/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue @@ -183,6 +183,11 @@ export default defineComponent({ type: Boolean, default: false, required: false + }, + itemId: { + type: [String, Number], + required: false, + default: null } }, setup(props) { @@ -212,7 +217,8 @@ export default defineComponent({ }), ...useUploadHelpers({ space: computed(() => props.space), - currentFolder: computed(() => props.item) + currentFolder: computed(() => props.item), + currentFolderId: computed(() => props.itemId) }), ...useRequest(), ...useGraphClient(), @@ -333,9 +339,10 @@ export default defineComponent({ return } + const { spaceId, currentFolder, currentFolderId } = file.meta if (!['public', 'share'].includes(file.meta.driveType)) { if (this.hasSpaces) { - const driveResponse = await this.graphClient.drives.getDrive(file.meta.spaceId) + const driveResponse = await this.graphClient.drives.getDrive(spaceId) this.UPDATE_SPACE_FIELD({ id: driveResponse.data.id, field: 'spaceQuota', @@ -347,8 +354,10 @@ export default defineComponent({ } } - const fileIsInCurrentPath = - file.meta.spaceId === this.space.id && file.meta.currentFolder === this.item + const sameFolder = this.itemId + ? currentFolderId === this.itemId + : currentFolder === this.item + const fileIsInCurrentPath = spaceId === this.space.id && sameFolder if (fileIsInCurrentPath) { bus.publish('app.files.list.load') } @@ -501,12 +510,11 @@ export default defineComponent({ this.UPSERT_RESOURCE(resource) if (this.newFileAction) { - const fileId = resource.fileId - this.$_fileActions_openEditor( this.newFileAction, this.space.getDriveAliasAndItem(resource), - fileId, + resource.webDavPath, + resource.fileId, EDITOR_MODE_CREATE ) this.hideModal() @@ -615,7 +623,7 @@ export default defineComponent({ return null }, - async onFilesSelected(filesToUpload: File[]) { + onFilesSelected(filesToUpload: File[]) { const uploader = new ResourcesUpload( filesToUpload, this.files, @@ -623,6 +631,7 @@ export default defineComponent({ this.$uppyService, this.space, this.item, + this.itemId, this.spaces, this.hasSpaces, this.createDirectoryTree, diff --git a/packages/web-app-files/src/components/FilesList/NotFoundMessage.vue b/packages/web-app-files/src/components/FilesList/NotFoundMessage.vue index 4a1e6cb9b72..6e37dcd2175 100644 --- a/packages/web-app-files/src/components/FilesList/NotFoundMessage.vue +++ b/packages/web-app-files/src/components/FilesList/NotFoundMessage.vue @@ -51,10 +51,10 @@ import { isLocationPublicActive, isLocationSpacesActive } from '../../router' -import { useRouter, useStore } from 'web-pkg/src/composables' +import { useRouter } from 'web-pkg/src/composables' import { defineComponent, PropType } from '@vue/composition-api' -import { Resource } from 'web-client' import { SpaceResource } from 'web-client/src/helpers' +import { createFileRouteOptions } from 'web-pkg/src/helpers/router' export default defineComponent({ name: 'NotFoundMessage', @@ -67,7 +67,6 @@ export default defineComponent({ }, setup(props) { const router = useRouter() - const store = useStore() const isProjectSpace = props.space?.driveType === 'project' return { showPublicLinkButton: isLocationPublicActive(router, 'files-public-link'), @@ -75,18 +74,13 @@ export default defineComponent({ showSpacesButton: isLocationSpacesActive(router, 'files-spaces-generic') && isProjectSpace, homeRoute: createLocationSpaces('files-spaces-generic', { params: { - driveAliasAndItem: props.space?.getDriveAliasAndItem({ - path: store.getters.homeFolder - } as Resource) - } - }), - publicLinkRoute: createLocationPublic('files-public-link', { - params: { - driveAliasAndItem: props.space?.getDriveAliasAndItem({ - path: '' - } as Resource) + driveAliasAndItem: 'personal' } }), + publicLinkRoute: createLocationPublic( + 'files-public-link', + createFileRouteOptions(props.space, {}) + ), spacesRoute: createLocationSpaces('files-spaces-projects') } } diff --git a/packages/web-app-files/src/components/FilesList/ResourceTable.vue b/packages/web-app-files/src/components/FilesList/ResourceTable.vue index a0bcef2cad8..e09a1131d2e 100644 --- a/packages/web-app-files/src/components/FilesList/ResourceTable.vue +++ b/packages/web-app-files/src/components/FilesList/ResourceTable.vue @@ -192,7 +192,6 @@ import maxSize from 'popper-max-size-modifier' import { mapGetters, mapActions, mapState } from 'vuex' import { EVENT_TROW_MOUNTED, EVENT_FILE_DROPPED } from '../../constants' import { SortDir } from '../../composables' -import * as path from 'path' import { determineSortFields } from '../../helpers/ui/resourceTable' import { useCapabilityProjectSpacesEnabled, @@ -208,6 +207,9 @@ import { formatDateFromJSDate, formatRelativeDateFromJSDate } from 'web-pkg/src/ import { SideBarEventTopics } from '../../composables/sideBar' import { buildShareSpaceResource, extractDomSelector, SpaceResource } from 'web-client/src/helpers' import { configurationManager } from 'web-pkg/src/configuration' +import { CreateTargetRouteOptions } from '../../helpers/folderLink' +import { createFileRouteOptions } from 'web-pkg/src/helpers/router' +import { basename, dirname } from 'path' export default defineComponent({ mixins: [Rename], @@ -589,7 +591,7 @@ export default defineComponent({ .length }, openRenameDialog(item) { - this.$_rename_trigger({ resources: [item] }, this.getMatchingSpace(item.storageId)) + this.$_rename_trigger({ resources: [item] }, this.getMatchingSpace(item)) }, openTagsSidebar() { bus.publish(SideBarEventTopics.open) @@ -606,45 +608,42 @@ export default defineComponent({ bus.publish(SideBarEventTopics.openWithPanel, panelToOpen) }, folderLink(file) { - return this.createFolderLink(file.path, file) + return this.createFolderLink({ path: file.path, fileId: file.fileId, resource: file }) }, parentFolderLink(file) { if (file.shareId && file.path === '/') { return createLocationShares('files-shares-with-me') } - return this.createFolderLink(path.dirname(file.path), file) + return this.createFolderLink({ + path: dirname(file.path), + ...(file.parentFolderId && { fileId: file.parentFolderId }), + resource: file + }) }, - createFolderLink(p, resource) { + createFolderLink(options: CreateTargetRouteOptions) { if (this.targetRouteCallback) { - return this.targetRouteCallback(p, resource) + return this.targetRouteCallback(options) } + const { path, fileId, resource } = options + let space if (resource.shareId) { - const space = buildShareSpaceResource({ + space = buildShareSpaceResource({ shareId: resource.shareId, - shareName: path.basename(resource.shareRoot), + shareName: basename(resource.shareRoot), serverUrl: configurationManager.serverUrl }) - return createLocationSpaces('files-spaces-generic', { - params: { - driveAliasAndItem: space.getDriveAliasAndItem({ path: p } as Resource) - }, - query: { - shareId: resource.shareId - } - }) + } else { + space = this.getMatchingSpace(resource) } - - const matchingSpace = this.getMatchingSpace(resource.storageId) - if (!matchingSpace) { + if (!space) { return {} } - return createLocationSpaces('files-spaces-generic', { - params: { - driveAliasAndItem: matchingSpace.getDriveAliasAndItem({ path: p } as Resource) - } - }) + return createLocationSpaces( + 'files-spaces-generic', + createFileRouteOptions(space, { path, fileId }) + ) }, fileDragged(file) { this.addSelectedResource(file) @@ -751,7 +750,7 @@ export default defineComponent({ this.emitSelect(this.resources.map((resource) => resource.id)) }, emitFileClick(resource) { - let space = this.getMatchingSpace(resource.storageId) + let space = this.getMatchingSpace(resource) if (!space) { space = buildShareSpaceResource({ shareId: resource.shareId, @@ -817,12 +816,20 @@ export default defineComponent({ ownerName: resource.owner[0].displayName }) }, - getMatchingSpace(storageId): SpaceResource { - return this.space || this.spaces.find((space) => space.id === storageId) + getMatchingSpace(resource: Resource): SpaceResource { + return ( + this.space || + this.spaces.find((space) => space.id === resource.storageId) || + buildShareSpaceResource({ + shareId: resource.shareId, + shareName: resource.name, + serverUrl: configurationManager.serverUrl + }) + ) }, getDefaultParentFolderName(resource) { if (this.hasProjectSpaces) { - const matchingSpace = this.getMatchingSpace(resource.storageId) + const matchingSpace = this.getMatchingSpace(resource) if (matchingSpace?.driveType === 'project') { return matchingSpace.name } @@ -835,7 +842,7 @@ export default defineComponent({ if (resource.shareId) { return resource.path === '/' ? this.$gettext('Shared with me') - : path.basename(resource.shareRoot) + : basename(resource.shareRoot) } return this.$gettext('Personal') diff --git a/packages/web-app-files/src/components/Search/List.vue b/packages/web-app-files/src/components/Search/List.vue index 1e0a23d5d78..900aa75ad71 100644 --- a/packages/web-app-files/src/components/Search/List.vue +++ b/packages/web-app-files/src/components/Search/List.vue @@ -86,6 +86,7 @@ import SideBar from '../../components/SideBar/SideBar.vue' import { buildShareSpaceResource, SpaceResource } from 'web-client/src/helpers' import { useStore } from 'web-pkg/src/composables' import { configurationManager } from 'web-pkg/src/configuration' +import { basename } from 'path' const visibilityObserver = new VisibilityObserver() @@ -120,7 +121,7 @@ export default defineComponent({ if (resource.shareId) { return buildShareSpaceResource({ shareId: resource.shareId, - shareName: resource.name, + shareName: basename(resource.shareRoot), serverUrl: configurationManager.serverUrl }) } diff --git a/packages/web-app-files/src/components/Search/Preview.vue b/packages/web-app-files/src/components/Search/Preview.vue index aed5bf19aa1..e9aef6eacd5 100644 --- a/packages/web-app-files/src/components/Search/Preview.vue +++ b/packages/web-app-files/src/components/Search/Preview.vue @@ -31,9 +31,10 @@ import { createLocationShares, createLocationSpaces } from '../../router' import { basename, dirname } from 'path' import { useAccessToken, useCapabilityShareJailEnabled, useStore } from 'web-pkg/src/composables' import { defineComponent } from '@vue/composition-api' -import { buildShareSpaceResource, Resource } from 'web-client/src/helpers' +import { buildShareSpaceResource } from 'web-client/src/helpers' import { configurationManager } from 'web-pkg/src/configuration' import { bus } from 'web-pkg/src/instance' +import { createFileRouteOptions } from 'web-pkg/src/helpers/router' const visibilityObserver = new VisibilityObserver() @@ -67,7 +68,7 @@ export default defineComponent({ attrs() { return this.resource.isFolder ? { - to: this.createFolderLink(this.resource.path) + to: this.createFolderLink(this.resource.path, this.resource.fileId) } : {} }, @@ -93,7 +94,7 @@ export default defineComponent({ return buildShareSpaceResource({ shareId: this.resource.shareId, - shareName: this.resource.name, + shareName: basename(this.resource.shareRoot), serverUrl: configurationManager.serverUrl }) }, @@ -118,13 +119,13 @@ export default defineComponent({ return !this.configuration?.options?.disablePreviews }, folderLink() { - return this.createFolderLink(this.resource.path) + return this.createFolderLink(this.resource.path, this.resource.fileId) }, parentFolderLink() { if (this.resource.shareId && this.resource.path === '/') { return createLocationShares('files-shares-with-me') } - return this.createFolderLink(dirname(this.resource.path)) + return this.createFolderLink(dirname(this.resource.path), this.resource.parentFolderId) } }, mounted() { @@ -157,32 +158,15 @@ export default defineComponent({ parentFolderClicked() { bus.publish('app.search.options-drop.hide') }, - createFolderLink(p: string) { - if (this.resource.shareId) { - const space = buildShareSpaceResource({ - shareId: this.resource.shareId, - shareName: basename(this.resource.shareRoot), - serverUrl: configurationManager.serverUrl - }) - return createLocationSpaces('files-spaces-generic', { - params: { - driveAliasAndItem: space.getDriveAliasAndItem({ path: p } as Resource) - }, - query: { - shareId: this.resource.shareId - } - }) - } - + createFolderLink(p: string, fileId: string | number) { if (!this.matchingSpace) { return {} } - return createLocationSpaces('files-spaces-generic', { - params: { - driveAliasAndItem: this.matchingSpace.getDriveAliasAndItem({ path: p } as Resource) - } - }) + return createLocationSpaces( + 'files-spaces-generic', + createFileRouteOptions(this.matchingSpace, { path: p, fileId }) + ) } } }) diff --git a/packages/web-app-files/src/components/Shares/SharedWithMeSection.vue b/packages/web-app-files/src/components/Shares/SharedWithMeSection.vue index 8fa9faca019..8fe80bf4ff2 100644 --- a/packages/web-app-files/src/components/Shares/SharedWithMeSection.vue +++ b/packages/web-app-files/src/components/Shares/SharedWithMeSection.vue @@ -105,10 +105,11 @@ import ContextActions from '../../components/FilesList/ContextActions.vue' import NoContentMessage from 'web-pkg/src/components/NoContentMessage.vue' import { useSelectedResources } from '../../composables/selection' import { SortDir } from '../../composables' -import { Resource } from 'web-client' import { Location } from 'vue-router' import { buildShareSpaceResource } from 'web-client/src/helpers' import { configurationManager } from 'web-pkg/src/configuration' +import { CreateTargetRouteOptions } from '../../helpers/folderLink' +import { createFileRouteOptions } from 'web-pkg/src/helpers/router' const visibilityObserver = new VisibilityObserver() @@ -186,24 +187,29 @@ export default defineComponent({ setup() { const store = useStore() const hasShareJail = useCapabilityShareJailEnabled() - const resourceTargetRouteCallback = (path: string, resource: Resource): Location => { + const resourceTargetRouteCallback = ({ + path, + fileId, + resource + }: CreateTargetRouteOptions): Location => { if (unref(hasShareJail)) { const space = buildShareSpaceResource({ shareId: resource.id, shareName: resource.name, serverUrl: configurationManager.serverUrl }) - return createLocationSpaces('files-spaces-generic', { - params: { driveAliasAndItem: space.getDriveAliasAndItem({ path } as Resource) }, - query: { shareId: resource.id } - }) + return createLocationSpaces( + 'files-spaces-generic', + createFileRouteOptions(space, { path, fileId }) + ) } const personalSpace = store.getters['runtime/spaces/spaces'].find( (space) => space.driveType === 'personal' ) - return createLocationSpaces('files-spaces-generic', { - params: { driveAliasAndItem: personalSpace.getDriveAliasAndItem({ path } as Resource) } - }) + return createLocationSpaces( + 'files-spaces-generic', + createFileRouteOptions(personalSpace, { path, fileId }) + ) } const personalSpace = computed(() => { 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 34d2d0bc0ba..acca6e72493 100644 --- a/packages/web-app-files/src/components/SideBar/Details/FileDetails.vue +++ b/packages/web-app-files/src/components/SideBar/Details/FileDetails.vue @@ -175,6 +175,7 @@ import { SideBarEventTopics } from '../../../composables/sideBar' import { Resource } from 'web-client' import { buildShareSpaceResource } from 'web-client/src/helpers' import { configurationManager } from 'web-pkg/src/configuration' +import { createFileRouteOptions } from 'web-pkg/src/helpers/router' export default defineComponent({ name: 'FileDetails', @@ -183,10 +184,12 @@ export default defineComponent({ }, setup() { const sharedParentDir = ref('') + const sharedParentFileId = ref('') const store = useStore() return { sharedParentDir, + sharedParentFileId, isPublicLinkContext: usePublicLinkContext({ store }), accessToken: useAccessToken({ store }), space: inject>('displayedSpace') @@ -270,25 +273,21 @@ export default defineComponent({ shareName: basename(this.file.shareRoot), serverUrl: configurationManager.serverUrl }) - return createLocationSpaces('files-spaces-generic', { - params: { - driveAliasAndItem: space.getDriveAliasAndItem({ path: this.file.path } as Resource) - }, - query: { - shareId: this.file.shareId - } - }) + return createLocationSpaces( + 'files-spaces-generic', + createFileRouteOptions(space, { path: this.file.path, fileId: this.file.fileId }) + ) } if (!this.matchingSpace) { return {} } - return createLocationSpaces('files-spaces-generic', { - params: { - driveAliasAndItem: this.matchingSpace.getDriveAliasAndItem({ - path: this.sharedParentDir - } as Resource) - } - }) + return createLocationSpaces( + 'files-spaces-generic', + createFileRouteOptions(this.matchingSpace, { + path: this.sharedParentDir, + fileId: this.sharedParentFileId + }) + ) }, showShares() { if (this.isPublicLinkContext) { @@ -416,6 +415,7 @@ export default defineComponent({ } this.sharedTime = this.sharedItem.stime this.sharedParentDir = sharePathParentOrCurrent + this.sharedParentFileId = shares[0].file?.source }, immediate: true } diff --git a/packages/web-app-files/src/components/SideBar/Shares/FileShares.vue b/packages/web-app-files/src/components/SideBar/Shares/FileShares.vue index 5b8fadb82c5..b1fb5e8e2d7 100644 --- a/packages/web-app-files/src/components/SideBar/Shares/FileShares.vue +++ b/packages/web-app-files/src/components/SideBar/Shares/FileShares.vue @@ -83,8 +83,8 @@ import { shareInviteCollaboratorHelpCern } from '../../../helpers/contextualHelpers' import { computed, defineComponent, PropType } from '@vue/composition-api' -import { Resource } from 'web-client' import { SpaceResource } from 'web-client/src/helpers' +import { createFileRouteOptions } from 'web-pkg/src/helpers/router' export default defineComponent({ name: 'FileShares', @@ -333,13 +333,13 @@ export default defineComponent({ // TODO: this doesn't work on files-spaces-share routes?! if (this.space && this.sharesTree[parentShare.path]) { - return createLocationSpaces('files-spaces-generic', { - params: { - driveAliasAndItem: this.space.getDriveAliasAndItem({ - path: parentShare.path - } as Resource) - } - }) + return createLocationSpaces( + 'files-spaces-generic', + createFileRouteOptions(this.space, { + path: parentShare.path, + fileId: parentShare.file.source + }) + ) } return null diff --git a/packages/web-app-files/src/components/SideBar/Shares/Links/DetailsAndEdit.vue b/packages/web-app-files/src/components/SideBar/Shares/Links/DetailsAndEdit.vue index a67303321ac..ffcd842e213 100644 --- a/packages/web-app-files/src/components/SideBar/Shares/Links/DetailsAndEdit.vue +++ b/packages/web-app-files/src/components/SideBar/Shares/Links/DetailsAndEdit.vue @@ -186,9 +186,9 @@ import { createLocationSpaces } from '../../../../router' import { LinkShareRoles } from 'web-client/src/helpers/share' import { defineComponent } from '@vue/runtime-core' import { formatDateFromDateTime, formatRelativeDateFromDateTime } from 'web-pkg/src/helpers' -import { Resource } from 'web-client' import { SpaceResource } from 'web-client/src/helpers' import { PropType } from '@vue/composition-api' +import { createFileRouteOptions } from 'web-pkg/src/helpers/router' export default defineComponent({ name: 'DetailsAndEdit', @@ -340,18 +340,19 @@ export default defineComponent({ }, viaRouterParams() { - const viaPath = this.link.path const matchingSpace = (this.space || this.spaces.find((space) => space.id === this.file.storageId)) as SpaceResource if (!matchingSpace) { return {} } - return createLocationSpaces('files-spaces-generic', { - params: { - driveAliasAndItem: matchingSpace.getDriveAliasAndItem({ path: viaPath } as Resource) - } - }) + return createLocationSpaces( + 'files-spaces-generic', + createFileRouteOptions(matchingSpace, { + path: this.link.path, + fileId: this.link.file.source + }) + ) }, localExpirationDate() { diff --git a/packages/web-app-files/src/composables/upload/useUploadHelpers.ts b/packages/web-app-files/src/composables/upload/useUploadHelpers.ts index 50e345c466e..2a2dc4d2e37 100644 --- a/packages/web-app-files/src/composables/upload/useUploadHelpers.ts +++ b/packages/web-app-files/src/composables/upload/useUploadHelpers.ts @@ -10,6 +10,7 @@ import { urlJoin } from 'web-pkg/src/utils' interface UploadHelpersOptions { space: ComputedRef currentFolder?: ComputedRef + currentFolderId?: ComputedRef } interface UploadHelpersResult { @@ -20,6 +21,7 @@ interface inputFileOptions { route: Ref space: Ref currentFolder: Ref + currentFolderId?: Ref } export function useUploadHelpers(options: UploadHelpersOptions): UploadHelpersResult { @@ -27,7 +29,8 @@ export function useUploadHelpers(options: UploadHelpersOptions): UploadHelpersRe inputFilesToUppyFiles: inputFilesToUppyFiles({ route: useRoute(), space: options.space, - currentFolder: options.currentFolder + currentFolder: options.currentFolder, + currentFolderId: options.currentFolderId }) } } @@ -45,7 +48,12 @@ const getRelativeFilePath = (file: File): string | undefined => { return urlJoin(relativePath) } -const inputFilesToUppyFiles = ({ route, space, currentFolder }: inputFileOptions) => { +const inputFilesToUppyFiles = ({ + route, + space, + currentFolder, + currentFolderId +}: inputFileOptions) => { return (files: File[]): UppyResource[] => { const uppyFiles: UppyResource[] = [] @@ -86,6 +94,7 @@ const inputFilesToUppyFiles = ({ route, space, currentFolder }: inputFileOptions driveAlias: unref(space).driveAlias, driveType: unref(space).driveType, currentFolder: unref(currentFolder), + currentFolderId: unref(currentFolderId), // upload data relativeFolder: directory, relativePath: relativeFilePath, // uppy needs this property to be named relativePath diff --git a/packages/web-app-files/src/helpers/breadcrumbs.ts b/packages/web-app-files/src/helpers/breadcrumbs.ts index 5abd7898a53..75e7cf3b3ea 100644 --- a/packages/web-app-files/src/helpers/breadcrumbs.ts +++ b/packages/web-app-files/src/helpers/breadcrumbs.ts @@ -1,5 +1,6 @@ import { bus } from 'web-pkg/src/instance' import { Location } from 'vue-router' +import omit from 'lodash-es/omit' export interface BreadcrumbItem { text: string @@ -23,7 +24,7 @@ export const breadcrumbsFromPath = ( text, to: { path: '/' + [...current].splice(0, current.length - resource.length + i + 1).join('/'), - query: currentRoute.query + query: omit(currentRoute.query, 'fileId') // TODO: we need the correct fileId in the query. until we have that we must omit it because otherwise we would correct the path to the one of the (wrong) fileId. } } as BreadcrumbItem) ) diff --git a/packages/web-app-files/src/helpers/folderLink/index.ts b/packages/web-app-files/src/helpers/folderLink/index.ts new file mode 100644 index 00000000000..c9f6f047dc0 --- /dev/null +++ b/packages/web-app-files/src/helpers/folderLink/index.ts @@ -0,0 +1 @@ +export * from './types' diff --git a/packages/web-app-files/src/helpers/folderLink/types.ts b/packages/web-app-files/src/helpers/folderLink/types.ts new file mode 100644 index 00000000000..fda2867cf18 --- /dev/null +++ b/packages/web-app-files/src/helpers/folderLink/types.ts @@ -0,0 +1,7 @@ +import { Resource } from 'web-client' + +export interface CreateTargetRouteOptions { + path: string + fileId?: string | number + resource: Resource +} diff --git a/packages/web-app-files/src/helpers/resource/actions/upload.ts b/packages/web-app-files/src/helpers/resource/actions/upload.ts index a1055e163e1..c4d226180f7 100644 --- a/packages/web-app-files/src/helpers/resource/actions/upload.ts +++ b/packages/web-app-files/src/helpers/resource/actions/upload.ts @@ -20,6 +20,7 @@ export class ResourcesUpload extends ConflictDialog { private $uppyService: any, private space: SpaceResource, private currentFolder: string, + private currentFolderId: string | number, private spaces: SpaceResource[], private hasSpaces: boolean, private createDirectoryTree: any, @@ -78,7 +79,7 @@ export class ResourcesUpload extends ConflictDialog { async handleUppyFileUpload(space: SpaceResource, currentFolder: string, files: UppyResource[]) { this.$uppyService.publish('uploadStarted') - await this.createDirectoryTree(space, currentFolder, files) + await this.createDirectoryTree(space, currentFolder, files, this.currentFolderId) this.$uppyService.publish('addedForUpload', files) this.$uppyService.uploadFiles(files) } diff --git a/packages/web-app-files/src/helpers/resources.ts b/packages/web-app-files/src/helpers/resources.ts index 273cd7e1801..606c2bd275b 100644 --- a/packages/web-app-files/src/helpers/resources.ts +++ b/packages/web-app-files/src/helpers/resources.ts @@ -16,8 +16,7 @@ import { } from 'web-client/src/helpers/share' import { extractExtensionFromFile, extractStorageId } from './resource' import { buildWebDavSpacesPath, extractDomSelector } from 'web-client/src/helpers/resource' -import { SHARE_JAIL_ID } from '../services/folder' -import { Resource, SpaceResource } from 'web-client/src/helpers' +import { Resource, SpaceResource, SHARE_JAIL_ID } from 'web-client/src/helpers' import { urlJoin } from 'web-pkg/src/utils' export function renameResource(space: SpaceResource, resource: Resource, newPath: string) { @@ -45,11 +44,11 @@ export function buildResource(resource): Resource { } const id = resource.fileInfo[DavProperty.FileId] - return { id, - fileId: resource.fileInfo[DavProperty.FileId], - storageId: extractStorageId(resource.fileInfo[DavProperty.FileId]), + fileId: id, + storageId: extractStorageId(id), + parentFolderId: resource.fileInfo[DavProperty.FileParent], mimeType: resource.fileInfo[DavProperty.MimeType], name, extension: isFolder ? '' : extension, @@ -252,6 +251,7 @@ export function buildSharedResource( id: share.id, fileId: share.item_source, storageId: extractStorageId(share.item_source), + parentFolderId: share.file_parent, type: share.item_type, mimeType: share.mimetype, isFolder, @@ -288,7 +288,7 @@ export function buildSharedResource( } resource.canDownload = () => parseInt(share.state) === ShareStatus.accepted resource.canShare = () => SharePermissions.share.enabled(share.permissions) - resource.canRename = () => SharePermissions.update.enabled(share.permissions) + resource.canRename = () => parseInt(share.state) === ShareStatus.accepted resource.canBeDeleted = () => SharePermissions.delete.enabled(share.permissions) resource.canEditTags = () => parseInt(share.state) === ShareStatus.accepted && @@ -422,7 +422,13 @@ function _fixAdditionalInfo(data) { export function buildCollaboratorShare(s, file, allowSharePermission): Share { const share: Share = { shareType: parseInt(s.share_type), - id: s.id + id: s.id, + itemSource: s.item_source, + file: { + parent: s.file_parent, + source: s.file_source, + target: s.file_target + } } if ( ShareTypes.containsAnyValue( @@ -479,6 +485,7 @@ export function buildDeletedResource(resource): Resource { extension, path: urlJoin(resource.fileInfo[DavProperty.TrashbinOriginalLocation], { leadingSlash: true }), id, + parentFolderId: resource.fileInfo[DavProperty.FileParent], indicators: [], canUpload: () => false, canDownload: () => false, diff --git a/packages/web-app-files/src/mixins/actions/navigate.ts b/packages/web-app-files/src/mixins/actions/navigate.ts index 81687b4999e..97771faeb55 100644 --- a/packages/web-app-files/src/mixins/actions/navigate.ts +++ b/packages/web-app-files/src/mixins/actions/navigate.ts @@ -9,6 +9,9 @@ import { } from '../../router' import { ShareStatus } from 'web-client/src/helpers/share' import merge from 'lodash-es/merge' +import { buildShareSpaceResource } from 'web-client/src/helpers' +import { configurationManager } from 'web-pkg/src/configuration' +import { createFileRouteOptions } from 'web-pkg/src/helpers/router' export default { computed: { @@ -49,18 +52,14 @@ export default { canBeDefault: true, componentType: 'router-link', route: ({ resources }) => { - const path = this.getPath(resources[0]) - const driveAliasAndItem = this.getDriveAliasAndItem(resources[0]) - const shareId = this.getShareId(resources[0]) - return merge({}, this.routeName, { - params: { - ...(path && { path }), - ...(driveAliasAndItem && { driveAliasAndItem }) - }, - query: { - ...(shareId && { shareId }) - } - }) + return merge( + {}, + this.routeName, + createFileRouteOptions(this.$_navigate_getSpace(resources[0]), { + path: resources[0].path, + fileId: resources[0].fileId + }) + ) }, class: 'oc-files-actions-navigate-trigger' } @@ -75,35 +74,24 @@ export default { } }, methods: { - getPath(resource) { - if (!isLocationPublicActive(this.$router, 'files-public-link')) { - return null + $_navigate_getSpace(resource) { + if (this.space) { + return this.space } - return resource.path - }, - getDriveAliasAndItem(resource) { - if ( - this.capabilities?.spaces?.share_jail && - isLocationSharesActive(this.$router, 'files-shares-with-me') - ) { - return `share/${resource.name}` - } - if (!this.space) { - return null - } - return this.space.driveAlias + resource.path - }, - getShareId(resource) { - if (this.$route.query?.shareId) { - return this.$route.query.shareId - } - if ( - this.capabilities?.spaces?.share_jail && - isLocationSharesActive(this.$router, 'files-shares-with-me') - ) { - return resource.id + const storageId = resource.storageId + // FIXME: Once we have the shareId in the OCS response, we can check for that and early return the share + const space = this.$store.getters['runtime/spaces/spaces'].find( + (space) => space.id === storageId + ) + if (space) { + return space } - return undefined + + return buildShareSpaceResource({ + shareId: resource.shareId, + shareName: resource.name, + serverUrl: configurationManager.serverUrl + }) } } } diff --git a/packages/web-app-files/src/mixins/actions/rename.ts b/packages/web-app-files/src/mixins/actions/rename.ts index 9ec2d60bc8b..58fad0fe9b7 100644 --- a/packages/web-app-files/src/mixins/actions/rename.ts +++ b/packages/web-app-files/src/mixins/actions/rename.ts @@ -1,10 +1,12 @@ import { mapActions, mapGetters, mapMutations, mapState } from 'vuex' import { isSameResource, extractNameWithoutExtension } from '../../helpers/resource' -import { isLocationTrashActive, isLocationSharesActive, isLocationSpacesActive } from '../../router' +import { isLocationTrashActive, isLocationSharesActive } from '../../router' import { Resource } from 'web-client' import { dirname, join } from 'path' import { WebDAV } from 'web-client/src/webdav' -import { SpaceResource } from 'web-client/src/helpers' +import { SpaceResource, isShareSpaceResource } from 'web-client/src/helpers' +import { createFileRouteOptions } from 'web-pkg/src/helpers/router' +import { renameResource } from '../../helpers/resources' export default { computed: { @@ -34,16 +36,6 @@ export default { if (resources.length !== 1) { return false } - // FIXME: once renaming shares in share_jail has been sorted out backend side we can enable renaming shares again - if ( - this.capabilities?.spaces?.share_jail === true && - (isLocationSharesActive(this.$router, 'files-shares-with-me') || - (isLocationSpacesActive(this.$router, 'files-spaces-generic') && - this.space.driveType === 'share' && - resources[0].path === '/')) - ) { - return false - } const renameDisabled = resources.some((resource) => { return !resource.canRename() @@ -64,7 +56,7 @@ export default { 'showMessage', 'toggleModalConfirmButton' ]), - ...mapMutations('Files', ['RENAME_FILE']), + ...mapMutations('Files', ['UPSERT_RESOURCE', 'SET_CURRENT_FOLDER']), async $_rename_trigger({ resources }, space?: SpaceResource) { let parentResources @@ -191,16 +183,43 @@ export default { }) this.hideModal() - if (isSameResource(resource, this.currentFolder)) { - return this.$router.push({ - params: { - driveAliasAndItem: this.space.getDriveAliasAndItem({ path: newPath } as Resource) - }, - query: this.$route.query - }) + const isCurrentFolder = isSameResource(resource, this.currentFolder) + + if (isShareSpaceResource(space) && resource.isReceivedShare()) { + space.rename(newName) + + if (isCurrentFolder) { + const currentFolder = { ...this.currentFolder } as Resource + currentFolder.name = newName + this.SET_CURRENT_FOLDER(currentFolder) + return this.$router.push( + createFileRouteOptions(space, { + path: '', + fileId: resource.fileId + }) + ) + } + + const sharedResource = { ...resource } + sharedResource.name = newName + this.UPSERT_RESOURCE(sharedResource) + return } - this.RENAME_FILE({ space, resource, newPath }) + if (isCurrentFolder) { + const currentFolder = { ...this.currentFolder } as Resource + renameResource(space, currentFolder, newPath) + this.SET_CURRENT_FOLDER(currentFolder) + return this.$router.push( + createFileRouteOptions(this.space, { + path: newPath, + fileId: resource.fileId + }) + ) + } + const fileResource = { ...resource } as Resource + renameResource(space, fileResource, newPath) + this.UPSERT_RESOURCE(fileResource) } catch (error) { console.error(error) this.toggleModalConfirmButton() diff --git a/packages/web-app-files/src/mixins/deleteResources.ts b/packages/web-app-files/src/mixins/deleteResources.ts index e90dc3c3170..35ee349673c 100644 --- a/packages/web-app-files/src/mixins/deleteResources.ts +++ b/packages/web-app-files/src/mixins/deleteResources.ts @@ -5,7 +5,8 @@ import { buildWebDavFilesTrashPath, buildWebDavSpacesTrashPath } from '../helper import PQueue from 'p-queue' import { isLocationTrashActive, isLocationSpacesActive } from '../router' import { clientService } from 'web-pkg/src/services' -import { urlJoin } from 'web-pkg/src/utils' +import { dirname } from 'path' +import { createFileRouteOptions } from 'web-pkg/src/helpers/router' export default { data: () => ({ @@ -184,21 +185,16 @@ export default { } } - let parentFolderPath if ( this.resourcesToDelete.length && isSameResource(this.resourcesToDelete[0], this.currentFolder) ) { - const resourcePath = this.resourcesToDelete[0].path - parentFolderPath = resourcePath.substr(0, resourcePath.lastIndexOf('/') + 1) - } - - if (parentFolderPath !== undefined) { - this.$router.push({ - params: { - driveAliasAndItem: urlJoin(this.space.driveAlias, parentFolderPath) - } - }) + return this.$router.push( + createFileRouteOptions(this.space, { + path: dirname(this.resourcesToDelete[0].path), + fileId: this.resourcesToDelete[0].parentFolderId + }) + ) } }) }, diff --git a/packages/web-app-files/src/mixins/fileActions.ts b/packages/web-app-files/src/mixins/fileActions.ts index cec567f4a8d..088c9045695 100644 --- a/packages/web-app-files/src/mixins/fileActions.ts +++ b/packages/web-app-files/src/mixins/fileActions.ts @@ -20,6 +20,7 @@ import { ShareStatus } from 'web-client/src/helpers/share' import isSearchActive from './helpers/isSearchActive' import { Resource } from 'web-client' import { SpaceResource } from 'web-client/src/helpers' +import { configurationManager } from 'web-pkg/src/configuration' const actionsMixins = [ 'navigate', @@ -96,7 +97,7 @@ export default { editor, options.space.getDriveAliasAndItem(options.resources[0]), options.resources[0].webDavPath, - options.resources[0].id, + options.resources[0].fileId, EDITOR_MODE_EDIT, options.space.shareId ), @@ -193,6 +194,7 @@ export default { }, query: { ...(shareId && { shareId }), + ...(fileId && configurationManager.options.routing.idBased && { fileId }), ...routeToContextQuery(this.$route) } } diff --git a/packages/web-app-files/src/mixins/spaces/actions/deletedFiles.js b/packages/web-app-files/src/mixins/spaces/actions/deletedFiles.js index c5eb8093f7f..64569434bb5 100644 --- a/packages/web-app-files/src/mixins/spaces/actions/deletedFiles.js +++ b/packages/web-app-files/src/mixins/spaces/actions/deletedFiles.js @@ -1,4 +1,5 @@ import { createLocationTrash } from '../../../router' +import { createFileRouteOptions } from 'web-pkg/src/helpers/router' export default { computed: { @@ -21,13 +22,12 @@ export default { } }, methods: { - $_deletedFiles_trigger({ resources }) { - this.$router.push( - createLocationTrash('files-trash-generic', { - params: { - driveAliasAndItem: resources[0].driveAlias - } - }) + $_deletedFiles_trigger() { + return this.$router.push( + createLocationTrash( + 'files-trash-generic', + createFileRouteOptions(this.space, { fileId: this.space.fileId }) + ) ) } } diff --git a/packages/web-app-files/src/mixins/spaces/actions/navigate.js b/packages/web-app-files/src/mixins/spaces/actions/navigate.js index 6e00d95a4db..ab85d495b46 100644 --- a/packages/web-app-files/src/mixins/spaces/actions/navigate.js +++ b/packages/web-app-files/src/mixins/spaces/actions/navigate.js @@ -1,4 +1,5 @@ import { createLocationSpaces, isLocationTrashActive } from '../../../router' +import { createFileRouteOptions } from 'web-pkg/src/helpers/router' export default { computed: { @@ -28,16 +29,14 @@ export default { }, methods: { $_navigate_space_trigger() { - const driveAlias = this.space?.driveAlias - if (!driveAlias) { + if (!this.space) { return } return this.$router.push( - createLocationSpaces('files-spaces-generic', { - params: { - driveAliasAndItem: driveAlias - } - }) + createLocationSpaces( + 'files-spaces-generic', + createFileRouteOptions(this.space, { fileId: this.space.fileId }) + ) ) } } diff --git a/packages/web-app-files/src/services/folder.ts b/packages/web-app-files/src/services/folder.ts index 7a4326cd70c..94080622c52 100644 --- a/packages/web-app-files/src/services/folder.ts +++ b/packages/web-app-files/src/services/folder.ts @@ -6,18 +6,15 @@ import { Store } from 'vuex' import { ClientService } from 'web-pkg/src/services/client' import { - FolderLoaderSpacesGeneric, - FolderLoaderSpacesShare, + FolderLoaderSpace, FolderLoaderFavorites, - FolderLoaderLegacyPersonal, - FolderLoaderPublicFiles, FolderLoaderSharedViaLink, FolderLoaderSharedWithMe, FolderLoaderSharedWithOthers, FolderLoaderTrashbin } from './folder/' -export { SHARE_JAIL_ID } from './folder/spaces/loaderShare' +export * from './folder/types' export type FolderLoaderTask = any @@ -38,14 +35,8 @@ export class FolderService { constructor() { this.loaders = [ - // legacy loaders - new FolderLoaderLegacyPersonal(), - // spaces loaders - new FolderLoaderSpacesGeneric(), - new FolderLoaderSpacesShare(), - // generic loaders + new FolderLoaderSpace(), new FolderLoaderFavorites(), - new FolderLoaderPublicFiles(), new FolderLoaderSharedViaLink(), new FolderLoaderSharedWithMe(), new FolderLoaderSharedWithOthers(), diff --git a/packages/web-app-files/src/services/folder/index.ts b/packages/web-app-files/src/services/folder/index.ts index e1ca3255f61..4141cf304ad 100644 --- a/packages/web-app-files/src/services/folder/index.ts +++ b/packages/web-app-files/src/services/folder/index.ts @@ -1,9 +1,7 @@ -export * from './legacy/loaderPersonal' -export * from './spaces/loaderSpaceGeneric' -export * from './spaces/loaderShare' +export * from './loaderSpace' export * from './loaderFavorites' -export * from './loaderPublicFiles' export * from './loaderSharedViaLink' export * from './loaderSharedWithMe' export * from './loaderSharedWithOthers' export * from './loaderTrashbin' +export * from './types' diff --git a/packages/web-app-files/src/services/folder/legacy/loaderPersonal.ts b/packages/web-app-files/src/services/folder/legacy/loaderPersonal.ts deleted file mode 100644 index 6c175968a5e..00000000000 --- a/packages/web-app-files/src/services/folder/legacy/loaderPersonal.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { FolderLoader, FolderLoaderTask, TaskContext } from '../../folder' -import Router from 'vue-router' -import { useTask } from 'vue-concurrency' -import { isLocationSpacesActive } from '../../../router' -import { Store } from 'vuex' -import get from 'lodash-es/get' -import { getIndicators } from '../../../helpers/statusIndicators' -import { SpaceResource } from 'web-client/src/helpers' - -export class FolderLoaderLegacyPersonal implements FolderLoader { - public isEnabled(store: Store): boolean { - return !get(store, 'getters.capabilities.spaces.share_jail', false) - } - - public isActive(router: Router): boolean { - return isLocationSpacesActive(router, 'files-spaces-generic') - } - - public getTask(context: TaskContext): FolderLoaderTask { - const { - store, - clientService: { owncloudSdk: client, webdav } - } = context - - return useTask(function* (signal1, signal2, space: SpaceResource, path: string = null) { - try { - store.commit('Files/CLEAR_CURRENT_FILES_LIST') - - const resources = yield webdav.listFiles(space, { path }) - - const currentFolder = resources.shift() - yield store.dispatch('Files/loadSharesTree', { - client, - path: currentFolder.path - }) - - for (const file of resources) { - file.indicators = getIndicators(file, store.state.Files.sharesTree, false) - } - - store.commit('Files/LOAD_FILES', { - currentFolder, - files: resources - }) - } catch (error) { - store.commit('Files/SET_CURRENT_FOLDER', null) - console.error(error) - } - }).restartable() - } -} diff --git a/packages/web-app-files/src/services/folder/loaderPublicFiles.ts b/packages/web-app-files/src/services/folder/loaderPublicFiles.ts deleted file mode 100644 index 62052c555c5..00000000000 --- a/packages/web-app-files/src/services/folder/loaderPublicFiles.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { FolderLoader, FolderLoaderTask, TaskContext } from '../folder' -import Router from 'vue-router' -import { useTask } from 'vue-concurrency' -import { isLocationPublicActive } from '../../router' - -import { Store } from 'vuex' -import { authService } from 'web-runtime/src/services/auth' -import { SpaceResource } from 'web-client/src/helpers' - -export class FolderLoaderPublicFiles implements FolderLoader { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public isEnabled(store: Store): boolean { - return true - } - - public isActive(router: Router): boolean { - return isLocationPublicActive(router, 'files-public-link') - } - - public getTask(context: TaskContext): FolderLoaderTask { - const { - store, - router, - clientService: { webdav } - } = context - - return useTask(function* (signal1, signal2, space: SpaceResource, path: string = null) { - store.commit('Files/CLEAR_CURRENT_FILES_LIST') - - try { - const resources = yield webdav.listFiles(space, { path }) - store.commit('Files/LOAD_FILES', { - currentFolder: resources[0], - files: resources.slice(1) - }) - } catch (error) { - store.commit('Files/SET_CURRENT_FOLDER', null) - console.error(error) - - if (error.statusCode === 401) { - return authService.handleAuthError(router.currentRoute) - } - } - }) - } -} diff --git a/packages/web-app-files/src/services/folder/loaderSpace.ts b/packages/web-app-files/src/services/folder/loaderSpace.ts new file mode 100644 index 00000000000..a8a9d91525f --- /dev/null +++ b/packages/web-app-files/src/services/folder/loaderSpace.ts @@ -0,0 +1,103 @@ +import { FolderLoader, FolderLoaderTask, TaskContext } from '../folder' +import Router from 'vue-router' +import { useTask } from 'vue-concurrency' +import { isLocationPublicActive, isLocationSpacesActive } from '../../router' +import { + useCapabilityFilesSharingResharing, + useCapabilityShareJailEnabled, + useCapabilitySpacesEnabled +} from 'web-pkg/src/composables' +import { getIndicators } from '../../helpers/statusIndicators' +import { SpaceResource } from 'web-client/src/helpers' +import { unref } from '@vue/composition-api' +import { FolderLoaderOptions } from './types' +import { authService } from 'web-runtime/src/services/auth' +import { useFileRouteReplace } from 'web-pkg/src/composables/router/useFileRouteReplace' +import { aggregateResourceShares } from '../../helpers/resources' + +export class FolderLoaderSpace implements FolderLoader { + public isEnabled(): boolean { + return true + } + + public isActive(router: Router): boolean { + // TODO: remove next check when isLocationSpacesActive doesn't return true for generic route when being on projects overview. + if (isLocationSpacesActive(router, 'files-spaces-projects')) { + return false + } + return ( + isLocationSpacesActive(router, 'files-spaces-generic') || + isLocationPublicActive(router, 'files-public-link') + ) + } + + public getTask(context: TaskContext): FolderLoaderTask { + const { + store, + router, + clientService: { owncloudSdk: client, webdav } + } = context + const { replaceInvalidFileRoute } = useFileRouteReplace({ router }) + const hasShareJail = useCapabilityShareJailEnabled(store) + const hasResharing = useCapabilityFilesSharingResharing(store) + const hasSpaces = useCapabilitySpacesEnabled(store) + + return useTask(function* ( + signal1, + signal2, + space: SpaceResource, + path: string = null, + fileId: string | number = null, + options: FolderLoaderOptions = {} + ) { + try { + store.commit('Files/CLEAR_CURRENT_FILES_LIST') + + const resources = yield webdav.listFiles(space, { path, fileId }) + let currentFolder = resources.shift() + replaceInvalidFileRoute({ space, resource: currentFolder, path, fileId }) + + if (path === '/') { + if (space.driveType === 'share') { + const parentShare = yield client.shares.getShare(space.shareId) + const aggregatedShares = aggregateResourceShares( + [parentShare.shareInfo], + true, + unref(hasResharing), + true + ) + currentFolder = aggregatedShares[0] + } else if (!['personal', 'public'].includes(space.driveType)) { + // note: in the future we might want to show the space as root for personal spaces as well (to show quota and the like). Currently not needed. + currentFolder = space + } + } + + if (options.loadShares) { + yield store.dispatch('Files/loadSharesTree', { + client, + path: currentFolder.path, + ...(unref(hasSpaces) && { storageId: currentFolder.fileId }), + includeRoot: currentFolder.path === '/' && space.driveType !== 'personal' + }) + + for (const file of resources) { + file.indicators = getIndicators(file, store.state.Files.sharesTree, unref(hasShareJail)) + } + } + + store.commit('Files/LOAD_FILES', { + currentFolder, + files: resources + }) + } catch (error) { + store.commit('Files/SET_CURRENT_FOLDER', null) + console.error(error) + + if (error.statusCode === 401) { + return authService.handleAuthError(router.currentRoute) + } + } + }).restartable() + } +} diff --git a/packages/web-app-files/src/services/folder/spaces/loaderShare.ts b/packages/web-app-files/src/services/folder/spaces/loaderShare.ts deleted file mode 100644 index 3b4a9137860..00000000000 --- a/packages/web-app-files/src/services/folder/spaces/loaderShare.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { FolderLoader, FolderLoaderTask, TaskContext } from '../../folder' -import Router from 'vue-router' -import { useTask } from 'vue-concurrency' -import { isLocationSpacesActive } from '../../../router' -import { aggregateResourceShares } from '../../../helpers/resources' -import { Store } from 'vuex' -import get from 'lodash-es/get' -import { useCapabilityFilesSharingResharing, useRouteParam } from 'web-pkg/src/composables' -import { getIndicators } from '../../../helpers/statusIndicators' -import { unref } from '@vue/composition-api' -import { SpaceResource } from 'web-client/src/helpers' - -export const SHARE_JAIL_ID = 'a0ca6a90-a365-4782-871e-d44447bbc668' - -export class FolderLoaderSpacesShare implements FolderLoader { - public isEnabled(store: Store): boolean { - return get(store, 'getters.capabilities.spaces.share_jail', false) - } - - public isActive(router: Router): boolean { - // TODO: remove next check when isLocationSpacesActive doesn't return true for generic route when being on projects overview. - if (isLocationSpacesActive(router, 'files-spaces-projects')) { - return false - } - if (!isLocationSpacesActive(router, 'files-spaces-generic')) { - return false - } - const driveAliasAndItem = useRouteParam('driveAliasAndItem') - return unref(driveAliasAndItem).startsWith('share/') - } - - public getTask(context: TaskContext): FolderLoaderTask { - const { store, router, clientService } = context - - return useTask(function* (signal1, signal2, space: SpaceResource, path: string = null) { - store.commit('Files/CLEAR_CURRENT_FILES_LIST') - - const hasResharing = useCapabilityFilesSharingResharing(store) - - const resources = yield clientService.webdav.listFiles(space, { path }) - let currentFolder = resources.shift() - - // sharing jail root -> load the parent share as current Folder - if (currentFolder.path === '/') { - const parentShare = yield clientService.owncloudSdk.shares.getShare(space.shareId) - const aggregatedShares = aggregateResourceShares( - [parentShare.shareInfo], - true, - unref(hasResharing), - true - ) - - currentFolder = aggregatedShares[0] - } - - if (hasResharing.value && resources.length) { - yield store.dispatch('Files/loadSharesTree', { - client: clientService.owncloudSdk, - path: currentFolder.path, - storageId: currentFolder.fileId, - includeRoot: currentFolder.path === '/' - }) - - for (const file of resources) { - file.indicators = getIndicators(file, store.state.Files.sharesTree, true) - } - } - - store.commit('Files/LOAD_FILES', { - currentFolder, - files: resources - }) - }) - } -} diff --git a/packages/web-app-files/src/services/folder/spaces/loaderSpaceGeneric.ts b/packages/web-app-files/src/services/folder/spaces/loaderSpaceGeneric.ts deleted file mode 100644 index a7c3bcca884..00000000000 --- a/packages/web-app-files/src/services/folder/spaces/loaderSpaceGeneric.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { FolderLoader, FolderLoaderTask, TaskContext } from '../../folder' -import Router from 'vue-router' -import { useTask } from 'vue-concurrency' -import { isLocationSpacesActive } from '../../../router' -import { Store } from 'vuex' -import get from 'lodash-es/get' -import { useCapabilityShareJailEnabled, useRouteParam } from 'web-pkg/src/composables' -import { getIndicators } from '../../../helpers/statusIndicators' -import { SpaceResource } from 'web-client/src/helpers' -import { unref } from '@vue/composition-api' - -export class FolderLoaderSpacesGeneric implements FolderLoader { - public isEnabled(store: Store): boolean { - return get(store, 'getters.capabilities.spaces.enabled', false) - } - - public isActive(router: Router): boolean { - // TODO: remove next check when isLocationSpacesActive doesn't return true for generic route when being on projects overview. - if (isLocationSpacesActive(router, 'files-spaces-projects')) { - return false - } - if (!isLocationSpacesActive(router, 'files-spaces-generic')) { - return false - } - const driveAliasAndItem = useRouteParam('driveAliasAndItem') - return !unref(driveAliasAndItem).startsWith('share/') - } - - public getTask(context: TaskContext): FolderLoaderTask { - const { - store, - clientService: { owncloudSdk: client, webdav } - } = context - const hasShareJail = useCapabilityShareJailEnabled(store) - - return useTask(function* (signal1, signal2, space: SpaceResource, path: string = null) { - try { - store.commit('Files/CLEAR_CURRENT_FILES_LIST') - - const resources = yield webdav.listFiles(space, { path }) - let currentFolder = resources.shift() - // FIXME / technical debt: at some point we want to use the space as current folder for all drive types. but somehow broken for personal spaces... - if (path === '/' && space.driveType !== 'personal') { - currentFolder = space - } - - yield store.dispatch('Files/loadSharesTree', { - client, - path: currentFolder.path, - storageId: currentFolder.fileId, - includeRoot: space.driveType === 'project' && currentFolder.path === '/' - }) - - for (const file of resources) { - file.indicators = getIndicators(file, store.state.Files.sharesTree, unref(hasShareJail)) - } - - store.commit('Files/LOAD_FILES', { - currentFolder, - files: resources - }) - } catch (error) { - store.commit('Files/SET_CURRENT_FOLDER', null) - console.error(error) - } - }).restartable() - } -} diff --git a/packages/web-app-files/src/services/folder/types.ts b/packages/web-app-files/src/services/folder/types.ts new file mode 100644 index 00000000000..9e63bc6fb87 --- /dev/null +++ b/packages/web-app-files/src/services/folder/types.ts @@ -0,0 +1,3 @@ +export interface FolderLoaderOptions { + loadShares?: boolean +} diff --git a/packages/web-app-files/src/store/mutations.ts b/packages/web-app-files/src/store/mutations.ts index 2bbb7aea824..250eaa26e8c 100644 --- a/packages/web-app-files/src/store/mutations.ts +++ b/packages/web-app-files/src/store/mutations.ts @@ -2,7 +2,6 @@ import Vue from 'vue' import pickBy from 'lodash-es/pickBy' import { set, has } from 'lodash-es' import { getIndicators } from '../helpers/statusIndicators' -import { renameResource } from '../helpers/resources' import { SpaceResource } from 'web-client/src/helpers' export default { @@ -86,16 +85,6 @@ export default { REMOVE_FILES(state, removedFiles) { state.files = [...state.files].filter((file) => !removedFiles.find((r) => r.id === file.id)) }, - RENAME_FILE(state, { space, resource, newPath }) { - const resources = [...state.files] - const fileIndex = resources.findIndex((f) => { - return f.id === resource.id - }) - - renameResource(space, resources[fileIndex], newPath) - - state.files = resources - }, CURRENT_FILE_OUTGOING_SHARES_SET(state, shares) { state.currentFileOutgoingShares = shares }, diff --git a/packages/web-app-files/src/views/spaces/DriveRedirect.vue b/packages/web-app-files/src/views/spaces/DriveRedirect.vue index 10569a38c18..07ac99ca352 100644 --- a/packages/web-app-files/src/views/spaces/DriveRedirect.vue +++ b/packages/web-app-files/src/views/spaces/DriveRedirect.vue @@ -8,8 +8,8 @@ import { computed, defineComponent, unref } from '@vue/composition-api' import { useRoute, useRouter, useStore } from 'web-pkg/src/composables' import AppLoadingSpinner from 'web-pkg/src/components/AppLoadingSpinner.vue' -import { Resource } from 'web-client' import { urlJoin } from 'web-pkg/src/utils' +import { createFileRouteOptions } from 'web-pkg/src/helpers/router' // 'personal/home' is used as personal drive alias from static contexts // (i.e. places where we can't load the actual personal space) @@ -53,16 +53,19 @@ export default defineComponent({ return store.getters.homeFolder }) + const { params, query } = createFileRouteOptions(unref(personalSpace), { + path: unref(itemPath) + }) + return ( router .replace({ ...unref(route), params: { ...unref(route).params, - driveAliasAndItem: unref(personalSpace).getDriveAliasAndItem({ - path: unref(itemPath) - } as Resource) - } + ...params + }, + query }) // avoid NavigationDuplicated error in console .catch(() => {}) diff --git a/packages/web-app-files/src/views/spaces/DriveResolver.vue b/packages/web-app-files/src/views/spaces/DriveResolver.vue index 0eb224993b5..a0ce1636ccc 100644 --- a/packages/web-app-files/src/views/spaces/DriveResolver.vue +++ b/packages/web-app-files/src/views/spaces/DriveResolver.vue @@ -5,8 +5,8 @@ :drive-alias-and-item="driveAliasAndItem" :append-home-folder="isSpaceRoute" /> - - + + diff --git a/packages/web-app-files/src/views/spaces/GenericSpace.vue b/packages/web-app-files/src/views/spaces/GenericSpace.vue index 4b56a9f04b0..de22d00c2c2 100644 --- a/packages/web-app-files/src/views/spaces/GenericSpace.vue +++ b/packages/web-app-files/src/views/spaces/GenericSpace.vue @@ -14,6 +14,7 @@ @@ -122,7 +123,17 @@ import { ResourceTransfer, TransferType } from '../../helpers/resource' import { Resource } from 'web-client' import { useCapabilityShareJailEnabled } from 'web-pkg/src/composables' import { Location } from 'vue-router' -import { isPublicSpaceResource, SpaceResource } from 'web-client/src/helpers' +import { + isPersonalSpaceResource, + isProjectSpaceResource, + isPublicSpaceResource, + isShareSpaceResource, + SpaceResource +} from 'web-client/src/helpers' +import { CreateTargetRouteOptions } from '../../helpers/folderLink' +import { FolderLoaderOptions } from '../../services/folder' +import { createFileRouteOptions } from 'web-pkg/src/helpers/router' +import omit from 'lodash-es/omit' const visibilityObserver = new VisibilityObserver() @@ -158,20 +169,21 @@ export default defineComponent({ type: String, required: false, default: null + }, + itemId: { + type: [String, Number], + required: false, + default: null } }, setup(props) { - const resourceTargetRouteCallback = (path: string, resource: Resource): Location => { + const resourceTargetRouteCallback = ({ path, fileId }: CreateTargetRouteOptions): Location => { + const { params, query } = createFileRouteOptions(props.space, { path, fileId }) if (isPublicSpaceResource(props.space)) { - return createLocationPublic('files-public-link', { - params: { driveAliasAndItem: props.space.getDriveAliasAndItem({ path } as Resource) } - }) + return createLocationPublic('files-public-link', { params, query }) } - return createLocationSpaces('files-spaces-generic', { - params: { driveAliasAndItem: props.space.getDriveAliasAndItem({ path } as Resource) }, - query: { ...(props.space.driveType === 'share' && { shareId: props.space.shareId }) } - }) + return createLocationSpaces('files-spaces-generic', { params, query }) } const hasSpaceHeader = computed(() => { // for now the space header is only available in the root of a project space. @@ -202,12 +214,12 @@ export default defineComponent({ breadcrumbs() { const rootBreadcrumbItems: BreadcrumbItem[] = [] - if (this.space.driveType === 'project') { + if (isProjectSpaceResource(this.space)) { rootBreadcrumbItems.push({ text: this.$gettext('Spaces'), to: createLocationSpaces('files-spaces-projects') }) - } else if (this.space.driveType === 'share') { + } else if (isShareSpaceResource(this.space)) { rootBreadcrumbItems.push( { text: this.$gettext('Shares'), @@ -221,19 +233,31 @@ export default defineComponent({ } let spaceBreadcrumbItem - if (this.space.driveType === 'personal') { + let { params, query } = createFileRouteOptions(this.space, { fileId: this.space.fileId }) + query = { ...this.$route.query, ...query } + if (isPersonalSpaceResource(this.space)) { spaceBreadcrumbItem = { text: this.hasShareJail ? this.$gettext('Personal') : this.$gettext('All files'), to: createLocationSpaces('files-spaces-generic', { - params: { driveAliasAndItem: this.space.driveAlias }, - query: this.$route.query + params, + query + }) + } + } else if (isShareSpaceResource(this.space)) { + spaceBreadcrumbItem = { + allowContextActions: true, + text: this.space.name, + to: createLocationSpaces('files-spaces-generic', { + params, + query: omit(query, 'fileId') }) } } else if (isPublicSpaceResource(this.space)) { spaceBreadcrumbItem = { text: this.$gettext('Public link'), to: createLocationPublic('files-public-link', { - params: { driveAliasAndItem: this.space.driveAlias } + params, + query }) } } else { @@ -241,8 +265,8 @@ export default defineComponent({ allowContextActions: !this.hasSpaceHeader, text: this.space.name, to: createLocationSpaces('files-spaces-generic', { - params: { driveAliasAndItem: this.space.driveAlias }, - query: this.$route.query + params, + query }) } } @@ -250,6 +274,7 @@ export default defineComponent({ return concatBreadcrumbs( ...rootBreadcrumbItems, spaceBreadcrumbItem, + // FIXME: needs file ids for each parent folder path ...breadcrumbsFromPath(this.$route, this.item) ) }, @@ -273,9 +298,12 @@ export default defineComponent({ mounted() { this.performLoaderTask(false) - const loadResourcesEventToken = bus.subscribe('app.files.list.load', (path: string) => { - this.performLoaderTask(true, path) - }) + const loadResourcesEventToken = bus.subscribe( + 'app.files.list.load', + (path?: string, fileId?: string | number) => { + this.performLoaderTask(true, path, fileId) + } + ) this.$on('beforeDestroy', () => bus.unsubscribe('app.files.list.load', loadResourcesEventToken)) }, @@ -292,8 +320,14 @@ export default defineComponent({ 'REMOVE_FILE_SELECTION' ]), - async performLoaderTask(sameRoute: boolean, path?: string) { - await this.loadResourcesTask.perform(this.space, path || this.item) + async performLoaderTask(sameRoute: boolean, path?: string, fileId?: string | number) { + const options: FolderLoaderOptions = { loadShares: !isPublicSpaceResource(this.space) } + await this.loadResourcesTask.perform( + this.space, + path || this.item, + fileId || this.itemId, + options + ) this.scrollToResourceFromRoute() this.refreshFileListHeaderPosition() this.accessibleBreadcrumb_focusAndAnnounceBreadcrumb(sameRoute) diff --git a/packages/web-app-files/src/views/spaces/GenericTrash.vue b/packages/web-app-files/src/views/spaces/GenericTrash.vue index 02bc6c4f4be..ed10452a24d 100644 --- a/packages/web-app-files/src/views/spaces/GenericTrash.vue +++ b/packages/web-app-files/src/views/spaces/GenericTrash.vue @@ -100,6 +100,11 @@ export default defineComponent({ type: Object as PropType, required: false, default: null + }, + itemId: { + type: [String, Number], + required: false, + default: null } }, diff --git a/packages/web-app-files/src/views/spaces/Projects.vue b/packages/web-app-files/src/views/spaces/Projects.vue index 013b7ae911e..2eab0fb0622 100644 --- a/packages/web-app-files/src/views/spaces/Projects.vue +++ b/packages/web-app-files/src/views/spaces/Projects.vue @@ -135,7 +135,6 @@ import AppLoadingSpinner from 'web-pkg/src/components/AppLoadingSpinner.vue' import { computed, defineComponent, unref } from '@vue/composition-api' import { useAccessToken, useStore } from 'web-pkg/src/composables' import { useTask } from 'vue-concurrency' -import { createLocationSpaces } from '../../router' import { mapMutations, mapActions, mapGetters } from 'vuex' import { loadPreview } from 'web-pkg/src/helpers/preview' import { ImageDimension } from '../../constants' @@ -149,6 +148,8 @@ import { bus } from 'web-pkg/src/instance' import { SideBarEventTopics, useSideBar } from '../../composables/sideBar' import { Resource } from '../../../../../tests/e2e/support/objects/app-files' import { WebDAV } from 'web-client/src/webdav' +import { createLocationSpaces } from '../../router' +import { createFileRouteOptions } from 'web-pkg/src/helpers/router' export default defineComponent({ components: { @@ -273,12 +274,13 @@ export default defineComponent({ 'SET_FILE_SELECTION' ]), - getSpaceProjectRoute({ driveAlias, disabled }) { - return disabled + getSpaceProjectRoute(space: SpaceResource) { + return space.disabled ? '#' - : createLocationSpaces('files-spaces-generic', { - params: { driveAliasAndItem: driveAlias } - }) + : createLocationSpaces( + 'files-spaces-generic', + createFileRouteOptions(space, { path: '', fileId: space.fileId }) + ) }, getSpaceCardAdditionalClass(space) { @@ -294,7 +296,7 @@ export default defineComponent({ bus.publish(SideBarEventTopics.openWithPanel, 'space-share-item') }, - getSpaceLinkProps(space) { + getSpaceLinkProps(space: SpaceResource) { if (space.disabled) { return { appearance: 'raw' diff --git a/packages/web-app-files/tests/unit/components/AppBar/CreateAndUpload.spec.js b/packages/web-app-files/tests/unit/components/AppBar/CreateAndUpload.spec.js index f62cedddf0d..0fb61c699e0 100644 --- a/packages/web-app-files/tests/unit/components/AppBar/CreateAndUpload.spec.js +++ b/packages/web-app-files/tests/unit/components/AppBar/CreateAndUpload.spec.js @@ -177,6 +177,7 @@ describe('CreateAndUpload component', () => { null, null, null, + null, wrapper.vm.spaces, true, jest.fn(), diff --git a/packages/web-app-files/tests/unit/components/FilesList/NotFoundMessage.spec.ts b/packages/web-app-files/tests/unit/components/FilesList/NotFoundMessage.spec.ts index 35b1067706f..b76faa3837c 100644 --- a/packages/web-app-files/tests/unit/components/FilesList/NotFoundMessage.spec.ts +++ b/packages/web-app-files/tests/unit/components/FilesList/NotFoundMessage.spec.ts @@ -7,7 +7,6 @@ import NotFoundMessage from '../../../../src/components/FilesList/NotFoundMessag import { createLocationPublic, createLocationSpaces } from '../../../../src/router' import { PublicSpaceResource, SpaceResource, Resource } from 'web-client/src/helpers' import { MockProxy, mock } from 'jest-mock-extended' -import { createStore } from 'vuex-extensions' import { join } from 'path' const localVue = createLocalVue() @@ -30,9 +29,6 @@ describe('NotFoundMessage', () => { space = mock({ driveType: 'personal' }) - space.getDriveAliasAndItem.mockImplementation((resource) => - join('personal/admin', resource.path) - ) }) it('should show home button', () => { @@ -51,14 +47,12 @@ describe('NotFoundMessage', () => { expect(reloadLinkButton.exists()).toBeFalsy() }) - it('should have property route to home', () => { + it('should have property route to personal space', () => { const wrapper = getMountedWrapper(space, spacesLocation) const homeButton = wrapper.find(selectors.homeButton) expect(homeButton.props().to.name).toBe(spacesLocation.name) - expect(homeButton.props().to.params.driveAliasAndItem).toBe( - space.getDriveAliasAndItem({ path: 'home' } as Resource) - ) + expect(homeButton.props().to.params.driveAliasAndItem).toBe('personal') }) }) @@ -104,13 +98,6 @@ describe('NotFoundMessage', () => { function getMountOpts(space, route) { return { localVue, - store: createStore(Vuex.Store, { - getters: { - homeFolder: () => { - return 'home' - } - } - }), stubs: stubs, mocks: { $route: route, diff --git a/packages/web-app-files/tests/unit/components/Search/Preview.spec.ts b/packages/web-app-files/tests/unit/components/Search/Preview.spec.ts index 6dc84b59fc2..dad718a19c8 100644 --- a/packages/web-app-files/tests/unit/components/Search/Preview.spec.ts +++ b/packages/web-app-files/tests/unit/components/Search/Preview.spec.ts @@ -106,7 +106,8 @@ function getWrapper({ data: { storageId: '1', name: 'lorem.txt', - path: '/' + path: '/', + shareRoot: '' } }, user = { id: 'test' } diff --git a/packages/web-app-files/tests/unit/helpers/breadcrumbs.spec.js b/packages/web-app-files/tests/unit/helpers/breadcrumbs.spec.js index e1e9285a2de..9d0bde2677f 100644 --- a/packages/web-app-files/tests/unit/helpers/breadcrumbs.spec.js +++ b/packages/web-app-files/tests/unit/helpers/breadcrumbs.spec.js @@ -4,7 +4,11 @@ describe('builds an array of breadcrumbitems', () => { it('from a path', () => { const breadCrumbs = breadcrumbsFromPath({ path: '/files/spaces/personal/home/test' }, '/test') expect(breadCrumbs).toEqual([ - { allowContextActions: true, text: 'test', to: { path: '/files/spaces/personal/home/test' } } + { + allowContextActions: true, + text: 'test', + to: { path: '/files/spaces/personal/home/test', query: {} } + } ]) }) diff --git a/packages/web-app-files/tests/unit/mixins/actions/rename.spec.ts b/packages/web-app-files/tests/unit/mixins/actions/rename.spec.ts index 3779dcaa034..8094081e4be 100644 --- a/packages/web-app-files/tests/unit/mixins/actions/rename.spec.ts +++ b/packages/web-app-files/tests/unit/mixins/actions/rename.spec.ts @@ -140,7 +140,9 @@ describe('rename', () => { function getWrapper() { const mocks = { ...defaultComponentMocks(), - space: mockDeep() + space: mockDeep({ + webDavPath: 'irrelevant' + }) } const storeOptions = { diff --git a/packages/web-app-files/tests/unit/mixins/spaces/deletedFiles.spec.js b/packages/web-app-files/tests/unit/mixins/spaces/deletedFiles.spec.js deleted file mode 100644 index d9ebba755e0..00000000000 --- a/packages/web-app-files/tests/unit/mixins/spaces/deletedFiles.spec.js +++ /dev/null @@ -1,90 +0,0 @@ -import Vuex from 'vuex' -import { createStore } from 'vuex-extensions' -import { mount, createLocalVue } from '@vue/test-utils' -import DeletedFiles from '@files/src/mixins/spaces/actions/deletedFiles.js' -import { createLocationSpaces, createLocationTrash } from '../../../../src/router' -import { buildSpace } from 'web-client/src/helpers' - -const localVue = createLocalVue() -localVue.use(Vuex) - -const Component = { - render() {}, - mixins: [DeletedFiles] -} - -describe('delete', () => { - afterEach(() => jest.clearAllMocks()) - - describe('isEnabled property', () => { - it('should be false when not resource given', () => { - const wrapper = getWrapper() - expect(wrapper.vm.$_deletedFiles_items[0].isEnabled({ resources: [] })).toBe(false) - }) - it('should be true when resource is given', () => { - const spaceMock = { - id: '1' - } - const wrapper = getWrapper() - expect( - wrapper.vm.$_deletedFiles_items[0].isEnabled({ resources: [buildSpace(spaceMock)] }) - ).toBe(true) - }) - }) - - describe('method "$_deletedFiles_trigger"', () => { - it('should trigger route change', async () => { - const spaceMock = { - id: '1', - driveAlias: 'project/mars' - } - - const wrapper = getWrapper() - await wrapper.vm.$_deletedFiles_trigger({ resources: [buildSpace(spaceMock)] }) - - expect(wrapper.vm.$router.push).toHaveBeenCalledWith( - createLocationTrash('files-trash-generic', { - params: { - driveAliasAndItem: spaceMock.driveAlias - } - }) - ) - }) - }) -}) - -function getWrapper() { - return mount(Component, { - localVue, - mocks: { - $router: { - currentRoute: createLocationSpaces('files-spaces-projects'), - resolve: (r) => { - return { href: r.name } - }, - push: jest.fn() - }, - $gettext: jest.fn() - }, - store: createStore(Vuex.Store, { - actions: { - createModal: jest.fn(), - hideModal: jest.fn(), - showMessage: jest.fn() - }, - getters: { - configuration: () => ({ - server: 'https://example.com' - }) - }, - modules: { - Files: { - namespaced: true, - mutations: { - REMOVE_FILE: jest.fn() - } - } - } - }) - }) -} diff --git a/packages/web-app-files/tests/unit/mixins/spaces/deletedFiles.spec.ts b/packages/web-app-files/tests/unit/mixins/spaces/deletedFiles.spec.ts new file mode 100644 index 00000000000..d48bbc49417 --- /dev/null +++ b/packages/web-app-files/tests/unit/mixins/spaces/deletedFiles.spec.ts @@ -0,0 +1,75 @@ +import Vuex from 'vuex' +import { createStore } from 'vuex-extensions' +import { mount } from '@vue/test-utils' +import DeletedFiles from 'files/src/mixins/spaces/actions/deletedFiles.js' +import { createLocationTrash } from '../../../../src/router' +import { buildSpace, SpaceResource } from 'web-client/src/helpers' +import { defaultComponentMocks } from '../../../../../../tests/unit/mocks/defaultComponentMocks' +import { mockDeep } from 'jest-mock-extended' +import { defaultStoreMockOptions } from '../../../../../../tests/unit/mocks/store/defaultStoreMockOptions' +import { defaultLocalVue } from '../../../../../../tests/unit/localVue/defaultLocalVue' + +const Component = { + template: '
', + mixins: [DeletedFiles] +} + +describe('delete', () => { + afterEach(() => jest.clearAllMocks()) + + describe('isEnabled property', () => { + it('should be false when not resource given', () => { + const { wrapper } = getWrapper() + expect(wrapper.vm.$_deletedFiles_items[0].isEnabled({ resources: [] })).toBe(false) + }) + it('should be true when resource is given', () => { + const spaceMock = { + id: '1' + } + const { wrapper } = getWrapper() + expect( + wrapper.vm.$_deletedFiles_items[0].isEnabled({ resources: [buildSpace(spaceMock)] }) + ).toBe(true) + }) + }) + + describe('method "$_deletedFiles_trigger"', () => { + it('should trigger route change', async () => { + const driveAliasAndItem = 'project/mars' + + const { mocks, wrapper } = getWrapper() + mocks.space.getDriveAliasAndItem.mockReturnValueOnce(driveAliasAndItem) + await wrapper.vm.$_deletedFiles_trigger() + + expect(wrapper.vm.$router.push).toHaveBeenCalledWith( + createLocationTrash('files-trash-generic', { + params: { + driveAliasAndItem + }, + query: {} + }) + ) + }) + }) +}) + +function getWrapper() { + const mocks = { + ...defaultComponentMocks(), + space: mockDeep() + } + const storeOptions = { + ...defaultStoreMockOptions + } + const localVue = defaultLocalVue() + const store = createStore(Vuex.Store, storeOptions) + return { + mocks, + storeOptions, + wrapper: mount(Component, { + localVue, + mocks, + store + }) + } +} diff --git a/packages/web-app-files/tests/unit/mixins/spaces/navigate.spec.js b/packages/web-app-files/tests/unit/mixins/spaces/navigate.spec.js index c83362ad10f..e639dcad045 100644 --- a/packages/web-app-files/tests/unit/mixins/spaces/navigate.spec.js +++ b/packages/web-app-files/tests/unit/mixins/spaces/navigate.spec.js @@ -40,7 +40,8 @@ describe('navigate', () => { createLocationSpaces('files-spaces-generic', { params: { driveAliasAndItem: resource.driveAlias - } + }, + query: {} }) ) }) @@ -64,8 +65,8 @@ function getWrapper({ invalidLocation = false } = {}) { }, $gettext: jest.fn(), space: { - driveAlias: 'project/mars', - driveType: 'project' + driveType: 'project', + getDriveAliasAndItem: () => 'project/mars' } }, store: createStore(Vuex.Store, { diff --git a/packages/web-app-pdf-viewer/src/App.vue b/packages/web-app-pdf-viewer/src/App.vue index b78c0101ec8..1c8c5716b26 100644 --- a/packages/web-app-pdf-viewer/src/App.vue +++ b/packages/web-app-pdf-viewer/src/App.vue @@ -52,6 +52,7 @@ export default defineComponent({ try { this.loading = true this.resource = await this.getFileInfo(fileContext) + this.replaceInvalidFileRoute(this.currentFileContext, this.resource) this.url = await this.getUrlForResource(fileContext.space, this.resource, { disposition: 'inline' }) diff --git a/packages/web-app-preview/src/App.vue b/packages/web-app-preview/src/App.vue index b2eeffe0292..53ba0dde723 100644 --- a/packages/web-app-preview/src/App.vue +++ b/packages/web-app-preview/src/App.vue @@ -118,6 +118,8 @@ import Preview from './index' import AppTopBar from 'web-pkg/src/components/AppTopBar.vue' import { loadPreview } from 'web-pkg/src/helpers' import { configurationManager } from 'web-pkg/src/configuration' +import { unref } from '@vue/composition-api' +import { createFileRouteOptions, mergeFileRouteOptions } from 'web-pkg/src/helpers/router' export default defineComponent({ name: 'Preview', @@ -262,10 +264,11 @@ export default defineComponent({ // update route and url updateLocalHistory() { - this.$route.params.driveAliasAndItem = this.currentFileContext.space?.getDriveAliasAndItem( - this.activeFilteredFile + const routeOptions = mergeFileRouteOptions( + this.$route, + createFileRouteOptions(unref(this.currentFileContext.space), this.activeFilteredFile) ) - history.pushState({}, document.title, this.$router.resolve(this.$route).href) + history.pushState({}, document.title, this.$router.resolve(routeOptions).href) }, loadMedium() { diff --git a/packages/web-app-text-editor/src/App.vue b/packages/web-app-text-editor/src/App.vue index 920bcd26886..716d496f6a9 100644 --- a/packages/web-app-text-editor/src/App.vue +++ b/packages/web-app-text-editor/src/App.vue @@ -86,8 +86,14 @@ export default defineComponent({ const defaults = useAppDefaults({ applicationId: 'text-editor' }) - const { applicationConfig, currentFileContext, getFileInfo, getFileContents, putFileContents } = - defaults + const { + applicationConfig, + currentFileContext, + getFileInfo, + getFileContents, + putFileContents, + replaceInvalidFileRoute + } = defaults const serverContent = ref() const currentContent = ref() const currentETag = ref() @@ -96,8 +102,9 @@ export default defineComponent({ const loadFileTask = useTask(function* () { resource.value = yield getFileInfo(currentFileContext, { - davProperties: [DavProperty.Permissions, DavProperty.Name] + davProperties: [DavProperty.FileId, DavProperty.Permissions, DavProperty.Name] }) + replaceInvalidFileRoute(currentFileContext, unref(resource)) isReadOnly.value = ![DavPermission.Updateable, DavPermission.FileUpdateable].some( (p) => (resource.value.permissions || '').indexOf(p) > -1 ) diff --git a/packages/web-client/src/helpers/resource/types.ts b/packages/web-client/src/helpers/resource/types.ts index d09a7d467fe..f10147697a6 100644 --- a/packages/web-client/src/helpers/resource/types.ts +++ b/packages/web-client/src/helpers/resource/types.ts @@ -4,6 +4,7 @@ import { User } from '../user' export interface Resource { id: number | string fileId?: string + parentFolderId?: string storageId?: string name?: string path: string @@ -32,7 +33,6 @@ export interface Resource { shareTypes?: number[] privateLink?: string description?: string - disabled?: boolean driveType?: 'personal' | 'project' | 'share' | 'public' | (string & unknown) driveAlias?: string tags?: string[] diff --git a/packages/web-client/src/helpers/space/functions.ts b/packages/web-client/src/helpers/space/functions.ts index 5a8894a5d87..56bef530e7f 100644 --- a/packages/web-client/src/helpers/space/functions.ts +++ b/packages/web-client/src/helpers/space/functions.ts @@ -1,10 +1,9 @@ import { User } from '../user' import { buildWebDavSpacesPath, extractDomSelector, Resource } from '../resource' import { SpacePeopleShareRoles, spaceRoleEditor, spaceRoleManager } from '../share' -import { PublicSpaceResource, ShareSpaceResource, SpaceResource } from './types' +import { PublicSpaceResource, ShareSpaceResource, SpaceResource, SHARE_JAIL_ID } from './types' import { DavProperty } from 'web-pkg/src/constants' import { buildWebDavPublicPath } from 'files/src/helpers/resources' -import { SHARE_JAIL_ID } from 'files/src/services/folder' import { urlJoin } from 'web-pkg/src/utils' export function buildPublicSpaceResource(data): PublicSpaceResource { @@ -43,14 +42,19 @@ export function buildShareSpaceResource({ shareName: string serverUrl: string }): ShareSpaceResource { - return buildSpace({ + const space = buildSpace({ id: [SHARE_JAIL_ID, shareId].join('!'), driveAlias: `share/${shareName}`, driveType: 'share', name: shareName, shareId, serverUrl - }) + }) as ShareSpaceResource + space.rename = (newName: string) => { + space.driveAlias = `share/${newName}` + space.name = newName + } + return space } export function buildSpace(data): SpaceResource { diff --git a/packages/web-client/src/helpers/space/types.ts b/packages/web-client/src/helpers/space/types.ts index b035c308643..5ab8306513e 100644 --- a/packages/web-client/src/helpers/space/types.ts +++ b/packages/web-client/src/helpers/space/types.ts @@ -8,7 +8,10 @@ // or all types get different members, the underscored props can be removed. import { Resource } from '../resource' +export const SHARE_JAIL_ID = 'a0ca6a90-a365-4782-871e-d44447bbc668' + export interface SpaceResource extends Resource { + disabled?: boolean webDavUrl: string getWebDavUrl(resource: Resource): string getDriveAliasAndItem(resource: Resource): string @@ -30,6 +33,7 @@ export const isProjectSpaceResource = (resource: Resource): resource is ProjectS export interface ShareSpaceResource extends SpaceResource { __shareSpaceResource?: any + rename(newName: string): void } export const isShareSpaceResource = (resource: Resource): resource is ShareSpaceResource => { return resource.driveType === 'share' diff --git a/packages/web-client/src/types.ts b/packages/web-client/src/types.ts index 30e9cc70604..7897b075c47 100644 --- a/packages/web-client/src/types.ts +++ b/packages/web-client/src/types.ts @@ -7,6 +7,7 @@ export type OwnCloudSdk = { getFileContents(...args): any putFileContents(...args): any getFavoriteFiles(...args): any + getPathForFileId(fileId: string | number): Promise search(...args): any copy(...args): any move(...args): any diff --git a/packages/web-client/src/webdav/getFileInfo.ts b/packages/web-client/src/webdav/getFileInfo.ts index 86fb3ffc568..c3354740f76 100644 --- a/packages/web-client/src/webdav/getFileInfo.ts +++ b/packages/web-client/src/webdav/getFileInfo.ts @@ -9,7 +9,7 @@ export const GetFileInfoFactory = ( return { async getFileInfo( space: SpaceResource, - resource?: { path?: string }, + resource?: { path?: string; fileId?: string | number }, options?: ListFilesOptions ): Promise { return ( diff --git a/packages/web-client/src/webdav/listFiles.ts b/packages/web-client/src/webdav/listFiles.ts index 9c50b198b2e..365e84a72fc 100644 --- a/packages/web-client/src/webdav/listFiles.ts +++ b/packages/web-client/src/webdav/listFiles.ts @@ -18,7 +18,7 @@ export const ListFilesFactory = ({ sdk }: WebDavOptions) => { return { async listFiles( space: SpaceResource, - { path }: { path?: string } = {}, + { path, fileId }: { path?: string; fileId?: string | number } = {}, { depth = 1, davProperties }: ListFilesOptions = {} ): Promise { let webDavResources: any[] @@ -30,6 +30,13 @@ export const ListFilesFactory = ({ sdk }: WebDavOptions) => { `${depth}` ) + // FIXME: This is a workaround for https://github.com/owncloud/ocis/issues/4758 + if (webDavResources.length === 1) { + webDavResources[0].name = urlJoin(space.id, path, { + leadingSlashes: true + }) + } + // We remove the /${publicLinkToken} prefix so the name is relative to the public link root // At first we tried to do this in buildResource but only the public link root resource knows it's a public link webDavResources.forEach((resource) => { @@ -42,12 +49,28 @@ export const ListFilesFactory = ({ sdk }: WebDavOptions) => { return webDavResources.map(buildResource) } - webDavResources = await sdk.files.list( - urlJoin(space.webDavPath, path), - `${depth}`, - davProperties || DavProperties.Default - ) - return webDavResources.map(buildResource) + const listFilesCorrectedPath = async () => { + const correctPath = await sdk.files.getPathForFileId(fileId) + return this.listFiles(space, { path: correctPath }, { depth, davProperties }) + } + + try { + webDavResources = await sdk.files.list( + urlJoin(space.webDavPath, path), + `${depth}`, + davProperties || DavProperties.Default + ) + const resources = webDavResources.map(buildResource) + if (fileId && fileId !== resources[0].fileId) { + return listFilesCorrectedPath() + } + return resources + } catch (e) { + if (e.statusCode === 404 && fileId) { + return listFilesCorrectedPath() + } + throw e + } } } } diff --git a/packages/web-client/src/webdav/moveFiles.ts b/packages/web-client/src/webdav/moveFiles.ts index 2972ffb4fa1..51973f62cf8 100644 --- a/packages/web-client/src/webdav/moveFiles.ts +++ b/packages/web-client/src/webdav/moveFiles.ts @@ -1,5 +1,10 @@ import { urlJoin } from 'web-pkg/src/utils' -import { isPublicSpaceResource, SpaceResource } from '../helpers' +import { + isPublicSpaceResource, + SpaceResource, + isShareSpaceResource, + SHARE_JAIL_ID +} from '../helpers' import { WebDavOptions } from './types' export const MoveFilesFactory = ({ sdk }: WebDavOptions) => { @@ -11,6 +16,13 @@ export const MoveFilesFactory = ({ sdk }: WebDavOptions) => { { path: targetPath }, options?: { overwrite?: boolean } ): Promise { + if (isShareSpaceResource(sourceSpace) && sourcePath === '/') { + return sdk.files.move( + `${sourceSpace.webDavPath}/${sourcePath || ''}`, + `/spaces/${SHARE_JAIL_ID}!${SHARE_JAIL_ID}/${targetPath || ''}`, + options?.overwrite || false + ) + } if (isPublicSpaceResource(sourceSpace)) { return sdk.publicFiles.move( urlJoin(sourceSpace.webDavPath.replace(/^\/public-files/, ''), sourcePath), diff --git a/packages/web-pkg/src/composables/appDefaults/types.ts b/packages/web-pkg/src/composables/appDefaults/types.ts index cc7ba254dda..aa76111b3ff 100644 --- a/packages/web-pkg/src/composables/appDefaults/types.ts +++ b/packages/web-pkg/src/composables/appDefaults/types.ts @@ -6,6 +6,7 @@ export interface FileContext { driveAliasAndItem: MaybeRef space: MaybeRef item: MaybeRef + itemId: MaybeRef fileName: MaybeRef routeName: MaybeRef routeParams: MaybeRef diff --git a/packages/web-pkg/src/composables/appDefaults/useAppDefaults.ts b/packages/web-pkg/src/composables/appDefaults/useAppDefaults.ts index 4f2b37d2527..0e1e204621b 100644 --- a/packages/web-pkg/src/composables/appDefaults/useAppDefaults.ts +++ b/packages/web-pkg/src/composables/appDefaults/useAppDefaults.ts @@ -50,7 +50,7 @@ export function useAppDefaults(options: AppDefaultsOptions): AppDefaultsResult { const isPublicLinkContext = usePublicLinkContext({ store }) const driveAliasAndItem = useRouteParam('driveAliasAndItem') - const { space, item } = useDriveResolver({ + const { space, item, itemId } = useDriveResolver({ store, driveAliasAndItem }) @@ -68,6 +68,7 @@ export function useAppDefaults(options: AppDefaultsOptions): AppDefaultsResult { driveAliasAndItem: unref(driveAliasAndItem), space: unref(space), item: unref(item), + itemId: unref(itemId), fileName: basename(path), routeName: queryItemAsString(unref(currentRoute).query[contextRouteNameKey]), ...contextQueryToFileContextProps(unref(currentRoute).query) diff --git a/packages/web-pkg/src/composables/appDefaults/useAppFileHandling.ts b/packages/web-pkg/src/composables/appDefaults/useAppFileHandling.ts index d590755e51c..2288771a2fa 100644 --- a/packages/web-pkg/src/composables/appDefaults/useAppFileHandling.ts +++ b/packages/web-pkg/src/composables/appDefaults/useAppFileHandling.ts @@ -65,7 +65,8 @@ export function useAppFileHandling({ return webdav.getFileInfo( unref(unref(fileContext).space), { - path: unref(unref(fileContext).item) + path: unref(unref(fileContext).item), + fileId: unref(unref(fileContext).itemId) }, options ) diff --git a/packages/web-pkg/src/composables/appDefaults/useAppFolderHandling.ts b/packages/web-pkg/src/composables/appDefaults/useAppFolderHandling.ts index e578bbaf554..528f155ec86 100644 --- a/packages/web-pkg/src/composables/appDefaults/useAppFolderHandling.ts +++ b/packages/web-pkg/src/composables/appDefaults/useAppFolderHandling.ts @@ -10,6 +10,9 @@ import { Resource } from 'web-client' import { FileContext } from './types' import { authService } from 'web-runtime/src/services/auth' import { Route } from 'vue-router' +import { useAppFileHandling } from './useAppFileHandling' +import { useFileRouteReplace } from '../router/useFileRouteReplace' +import { DavProperty } from '../../constants' interface AppFolderHandlingOptions { store: Store @@ -27,12 +30,15 @@ export interface AppFolderHandlingResult { export function useAppFolderHandling({ store, currentRoute, - clientService: { webdav } + clientService }: AppFolderHandlingOptions): AppFolderHandlingResult { const isFolderLoading = ref(false) const activeFiles = computed(() => { return store.getters['Files/activeFiles'] }) + const { webdav } = clientService + const { replaceInvalidFileRoute } = useFileRouteReplace() + const { getFileInfo } = useAppFileHandling({ clientService }) const loadFolderForFileContext = async (context: MaybeRef) => { if (store.getters.activeFile && store.getters.activeFile.path !== '') { @@ -44,8 +50,18 @@ export function useAppFolderHandling({ try { context = unref(context) const space = unref(context.space) - const path = dirname(unref(context.item)) + const pathResource = await getFileInfo(context, { + davProperties: [DavProperty.FileId] + }) + replaceInvalidFileRoute({ + space, + resource: pathResource, + path: unref(context.item), + fileId: unref(context.itemId) + }) + + const path = dirname(pathResource.path) const resources = await webdav.listFiles(space, { path }) diff --git a/packages/web-pkg/src/composables/appDefaults/useAppNavigation.ts b/packages/web-pkg/src/composables/appDefaults/useAppNavigation.ts index c90ee48bfb2..86765527099 100644 --- a/packages/web-pkg/src/composables/appDefaults/useAppNavigation.ts +++ b/packages/web-pkg/src/composables/appDefaults/useAppNavigation.ts @@ -4,6 +4,8 @@ import VueRouter, { Location } from 'vue-router' import { MaybeRef } from '../../utils' import { FileContext } from './types' import { LocationQuery, LocationParams } from '../router' +import { Resource } from 'web-client' +import { useFileRouteReplace } from '../router/useFileRouteReplace' interface AppNavigationOptions { router: VueRouter @@ -12,6 +14,7 @@ interface AppNavigationOptions { export interface AppNavigationResult { closeApp(): void + replaceInvalidFileRoute(context: MaybeRef, resource: Resource): void } export const contextRouteNameKey = 'contextRouteName' @@ -30,7 +33,7 @@ export const routeToContextQuery = (location: Location): LocationQuery => { const { params, query } = location const contextQuery = {} - const contextQueryItems = ['shareId'].concat( + const contextQueryItems = ['fileId', 'shareId'].concat( (location as any).meta?.contextQueryItems || [] ) as string[] for (const queryItem of contextQueryItems) { @@ -82,11 +85,25 @@ export function useAppNavigation({ }) } + const { replaceInvalidFileRoute: replaceInvalidFileRouteGeneric } = useFileRouteReplace({ + router + }) + const replaceInvalidFileRoute = (context: MaybeRef, resource: Resource) => { + const ctx = unref(context) + return replaceInvalidFileRouteGeneric({ + space: unref(ctx.space), + resource, + path: unref(ctx.item), + fileId: unref(ctx.itemId) + }) + } + const closeApp = () => { return navigateToContext(currentFileContext) } return { + replaceInvalidFileRoute, closeApp } } diff --git a/packages/web-pkg/src/composables/configuration/index.ts b/packages/web-pkg/src/composables/configuration/index.ts new file mode 100644 index 00000000000..6908e3ec987 --- /dev/null +++ b/packages/web-pkg/src/composables/configuration/index.ts @@ -0,0 +1 @@ +export * from './useConfigurationManager' diff --git a/packages/web-pkg/src/composables/configuration/useConfigurationManager.ts b/packages/web-pkg/src/composables/configuration/useConfigurationManager.ts new file mode 100644 index 00000000000..9b7faef12ac --- /dev/null +++ b/packages/web-pkg/src/composables/configuration/useConfigurationManager.ts @@ -0,0 +1,5 @@ +import { configurationManager } from '../../configuration' + +export const useConfigurationManager = () => { + return configurationManager +} diff --git a/packages/web-pkg/src/composables/driveResolver/useDriveResolver.ts b/packages/web-pkg/src/composables/driveResolver/useDriveResolver.ts index 4fa17500efe..a3bb4ee8ed5 100644 --- a/packages/web-pkg/src/composables/driveResolver/useDriveResolver.ts +++ b/packages/web-pkg/src/composables/driveResolver/useDriveResolver.ts @@ -9,6 +9,7 @@ import { useSpacesLoading } from './useSpacesLoading' import { queryItemAsString } from '../appDefaults' import { configurationManager } from '../../configuration' import { urlJoin } from 'web-pkg/src/utils' +import { useCapabilitySpacesEnabled } from '../capability' interface DriveResolverOptions { store?: Store @@ -19,10 +20,17 @@ export const useDriveResolver = (options: DriveResolverOptions = {}) => { const store = options.store || useStore() const { areSpacesLoading } = useSpacesLoading({ store }) const shareId = useRouteQuery('shareId') + const fileIdQueryItem = useRouteQuery('fileId') + const fileId = computed(() => { + return queryItemAsString(unref(fileIdQueryItem)) + }) + const hasSpaces = useCapabilitySpacesEnabled(store) + const { graphClient } = useGraphClient({ store }) - const spaces = computed(() => store.getters['runtime/spaces/spaces']) + const spaces = computed(() => store.getters['runtime/spaces/spaces']) const space = ref(null) const item: Ref = ref(null) + watch( [options.driveAliasAndItem, areSpacesLoading], ([driveAliasAndItem]) => { @@ -31,14 +39,19 @@ export const useDriveResolver = (options: DriveResolverOptions = {}) => { item.value = null return } - if (unref(space) && driveAliasAndItem.startsWith(unref(space).driveAlias)) { + + const isOnlyItemPathChanged = + unref(space) && driveAliasAndItem.startsWith(unref(space).driveAlias) + if (isOnlyItemPathChanged) { item.value = urlJoin(driveAliasAndItem.slice(unref(space).driveAlias.length), { leadingSlash: true }) return } + let matchingSpace = null let path = null + const driveAliasAndItemSegments = driveAliasAndItem.split('/') if (driveAliasAndItem.startsWith('public/')) { const [publicLinkToken, ...item] = driveAliasAndItem.split('/').slice(1) matchingSpace = unref(spaces).find((s) => s.id === publicLinkToken) @@ -52,15 +65,31 @@ export const useDriveResolver = (options: DriveResolverOptions = {}) => { }) path = item.join('/') } else { - unref(spaces).forEach((s) => { - if (!driveAliasAndItem.startsWith(s.driveAlias)) { - return - } - if (!matchingSpace || s.driveAlias.length > matchingSpace.driveAlias.length) { - matchingSpace = s - path = driveAliasAndItem.slice(s.driveAlias.length) - } - }) + if (unref(hasSpaces) && unref(fileId)) { + matchingSpace = unref(spaces).find((s) => { + return unref(fileId).startsWith(`${s.fileId}`) + }) + } else { + matchingSpace = unref(spaces).find((s) => { + if (!driveAliasAndItem.startsWith(s.driveAlias)) { + return false + } + + const driveAliasSegments = s.driveAlias.split('/') + if ( + driveAliasAndItemSegments.length < driveAliasSegments.length || + driveAliasAndItemSegments.slice(0, driveAliasSegments.length).join('/') !== + s.driveAlias + ) { + return false + } + + return s + }) + } + if (matchingSpace) { + path = driveAliasAndItem.slice(matchingSpace.driveAlias.length) + } } space.value = matchingSpace item.value = urlJoin(path, { @@ -84,6 +113,7 @@ export const useDriveResolver = (options: DriveResolverOptions = {}) => { ) return { space, - item + item, + itemId: fileId } } diff --git a/packages/web-pkg/src/composables/router/useFileRouteReplace.ts b/packages/web-pkg/src/composables/router/useFileRouteReplace.ts new file mode 100644 index 00000000000..ffde3f9b2fe --- /dev/null +++ b/packages/web-pkg/src/composables/router/useFileRouteReplace.ts @@ -0,0 +1,48 @@ +import { useRouter } from './useRouter' +import { useConfigurationManager } from '../configuration' +import { Resource, SpaceResource } from 'web-client/src/helpers' +import { createFileRouteOptions, mergeFileRouteOptions } from '../../helpers/router' +import Router from 'vue-router' +import { ConfigurationManager } from '../../configuration' + +export interface FileRouteReplaceOptions { + router?: Router + configurationManager?: ConfigurationManager +} + +export const useFileRouteReplace = (options: FileRouteReplaceOptions = {}) => { + const router = options.router || useRouter() + const configurationManager = options.configurationManager || useConfigurationManager() + + const replaceInvalidFileRoute = ({ + space, + resource, + path, + fileId + }: { + space: SpaceResource + resource: Resource + path: string + fileId?: string | number + }): boolean => { + if (!configurationManager?.options?.routing?.idBased) { + return false + } + if (path === resource.path && fileId === resource.fileId) { + return false + } + + const routeOptions = mergeFileRouteOptions( + router.currentRoute, + createFileRouteOptions(space, resource, { + configurationManager + }) + ) + router.replace(routeOptions) + return true + } + + return { + replaceInvalidFileRoute + } +} diff --git a/packages/web-pkg/src/configuration/manager.ts b/packages/web-pkg/src/configuration/manager.ts index 45c5d651940..04a473355ab 100644 --- a/packages/web-pkg/src/configuration/manager.ts +++ b/packages/web-pkg/src/configuration/manager.ts @@ -1,24 +1,35 @@ -import { OAuth2Configuration, OIDCConfiguration, RuntimeConfiguration } from './types' +import { + OAuth2Configuration, + OIDCConfiguration, + OptionsConfiguration, + RuntimeConfiguration +} from './types' import isNil from 'lodash-es/isNil' +import get from 'lodash-es/get' +import set from 'lodash-es/set' import { urlJoin } from 'web-pkg/src/utils' export interface RawConfig { server: string auth?: any openIdConnect?: any + options?: OptionsConfiguration } export class ConfigurationManager { - private runtimeConfiguration: RuntimeConfiguration + private readonly runtimeConfiguration: RuntimeConfiguration + private readonly optionsConfiguration: OptionsConfiguration private oAuth2Configuration: OAuth2Configuration private oidcConfiguration: OIDCConfiguration constructor() { this.runtimeConfiguration = { serverUrl: '' } + this.optionsConfiguration = {} } public initialize(rawConfig: RawConfig): void { this.serverUrl = rawConfig.server + this.options = rawConfig.options this.oAuth2Configuration = rawConfig.auth ? (rawConfig.auth as OAuth2Configuration) : null this.oidcConfiguration = rawConfig.openIdConnect ? (rawConfig.openIdConnect as OIDCConfiguration) @@ -51,6 +62,14 @@ export class ConfigurationManager { get oidc(): OIDCConfiguration { return this.oidcConfiguration } + + set options(options: OptionsConfiguration) { + set(this.optionsConfiguration, 'routing.idBased', get(options, 'routing.idBased', true)) + } + + get options(): OptionsConfiguration { + return this.optionsConfiguration + } } export const configurationManager = new ConfigurationManager() diff --git a/packages/web-pkg/src/configuration/types.ts b/packages/web-pkg/src/configuration/types.ts index 6e23d6a870b..9323518a152 100644 --- a/packages/web-pkg/src/configuration/types.ts +++ b/packages/web-pkg/src/configuration/types.ts @@ -2,6 +2,14 @@ export interface RuntimeConfiguration { serverUrl: string } +export interface RoutingOptionsConfiguration { + idBased?: boolean +} + +export interface OptionsConfiguration { + routing?: RoutingOptionsConfiguration +} + export interface OAuth2Configuration { clientId: string clientSecret?: string diff --git a/packages/web-pkg/src/constants/dav.ts b/packages/web-pkg/src/constants/dav.ts index bea64663b39..40120e6997b 100644 --- a/packages/web-pkg/src/constants/dav.ts +++ b/packages/web-pkg/src/constants/dav.ts @@ -15,6 +15,7 @@ export abstract class DavProperty { static readonly Permissions: string = '{http://owncloud.org/ns}permissions' static readonly IsFavorite: string = '{http://owncloud.org/ns}favorite' static readonly FileId: string = '{http://owncloud.org/ns}fileid' + static readonly FileParent: string = '{http://owncloud.org/ns}file-parent' static readonly Name: string = '{http://owncloud.org/ns}name' static readonly OwnerId: string = '{http://owncloud.org/ns}owner-id' static readonly OwnerDisplayName: string = '{http://owncloud.org/ns}owner-display-name' @@ -54,6 +55,7 @@ export abstract class DavProperties { DavProperty.Permissions, DavProperty.IsFavorite, DavProperty.FileId, + DavProperty.FileParent, DavProperty.Name, DavProperty.OwnerId, DavProperty.OwnerDisplayName, @@ -85,6 +87,7 @@ export abstract class DavProperties { DavProperty.TrashbinOriginalLocation, DavProperty.TrashbinOriginalFilename, DavProperty.TrashbinDeletedDate, - DavProperty.Permissions + DavProperty.Permissions, + DavProperty.FileParent ] } diff --git a/packages/web-pkg/src/helpers/router/index.ts b/packages/web-pkg/src/helpers/router/index.ts new file mode 100644 index 00000000000..ded58954008 --- /dev/null +++ b/packages/web-pkg/src/helpers/router/index.ts @@ -0,0 +1 @@ +export * from './routeOptions' diff --git a/packages/web-pkg/src/helpers/router/routeOptions.ts b/packages/web-pkg/src/helpers/router/routeOptions.ts new file mode 100644 index 00000000000..acb399fdb7c --- /dev/null +++ b/packages/web-pkg/src/helpers/router/routeOptions.ts @@ -0,0 +1,51 @@ +import { isShareSpaceResource, Resource, SpaceResource } from 'web-client/src/helpers' +import { configurationManager, ConfigurationManager } from '../../configuration' +import { LocationParams, LocationQuery } from '../../composables' +import { isUndefined } from 'lodash-es' +import { Route } from 'vue-router' + +/** + * Creates route options for routing into a file location: + * - params.driveAliasAndItem + * - query.shareId + * - query.fileId + * + * Both query options are optional. + * + * @param space {SpaceResource} + * @param target {path: string, fileId: string | number} + * @param options {configurationManager: ConfigurationManager} + */ +export const createFileRouteOptions = ( + space: SpaceResource, + target: { path?: string; fileId?: string | number }, + options?: { configurationManager?: ConfigurationManager } +): { params: LocationParams; query: LocationQuery } => { + const config = options?.configurationManager || configurationManager + return { + params: { + driveAliasAndItem: space.getDriveAliasAndItem({ path: target.path || '' } as Resource) + }, + query: { + ...(isShareSpaceResource(space) && { shareId: space.shareId }), + ...(config?.options?.routing?.idBased && + !isUndefined(target.fileId) && { fileId: `${target.fileId}` }) + } + } +} + +export const mergeFileRouteOptions = ( + originalRoute: Route, + routeOptions: { params: LocationParams; query: LocationQuery } +): Route => { + return Object.assign({}, originalRoute, { + params: { + ...originalRoute.params, + ...routeOptions.params + }, + query: { + ...originalRoute.query, + ...routeOptions.query + } + }) +} diff --git a/packages/web-runtime/src/components/UploadInfo.vue b/packages/web-runtime/src/components/UploadInfo.vue index 57e9c859225..932a0e0b48f 100644 --- a/packages/web-runtime/src/components/UploadInfo.vue +++ b/packages/web-runtime/src/components/UploadInfo.vue @@ -146,6 +146,8 @@ import { mapGetters } from 'vuex' import { defineComponent } from '@vue/composition-api' import { UppyResource } from '../composables/upload' import { urlJoin } from 'web-pkg/src/utils' +import { isUndefined } from 'lodash-es' +import { configurationManager } from 'web-pkg/src/configuration' export default defineComponent({ setup() { @@ -441,18 +443,31 @@ export default defineComponent({ ...file.targetRoute, params: { ...file.targetRoute.params, - driveAliasAndItem: urlJoin(file.targetRoute.params.driveAliasAndItem, file.name) + driveAliasAndItem: urlJoin(file.targetRoute.params.driveAliasAndItem, file.name, { + leadingSlash: false + }) + }, + query: { + ...file.targetRoute.query, + ...(configurationManager.options.routing.idBased && + !isUndefined(file.meta.fileId) && { fileId: file.meta.fileId }) } } }, parentFolderLink(file: any) { - return file.targetRoute + return { + ...file.targetRoute, + query: { + ...file.targetRoute.query, + ...(configurationManager.options.routing.idBased && + !isUndefined(file.meta.currentFolderId) && { fileId: file.meta.currentFolderId }) + } + } }, buildRouteFromUppyResource(resource) { if (!resource.meta.routeName) { return null } - return { name: resource.meta.routeName, params: { diff --git a/packages/web-runtime/src/composables/upload/useUpload.ts b/packages/web-runtime/src/composables/upload/useUpload.ts index 62185f76389..e3bcaf5c057 100644 --- a/packages/web-runtime/src/composables/upload/useUpload.ts +++ b/packages/web-runtime/src/composables/upload/useUpload.ts @@ -29,6 +29,8 @@ export interface UppyResource { driveAlias: string driveType: string currentFolder: string // current folder path during upload initiation + currentFolderId?: string | number + fileId?: string | number // upload data relativeFolder: string relativePath: string @@ -47,7 +49,12 @@ interface UploadOptions { } interface UploadResult { - createDirectoryTree(space: SpaceResource, currentPath: string, files: UppyResource[]): void + createDirectoryTree( + space: SpaceResource, + currentPath: string, + files: UppyResource[], + currentFolderId?: string | number + ): void } export function useUpload(options: UploadOptions): UploadResult { @@ -122,7 +129,12 @@ const createDirectoryTree = ({ clientService: ClientService uppyService: UppyService }) => { - return async (space: SpaceResource, currentFolder: string, files: UppyResource[]) => { + return async ( + space: SpaceResource, + currentFolder: string, + files: UppyResource[], + currentFolderId?: string | number + ) => { const { webdav } = clientService const createdFolders = [] for (const file of files) { @@ -159,6 +171,7 @@ const createDirectoryTree = ({ driveAlias: space.driveAlias, driveType: space.driveType, currentFolder, + currentFolderId, // upload data relativeFolder: createdSubFolders, uploadId, @@ -171,13 +184,17 @@ const createDirectoryTree = ({ uppyService.publish('addedForUpload', [uppyResource]) + let folder try { - await webdav.createFolder(space, { path: join(currentFolder, folderToCreate) }) + folder = await webdav.createFolder(space, { path: join(currentFolder, folderToCreate) }) } catch (error) { console.error(error) } - uppyService.publish('uploadSuccess', uppyResource) + uppyService.publish('uploadSuccess', { + ...uppyResource, + meta: { ...uppyResource.meta, fileId: folder?.fileId } + }) createdSubFolders += `/${subFolder}` createdFolders.push(createdSubFolders) diff --git a/packages/web-runtime/src/index.ts b/packages/web-runtime/src/index.ts index f60dcc65248..d8944257a48 100644 --- a/packages/web-runtime/src/index.ts +++ b/packages/web-runtime/src/index.ts @@ -34,6 +34,8 @@ import { isPublicSpaceResource, Resource } from 'web-client/src/helpers' +import { WebDAV } from 'web-client/src/webdav' +import { DavProperty } from 'web-pkg/src/constants' export const bootstrap = async (configurationPath: string): Promise => { const runtimeConfiguration = await announceConfiguration(configurationPath) @@ -90,13 +92,14 @@ export const renderSuccess = (): void => { (state, getters) => { return getters['runtime/auth/isUserContextReady'] }, - (userContextReady) => { + async (userContextReady) => { if (!userContextReady) { return } + const clientService = instance.$clientService + // Load spaces to make them available across the application if (store.getters.capabilities?.spaces?.enabled) { - const clientService = instance.$clientService const graphClient = clientService.graphAuthenticated( store.getters.configuration.server, store.getters['runtime/auth/accessToken'] @@ -120,6 +123,14 @@ export const renderSuccess = (): void => { webDavPath: `/files/${user.id}`, serverUrl: configurationManager.serverUrl }) + const personalHomeInfo = await (clientService.webdav as WebDAV).getFileInfo( + space, + { + path: '' + }, + { davProperties: [DavProperty.FileId] } + ) + space.fileId = personalHomeInfo.fileId store.commit('runtime/spaces/ADD_SPACES', [space]) store.commit('runtime/spaces/SET_SPACES_INITIALIZED', true) }, diff --git a/packages/web-runtime/tests/unit/components/UploadInfo.spec.ts b/packages/web-runtime/tests/unit/components/UploadInfo.spec.ts index c0fed873851..84bdc077c17 100644 --- a/packages/web-runtime/tests/unit/components/UploadInfo.spec.ts +++ b/packages/web-runtime/tests/unit/components/UploadInfo.spec.ts @@ -19,7 +19,17 @@ const selectors = { } } +jest.mock('web-pkg/src/configuration', () => ({ + configurationManager: { + options: { routing: { idBased: true } } + } +})) + describe('UploadInfo component', () => { + afterEach(() => { + jest.clearAllMocks() + }) + it('should render the component in a hidden state per default', () => { const { wrapper } = getShallowWrapper() const overlay = wrapper.find(selectors.overlay) diff --git a/tests/drone/config-oc10-oauth.json b/tests/drone/config-oc10-oauth.json index fe076bb417c..1b0da2e112b 100644 --- a/tests/drone/config-oc10-oauth.json +++ b/tests/drone/config-oc10-oauth.json @@ -14,6 +14,9 @@ "shares": { "showAllOnLoad": true } + }, + "routing": { + "idBased": false } }, "apps": [ diff --git a/tests/drone/config-oc10-openid.json b/tests/drone/config-oc10-openid.json index 6af143d83f1..8682a681263 100644 --- a/tests/drone/config-oc10-openid.json +++ b/tests/drone/config-oc10-openid.json @@ -15,6 +15,9 @@ "shares": { "showAllOnLoad": true } + }, + "routing": { + "idBased": false } }, "apps": [ diff --git a/tests/drone/config-ocis.json b/tests/drone/config-ocis.json index 2df62ab03dc..6721a841836 100644 --- a/tests/drone/config-ocis.json +++ b/tests/drone/config-ocis.json @@ -16,6 +16,9 @@ "shares": { "showAllOnLoad": true } + }, + "routing": { + "idBased": false } }, "apps": [ diff --git a/tests/unit/mocks/store/filesModuleMockOptions.ts b/tests/unit/mocks/store/filesModuleMockOptions.ts index 20ae4074de2..0b05277ce08 100644 --- a/tests/unit/mocks/store/filesModuleMockOptions.ts +++ b/tests/unit/mocks/store/filesModuleMockOptions.ts @@ -11,7 +11,8 @@ export const filesModuleMockOptions = { SET_SELECTED_IDS: jest.fn(), RENAME_FILE: jest.fn(), SET_HIDDEN_FILES_VISIBILITY: jest.fn(), - SET_FILE_EXTENSIONS_VISIBILITY: jest.fn() + SET_FILE_EXTENSIONS_VISIBILITY: jest.fn(), + UPSERT_RESOURCE: jest.fn() }, actions: { deleteFiles: jest.fn()