Skip to content

Commit

Permalink
Add open action for editors (#11085)
Browse files Browse the repository at this point in the history
* Add open action for  editors

Co-authored-by: Jannik Stehle <[email protected]>
  • Loading branch information
AlexAndBear and JammingBen committed Jul 4, 2024
1 parent 833b11c commit fd350b8
Show file tree
Hide file tree
Showing 26 changed files with 535 additions and 69 deletions.
7 changes: 7 additions & 0 deletions changelog/unreleased/enhancement-open-file-directly-from-app
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions docs/embed-mode/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,35 @@ By default, the Embed mode allows users to select resources. In certain cases (e
</script>
```
## 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
<iframe src="https://my-owncloud-web-instance?embed=true&embed-target=file&embed-file-types=txt,image/png"></iframe>
<script>
function selectEventHandler(event) {
if (event.data?.name !== 'owncloud-embed:file-pick') {
return
}
const file = event.data.data
doSomethingWithPickedFile(file)
}
window.addEventListener('message', selectEventHandler)
</script>
```
## 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.
Expand Down
4 changes: 3 additions & 1 deletion packages/web-app-files/src/extensions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Extension,
useCapabilityStore,
useConfigStore,
useFileActionsCopyQuickLink,
useFileActionsShowShares,
useRouter,
Expand All @@ -14,6 +15,7 @@ import { quickActionsExtensionPoint } from './extensionPoints'

export const extensions = () => {
const capabilityStore = useCapabilityStore()
const configStore = useConfigStore()
const router = useRouter()
const { search: searchFunction } = useSearch()

Expand All @@ -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',
Expand Down
10 changes: 8 additions & 2 deletions packages/web-app-files/src/search/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import List from './list'
import { Router } from 'vue-router'
import {
CapabilityStore,
ConfigStore,
SearchFunction,
SearchList,
SearchPreview,
Expand All @@ -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
}
Expand Down
11 changes: 8 additions & 3 deletions packages/web-app-files/src/search/sdk/preview.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -9,18 +9,23 @@ 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<SearchResult> {
return this.searchFunction(term, previewSearchLimit)
}

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
)
}
}
24 changes: 12 additions & 12 deletions packages/web-app-files/src/views/spaces/GenericSpace.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,17 @@
@item-dropped="fileDropped"
>
<template #actions="{ limitedScreenSpace }">
<create-and-upload
v-if="!isEmbedModeEnabled"
key="create-and-upload-actions"
data-testid="actions-create-and-upload"
:space="space"
:item="item"
:item-id="itemId"
:limited-screen-space="limitedScreenSpace"
/>
<oc-button
v-if="isEmbedModeEnabled"
v-if="isEmbedModeEnabled && !isFilePicker"
key="new-folder-btn"
v-oc-tooltip="limitedScreenSpace ? $gettext('New folder') : ''"
class="oc-mr-s"
Expand All @@ -30,16 +39,6 @@
<oc-icon name="add" />
<span v-if="!limitedScreenSpace" v-text="$gettext('New folder')" />
</oc-button>

<create-and-upload
v-else
key="create-and-upload-actions"
data-testid="actions-create-and-upload"
:space="space"
:item="item"
:item-id="itemId"
:limited-screen-space="limitedScreenSpace"
/>
</template>
</app-bar>
<app-loading-spinner v-if="areResourcesLoading" />
Expand Down Expand Up @@ -260,7 +259,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)
Expand Down Expand Up @@ -607,6 +606,7 @@ export default defineComponent({
whitespaceContextMenu,
createNewFolderAction,
isEmbedModeEnabled,
isFilePicker,
currentFolder,
totalResourcesCount,
totalResourcesSize,
Expand Down
7 changes: 4 additions & 3 deletions packages/web-app-files/tests/unit/search/sdk.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -14,7 +14,7 @@ const getStore = (reports: string[] = []) => {

describe('SDKProvider', () => {
it('is only available if announced via capabilities', () => {
const search = new SDKSearch(getStore(), mock<Router>(), vi.fn())
const search = new SDKSearch(getStore(), mock<Router>(), vi.fn(), mock<ConfigStore>())
expect(search.available).toBe(false)
})

Expand All @@ -30,7 +30,8 @@ describe('SDKProvider', () => {
mock<Router>({
currentRoute: ref(mock<RouteLocation>({ name: v.route }))
}),
vi.fn()
vi.fn(),
mock<ConfigStore>()
)

expect(!!search.previewSearch.available).toBe(!!v.available)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
22 changes: 15 additions & 7 deletions packages/web-app-search/src/portals/SearchBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
<oc-icon name="search" fill-type="line"></oc-icon>
</oc-button>
<oc-drop
v-if="showDrop"
id="files-global-search-options"
ref="optionsDropRef"
mode="manual"
Expand Down Expand Up @@ -239,15 +240,15 @@ export default defineComponent({
}
if (unref(optionsDrop)) {
unref(optionsDrop).hide()
unref(optionsDrop)?.hide()
}
if (unref(activePreviewIndex) === null) {
router.push(getSearchResultLocation('files.sdk'))
}
if (unref(activePreviewIndex) !== null) {
unref(optionsDrop)
.$el.querySelectorAll('.preview')
?.$el.querySelectorAll('.preview')
[unref(activePreviewIndex)].firstChild.click()
}
}
Expand Down Expand Up @@ -283,17 +284,17 @@ export default defineComponent({
if (!unref(term)) {
return
}
unref(optionsDrop).show()
unref(optionsDrop)?.show()
await search()
}
const updateTerm = (input: string) => {
restoreSearchFromRoute.value = false
term.value = input
if (!unref(term)) {
return unref(optionsDrop).hide()
return unref(optionsDrop)?.hide()
}
return unref(optionsDrop).show()
return unref(optionsDrop)?.show()
}
const debouncedSearch = debounce(search, 500)
Expand All @@ -306,6 +307,12 @@ export default defineComponent({
debouncedSearch()
})
const showDrop = computed(() => {
return unref(availableProviders).some(
(provider) => provider?.previewSearch?.available === true
)
})
return {
userContextReady,
publicLinkContextReady,
Expand All @@ -328,7 +335,8 @@ export default defineComponent({
search,
showPreview,
updateTerm,
getSearchResultLocation
getSearchResultLocation,
showDrop
}
},
Expand Down Expand Up @@ -506,7 +514,7 @@ export default defineComponent({
this.showCancelButton = false
},
hideOptionsDrop() {
this.optionsDrop.hide()
this.optionsDrop?.hide()
}
}
})
Expand Down
8 changes: 7 additions & 1 deletion packages/web-pkg/src/components/AppTemplates/AppWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,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',
Expand Down Expand Up @@ -137,6 +138,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()
Expand Down Expand Up @@ -463,7 +467,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) =>
Expand Down
26 changes: 17 additions & 9 deletions packages/web-pkg/src/components/FilesList/ResourceTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -508,8 +508,9 @@ export default defineComponent({
const {
isLocationPicker,
isFilePicker,
postMessage,
isEnabled: isEmbedModeEnabled,
extensions: embedModeExtensions
fileTypes: embedModeFileTypes
} = useEmbedMode()
const configStore = useConfigStore()
Expand Down Expand Up @@ -540,13 +541,12 @@ export default defineComponent({
const getTagToolTip = (text: string) => (text.length > 7 ? text : '')
const isResourceDisabled = (resource: Resource) => {
if (
unref(isEmbedModeEnabled) &&
unref(embedModeExtensions)?.length &&
!unref(embedModeExtensions).includes(resource.extension) &&
!resource.isFolder
) {
return true
if (unref(isEmbedModeEnabled) && unref(embedModeFileTypes)?.length) {
return (
!unref(embedModeFileTypes).includes(resource.extension) &&
!unref(embedModeFileTypes).includes(resource.mimeType) &&
!resource.isFolder
)
}
return resource.processing === true
}
Expand Down Expand Up @@ -587,6 +587,7 @@ export default defineComponent({
space: ref(props.space),
targetRouteCallback: computed(() => props.targetRouteCallback)
}),
postMessage,
isFilePicker,
isLocationPicker,
isEmbedModeEnabled,
Expand Down Expand Up @@ -988,6 +989,13 @@ export default defineComponent({
*/
const resource = data[0]
if (this.isEmbedModeEnabled && this.isFilePicker) {
return this.postMessage<Resource>(
'owncloud-embed:file-pick',
JSON.parse(JSON.stringify(resource))
)
}
if (this.isResourceDisabled(resource)) {
return
}
Expand Down Expand Up @@ -1054,7 +1062,7 @@ export default defineComponent({
return false
}
if (this.isEmbedModeEnabled && !resource.isFolder) {
if (this.isEmbedModeEnabled && !this.isFilePicker && !resource.isFolder) {
return false
}
Expand Down
Loading

0 comments on commit fd350b8

Please sign in to comment.