From 7d7431dc3b0b850148cb8cfb320b47a631571b54 Mon Sep 17 00:00:00 2001 From: Pascal Wengerter Date: Mon, 30 Oct 2023 07:14:19 +0100 Subject: [PATCH] Shared with me: Show / Hide shares (#9531) (#9718) * Next iteration of share show/hide * Rebase and add changelog item * put remaining parts together to make it work * fixup! put remaining parts together to make it work * keep original action names internally for accepting/declining shares * test: fix tests * do not allow decline action on pending shares * feat: rename hide to hidden, use PUT for the request to set it * fix: center no-content-message for shared-with-me-section * feat: add action to hide shares to context menu * fix: ensure the share hidden-param does not get sent when not needed * fix pipeline --------- Co-authored-by: Jannik Stehle --- .../unreleased/enhancement-show-hide-shares | 9 + .../components/Shares/SharedWithMeSection.vue | 63 +++--- .../src/services/folder/loaderSharedWithMe.ts | 3 +- .../src/views/shares/SharedWithMe.vue | 205 ++++++------------ .../unit/views/shares/SharedWithMe.spec.ts | 65 ++---- .../web-client/src/helpers/resource/types.ts | 1 + .../web-client/src/helpers/share/functions.ts | 1 + .../web-pkg/src/components/AppBar/AppBar.vue | 3 + .../components/FilesList/ContextActions.vue | 4 +- .../components/Filters/ItemFilterInline.vue | 111 ++++++++++ .../web-pkg/src/components/Filters/index.ts | 2 + .../web-pkg/src/components/Filters/types.ts | 4 + packages/web-pkg/src/components/index.ts | 1 + .../src/composables/actions/files/index.ts | 1 + .../actions/files/useFileActions.ts | 3 + .../files/useFileActionsAcceptShare.ts | 12 +- .../files/useFileActionsDeclineShare.ts | 17 +- .../files/useFileActionsToggleHideShare.ts | 100 +++++++++ .../src/helpers/share/triggerShareAction.ts | 23 +- .../Filters/ItemFilterInline.spec.ts | 70 ++++++ .../helpers/share/triggerShareAction.spec.ts | 2 +- packages/web-runtime/package.json | 2 +- pnpm-lock.yaml | 8 +- .../pageObjects/sharedWithMePage.js | 37 +--- .../stepDefinitions/sharingContext.js | 4 +- .../e2e/cucumber/features/smoke/share.feature | 12 +- tests/e2e/cucumber/steps/ui/shares.ts | 16 ++ .../objects/app-files/share/actions.ts | 46 +--- .../support/objects/app-files/share/index.ts | 6 +- .../support/objects/app-files/share/utils.ts | 12 + 30 files changed, 523 insertions(+), 320 deletions(-) create mode 100644 changelog/unreleased/enhancement-show-hide-shares create mode 100644 packages/web-pkg/src/components/Filters/ItemFilterInline.vue create mode 100644 packages/web-pkg/src/components/Filters/index.ts create mode 100644 packages/web-pkg/src/components/Filters/types.ts create mode 100644 packages/web-pkg/src/composables/actions/files/useFileActionsToggleHideShare.ts create mode 100644 packages/web-pkg/tests/unit/components/Filters/ItemFilterInline.spec.ts diff --git a/changelog/unreleased/enhancement-show-hide-shares b/changelog/unreleased/enhancement-show-hide-shares new file mode 100644 index 00000000000..2e18942ba07 --- /dev/null +++ b/changelog/unreleased/enhancement-show-hide-shares @@ -0,0 +1,9 @@ +Enhancement: Personal shares can be shown and hidden + +On the shared-with-me page, there is no distinction between pending, accepted and rejected shares anymore. +Instead, the user can toggle to display either shown or hidden shares. + +Furthermore, accepting and rejecting shares has been renamed to "enable sync"/"disable sync" to better reflect what's happening on the server and on other devices. + +https://github.com/owncloud/web/issues/9531 +https://github.com/owncloud/web/pull/9718 diff --git a/packages/web-app-files/src/components/Shares/SharedWithMeSection.vue b/packages/web-app-files/src/components/Shares/SharedWithMeSection.vue index 6e643e2c5c0..08a479bd2a5 100644 --- a/packages/web-app-files/src/components/Shares/SharedWithMeSection.vue +++ b/packages/web-app-files/src/components/Shares/SharedWithMeSection.vue @@ -1,6 +1,6 @@ + diff --git a/packages/web-app-files/tests/unit/views/shares/SharedWithMe.spec.ts b/packages/web-app-files/tests/unit/views/shares/SharedWithMe.spec.ts index 18fc146fa40..5493bcb37b2 100644 --- a/packages/web-app-files/tests/unit/views/shares/SharedWithMe.spec.ts +++ b/packages/web-app-files/tests/unit/views/shares/SharedWithMe.spec.ts @@ -1,13 +1,11 @@ import SharedWithMe from '../../../../src/views/shares/SharedWithMe.vue' import { useResourcesViewDefaults } from 'web-app-files/src/composables' -import { useSort } from '@ownclouders/web-pkg' +import { InlineFilterOption, useSort } from '@ownclouders/web-pkg' import { useResourcesViewDefaultsMock } from 'web-app-files/tests/mocks/useResourcesViewDefaultsMock' -import { ShareStatus } from '@ownclouders/web-client/src/helpers/share' import { ref } from 'vue' import { defaultStubs, RouteLocation } from 'web-test-helpers' import { useSortMock } from 'web-app-files/tests/mocks/useSortMock' -import { mock, mockDeep } from 'jest-mock-extended' -import { Resource } from '@ownclouders/web-client' +import { mock } from 'jest-mock-extended' import { createStore, defaultPlugins, @@ -41,48 +39,27 @@ describe('SharedWithMe view', () => { expect(wrapper.find('oc-spinner-stub').exists()).toBeFalsy() }) }) - describe('sections', () => { - it('always shows the "accepted"- and "declined"-sections', () => { + describe('filter', () => { + it('shows the share visibility filter', () => { const { wrapper } = getMountedWrapper() - expect(wrapper.find('#files-shared-with-me-accepted-section').exists()).toBeTruthy() - expect(wrapper.find('#files-shared-with-me-declined-section').exists()).toBeTruthy() + expect(wrapper.find('.share-visibility-filter').exists()).toBeTruthy() + expect(wrapper.find('item-filter-inline-stub').exists()).toBeTruthy() }) - describe('pending', () => { - it('shows when a share is pending', () => { - const { wrapper } = getMountedWrapper({ - files: [mockDeep({ status: ShareStatus.pending })] - }) - expect(wrapper.find('#files-shared-with-me-pending-section').exists()).toBeTruthy() - }) - it('does not show when no share is pending', () => { - const { wrapper } = getMountedWrapper({ - files: [mockDeep({ status: ShareStatus.accepted })] - }) - expect(wrapper.find('#files-shared-with-me-pending-section').exists()).toBeFalsy() - }) - }) - describe('accepted', () => { - it('shows an accepted share', () => { - const { wrapper } = getMountedWrapper({ - files: [mockDeep({ status: ShareStatus.accepted })] - }) - expect(wrapper.find('#files-shared-with-me-accepted-section').exists()).toBeTruthy() - expect( - wrapper.findComponent('#files-shared-with-me-accepted-section').props().items.length - ).toEqual(1) - }) + it('shows all visible shares', () => { + const { wrapper } = getMountedWrapper() + expect(wrapper.findAll('shared-with-me-section-stub').length).toBe(1) + expect(wrapper.findComponent('shared-with-me-section-stub').props('title')).toEqual( + 'Shares' + ) }) - describe('declined', () => { - it('shows a declined share', async () => { - const { wrapper } = getMountedWrapper({ - files: [mockDeep({ status: ShareStatus.declined })] - }) - await wrapper.vm.loadResourcesTask.last - expect(wrapper.find('#files-shared-with-me-declined-section').exists()).toBeTruthy() - expect( - wrapper.findComponent('#files-shared-with-me-declined-section').props().items.length - ).toEqual(1) - }) + it('shows all hidden shares', async () => { + const { wrapper } = getMountedWrapper() + wrapper.vm.setAreHiddenFilesShown(mock({ name: 'hidden' })) + await wrapper.vm.$nextTick() + expect(wrapper.findAll('shared-with-me-section-stub').length).toBe(1) + expect(wrapper.findComponent('shared-with-me-section-stub').props('title')).toEqual( + 'Hidden Shares' + ) }) }) }) @@ -110,7 +87,7 @@ function getMountedWrapper({ mocks = {}, loading = false, files = [] } = {}) { global: { plugins: [...defaultPlugins(), store], mocks: defaultMocks, - stubs: defaultStubs + stubs: { ...defaultStubs, itemFilterInline: true } } }) } diff --git a/packages/web-client/src/helpers/resource/types.ts b/packages/web-client/src/helpers/resource/types.ts index 54b77b950fb..ad075bf4364 100644 --- a/packages/web-client/src/helpers/resource/types.ts +++ b/packages/web-client/src/helpers/resource/types.ts @@ -115,6 +115,7 @@ export interface Resource { sharedWith?: string shareOwner?: string shareOwnerDisplayname?: string + hidden?: boolean extension?: string share?: any diff --git a/packages/web-client/src/helpers/share/functions.ts b/packages/web-client/src/helpers/share/functions.ts index ff31dfba668..ae30205803e 100644 --- a/packages/web-client/src/helpers/share/functions.ts +++ b/packages/web-client/src/helpers/share/functions.ts @@ -174,6 +174,7 @@ export function buildSharedResource( ] resource.sharedWith = share.sharedWith || [] resource.status = parseInt(share.state) + resource.hidden = share.hidden === 'true' || share.hidden === true resource.name = path.basename(share.file_target) if (hasShareJail) { // FIXME, HACK 1: path needs to be '/' because the share has it's own webdav endpoint (we access it's root). should ideally be removed backend side. diff --git a/packages/web-pkg/src/components/AppBar/AppBar.vue b/packages/web-pkg/src/components/AppBar/AppBar.vue index d456338351f..ec4e8cc1695 100644 --- a/packages/web-pkg/src/components/AppBar/AppBar.vue +++ b/packages/web-pkg/src/components/AppBar/AppBar.vue @@ -101,6 +101,7 @@ import { } from '../../composables/actions' import { useCapabilitySpacesMaxQuota, + useFileActionsToggleHideShare, useRouteMeta, useStore, ViewModeConstants @@ -166,6 +167,7 @@ export default defineComponent({ const { $gettext } = useGettext() const { actions: acceptShareActions } = useFileActionsAcceptShare({ store }) + const { actions: hideShareActions } = useFileActionsToggleHideShare({ store }) const { actions: copyActions } = useFileActionsCopy({ store }) const { actions: declineShareActions } = useFileActionsDeclineShare({ store }) const { actions: deleteActions } = useFileActionsDelete({ store }) @@ -190,6 +192,7 @@ export default defineComponent({ const batchActions = computed(() => { let actions = [ + ...unref(hideShareActions), ...unref(acceptShareActions), ...unref(declineShareActions), ...unref(downloadArchiveActions), diff --git a/packages/web-pkg/src/components/FilesList/ContextActions.vue b/packages/web-pkg/src/components/FilesList/ContextActions.vue index 58123b10b8e..9fc8d86e027 100644 --- a/packages/web-pkg/src/components/FilesList/ContextActions.vue +++ b/packages/web-pkg/src/components/FilesList/ContextActions.vue @@ -5,7 +5,7 @@ + diff --git a/packages/web-pkg/src/components/Filters/index.ts b/packages/web-pkg/src/components/Filters/index.ts new file mode 100644 index 00000000000..4784d7a7733 --- /dev/null +++ b/packages/web-pkg/src/components/Filters/index.ts @@ -0,0 +1,2 @@ +export { default as ItemFilterInline } from './ItemFilterInline.vue' +export * from './types' diff --git a/packages/web-pkg/src/components/Filters/types.ts b/packages/web-pkg/src/components/Filters/types.ts new file mode 100644 index 00000000000..c24e302dbe7 --- /dev/null +++ b/packages/web-pkg/src/components/Filters/types.ts @@ -0,0 +1,4 @@ +export type InlineFilterOption = { + name: string + label: string +} diff --git a/packages/web-pkg/src/components/index.ts b/packages/web-pkg/src/components/index.ts index 4460b13cff5..806502c1ca1 100644 --- a/packages/web-pkg/src/components/index.ts +++ b/packages/web-pkg/src/components/index.ts @@ -2,6 +2,7 @@ export * from './AppBar' export * from './AppTemplates' export * from './ContextActions' export * from './FilesList' +export * from './Filters' export * from './SideBar' export * from './Search' export * from './Spaces' diff --git a/packages/web-pkg/src/composables/actions/files/index.ts b/packages/web-pkg/src/composables/actions/files/index.ts index fe428e68736..0ff81baa772 100644 --- a/packages/web-pkg/src/composables/actions/files/index.ts +++ b/packages/web-pkg/src/composables/actions/files/index.ts @@ -1,6 +1,7 @@ export * from './useFileActions' export * from './useFileActionsSetReadme' export * from './useFileActionsAcceptShare' +export * from './useFileActionsToggleHideShare' export * from './useFileActionsCopy' export * from './useFileActionsCreateQuicklink' export * from './useFileActionsDeclineShare' diff --git a/packages/web-pkg/src/composables/actions/files/useFileActions.ts b/packages/web-pkg/src/composables/actions/files/useFileActions.ts index b7d770c953b..30039e2d77b 100644 --- a/packages/web-pkg/src/composables/actions/files/useFileActions.ts +++ b/packages/web-pkg/src/composables/actions/files/useFileActions.ts @@ -20,6 +20,7 @@ import { import { useFileActionsAcceptShare, + useFileActionsToggleHideShare, useFileActionsCopy, useFileActionsDeclineShare, useFileActionsDelete, @@ -54,6 +55,7 @@ export const useFileActions = ({ store }: { store?: Store } = {}) => { const { openUrl } = useWindowOpen() const { actions: acceptShareActions } = useFileActionsAcceptShare({ store }) + const { actions: hideShareActions } = useFileActionsToggleHideShare({ store }) const { actions: copyActions } = useFileActionsCopy({ store }) const { actions: deleteActions } = useFileActionsDelete({ store }) const { actions: declineShareActions } = useFileActionsDeclineShare({ store }) @@ -78,6 +80,7 @@ export const useFileActions = ({ store }: { store?: Store } = {}) => { ...unref(showEditTagsActions), ...unref(restoreActions), ...unref(acceptShareActions), + ...unref(hideShareActions), ...unref(declineShareActions), ...unref(favoriteActions), ...unref(navigateActions) diff --git a/packages/web-pkg/src/composables/actions/files/useFileActionsAcceptShare.ts b/packages/web-pkg/src/composables/actions/files/useFileActionsAcceptShare.ts index d856e979825..197f5e055c0 100644 --- a/packages/web-pkg/src/composables/actions/files/useFileActionsAcceptShare.ts +++ b/packages/web-pkg/src/composables/actions/files/useFileActionsAcceptShare.ts @@ -17,7 +17,7 @@ import { FileAction, FileActionOptions } from '../../actions' export const useFileActionsAcceptShare = ({ store }: { store?: Store } = {}) => { store = store || useStore() const router = useRouter() - const { $ngettext } = useGettext() + const { $gettext, $ngettext } = useGettext() const hasResharing = useCapabilityFilesSharingResharing() const hasShareJail = useCapabilityShareJailEnabled() @@ -60,8 +60,8 @@ export const useFileActionsAcceptShare = ({ store }: { store?: Store } = {} if (isLocationSpacesActive(router, 'files-spaces-generic')) { store.dispatch('showMessage', { title: $ngettext( - 'The selected share was accepted successfully', - 'The selected shares were accepted successfully', + 'Sync for the selected share was enabled successfully', + 'Sync for the selected shares was enabled successfully', resources.length ) }) @@ -72,8 +72,8 @@ export const useFileActionsAcceptShare = ({ store }: { store?: Store } = {} store.dispatch('showErrorMessage', { title: $ngettext( - 'Failed to accept the selected share.', - 'Failed to accept selected shares.', + 'Failed to enable sync for the the selected share', + 'Failed to enable sync for the selected shares', resources.length ), errors @@ -85,7 +85,7 @@ export const useFileActionsAcceptShare = ({ store }: { store?: Store } = {} name: 'accept-share', icon: 'check', handler: (args) => loadingService.addTask(() => handler(args)), - label: ({ resources }) => $ngettext('Accept share', 'Accept shares', resources.length), + label: () => $gettext('Enable sync'), isEnabled: ({ space, resources }) => { if ( !isLocationSharesActive(router, 'files-shares-with-me') && diff --git a/packages/web-pkg/src/composables/actions/files/useFileActionsDeclineShare.ts b/packages/web-pkg/src/composables/actions/files/useFileActionsDeclineShare.ts index 1bdc7b87ba7..d7da492c7d7 100644 --- a/packages/web-pkg/src/composables/actions/files/useFileActionsDeclineShare.ts +++ b/packages/web-pkg/src/composables/actions/files/useFileActionsDeclineShare.ts @@ -20,7 +20,7 @@ import { FileAction, FileActionOptions } from '../types' export const useFileActionsDeclineShare = ({ store }: { store?: Store } = {}) => { store = store || useStore() const router = useRouter() - const { $ngettext } = useGettext() + const { $gettext, $ngettext } = useGettext() const hasResharing = useCapabilityFilesSharingResharing() const hasShareJail = useCapabilityShareJailEnabled() @@ -58,13 +58,11 @@ export const useFileActionsDeclineShare = ({ store }: { store?: Store } = { await Promise.all(triggerPromises) if (errors.length === 0) { - store.dispatch('Files/resetFileSelection') - if (isLocationSpacesActive(router, 'files-spaces-generic')) { store.dispatch('showMessage', { title: $ngettext( - 'The selected share was declined successfully', - 'The selected shares were declined successfully', + 'Sync for the selected share was disabled successfully', + 'Sync for the selected shares was disabled successfully', resources.length ) }) @@ -76,8 +74,8 @@ export const useFileActionsDeclineShare = ({ store }: { store?: Store } = { store.dispatch('showErrorMessage', { title: $ngettext( - 'Failed to decline the selected share', - 'Failed to decline selected shares', + 'Failed to disable sync for the the selected share', + 'Failed to disable sync for the selected shares', resources.length ), errors @@ -89,7 +87,7 @@ export const useFileActionsDeclineShare = ({ store }: { store?: Store } = { name: 'decline-share', icon: 'spam-3', handler: (args) => loadingService.addTask(() => handler(args)), - label: ({ resources }) => $ngettext('Decline share', 'Decline shares', resources.length), + label: () => $gettext('Disable sync'), isEnabled: ({ space, resources }) => { if ( !isLocationSharesActive(router, 'files-shares-with-me') && @@ -108,8 +106,9 @@ export const useFileActionsDeclineShare = ({ store }: { store?: Store } = { return false } + // decline (= unsync) is only available for accepted (= synced) shares const declineDisabled = resources.some((resource) => { - return resource.status === ShareStatus.declined + return resource.status !== ShareStatus.accepted }) return !declineDisabled }, diff --git a/packages/web-pkg/src/composables/actions/files/useFileActionsToggleHideShare.ts b/packages/web-pkg/src/composables/actions/files/useFileActionsToggleHideShare.ts new file mode 100644 index 00000000000..8abb0d0d463 --- /dev/null +++ b/packages/web-pkg/src/composables/actions/files/useFileActionsToggleHideShare.ts @@ -0,0 +1,100 @@ +import { triggerShareAction } from '../../../helpers/share/triggerShareAction' + +import { Store } from 'vuex' +import PQueue from 'p-queue' +import { isLocationSharesActive } from '../../../router' +import { useCapabilityFilesSharingResharing, useCapabilityShareJailEnabled } from '../../capability' +import { useClientService } from '../../clientService' +import { useConfigurationManager } from '../../configuration' +import { useLoadingService } from '../../loadingService' +import { useRouter } from '../../router' +import { useStore } from '../../store' +import { computed, unref } from 'vue' +import { useGettext } from 'vue3-gettext' +import { FileAction, FileActionOptions } from '../../actions' + +export const useFileActionsToggleHideShare = ({ store }: { store?: Store } = {}) => { + store = store || useStore() + const router = useRouter() + const { $gettext } = useGettext() + + const hasResharing = useCapabilityFilesSharingResharing() + const hasShareJail = useCapabilityShareJailEnabled() + const clientService = useClientService() + const loadingService = useLoadingService() + const configurationManager = useConfigurationManager() + + const handler = async ({ resources }: FileActionOptions) => { + const errors = [] + const triggerPromises = [] + const triggerQueue = new PQueue({ concurrency: 4 }) + const hidden = !resources[0].hidden + + resources.forEach((resource) => { + triggerPromises.push( + triggerQueue.add(async () => { + try { + const share = await triggerShareAction({ + resource, + status: resource.status, + hidden, + hasResharing: unref(hasResharing), + hasShareJail: unref(hasShareJail), + client: clientService.owncloudSdk, + spaces: store.getters['runtime/spaces/spaces'], + fullShareOwnerPaths: configurationManager.options.routing.fullShareOwnerPaths + }) + if (share) { + store.commit('Files/UPDATE_RESOURCE', share) + } + } catch (error) { + console.error(error) + errors.push(error) + } + }) + ) + }) + + await Promise.all(triggerPromises) + + if (errors.length === 0) { + store.dispatch('Files/resetFileSelection') + store.dispatch('showMessage', { + title: hidden + ? $gettext('The share was hidden successfully') + : $gettext('The share was unhidden successfully') + }) + + return + } + + store.dispatch('showErrorMessage', { + title: hidden + ? $gettext('Failed to hide the share') + : $gettext('Failed to unhide share share'), + errors + }) + } + + const actions = computed((): FileAction[] => [ + { + name: 'toggle-hide-share', + icon: 'eye-off', // FIXME: change icon based on hidden status + handler: (args) => loadingService.addTask(() => handler(args)), + label: ({ resources }) => (resources[0].hidden ? $gettext('Unhide') : $gettext('Hide')), + isEnabled: ({ resources }) => { + if (resources.length === 0) { + return false + } + + return isLocationSharesActive(router, 'files-shares-with-me') + }, + componentType: 'button', + class: 'oc-files-actions-hide-share-trigger' + } + ]) + + return { + actions + } +} diff --git a/packages/web-pkg/src/helpers/share/triggerShareAction.ts b/packages/web-pkg/src/helpers/share/triggerShareAction.ts index fb4639c37e8..9eb33b7a18d 100644 --- a/packages/web-pkg/src/helpers/share/triggerShareAction.ts +++ b/packages/web-pkg/src/helpers/share/triggerShareAction.ts @@ -9,6 +9,7 @@ export async function triggerShareAction({ hasResharing, hasShareJail, client, + hidden = undefined, spaces = [], fullShareOwnerPaths = false }: { @@ -17,20 +18,22 @@ export async function triggerShareAction({ hasResharing: boolean hasShareJail: boolean client: OwnCloudSdk + hidden?: boolean spaces?: SpaceResource[] fullShareOwnerPaths?: boolean }) { - const method = _getRequestMethod(status) + const method = _getRequestMethod(status, hidden) if (!method) { throw new Error('invalid new share status') } + let action = `api/v1/shares/pending/${resource.share.id}` + if (hidden !== undefined) { + action += `?hidden=${hidden ? 'true' : 'false'}` + } + // exec share action - let response = await client.requests.ocs({ - service: 'apps/files_sharing', - action: `api/v1/shares/pending/${resource.share.id}`, - method - }) + let response = await client.requests.ocs({ service: 'apps/files_sharing', action, method }) // exit on failure if (response.status !== 200) { @@ -56,12 +59,18 @@ export async function triggerShareAction({ return null } -function _getRequestMethod(status) { +function _getRequestMethod(status: ShareStatus, hidden: boolean) { + if (hidden !== undefined) { + // setting the hidden state is always done via PUT + return 'PUT' + } switch (status) { case ShareStatus.accepted: return 'POST' case ShareStatus.declined: return 'DELETE' + case ShareStatus.pending: + return 'POST' default: return null } diff --git a/packages/web-pkg/tests/unit/components/Filters/ItemFilterInline.spec.ts b/packages/web-pkg/tests/unit/components/Filters/ItemFilterInline.spec.ts new file mode 100644 index 00000000000..85b3db8c469 --- /dev/null +++ b/packages/web-pkg/tests/unit/components/Filters/ItemFilterInline.spec.ts @@ -0,0 +1,70 @@ +import ItemFilterInline from '../../../../src/components/Filters/ItemFilterInline.vue' +import { InlineFilterOption } from '../../../../src/components/Filters/types' +import { defaultComponentMocks, defaultPlugins, mount } from 'web-test-helpers' +import { queryItemAsString } from '../../../../src/composables/appDefaults' +import { mock } from 'jest-mock-extended' + +jest.mock('../../../../src/composables/appDefaults', () => ({ + appDefaults: jest.fn(), + queryItemAsString: jest.fn() +})) + +const selectors = { + filterOption: '.item-inline-filter-option', + filterOptionLabel: '.item-inline-filter-option-label', + selectedOptionLabel: '.item-inline-filter-option-selected .item-inline-filter-option-label' +} + +describe('ItemFilterInline', () => { + const filterOptions = [ + mock({ name: 'filter1', label: 'filter1' }), + mock({ name: 'filter2', label: 'filter2' }) + ] + + it('renders all given options', () => { + const { wrapper } = getWrapper({ props: { filterOptions } }) + expect(wrapper.findAll(selectors.filterOption).length).toBe(filterOptions.length) + expect(wrapper.findAll(selectors.filterOption).at(0).text()).toEqual(filterOptions[0].label) + expect(wrapper.findAll(selectors.filterOption).at(1).text()).toEqual(filterOptions[1].label) + }) + it('emits the "toggleFilter"-event on click on an option', async () => { + const { wrapper } = getWrapper({ props: { filterOptions } }) + await wrapper.find(selectors.filterOption).trigger('click') + expect(wrapper.emitted('toggleFilter').length).toBeGreaterThan(0) + }) + describe('route query', () => { + it('sets the active option as query param', async () => { + const { wrapper, mocks } = getWrapper({ props: { filterOptions } }) + const currentRouteQuery = (mocks.$router.currentRoute as any).query + expect(mocks.$router.push).not.toHaveBeenCalled() + await wrapper.find(selectors.filterOption).trigger('click') + expect(currentRouteQuery[wrapper.vm.queryParam]).toBeDefined() + expect(mocks.$router.push).toHaveBeenCalled() + }) + it('sets the active optin initially when given via query param', async () => { + const initialQuery = filterOptions[1].name + const { wrapper } = getWrapper({ initialQuery, props: { filterOptions } }) + await wrapper.vm.$nextTick() + expect(wrapper.find(selectors.selectedOptionLabel).text()).toEqual(initialQuery) + }) + }) +}) + +function getWrapper({ props = {}, initialQuery = '' } = {}) { + jest.mocked(queryItemAsString).mockImplementation(() => initialQuery) + const mocks = defaultComponentMocks() + return { + mocks, + wrapper: mount(ItemFilterInline, { + props: { + filterName: 'InlineFilter', + ...props + }, + global: { + plugins: [...defaultPlugins()], + mocks, + provide: mocks + } + }) + } +} diff --git a/packages/web-pkg/tests/unit/helpers/share/triggerShareAction.spec.ts b/packages/web-pkg/tests/unit/helpers/share/triggerShareAction.spec.ts index c35d6570be5..d7a91f2322d 100644 --- a/packages/web-pkg/tests/unit/helpers/share/triggerShareAction.spec.ts +++ b/packages/web-pkg/tests/unit/helpers/share/triggerShareAction.spec.ts @@ -22,7 +22,7 @@ describe('method triggerShareAction', () => { await expect( triggerShareAction({ resource: null, - status: ShareStatus.pending, + status: 3 as any, hasResharing: true, hasShareJail: false, client: null diff --git a/packages/web-runtime/package.json b/packages/web-runtime/package.json index 16bdb63be9e..8da947ec66a 100644 --- a/packages/web-runtime/package.json +++ b/packages/web-runtime/package.json @@ -26,7 +26,7 @@ "luxon": "^2.4.0", "marked": "^4.0.12", "oidc-client-ts": "^2.1.0", - "owncloud-sdk": "3.1.0-alpha.9", + "owncloud-sdk": "3.1.0-alpha.10", "p-queue": "^6.6.2", "pinia": "^2.1.3", "portal-vue": "3.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eee190d680d..fa000ce30fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1132,8 +1132,8 @@ importers: specifier: ^2.1.0 version: 2.1.0 owncloud-sdk: - specifier: 3.1.0-alpha.9 - version: 3.1.0-alpha.9(axios@1.4.0)(cross-fetch@3.1.4)(promise@8.1.0)(qs@6.10.3)(utf8@3.0.0)(uuid@9.0.0)(webdav@5.3.0)(xml-js@1.6.11) + specifier: 3.1.0-alpha.10 + version: 3.1.0-alpha.10(axios@1.4.0)(cross-fetch@3.1.4)(promise@8.1.0)(qs@6.10.3)(utf8@3.0.0)(uuid@9.0.0)(webdav@5.3.0)(xml-js@1.6.11) p-queue: specifier: ^6.6.2 version: 6.6.2 @@ -16890,8 +16890,8 @@ packages: engines: {node: '>=0.10.0'} dev: true - /owncloud-sdk@3.1.0-alpha.9(axios@1.4.0)(cross-fetch@3.1.4)(promise@8.1.0)(qs@6.10.3)(utf8@3.0.0)(uuid@9.0.0)(webdav@5.3.0)(xml-js@1.6.11): - resolution: {integrity: sha512-RFisWiv7ZJbWnLKSt2UyPVWbsZqYQ1crWQwpRGwsnKD0Ow5qMXvhbKzZh0EsEF6KEGiQVV3j1lfmgxUDIgBKgA==} + /owncloud-sdk@3.1.0-alpha.10(axios@1.4.0)(cross-fetch@3.1.4)(promise@8.1.0)(qs@6.10.3)(utf8@3.0.0)(uuid@9.0.0)(webdav@5.3.0)(xml-js@1.6.11): + resolution: {integrity: sha512-cAq6BKkyDvm5MpcksicpBsfkr4ZEB+nm1Yl74FH3Qtm/wbI4uWwaTjqQmzYF5prAQDBDbi9F6nADp83zqfmPTA==} peerDependencies: axios: ^0.27.2 cross-fetch: ^3.0.6 diff --git a/tests/acceptance/pageObjects/sharedWithMePage.js b/tests/acceptance/pageObjects/sharedWithMePage.js index 6b726417073..63c732a3da1 100644 --- a/tests/acceptance/pageObjects/sharedWithMePage.js +++ b/tests/acceptance/pageObjects/sharedWithMePage.js @@ -42,16 +42,21 @@ module.exports = { */ hasShareStatusByFilenameAndUser: async function (status, filename, owner) { let selector = - util.format(this.elements.shareTable.selector, status) + this.api.page.FilesPageElement.filesList().getFileRowSelectorByFileName(filename) if (owner) { selector += util.format(this.elements.shareOwnerName.selector, owner) } + selector += this.elements.syncEnabled.selector let isPresent = false await this.api.element('xpath', selector, function (result) { isPresent = !!(result.value && result.value.ELEMENT) }) - return isPresent + + if (status === SHARE_STATE.accepted) { + return isPresent + } + + return !isPresent }, /** * Checks if the share matching the given status and filename is present on the given page. @@ -74,26 +79,6 @@ module.exports = { .click('@batchDeclineSharesButton') .waitForAjaxCallsToStartAndFinish() }, - /** - * @param {string} filename - * @param {string} action - It takes one of the following : Decline and Accept - * @param {string} user - The user who created the share - *Performs required action, such as accept and decline, on the file row element of the desired file name - * shared by specific user - */ - declineAcceptFile: function (action, filename, user) { - const actionLocatorButton = { - locateStrategy: this.elements.shareStatusActionOnFileRow.locateStrategy, - selector: - this.api.page.FilesPageElement.filesList().getFileRowSelectorByFileName(filename) + - util.format(this.elements.shareOwnerName.selector, user) + - util.format(this.elements.shareStatusActionOnFileRow.selector, action) - } - return this.waitForElementVisible(actionLocatorButton) - .initAjaxCounters() - .click(actionLocatorButton) - .waitForOutstandingAjaxCalls() - }, /** * gets the username of user that the element(file/folder/resource) on the shared-with-me page is shared by * @@ -137,15 +122,15 @@ module.exports = { } }, elements: { - shareTable: { - selector: '//table[@data-test-share-status="%s"]', - locateStrategy: 'xpath' - }, shareOwnerName: { selector: '//td[contains(@class,"oc-table-data-cell-owner")]//span[@data-test-user-name="%s"]', locateStrategy: 'xpath' }, + syncEnabled: { + selector: '/ancestor::tr//span[contains(@class,"sync-enabled")]', + locateStrategy: 'xpath' + }, sharedFrom: { // ugly hack: oc-avatar has a parent div.oc-avatars, which is also matched by `contains(@class, 'oc-avatar')`. // to solve this we try matching on the class surrounded by blanks, which is not matching the oc-avatars anymore. diff --git a/tests/acceptance/stepDefinitions/sharingContext.js b/tests/acceptance/stepDefinitions/sharingContext.js index 4065ec2385a..827f015eb5b 100644 --- a/tests/acceptance/stepDefinitions/sharingContext.js +++ b/tests/acceptance/stepDefinitions/sharingContext.js @@ -809,7 +809,7 @@ When( 'the user declines share {string} offered by user {string} using the webUI', async function (filename, user) { await client.pause(200) - return client.page.sharedWithMePage().declineAcceptFile('Decline', filename, user) + return client.page.FilesPageElement.filesList().declineShare(filename) } ) @@ -817,7 +817,7 @@ When( 'the user accepts share {string} offered by user {string} using the webUI', async function (filename, user) { await client.pause(200) - return client.page.sharedWithMePage().declineAcceptFile('Accept', filename, user) + return client.page.FilesPageElement.filesList().acceptShare(filename) } ) diff --git a/tests/e2e/cucumber/features/smoke/share.feature b/tests/e2e/cucumber/features/smoke/share.feature index 570e9d1d173..428d6ea2c45 100644 --- a/tests/e2e/cucumber/features/smoke/share.feature +++ b/tests/e2e/cucumber/features/smoke/share.feature @@ -33,10 +33,7 @@ Feature: share | name | | folder_to_shared | | folder_to_customShared | - And "Brian" declines the following share - | name | - | shared_folder | - Then "Brian" should not be able to open the folder "shared_folder" + Then "Brian" should not see a sync status for the folder "shared_folder" When "Brian" accepts the following share from the context menu | name | | shared_folder | @@ -99,17 +96,14 @@ Feature: share And "Brian" opens the "files" app And "Brian" navigates to the shared with me page - Then "Brian" should not be able to open the file "shareToBrian.txt" + Then "Brian" should not see a sync status for the file "shareToBrian.txt" When "Brian" accepts the following share | name | | shareToBrian.txt | | shareToBrian.md | | testavatar.jpeg | | simple.pdf | - And "Brian" declines the following share - | name | - | sharedFile.txt | - Then "Brian" should not be able to open the file "sharedFile.txt" + Then "Brian" should not see a sync status for the file "sharedFile.txt" When "Brian" accepts the following share from the context menu | name | | sharedFile.txt | diff --git a/tests/e2e/cucumber/steps/ui/shares.ts b/tests/e2e/cucumber/steps/ui/shares.ts index bc0f6ab6424..73c1a0fe15e 100644 --- a/tests/e2e/cucumber/steps/ui/shares.ts +++ b/tests/e2e/cucumber/steps/ui/shares.ts @@ -202,6 +202,22 @@ When( } ) +When( + /"([^"]*)" (should|should not) see a sync status for the (folder|file) "([^"]*)"?$/, + async function ( + this: World, + stepUser: string, + condition: string, + _: string, + resource: string + ): Promise { + const shouldSee = condition === 'should' + const { page } = this.actorsEnvironment.getActor({ key: stepUser }) + const shareObject = new objects.applicationFiles.Share({ page }) + expect(await shareObject.resourceIsSynced(resource)).toBe(shouldSee) + } +) + Then( /"([^"]*)" (should|should not) be able to see the following shares$/, async function ( diff --git a/tests/e2e/support/objects/app-files/share/actions.ts b/tests/e2e/support/objects/app-files/share/actions.ts index d317b0626bf..fca2563e6a9 100644 --- a/tests/e2e/support/objects/app-files/share/actions.ts +++ b/tests/e2e/support/objects/app-files/share/actions.ts @@ -7,10 +7,6 @@ import { copyLinkArgs, clearCurrentPopup } from '../link/actions' import { config } from '../../../../config.js' import { createdLinkStore } from '../../../store' -const filesSharedWithMeAccepted = - '#files-shared-with-me-accepted-section [data-test-resource-name="%s"]' -const shareAcceptDeclineButton = - '//*[@data-test-resource-name="%s"]/ancestor::tr//button[contains(@class, "file-row-share-%s")]' const quickShareButton = '//*[@data-test-resource-name="%s"]/ancestor::tr//button[contains(@class, "files-quick-action-collaborators")]' const noPermissionToShareLabel = @@ -19,14 +15,11 @@ const actionMenuDropdownButton = '//*[@data-test-resource-name="%s"]/ancestor::tr//button[contains(@class, "resource-table-btn-action-dropdown")]' const actionsTriggerButton = '//*[@data-test-resource-name="%s"]/ancestor::tr//button[contains(@class, "oc-files-actions-%s-trigger")]' -const filesSharedWithMeDeclined = - '#files-shared-with-me-declined-section [data-test-resource-name="%s"]' const publicLinkInputField = '//h4[contains(@class, "oc-files-file-link-name") and text()="%s"]' + '/following-sibling::div//p[contains(@class,"oc-files-file-link-url")]' -const showAllButton = '#files-shared-with-me-pending-section #files-shared-with-me-show-all' -const selecAllCheckbox = '#files-shared-with-me-pending-section #resource-table-select-all' +const selecAllCheckbox = '#resource-table-select-all' const acceptButton = '.oc-files-actions-accept-share-trigger' const pendingShareItem = '//div[@id="files-shared-with-me-pending-section"]//tr[contains(@class,"oc-tbody-tr")]' @@ -83,27 +76,11 @@ export interface ShareStatusArgs extends Omit { } export const acceptShare = async (args: ShareStatusArgs): Promise => { - const { resource, via, page } = args - if (via === 'CONTEXT_MENU') { - await clickActionInContextMenu({ page, resource }, 'accept-share') - } else { - await Promise.all([ - page.waitForResponse( - (resp) => - resp.url().includes('shares') && - resp.status() === 200 && - resp.request().method() === 'POST' - ), - page.locator(util.format(shareAcceptDeclineButton, resource, 'status-accept')).click() - ]) - } - await page.locator(util.format(filesSharedWithMeAccepted, resource)).waitFor() + const { resource, page } = args + await clickActionInContextMenu({ page, resource }, 'accept-share') } export const acceptAllShare = async ({ page }: { page: Page }): Promise => { - if (await page.locator(showAllButton).isVisible()) { - await page.locator(showAllButton).click() - } await page.locator(selecAllCheckbox).click() const numberOfPendingShares = await page.locator(pendingShareItem).count() const checkResponses = [] @@ -122,21 +99,8 @@ export const acceptAllShare = async ({ page }: { page: Page }): Promise => } export const declineShare = async (args: ShareStatusArgs): Promise => { - const { page, resource, via } = args - if (via === 'CONTEXT_MENU') { - await clickActionInContextMenu({ page, resource }, 'decline-share') - } else { - await Promise.all([ - page.waitForResponse( - (resp) => - resp.url().includes('shares') && - resp.status() === 200 && - resp.request().method() === 'DELETE' - ), - page.locator(util.format(shareAcceptDeclineButton, resource, 'decline')).click() - ]) - } - await page.locator(util.format(filesSharedWithMeDeclined, resource)).waitFor() + const { page, resource } = args + await clickActionInContextMenu({ page, resource }, 'decline-share') } export const clickActionInContextMenu = async ( diff --git a/tests/e2e/support/objects/app-files/share/index.ts b/tests/e2e/support/objects/app-files/share/index.ts index ca2fc19a06b..7ad8fceb2fb 100644 --- a/tests/e2e/support/objects/app-files/share/index.ts +++ b/tests/e2e/support/objects/app-files/share/index.ts @@ -1,6 +1,6 @@ import { Page } from '@playwright/test' import * as po from './actions' -import { resourceIsNotOpenable, isAcceptedSharePresent } from './utils' +import { resourceIsNotOpenable, isAcceptedSharePresent, resourceIsSynced } from './utils' import { copyLinkArgs } from '../link/actions' export class Share { #page: Page @@ -62,6 +62,10 @@ export class Share { return await resourceIsNotOpenable({ page: this.#page, resource }) } + async resourceIsSynced(resource): Promise { + return await resourceIsSynced({ page: this.#page, resource }) + } + async setDenyShare(args: Omit): Promise { const startUrl = this.#page.url() await po.setDenyShare({ ...args, page: this.#page }) diff --git a/tests/e2e/support/objects/app-files/share/utils.ts b/tests/e2e/support/objects/app-files/share/utils.ts index bb3e7923272..42d30bb4763 100644 --- a/tests/e2e/support/objects/app-files/share/utils.ts +++ b/tests/e2e/support/objects/app-files/share/utils.ts @@ -4,6 +4,8 @@ import util from 'util' const acceptedShareItem = '//*[@data-test-resource-name="%s"]/ancestor::tr//span[@data-test-user-name="%s"]' const itemSelector = '.files-table [data-test-resource-name="%s"]' +const syncEnabled = + '//*[@data-test-resource-name="%s"]//ancestor::tr//span[contains(@class, "sync-enabled")]' export const resourceIsNotOpenable = async ({ page, @@ -24,6 +26,16 @@ export const resourceIsNotOpenable = async ({ } } +export const resourceIsSynced = ({ + page, + resource +}: { + page: Page + resource: string +}): Promise => { + return page.locator(util.format(syncEnabled, resource)).isVisible() +} + export const isAcceptedSharePresent = async ({ page, resource,