diff --git a/changelog/unreleased/enhancement-add-mode-config b/changelog/unreleased/enhancement-add-mode-config new file mode 100644 index 00000000000..132560fbccf --- /dev/null +++ b/changelog/unreleased/enhancement-add-mode-config @@ -0,0 +1,7 @@ +Enhancement: Add `mode` config option + +We've added a new config option called `mode`. This option can be set via config.json in the options object or query parameter. +This config option asserts different modes of the UI. Currently, it will be used in the embed mode to hide certain parts of the UI. + +https://github.com/owncloud/web/pull/9818 +https://github.com/owncloud/web/issues/9768 diff --git a/packages/web-pkg/src/composables/embedMode/index.ts b/packages/web-pkg/src/composables/embedMode/index.ts new file mode 100644 index 00000000000..0108eb03a51 --- /dev/null +++ b/packages/web-pkg/src/composables/embedMode/index.ts @@ -0,0 +1 @@ +export * from './useEmbedMode' diff --git a/packages/web-pkg/src/composables/embedMode/useEmbedMode.ts b/packages/web-pkg/src/composables/embedMode/useEmbedMode.ts new file mode 100644 index 00000000000..912d8c77df2 --- /dev/null +++ b/packages/web-pkg/src/composables/embedMode/useEmbedMode.ts @@ -0,0 +1,10 @@ +import { computed } from 'vue' +import { useStore } from '../store' + +export const useEmbedMode = () => { + const store = useStore() + + const isEnabled = computed(() => store.getters.configuration.options.mode === 'embed') + + return { isEnabled } +} diff --git a/packages/web-pkg/src/composables/index.ts b/packages/web-pkg/src/composables/index.ts index 064a9a997a1..9d854304270 100644 --- a/packages/web-pkg/src/composables/index.ts +++ b/packages/web-pkg/src/composables/index.ts @@ -29,3 +29,4 @@ export * from './spaces' export * from './store' export * from './upload' export * from './viewMode' +export * from './embedMode' diff --git a/packages/web-pkg/src/configuration/types.ts b/packages/web-pkg/src/configuration/types.ts index 95e574601fb..43b54509afa 100644 --- a/packages/web-pkg/src/configuration/types.ts +++ b/packages/web-pkg/src/configuration/types.ts @@ -26,6 +26,7 @@ export interface OptionsConfiguration { openLinksWithDefaultApp?: boolean tokenStorageLocal?: boolean disabledExtensions?: string[] + mode?: string } export interface OAuth2Configuration { diff --git a/packages/web-runtime/src/components/Topbar/TopBar.vue b/packages/web-runtime/src/components/Topbar/TopBar.vue index 9d26d867b54..c5974ccfcbf 100644 --- a/packages/web-runtime/src/components/Topbar/TopBar.vue +++ b/packages/web-runtime/src/components/Topbar/TopBar.vue @@ -5,7 +5,10 @@ :aria-label="$gettext('Top bar')" >
- +
- - - - - - - - + @@ -43,6 +48,7 @@ import FeedbackLink from './FeedbackLink.vue' import ThemeSwitcher from './ThemeSwitcher.vue' import { useCapabilityNotifications, + useEmbedMode, useRouter, useStore, useUserContext @@ -71,6 +77,7 @@ export default { const isUserContext = useUserContext({ store }) const language = useGettext() const router = useRouter() + const { isEnabled: isEmbedModeEnabled } = useEmbedMode() const logoWidth = ref('150px') const isNotificationBellEnabled = computed(() => { @@ -169,7 +176,8 @@ export default { isNotificationBellEnabled, userMenuItems, appMenuItems, - logoWidth + logoWidth, + isEmbedModeEnabled } }, computed: { diff --git a/packages/web-runtime/src/container/bootstrap.ts b/packages/web-runtime/src/container/bootstrap.ts index 94fa254d102..87f0033df54 100644 --- a/packages/web-runtime/src/container/bootstrap.ts +++ b/packages/web-runtime/src/container/bootstrap.ts @@ -30,6 +30,7 @@ import { import { merge } from 'lodash-es' import { AppConfigObject } from '@ownclouders/web-pkg' import { MESSAGE_TYPE } from '@ownclouders/web-client/src/sse' +import { getQueryParam } from '../helpers/url' /** * fetch runtime configuration, this step is optional, all later steps can use a static @@ -49,6 +50,11 @@ export const announceConfiguration = async (path: string): Promise { throw new Error(`config could not be parsed. ${error}`) })) as RawConfig + + if (!rawConfig.options?.mode) { + rawConfig.options = { ...rawConfig.options, mode: getQueryParam('mode') ?? 'web' } + } + configurationManager.initialize(rawConfig) // TODO: we might want to get rid of exposing the raw config. needs more refactoring though. return rawConfig diff --git a/packages/web-runtime/src/helpers/url.ts b/packages/web-runtime/src/helpers/url.ts new file mode 100644 index 00000000000..67f4b1e3c85 --- /dev/null +++ b/packages/web-runtime/src/helpers/url.ts @@ -0,0 +1,5 @@ +export const getQueryParam = (paramName: string): string | null => { + const searchParams = new URLSearchParams(window.location.search) + + return searchParams.get(paramName) +} diff --git a/packages/web-runtime/tests/unit/components/Topbar/TopBar.spec.ts b/packages/web-runtime/tests/unit/components/Topbar/TopBar.spec.ts index d5fc36addc3..ec97fa44512 100644 --- a/packages/web-runtime/tests/unit/components/Topbar/TopBar.spec.ts +++ b/packages/web-runtime/tests/unit/components/Topbar/TopBar.spec.ts @@ -41,9 +41,30 @@ describe('Top Bar component', () => { expect(wrapper.find('notifications-stub').exists()).toBeFalsy() }) }) + it.each(['applications-menu', 'theme-switcher', 'feedback-link', 'notifications', 'user-menu'])( + 'should hide %s when mode is "embed"', + (componentName) => { + const { wrapper } = getWrapper({ + configuration: { options: { disableFeedbackLink: false, mode: 'embed' } } + }) + expect(wrapper.find(`${componentName}-stub`).exists()).toBeFalsy() + } + ) + it.each(['applications-menu', 'theme-switcher', 'feedback-link', 'notifications', 'user-menu'])( + 'should not hide %s when mode is not "embed"', + (componentName) => { + const { wrapper } = getWrapper({ + configuration: { options: { disableFeedbackLink: false, mode: 'web' } }, + capabilities: { + notifications: { 'ocs-endpoints': ['list', 'get', 'delete'] } + } + }) + expect(wrapper.find(`${componentName}-stub`).exists()).toBeTruthy() + } + ) }) -const getWrapper = ({ capabilities = {}, isUserContextReady = true } = {}) => { +const getWrapper = ({ capabilities = {}, isUserContextReady = true, configuration = {} } = {}) => { const mocks = { ...defaultComponentMocks() } const storeOptions = { ...defaultStoreMockOptions, @@ -62,7 +83,8 @@ const getWrapper = ({ capabilities = {}, isUserContextReady = true } = {}) => { logo: { topbar: 'example-logo.svg' } - } + }, + ...configuration })) storeOptions.getters.user.mockImplementation(() => ({ id: 'einstein' })) storeOptions.modules.runtime.modules.auth.getters.isUserContextReady.mockReturnValue( diff --git a/packages/web-runtime/tests/unit/container/bootstrap.spec.ts b/packages/web-runtime/tests/unit/container/bootstrap.spec.ts index fe2e008a31c..69e9bc97cbc 100644 --- a/packages/web-runtime/tests/unit/container/bootstrap.spec.ts +++ b/packages/web-runtime/tests/unit/container/bootstrap.spec.ts @@ -2,11 +2,13 @@ import { mock, mockDeep } from 'jest-mock-extended' import { createApp, defineComponent, App } from 'vue' import { createStore } from 'vuex' import { ConfigurationManager } from '@ownclouders/web-pkg' +import fetchMock from 'jest-fetch-mock' import { initializeApplications, announceApplicationsReady, announceCustomScripts, - announceCustomStyles + announceCustomStyles, + announceConfiguration } from '../../../src/container/bootstrap' import { buildApplication } from '../../../src/container/application' import { defaultStoreMockOptions } from 'web-test-helpers/src' @@ -114,3 +116,45 @@ describe('announceCustomStyles', () => { expect(elements.length).toBeFalsy() }) }) + +describe('announceConfiguration', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + it('should set "web" as the default mode when none is set', async () => { + fetchMock.mockResponseOnce('{}') + const config = await announceConfiguration('/config.json') + expect(config.options.mode).toStrictEqual('web') + }) + + it('should use the mode that is defined in config.json', async () => { + fetchMock.mockResponseOnce('{ "options": { "mode": "config-mode" } }') + const config = await announceConfiguration('/config.json') + expect(config.options.mode).toStrictEqual('config-mode') + }) + + it('should use the mode that is defined in URL query when config.json does not set it', async () => { + Object.defineProperty(window, 'location', { + value: { + search: '?mode=query-mode' + }, + writable: true + }) + fetchMock.mockResponseOnce('{}') + const config = await announceConfiguration('/config.json') + expect(config.options.mode).toStrictEqual('query-mode') + }) + + it('should not use the mode that is defined in URL query when config.json sets one', async () => { + Object.defineProperty(window, 'location', { + value: { + search: '?mode=query-mode' + }, + writable: true + }) + fetchMock.mockResponseOnce('{ "options": { "mode": "config-mode" } }') + const config = await announceConfiguration('/config.json') + expect(config.options.mode).toStrictEqual('config-mode') + }) +})