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

Add open action for editors #11085

Merged
merged 21 commits into from
Jul 4, 2024
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/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' &&
AlexAndBear marked this conversation as resolved.
Show resolved Hide resolved
!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 @@ -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)
Expand Down Expand Up @@ -616,6 +615,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 @@ -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',
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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) =>
Expand Down
2 changes: 1 addition & 1 deletion packages/web-pkg/src/components/AppTopBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
</oc-drop>
</template>
<span v-if="hasAutosave" class="oc-flex oc-flex-middle">
<oc-icon name="refresh" color="white" v-oc-tooltip="autoSaveTooltipText" />
<oc-icon v-oc-tooltip="autoSaveTooltipText" name="refresh" color="white" />
</span>
<template v-if="mainActions.length && resource">
<context-action-menu
Expand Down
Loading