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

web-app-external: view mode #9879

Merged
merged 11 commits into from
Nov 10, 2023
1 change: 1 addition & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Depending on the backend you are using, there are sample config files provided i
- `options.upload.xhr.timeout` Specifies the timeout for XHR uploads in milliseconds.
- `options.editor.autosaveEnabled` Specifies if the autosave for the editor apps is enabled.
- `options.editor.autosaveInterval` Specifies the time interval for the autosave of editor apps in seconds.
- `options.editor.openAsPreview` Specifies if non-personal files i.e. files in shares, spaces or public links are being opened in read only mode so the user needs to manually switch to edit mode. Can be set to `true`, `false` or an array of web app/editor names.
- `options.contextHelpersReadMore` Specifies whether the "Read more" link should be displayed or not.
- `options.openLinksWithDefaultApp` Specifies whether single file link shares should be opened with default app or not.
- `options.tokenStorageLocal` Specifies whether the access token will be stored in the local storage when set to `true` or in the session storage when set to `false`. If stored in the local storage, login state will be persisted across multiple browser tabs, means no additional logins are required. Defaults to `true`.
Expand Down
86 changes: 76 additions & 10 deletions packages/web-app-external/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,34 @@ import { PropType, computed, defineComponent, unref, nextTick, ref, watch, VNode
import { useTask } from 'vue-concurrency'
import { useGettext } from 'vue3-gettext'

import { Resource } from '@ownclouders/web-client/src'
import { Resource, SpaceResource } from '@ownclouders/web-client/src'
import { urlJoin } from '@ownclouders/web-client/src/utils'
import { queryItemAsString, useRequest, useRouteQuery, useStore } from '@ownclouders/web-pkg'
import { configurationManager } from '@ownclouders/web-pkg'
import {
isSameResource,
queryItemAsString,
useConfigurationManager,
useRequest,
useRouteQuery,
useStore
} from '@ownclouders/web-pkg'
import {
isProjectSpaceResource,
isPublicSpaceResource,
isShareSpaceResource
} from '@ownclouders/web-client/src/helpers'

export default defineComponent({
name: 'ExternalApp',
props: {
resource: { type: Object as PropType<Resource>, required: true }
space: { type: Object as PropType<SpaceResource>, required: true },
resource: { type: Object as PropType<Resource>, required: true },
isReadOnly: { type: Boolean, required: true }
},
emits: ['update:applicationName'],
setup(props, { emit }) {
const language = useGettext()
const store = useStore()
const configurationManager = useConfigurationManager()

const { $gettext } = language
const { makeRequest } = useRequest()
Expand Down Expand Up @@ -73,8 +87,15 @@ export default defineComponent({
})
}

const loadAppUrl = useTask(function* () {
const loadAppUrl = useTask(function* (signal, viewMode: string) {
try {
if (props.isReadOnly && viewMode === 'write') {
Copy link
Contributor

@lookacat lookacat Nov 9, 2023

Choose a reason for hiding this comment

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

would it make sense to create an enum for the viewMode since its used at multiple locations?

Copy link
Member Author

Choose a reason for hiding this comment

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

seems overkill to me tbh. In theory we could also support more modes which are defined by the backend... this is just some specific handling for an edge case

store.dispatch('showErrorMessage', {
title: $gettext('Cannot open file in edit mode as it is read-only')
})
return
}

const fileId = props.resource.fileId
const baseUrl = urlJoin(
configurationManager.serverUrl,
Expand All @@ -84,7 +105,8 @@ export default defineComponent({
const query = stringify({
file_id: fileId,
lang: language.current,
...(unref(applicationName) && { app_name: unref(applicationName) })
...(unref(applicationName) && { app_name: unref(applicationName) }),
...(viewMode && { view_mode: viewMode })
})

const url = `${baseUrl}?${query}`
Expand Down Expand Up @@ -132,12 +154,56 @@ export default defineComponent({
}
}).restartable()

const determineOpenAsPreview = (appName: string) => {
const openAsPreview = configurationManager.options.editor.openAsPreview
return (
openAsPreview === true || (Array.isArray(openAsPreview) && openAsPreview.includes(appName))
)
}

// switch to write mode when edit is clicked
const catchClickMicrosoftEdit = (event) => {
try {
if (JSON.parse(event.data)?.MessageId === 'UI_Edit') {
loadAppUrl.perform('write')
}
} catch (e) {}
}
watch(
props.resource,
() => {
loadAppUrl.perform()
applicationName,
(newAppName, oldAppName) => {
if (determineOpenAsPreview(newAppName) && newAppName !== oldAppName) {
window.addEventListener('message', catchClickMicrosoftEdit)
} else {
window.removeEventListener('message', catchClickMicrosoftEdit)
}
},
{
immediate: true
}
)
dschmidt marked this conversation as resolved.
Show resolved Hide resolved

watch(
[props.resource],
([newResource], [oldResource]) => {
if (isSameResource(newResource, oldResource)) {
return
}

debugger
dschmidt marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

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

@dschmidt you forgot to delete it
Screenshot 2023-11-16 at 15 55 50

Copy link
Contributor

Choose a reason for hiding this comment

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

I'll drop it in my PR

Copy link
Member Author

Choose a reason for hiding this comment

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

doh 😂


let viewMode = props.isReadOnly ? 'view' : 'write'
if (
determineOpenAsPreview(unref(applicationName)) &&
(isShareSpaceResource(props.space) ||
isPublicSpaceResource(props.space) ||
isProjectSpaceResource(props.space))
) {
viewMode = 'view'
}
loadAppUrl.perform(viewMode)
},
{ immediate: true }
{ immediate: true, deep: true }
)

return {
Expand Down
18 changes: 11 additions & 7 deletions packages/web-app-external/tests/unit/app.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { mock } from 'jest-mock-extended'
import { mock, mockDeep } from 'jest-mock-extended'
import {
createStore,
defaultPlugins,
defaultStoreMockOptions,
shallowMount
} from 'web-test-helpers'
import { useRequest, useRouteQuery } from '@ownclouders/web-pkg'
import { ConfigurationManager, useRequest, useRouteQuery } from '@ownclouders/web-pkg'
import { ref } from 'vue'

import { Resource } from '@ownclouders/web-client'
Expand All @@ -14,7 +14,15 @@ import App from '../../src/App.vue'
jest.mock('@ownclouders/web-pkg', () => ({
...jest.requireActual('@ownclouders/web-pkg'),
useRequest: jest.fn(),
useRouteQuery: jest.fn()
useRouteQuery: jest.fn(),
useConfigurationManager: () =>
mockDeep<ConfigurationManager>({
options: {
editor: {
openAsPreview: false
}
}
})
}))

const appUrl = 'https://example.test/d12ab86/loe009157-MzBw'
Expand All @@ -38,10 +46,6 @@ describe('The app provider extension', () => {
jest.spyOn(console, 'error').mockImplementation(() => undefined)
})

afterEach(() => {
jest.clearAllMocks()
})

it('should fail for unauthenticated users', async () => {
const makeRequest = jest.fn().mockResolvedValue({
ok: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ export default defineComponent({

const slotAttrs = computed(() => ({
url: unref(url),
space: unref(unref(currentFileContext).space),
resource: unref(resource),
isDirty: unref(isDirty),
isReadOnly: unref(isReadOnly),
Expand Down
10 changes: 10 additions & 0 deletions packages/web-pkg/src/configuration/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ export class ConfigurationManager {
'openLinksWithDefaultApp',
get(options, 'openLinksWithDefaultApp', true)
)

// when this setting is enabled, non-personal files i.e. files in shares, spaces or public links
// are opened in read only mode and the user needs another click to switch to edit mode.
// it can be set to true/false or an array of web app/editor names.
set(
this.optionsConfiguration,
'editor.openAsPreview',
get(options, 'editor.openAsPreview', false)
)

set(this.optionsConfiguration, 'upload.companionUrl', get(options, 'upload.companionUrl', ''))
set(this.optionsConfiguration, 'tokenStorageLocal', get(options, 'tokenStorageLocal', true))
set(this.optionsConfiguration, 'loginUrl', get(options, 'loginUrl', ''))
Expand Down
3 changes: 3 additions & 0 deletions packages/web-pkg/src/configuration/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export interface OptionsConfiguration {
mode?: string
isRunningOnEos?: boolean
embedTarget?: string
editor?: {
openAsPreview?: boolean | string[]
}
}

export interface OAuth2Configuration {
Expand Down