Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: resolving internal links into share spaces and shared files #9821

Merged
merged 5 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog/unreleased/bugfix-link-resolving-into-default-app
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Bugfix: Link resolving into default app

Internal and public file links now reliably resolve into the default app when `openLinksWithDefaultApp` is enabled.

https://github.com/owncloud/web/issues/9799
https://github.com/owncloud/web/issues/9776
https://github.com/owncloud/web/pull/9821
1 change: 1 addition & 0 deletions packages/web-app-files/src/composables/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './actions'
export * from './resourcesViewDefaults'
export * from './openWithDefaultApp'
export * from './shares'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useOpenWithDefaultApp'
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useFileActions } from '@ownclouders/web-pkg'
import { Resource, SpaceResource } from '@ownclouders/web-client'

export function useOpenWithDefaultApp() {
const { getDefaultEditorAction } = useFileActions()

const openWithDefaultApp = ({
space,
resource
}: {
space: SpaceResource
resource: Resource
}) => {
if (!resource || resource.isFolder) {
return
}

const fileActionsOptions = {
resources: [resource],
space: space
}

const defaultEditorAction = getDefaultEditorAction(fileActionsOptions)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

always nice to see how composables can actually be used in other composables... that turned out nice 💪

Copy link
Member

@dschmidt dschmidt Oct 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apparently "composables" weren't named that way by accident ;-)

if (defaultEditorAction) {
defaultEditorAction.handler({ ...fileActionsOptions })
}
}

return { openWithDefaultApp }
}
29 changes: 21 additions & 8 deletions packages/web-app-files/src/views/shares/SharedWithMe.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ import { useResourcesViewDefaults } from '../../composables'

import { AppLoadingSpinner, InlineFilterOption } from '@ownclouders/web-pkg'
import { AppBar, ItemFilterInline } from '@ownclouders/web-pkg'
import { queryItemAsString, useRouteQuery } from '@ownclouders/web-pkg'
import SharedWithMeSection from '../../components/Shares/SharedWithMeSection.vue'
import { computed, defineComponent, ref, unref } from 'vue'
import { computed, defineComponent, onMounted, ref, unref } from 'vue'
import { Resource } from '@ownclouders/web-client'
import SideBar from '../../components/SideBar/SideBar.vue'
import FilesViewWrapper from '../../components/FilesViewWrapper.vue'
Expand All @@ -55,6 +56,7 @@ import { useGroupingSettings } from '@ownclouders/web-pkg'
import SharesNavigation from 'web-app-files/src/components/AppBar/SharesNavigation.vue'
import { useGettext } from 'vue3-gettext'
import { useStore } from '@ownclouders/web-pkg'
import { useOpenWithDefaultApp } from '../../composables'

export default defineComponent({
components: {
Expand All @@ -68,6 +70,8 @@ export default defineComponent({
},

setup() {
const { openWithDefaultApp } = useOpenWithDefaultApp()

const {
areResourcesLoading,
sortFields,
Expand Down Expand Up @@ -123,8 +127,23 @@ export default defineComponent({
return getMatchingSpace(resource)
})

const openWithDefaultAppQuery = useRouteQuery('openWithDefaultApp')
const performLoaderTask = async () => {
await loadResourcesTask.perform()
scrollToResourceFromRoute(unref(items), 'files-app-bar')
if (queryItemAsString(unref(openWithDefaultAppQuery)) === 'true') {
openWithDefaultApp({
space: unref(selectedShareSpace),
resource: unref(selectedResources)[0]
})
}
}

onMounted(() => {
performLoaderTask()
})

return {
// defaults
loadResourcesTask,
areResourcesLoading,
selectedResources,
Expand All @@ -133,7 +152,6 @@ export default defineComponent({
sideBarOpen,
sideBarActivePanel,
selectedShareSpace,
scrollToResourceFromRoute,

areHiddenFilesShown,
visibilityOptions,
Expand All @@ -151,11 +169,6 @@ export default defineComponent({
// CERN
...useGroupingSettings({ sortBy: sortBy, sortDir: sortDir })
}
},

async created() {
await this.loadResourcesTask.perform()
this.scrollToResourceFromRoute(this.items, 'files-app-bar')
}
})
</script>
16 changes: 14 additions & 2 deletions packages/web-app-files/src/views/spaces/DriveResolver.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { computed, defineComponent, onMounted, ref, unref } from 'vue'
import {
queryItemAsString,
useClientService,
useConfigurationManager,
useDriveResolver,
useGetMatchingSpace,
useRouteParam,
Expand Down Expand Up @@ -65,6 +66,7 @@ export default defineComponent({
const isTrashRoute = useActiveLocation(isLocationTrashActive, 'files-trash-generic')
const resolvedDrive = useDriveResolver({ store, driveAliasAndItem })
const { getInternalSpace } = useGetMatchingSpace()
const configurationManager = useConfigurationManager()

const loading = ref(true)
const isLoading = computed(() => {
Expand Down Expand Up @@ -103,7 +105,13 @@ export default defineComponent({
return router.push(
createLocationSpaces('files-spaces-generic', {
params,
query: { ...query, scrollTo: unref(resource).fileId }
query: {
...query,
scrollTo: unref(resource).fileId,
...(configurationManager.options.openLinksWithDefaultApp && {
openWithDefaultApp: 'true'
})
}
})
)
}
Expand All @@ -112,7 +120,11 @@ export default defineComponent({
return router.push({
name: 'resolvePrivateLink',
params: { fileId: unref(fileId) },
query: { openWithDefaultApp: 'false' }
query: {
...(configurationManager.options.openLinksWithDefaultApp && {
openWithDefaultApp: 'true'
})
}
})
}

Expand Down
26 changes: 4 additions & 22 deletions packages/web-app-files/src/views/spaces/GenericSpace.vue
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ import {
useKeyboardTableNavigation,
useKeyboardTableSpaceActions
} from 'web-app-files/src/composables/keyboardActions'
import { useOpenWithDefaultApp } from '../../composables'

const visibilityObserver = new VisibilityObserver()

Expand Down Expand Up @@ -260,18 +261,18 @@ export default defineComponent({
setup(props) {
const store = useStore()
const { $gettext, $ngettext } = useGettext()
const { getDefaultEditorAction } = useFileActions()
const openWithDefaultAppQuery = useRouteQuery('openWithDefaultApp')
const clientService = useClientService()
const hasShareJail = useCapabilityShareJailEnabled()
const { breadcrumbsFromPath, concatBreadcrumbs } = useBreadcrumbsFromPath()
const { openWithDefaultApp } = useOpenWithDefaultApp()
const { actions: createNewFolder } = useFileActionsCreateNewFolder({
store,
space: props.space
})
const { isEnabled: isEmbedModeEnabled } = useEmbedMode()

let loadResourcesEventToken
let loadResourcesEventToken: string

const canUpload = computed(() => {
return store.getters['Files/currentFolder']?.canUpload({ user: store.getters.user })
Expand Down Expand Up @@ -432,25 +433,6 @@ export default defineComponent({
}
}

const openWithDefaultApp = () => {
const highlightedFile = store.getters['Files/highlightedFile']

if (!highlightedFile || highlightedFile.isFolder) {
return
}

const fileActionsOptions = {
resources: [highlightedFile],
space: props.space
}

const defaultEditorAction = getDefaultEditorAction(fileActionsOptions)

if (defaultEditorAction) {
defaultEditorAction.handler({ ...fileActionsOptions })
}
}

const resourcesViewDefaults = useResourcesViewDefaults<Resource, any, any[]>()

const keyActions = useKeyboardActions()
Expand Down Expand Up @@ -487,7 +469,7 @@ export default defineComponent({
focusAndAnnounceBreadcrumb(sameRoute)

if (unref(openWithDefaultAppQuery) === 'true') {
openWithDefaultApp()
openWithDefaultApp({ space: props.space, resource: store.getters['Files/highlightedFile'] })
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { getComposableWrapper } from 'web-test-helpers'
import { mock } from 'jest-mock-extended'
import { Resource, SpaceResource } from '@ownclouders/web-client'
import { useOpenWithDefaultApp } from '../../../../src/composables/openWithDefaultApp'
import { useFileActions, Action } from '@ownclouders/web-pkg'

jest.mock('@ownclouders/web-pkg', () => ({
...jest.requireActual('@ownclouders/web-pkg'),
useFileActions: jest.fn()
}))

describe('useOpenWithDefaultApp', () => {
it('should be valid', () => {
expect(useOpenWithDefaultApp).toBeDefined()
})
describe('method "openWithDefaultApp"', () => {
it('should call the default action handler for files', () => {
getWrapper({
setup: ({ openWithDefaultApp }, { defaultEditorAction }) => {
openWithDefaultApp({
space: mock<SpaceResource>(),
resource: mock<Resource>({ isFolder: false })
})
expect(defaultEditorAction.handler).toHaveBeenCalled()
}
})
})
it('should not call the default action handler for folders', () => {
getWrapper({
setup: ({ openWithDefaultApp }, { defaultEditorAction }) => {
openWithDefaultApp({
space: mock<SpaceResource>(),
resource: mock<Resource>({ isFolder: true })
})
expect(defaultEditorAction.handler).not.toHaveBeenCalled()
}
})
})
})
})

function getWrapper({
setup,
defaultEditorAction = mock<Action>({ handler: jest.fn() })
}: {
setup: (
instance: ReturnType<typeof useOpenWithDefaultApp>,
mocks: { defaultEditorAction: any }
) => void
defaultEditorAction?: any
}) {
jest.mocked(useFileActions).mockReturnValue(
mock<ReturnType<typeof useFileActions>>({
getDefaultEditorAction: () => defaultEditorAction
})
)

const mocks = { defaultEditorAction }

return {
wrapper: getComposableWrapper(() => {
const instance = useOpenWithDefaultApp()
setup(instance, mocks)
})
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import SharedWithMe from '../../../../src/views/shares/SharedWithMe.vue'
import { useResourcesViewDefaults } from 'web-app-files/src/composables'
import { InlineFilterOption, useSort } from '@ownclouders/web-pkg'
import { queryItemAsString, InlineFilterOption, useSort } from '@ownclouders/web-pkg'
import { useResourcesViewDefaultsMock } from 'web-app-files/tests/mocks/useResourcesViewDefaultsMock'
import { ref } from 'vue'
import { defaultStubs, RouteLocation } from 'web-test-helpers'
Expand All @@ -13,11 +13,15 @@ import {
defaultStoreMockOptions,
defaultComponentMocks
} from 'web-test-helpers'
import { useOpenWithDefaultApp } from '../../../../src/composables/openWithDefaultApp'

jest.mock('web-app-files/src/composables')
jest.mock('web-app-files/src/composables/resourcesViewDefaults')
jest.mock('web-app-files/src/composables/openWithDefaultApp/useOpenWithDefaultApp')
jest.mock('@ownclouders/web-pkg', () => ({
...jest.requireActual('@ownclouders/web-pkg'),
useSort: jest.fn().mockImplementation(() => useSortMock())
useSort: jest.fn().mockImplementation(() => useSortMock()),
queryItemAsString: jest.fn(),
useRouteQuery: jest.fn()
}))

describe('SharedWithMe view', () => {
Expand All @@ -39,6 +43,18 @@ describe('SharedWithMe view', () => {
expect(wrapper.find('oc-spinner-stub').exists()).toBeFalsy()
})
})
describe('open with default app', () => {
it('gets called if given via route query param', async () => {
const { wrapper, mocks } = getMountedWrapper({ openWithDefaultAppQuery: 'true' })
await wrapper.vm.loadResourcesTask.last
expect(mocks.openWithDefaultApp).toHaveBeenCalled()
})
it('gets not called if not given via route query param', async () => {
const { wrapper, mocks } = getMountedWrapper()
await wrapper.vm.loadResourcesTask.last
expect(mocks.openWithDefaultApp).not.toHaveBeenCalled()
})
})
describe('filter', () => {
it('shows the share visibility filter', () => {
const { wrapper } = getMountedWrapper()
Expand All @@ -64,19 +80,30 @@ describe('SharedWithMe view', () => {
})
})

function getMountedWrapper({ mocks = {}, loading = false, files = [] } = {}) {
function getMountedWrapper({
mocks = {},
loading = false,
files = [],
openWithDefaultAppQuery = ''
} = {}) {
jest.mocked(useResourcesViewDefaults).mockImplementation(() =>
useResourcesViewDefaultsMock({
storeItems: ref(files),
areResourcesLoading: ref(loading)
})
)
jest.mocked(useSort).mockImplementation((options) => useSortMock({ items: ref(options.items) }))
jest.mocked(queryItemAsString).mockImplementationOnce(() => openWithDefaultAppQuery)

const openWithDefaultApp = jest.fn()
jest.mocked(useOpenWithDefaultApp).mockReturnValue({ openWithDefaultApp })

const defaultMocks = {
...defaultComponentMocks({
currentRoute: mock<RouteLocation>({ name: 'files-shares-with-me' })
}),
...(mocks && mocks)
...(mocks && mocks),
openWithDefaultApp
}
const storeOptions = { ...defaultStoreMockOptions }
const store = createStore(storeOptions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,7 @@ describe('DriveResolver view', () => {
await wrapper.vm.$nextTick()
expect(mocks.$router.push).toHaveBeenCalledWith(
expect.objectContaining({
name: 'resolvePrivateLink',
query: { openWithDefaultApp: 'false' }
name: 'resolvePrivateLink'
})
)
})
Expand Down
3 changes: 0 additions & 3 deletions tests/e2e/cucumber/features/smoke/internalLink.feature
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ Feature: internal link share
When "Alice" edits the public link named "Link" of resource "myfolder" changing role to "Invited people"
And "Brian" opens the public link "Link"
And "Brian" logs in from the internal link

# comment out after fixing #9776
# And "Brian" opens shared-with-me page from the internal link
ScharfViktor marked this conversation as resolved.
Show resolved Hide resolved
And "Brian" opens the "files" app
And "Brian" navigates to the shared with me page
And "Brian" uploads the following resource
Expand Down
Loading