diff --git a/changelog/unreleased/enhancement-open-file-directly-from-app b/changelog/unreleased/enhancement-open-file-directly-from-app new file mode 100644 index 00000000000..007beb7ab2b --- /dev/null +++ b/changelog/unreleased/enhancement-open-file-directly-from-app @@ -0,0 +1,7 @@ +Enhancement: Open file directly from app + +We've added an 'Open' item to the drop down menu in the app top bar, +so the user can open a different file directly from the opened app. + +https://github.com/owncloud/web/pull/11085 +https://github.com/owncloud/web/issues/11013 diff --git a/docs/embed-mode/_index.md b/docs/embed-mode/_index.md index 94c1eacbb2d..27511139fa1 100644 --- a/docs/embed-mode/_index.md +++ b/docs/embed-mode/_index.md @@ -82,6 +82,35 @@ By default, the Embed mode allows users to select resources. In certain cases (e ``` +## File picker + +The File Picker mode in ownCloud Web is designed for embedding an interface that allows users to pick a single file. +This mode can be configured to restrict the file types that users can select. To enable the File Picker mode, you need +to include the embed-target=file query parameter in the iframe URL. Furthermore, you can specify allowed file types +using the embed-file-types parameter. The file types can be specified using file extensions, MIME types, or a +combination of both. If the embed-file-types parameter is not provided, all file types will be selectable by default. + +### Example + +```html + + + + +``` + ## Delegate authentication If you already have a valid `access_token` that can be used to call the API from within the Embed mode and do not want to force the user to authenticate again, you can delegate the authentication. Delegating authentication will disable internal login form in ownCloud Web and will instead use events to obtain the token and update it. diff --git a/packages/web-app-files/src/extensions.ts b/packages/web-app-files/src/extensions.ts index de58705d2bd..09a4e35bb99 100644 --- a/packages/web-app-files/src/extensions.ts +++ b/packages/web-app-files/src/extensions.ts @@ -1,6 +1,7 @@ import { Extension, useCapabilityStore, + useConfigStore, useFileActionsCopyQuickLink, useFileActionsShowShares, useRouter, @@ -14,6 +15,7 @@ import { quickActionsExtensionPoint } from './extensionPoints' export const extensions = () => { const capabilityStore = useCapabilityStore() + const configStore = useConfigStore() const router = useRouter() const { search: searchFunction } = useSearch() @@ -30,7 +32,7 @@ export const extensions = () => { id: 'com.github.owncloud.web.files.search', extensionPointIds: ['app.search.provider'], type: 'search', - searchProvider: new SDKSearch(capabilityStore, router, searchFunction) + searchProvider: new SDKSearch(capabilityStore, router, searchFunction, configStore) }, { id: 'com.github.owncloud.web.files.quick-action.collaborator', diff --git a/packages/web-app-files/src/search/sdk/index.ts b/packages/web-app-files/src/search/sdk/index.ts index 77ea0465564..0d21decd567 100644 --- a/packages/web-app-files/src/search/sdk/index.ts +++ b/packages/web-app-files/src/search/sdk/index.ts @@ -3,6 +3,7 @@ import List from './list' import { Router } from 'vue-router' import { CapabilityStore, + ConfigStore, SearchFunction, SearchList, SearchPreview, @@ -20,10 +21,15 @@ export default class Provider implements SearchProvider { public readonly listSearch: SearchList private readonly capabilityStore: CapabilityStore - constructor(capabilityStore: CapabilityStore, router: Router, searchFunction: SearchFunction) { + constructor( + capabilityStore: CapabilityStore, + router: Router, + searchFunction: SearchFunction, + configStore: ConfigStore + ) { this.id = 'files.sdk' this.displayName = $gettext('Files') - this.previewSearch = new Preview(router, searchFunction) + this.previewSearch = new Preview(router, searchFunction, configStore) this.listSearch = new List(searchFunction) this.capabilityStore = capabilityStore } diff --git a/packages/web-app-files/src/search/sdk/preview.ts b/packages/web-app-files/src/search/sdk/preview.ts index 14b2f1816c9..9a46b77279a 100644 --- a/packages/web-app-files/src/search/sdk/preview.ts +++ b/packages/web-app-files/src/search/sdk/preview.ts @@ -1,4 +1,4 @@ -import { SearchFunction, SearchPreview, SearchResult } from '@ownclouders/web-pkg' +import { ConfigStore, SearchFunction, SearchPreview, SearchResult } from '@ownclouders/web-pkg' import { Component, unref } from 'vue' import { Router } from 'vue-router' import { ResourcePreview } from '@ownclouders/web-pkg' @@ -9,11 +9,13 @@ export default class Preview implements SearchPreview { public readonly component: Component private readonly router: Router private readonly searchFunction: SearchFunction + private readonly configStore: ConfigStore - constructor(router: Router, searchFunction: SearchFunction) { + constructor(router: Router, searchFunction: SearchFunction, configStore: ConfigStore) { this.component = ResourcePreview this.router = router this.searchFunction = searchFunction + this.configStore = configStore } public search(term: string): Promise { @@ -21,6 +23,9 @@ export default class Preview implements SearchPreview { } public get available(): boolean { - return unref(this.router.currentRoute).name !== 'search-provider-list' + return ( + unref(this.router.currentRoute).name !== 'search-provider-list' && + !this.configStore.options?.embed?.enabled + ) } } diff --git a/packages/web-app-files/src/views/spaces/GenericSpace.vue b/packages/web-app-files/src/views/spaces/GenericSpace.vue index 381df4d7519..2f32bfd930c 100644 --- a/packages/web-app-files/src/views/spaces/GenericSpace.vue +++ b/packages/web-app-files/src/views/spaces/GenericSpace.vue @@ -15,8 +15,17 @@ @item-dropped="fileDropped" > @@ -269,7 +268,7 @@ export default defineComponent({ const { actions: createNewFolder } = useFileActionsCreateNewFolder({ space }) - const { isEnabled: isEmbedModeEnabled } = useEmbedMode() + const { isEnabled: isEmbedModeEnabled, isFilePicker } = useEmbedMode() const configStore = useConfigStore() const { options: configOptions } = storeToRefs(configStore) @@ -616,6 +615,7 @@ export default defineComponent({ whitespaceContextMenu, createNewFolderAction, isEmbedModeEnabled, + isFilePicker, currentFolder, totalResourcesCount, totalResourcesSize, diff --git a/packages/web-app-files/tests/unit/search/sdk.spec.ts b/packages/web-app-files/tests/unit/search/sdk.spec.ts index fb04b43500b..21ace33f519 100644 --- a/packages/web-app-files/tests/unit/search/sdk.spec.ts +++ b/packages/web-app-files/tests/unit/search/sdk.spec.ts @@ -3,7 +3,7 @@ import { RouteLocation, Router } from 'vue-router' import { mock } from 'vitest-mock-extended' import { ref } from 'vue' import { createTestingPinia } from 'web-test-helpers/src' -import { useCapabilityStore } from '@ownclouders/web-pkg' +import { ConfigStore, useCapabilityStore } from '@ownclouders/web-pkg' const getStore = (reports: string[] = []) => { createTestingPinia({ @@ -14,7 +14,7 @@ const getStore = (reports: string[] = []) => { describe('SDKProvider', () => { it('is only available if announced via capabilities', () => { - const search = new SDKSearch(getStore(), mock(), vi.fn()) + const search = new SDKSearch(getStore(), mock(), vi.fn(), mock()) expect(search.available).toBe(false) }) @@ -30,7 +30,8 @@ describe('SDKProvider', () => { mock({ currentRoute: ref(mock({ name: v.route })) }), - vi.fn() + vi.fn(), + mock() ) expect(!!search.previewSearch.available).toBe(!!v.available) diff --git a/packages/web-app-files/tests/unit/views/spaces/GenericSpace.spec.ts b/packages/web-app-files/tests/unit/views/spaces/GenericSpace.spec.ts index 0cc2fb463bf..2b179ba1bea 100644 --- a/packages/web-app-files/tests/unit/views/spaces/GenericSpace.spec.ts +++ b/packages/web-app-files/tests/unit/views/spaces/GenericSpace.spec.ts @@ -253,6 +253,20 @@ describe('GenericSpace view', () => { expect(wrapper.find(selectors.btnCreateFolder).exists()).toBe(true) }) + it('should not render create folder button when in embed mode but is file picker', () => { + mockUseEmbedMode.mockReturnValue({ + isEnabled: computed(() => true), + isFilePicker: computed(() => true) + }) + + const { wrapper } = getMountedWrapper({ + stubs: { 'app-bar': AppBarStub, CreateAndUpload: true } + }) + + expect(wrapper.find(selectors.actionsCreateAndUpload).exists()).toBe(false) + expect(wrapper.find(selectors.btnCreateFolder).exists()).toBe(false) + }) + it('should not render create and upload actions when in embed mode', () => { mockUseEmbedMode.mockReturnValue({ isEnabled: computed(() => true) diff --git a/packages/web-app-search/src/portals/SearchBar.vue b/packages/web-app-search/src/portals/SearchBar.vue index 957bb00ef6b..2323e6b5eab 100644 --- a/packages/web-app-search/src/portals/SearchBar.vue +++ b/packages/web-app-search/src/portals/SearchBar.vue @@ -47,6 +47,7 @@ { + return unref(availableProviders).some( + (provider) => provider?.previewSearch?.available === true + ) + }) + return { userContextReady, publicLinkContextReady, @@ -328,7 +335,8 @@ export default defineComponent({ search, showPreview, updateTerm, - getSearchResultLocation + getSearchResultLocation, + showDrop } }, @@ -506,7 +514,7 @@ export default defineComponent({ this.showCancelButton = false }, hideOptionsDrop() { - this.optionsDrop.hide() + this.optionsDrop?.hide() } } }) diff --git a/packages/web-pkg/src/components/AppTemplates/AppWrapper.vue b/packages/web-pkg/src/components/AppTemplates/AppWrapper.vue index adbbd17632f..7f3487f9aa2 100644 --- a/packages/web-pkg/src/components/AppTemplates/AppWrapper.vue +++ b/packages/web-pkg/src/components/AppTemplates/AppWrapper.vue @@ -87,6 +87,7 @@ import { import { DavPermission } from '@ownclouders/web-client/webdav' import { HttpError } from '@ownclouders/web-client' import { dirname } from 'path' +import { useFileActionsOpenWithApp } from '../../composables/actions/files/useFileActionsOpenWithApp' export default defineComponent({ name: 'AppWrapper', @@ -135,6 +136,9 @@ export default defineComponent({ const configStore = useConfigStore() const resourcesStore = useResourcesStore() + const { actions: openWithAppActions } = useFileActionsOpenWithApp({ + appId: props.applicationId + }) const { actions: createQuickLinkActions } = useFileActionsCopyQuickLink() const { actions: downloadFileActions } = useFileActionsDownloadFile() const { actions: showDetailsActions } = useFileActionsShowDetails() @@ -459,7 +463,9 @@ export default defineComponent({ }) const menuItemsContext = computed(() => { - return [...unref(fileActionsSave)].filter((item) => item.isVisible(unref(actionOptions))) + return [...unref(openWithAppActions), ...unref(fileActionsSave)].filter((item) => + item.isVisible(unref(actionOptions)) + ) }) const menuItemsShare = computed(() => { return [...unref(showSharesActions), ...unref(createQuickLinkActions)].filter((item) => diff --git a/packages/web-pkg/src/components/AppTopBar.vue b/packages/web-pkg/src/components/AppTopBar.vue index 41dc20b4167..b0afecd8fed 100644 --- a/packages/web-pkg/src/components/AppTopBar.vue +++ b/packages/web-pkg/src/components/AppTopBar.vue @@ -44,7 +44,7 @@ - +