diff --git a/changelog/unreleased/enhancement-enable-search-all-files b/changelog/unreleased/enhancement-enable-search-all-files new file mode 100644 index 00000000000..c8a9f3db2d8 --- /dev/null +++ b/changelog/unreleased/enhancement-enable-search-all-files @@ -0,0 +1,7 @@ +Enhancement: Enable search all files for ocis backend + +We've enabled the search all files feature for ocis backend: +* Find files in sub directories +* Find files in other places like project spaces + +https://github.com/owncloud/web/pull/6841 diff --git a/packages/web-app-files/src/components/FilesList/ResourceTable.vue b/packages/web-app-files/src/components/FilesList/ResourceTable.vue index d06c88a8268..cca3ceed7e5 100644 --- a/packages/web-app-files/src/components/FilesList/ResourceTable.vue +++ b/packages/web-app-files/src/components/FilesList/ResourceTable.vue @@ -50,7 +50,7 @@ :key="`${item.path}-${resourceDomSelector(item)}-${item.thumbnail}`" :resource="item" :is-path-displayed="arePathsDisplayed" - :parent-folder-name-default="defaultParentFolderName" + :parent-folder-name-default="getDefaultParentFolderName(item)" :is-thumbnail-displayed="areThumbnailsDisplayed" :is-extension-displayed="areFileExtensionsShown" :is-resource-clickable="isResourceClickable(item.id)" @@ -176,6 +176,7 @@ import Rename from '../../mixins/actions/rename' import { defineComponent, PropType } from '@vue/composition-api' import { extractDomSelector, Resource } from '../../helpers/resource' import { ShareTypes } from '../../helpers/share' +import { createLocationSpaces } from '../../router' export default defineComponent({ mixins: [Rename], @@ -353,7 +354,7 @@ export default defineComponent({ }, computed: { ...mapGetters(['configuration']), - ...mapState('Files', ['areFileExtensionsShown']), + ...mapState('Files', ['areFileExtensionsShown', 'spaces']), popperOptions() { return { modifiers: [ @@ -512,9 +513,6 @@ export default defineComponent({ }, currentLanguage() { return (this.$language?.current || '').split('_')[0] - }, - defaultParentFolderName() { - return this.hasSpaces ? this.$gettext('Personal') : this.$gettext('All files and folders') } }, methods: { @@ -543,11 +541,20 @@ export default defineComponent({ if (this.targetRoute === null) { return {} } + + const matchingSpace = this.getMatchingSpace(storageId) + + if (matchingSpace && matchingSpace?.driveType === 'project') { + return createLocationSpaces('files-spaces-project', { + params: { storageId, item: path.replace(/^\//, '') || undefined } + }) + } + return { name: this.targetRoute.name, query: this.targetRoute.query, params: { - item: path.replace(/^\//, ''), + item: path.replace(/^\//, '') || undefined, ...this.targetRoute.params, ...(storageId && { storageId }) } @@ -693,6 +700,21 @@ export default defineComponent({ resourceType, ownerName: resource.owner[0].displayName }) + }, + getMatchingSpace(storageId) { + return this.spaces.find((space) => space.id === storageId) + }, + getDefaultParentFolderName(resource) { + if (!this.hasSpaces) { + return this.$gettext('All files and folders') + } + const matchingSpace = this.getMatchingSpace(resource.storageId) + + if (matchingSpace && matchingSpace?.driveType === 'project') { + return matchingSpace.name + } + + return this.$gettext('Personal') } } }) diff --git a/packages/web-app-files/src/components/Search/Preview.vue b/packages/web-app-files/src/components/Search/Preview.vue index e3b63eab5b7..c521dbf86b0 100644 --- a/packages/web-app-files/src/components/Search/Preview.vue +++ b/packages/web-app-files/src/components/Search/Preview.vue @@ -1,11 +1,12 @@ @@ -17,7 +18,7 @@ import { ImageDimension } from '../../constants' import { loadPreview } from '../../helpers/resource' import debounce from 'lodash-es/debounce' import Vue from 'vue' -import { mapGetters } from 'vuex' +import { mapGetters, mapState } from 'vuex' import { createLocationSpaces } from '../../router' import path from 'path' import { useCapabilitySpacesEnabled } from 'web-pkg/src/composables' @@ -43,7 +44,8 @@ export default { setup() { return { hasSpaces: useCapabilitySpacesEnabled(), - resourceTargetLocation: createLocationSpaces('files-spaces-personal-home') + resourceTargetLocation: createLocationSpaces('files-spaces-personal-home'), + resourceTargetLocationSpace: createLocationSpaces('files-spaces-project') } }, data() { @@ -53,9 +55,21 @@ export default { }, computed: { ...mapGetters(['configuration', 'user', 'getToken']), + ...mapState('Files', ['spaces']), + matchingSpace() { + return this.spaces.find((space) => space.id === this.resource.storageId) + }, defaultParentFolderName() { - return this.hasSpaces ? this.$gettext('Personal') : this.$gettext('All files and folders') + if (!this.hasSpaces) { + return this.$gettext('All files and folders') + } + + if (this.matchingSpace?.driveType === 'project') { + return this.matchingSpace.name + } + + return this.$gettext('Personal') } }, beforeMount() { @@ -91,14 +105,20 @@ export default { return this.createFolderLink(path.dirname(file.path), file.storageId) }, createFolderLink(path, storageId) { - if (this.resourceTargetLocation === null) { + if (this.resourceTargetLocation === null || this.resourceTargetLocationSpace === null) { return {} } + + if (this.matchingSpace?.driveType === 'project') { + return createLocationSpaces('files-spaces-project', { + params: { storageId, item: path.replace(/^\//, '') || undefined } + }) + } + return { name: this.resourceTargetLocation.name, - query: this.resourceTargetLocation.query, params: { - item: path.replace(/^\//, ''), + item: path.replace(/^\//, '') || undefined, ...this.resourceTargetLocation.params, ...(storageId && { storageId }) } diff --git a/packages/web-app-files/src/helpers/resources.ts b/packages/web-app-files/src/helpers/resources.ts index ae2e14a63e4..6c5064ae316 100644 --- a/packages/web-app-files/src/helpers/resources.ts +++ b/packages/web-app-files/src/helpers/resources.ts @@ -122,7 +122,7 @@ export function buildSpace(space) { spaceReadmeData = space.special.find((el) => el.specialFolder.name === 'readme') } - if (space.root) { + if (space.root?.permissions) { for (const permission of space.root.permissions) { for (const role of SpacePeopleShareRoles.list()) { if (permission.roles.includes(role.name)) { @@ -146,6 +146,7 @@ export function buildSpace(space) { extension: '', path: '', webDavPath: '', + driveType: space.driveType, type: 'space', isFolder: true, mdate: space.lastModifiedDateTime, @@ -161,7 +162,7 @@ export function buildSpace(space) { privateLink: '', downloadURL: '', ownerDisplayName: '', - ownerId: '', + ownerId: space.owner?.user?.id, disabled, spaceQuota: space.quota, spaceRoles, diff --git a/packages/web-app-files/src/index.js b/packages/web-app-files/src/index.js index 24ad526a7cd..1409f9579af 100644 --- a/packages/web-app-files/src/index.js +++ b/packages/web-app-files/src/index.js @@ -22,6 +22,7 @@ import { archiverService, thumbnailService, Registry } from './services' import fileSideBars from './fileSideBars' import { buildRoutes } from './router' import get from 'lodash-es/get' +import { clientService } from 'web-pkg/src/services' // dirty: importing view from other extension within project import SearchResults from '../../web-app-search/src/views/List.vue' @@ -125,6 +126,9 @@ export default { bus.publish('app.search.register.provider', Registry.sdkSearch) }, userReady({ store }) { + // Load spaces to make them available across the application + store.dispatch('Files/loadSpaces', { clientService }) + archiverService.initialize( store.getters.configuration.server || window.location.origin, get(store, 'getters.capabilities.files.archivers', [ diff --git a/packages/web-app-files/src/search/sdk/list.ts b/packages/web-app-files/src/search/sdk/list.ts index fec6c34c13a..94da728e099 100644 --- a/packages/web-app-files/src/search/sdk/list.ts +++ b/packages/web-app-files/src/search/sdk/list.ts @@ -24,7 +24,12 @@ export default class List implements SearchList { ) return plainResources.map((plainResource) => { - const resource = buildResource(plainResource) + let resourceName = decodeURIComponent(plainResource.name) + if (resourceName.startsWith('/dav')) { + resourceName = resourceName.slice(4) + } + + const resource = buildResource({ ...plainResource, name: resourceName }) return { id: resource.id, data: resource } }) } diff --git a/packages/web-app-files/src/search/sdk/preview.ts b/packages/web-app-files/src/search/sdk/preview.ts index 8f94c5cb92e..8e6936d8592 100644 --- a/packages/web-app-files/src/search/sdk/preview.ts +++ b/packages/web-app-files/src/search/sdk/preview.ts @@ -42,7 +42,12 @@ export default class Preview implements SearchPreview { return this.cache.set( term, plainResources.map((plainResource) => { - const resource = buildResource(plainResource) + let resourceName = decodeURIComponent(plainResource.name) + if (resourceName.startsWith('/dav')) { + resourceName = resourceName.slice(4) + } + + const resource = buildResource({ ...plainResource, name: resourceName }) return { id: resource.id, data: resource } }) ) diff --git a/packages/web-app-files/src/store/actions.js b/packages/web-app-files/src/store/actions.js index ad0e393cfc1..616989dd1cd 100644 --- a/packages/web-app-files/src/store/actions.js +++ b/packages/web-app-files/src/store/actions.js @@ -678,6 +678,21 @@ export default { }) }, + async loadSpaces(context, { clientService }) { + const graphClient = clientService.graphAuthenticated( + context.rootGetters.configuration.server, + context.rootGetters.getToken + ) + const graphResponse = await graphClient.drives.listMyDrives() + + if (!graphResponse.data) { + return + } + + const spaces = graphResponse.data.value.map((space) => buildSpace(space)) + context.commit('LOAD_SPACES', spaces) + }, + async loadPreview({ commit, rootGetters }, { resource, isPublic, dimensions, type }) { if ( rootGetters.previewFileExtensions.length && diff --git a/packages/web-app-files/src/store/mutations.js b/packages/web-app-files/src/store/mutations.js index 03de84a2ebf..fab657456b0 100644 --- a/packages/web-app-files/src/store/mutations.js +++ b/packages/web-app-files/src/store/mutations.js @@ -6,6 +6,12 @@ import { renameResource } from '../helpers/resources' import { ShareTypes } from '../helpers/share' export default { + LOAD_SPACES(state, spaces) { + state.spaces = spaces + }, + CLEAR_SPACES(state) { + state.spaces = [] + }, LOAD_FILES(state, { currentFolder, files }) { state.currentFolder = currentFolder state.files = files diff --git a/packages/web-app-files/src/store/state.js b/packages/web-app-files/src/store/state.js index 3e06966a034..7e587852dfe 100644 --- a/packages/web-app-files/src/store/state.js +++ b/packages/web-app-files/src/store/state.js @@ -6,6 +6,7 @@ export default { inProgress: [], shareOpen: null, versions: [], + spaces: [], /** * Outgoing shares and links from currently highlighted element diff --git a/packages/web-app-files/tests/unit/components/FilesList/ResourceTable.spec.js b/packages/web-app-files/tests/unit/components/FilesList/ResourceTable.spec.js index 0919ed5abd7..d6648cdbe1c 100644 --- a/packages/web-app-files/tests/unit/components/FilesList/ResourceTable.spec.js +++ b/packages/web-app-files/tests/unit/components/FilesList/ResourceTable.spec.js @@ -280,7 +280,10 @@ function getMountedWrapper(options = {}) { }, modules: { Files: { - namespaced: true + namespaced: true, + state: { + spaces: [] + } } } }), diff --git a/packages/web-app-files/tests/unit/components/Search/Preview.spec.js b/packages/web-app-files/tests/unit/components/Search/Preview.spec.js index a95d354221d..36a84187256 100644 --- a/packages/web-app-files/tests/unit/components/Search/Preview.spec.js +++ b/packages/web-app-files/tests/unit/components/Search/Preview.spec.js @@ -3,14 +3,13 @@ import DesignSystem from 'owncloud-design-system' import Preview from '@files/src/components/Search/Preview.vue' import VueCompositionAPI from '@vue/composition-api' +import { createStore } from 'vuex-extensions' +import Vuex from 'vuex' const localVue = createLocalVue() localVue.use(DesignSystem) localVue.use(VueCompositionAPI) - -const selectors = { - searchPreview: '.files-search-preview' -} +localVue.use(Vuex) const searchResult = { id: 1234, @@ -21,10 +20,6 @@ const searchResult = { } } -const spyTriggerDefaultAction = jest - .spyOn(Preview.mixins[0].methods, '$_fileActions_triggerDefaultAction') - .mockImplementation() - describe('Preview component', () => { it('should set correct props on oc-resource component', () => { const wrapper = getWrapper() @@ -33,16 +28,6 @@ describe('Preview component', () => { expect(ocResource.exists()).toBeTruthy() expect(ocResource.props().resource).toMatchObject(searchResult.data) }) - it('should trigger the default action when search preview button is clicked', async () => { - const wrapper = getWrapper() - const searchPreview = wrapper.find(selectors.searchPreview) - - expect(spyTriggerDefaultAction).toHaveBeenCalledTimes(0) - - await searchPreview.trigger('click') - - expect(spyTriggerDefaultAction).toHaveBeenCalledTimes(1) - }) describe('folder and parent folder link', () => { it('should be empty if no resource target location given', () => { const wrapper = getWrapper({ resourceTargetLocation: null }) @@ -55,6 +40,41 @@ describe('Preview component', () => { expect(wrapper.vm.parentFolderLink(searchResult.data).params.storageId).toEqual(1) }) }) + + describe('computed method "defaultParentFolderName"', () => { + it('should equal "All files and folders" if spaces capability is not present', () => { + const wrapper = getWrapper({ + resourceTargetLocation: null, + hasSpaces: false + }) + expect(wrapper.vm.defaultParentFolderName).toEqual('All files and folders') + }) + it('should equal the space name if resource storage is representing a project space', () => { + const wrapper = getWrapper({ + resourceTargetLocation: null, + spaces: [ + { + id: 1, + driveType: 'project', + name: 'New space' + } + ] + }) + expect(wrapper.vm.defaultParentFolderName).toEqual('New space') + }) + it('should equal "Personal" if resource storage is not representing a project space', () => { + const wrapper = getWrapper({ + resourceTargetLocation: null, + spaces: [ + { + id: 1, + driveType: 'personal' + } + ] + }) + expect(wrapper.vm.defaultParentFolderName).toEqual('Personal') + }) + }) }) function getWrapper({ @@ -62,10 +82,25 @@ function getWrapper({ route = { query: {}, params: {} - } + }, + spaces = [], + hasSpaces = true } = {}) { return shallowMount(Preview, { localVue, + store: createStore(Vuex.Store, { + getters: { + configuration: () => {} + }, + modules: { + Files: { + namespaced: true, + state: { + spaces + } + } + } + }), mocks: { $route: route }, @@ -78,7 +113,8 @@ function getWrapper({ }, setup: () => { return { - resourceTargetLocation + resourceTargetLocation, + hasSpaces } } }) diff --git a/packages/web-app-files/tests/unit/components/components.setup.js b/packages/web-app-files/tests/unit/components/components.setup.js index 7bdab556d02..72859c9e957 100644 --- a/packages/web-app-files/tests/unit/components/components.setup.js +++ b/packages/web-app-files/tests/unit/components/components.setup.js @@ -113,6 +113,7 @@ export const getStore = function ({ resource: null, filesPageLimit: 100, files: [], + spaces: [], activeFiles: activeFiles, currentFolder: currentFolder, currentPage: currentPage, diff --git a/packages/web-app-files/tests/unit/views/views.setup.js b/packages/web-app-files/tests/unit/views/views.setup.js index 70095223c0a..de5c3d8c42e 100644 --- a/packages/web-app-files/tests/unit/views/views.setup.js +++ b/packages/web-app-files/tests/unit/views/views.setup.js @@ -154,6 +154,7 @@ export const getStore = function ({ resource: null, filesPageLimit: 100, files: [], + spaces: [], activeFiles: activeFiles, currentFolder: currentFolder, currentPage: currentPage, diff --git a/packages/web-runtime/package.json b/packages/web-runtime/package.json index e4da2f13f49..11d63b62c8c 100644 --- a/packages/web-runtime/package.json +++ b/packages/web-runtime/package.json @@ -24,7 +24,7 @@ "marked": "^4.0.12", "oidc-client": "1.11.5", "owncloud-design-system": "^13.1.0-rc.5", - "owncloud-sdk": "~3.0.0-alpha.6", + "owncloud-sdk": "~3.0.0-alpha.8", "p-queue": "^6.1.1", "popper-max-size-modifier": "^0.2.0", "portal-vue": "^2.1.7", diff --git a/yarn.lock b/yarn.lock index bb61092dad2..a38066ed745 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9659,9 +9659,9 @@ __metadata: languageName: node linkType: hard -"owncloud-sdk@npm:~3.0.0-alpha.6": - version: 3.0.0-alpha.6 - resolution: "owncloud-sdk@npm:3.0.0-alpha.6" +"owncloud-sdk@npm:~3.0.0-alpha.8": + version: 3.0.0-alpha.8 + resolution: "owncloud-sdk@npm:3.0.0-alpha.8" peerDependencies: axios: ^0.26.0 cross-fetch: ^3.0.6 @@ -9671,7 +9671,7 @@ __metadata: uuid: ^8.2.0 webdav: 4.9.0 xml-js: ^1.6.11 - checksum: 4f5d5e92f108ed2e1fa1145396cb29d2d6ff48737edb8de8e655e77d9960acccb1e5136451f681d9c731a95f94e37fd1b3abb0202d61e6724104bdece3052151 + checksum: 96e09c641010a9356bba63e5fe8aa259cf54279a8bb2c3ae81653f5d9e5598acd4050be707477229c6e53cd7c9d798d451c455182e0160d452738249ecf03c63 languageName: node linkType: hard @@ -13716,7 +13716,7 @@ __metadata: marked: ^4.0.12 oidc-client: 1.11.5 owncloud-design-system: ^13.1.0-rc.5 - owncloud-sdk: ~3.0.0-alpha.6 + owncloud-sdk: ~3.0.0-alpha.8 p-queue: ^6.1.1 popper-max-size-modifier: ^0.2.0 portal-vue: ^2.1.7