From af34c3559e5067700097e7226da63a44f165fccc Mon Sep 17 00:00:00 2001 From: Benedikt Kulmann Date: Mon, 4 Aug 2025 11:08:48 +0200 Subject: [PATCH 1/5] chore: add openstreetmap to dev stack csp rules --- dev/docker/opencloud/csp.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/docker/opencloud/csp.yaml b/dev/docker/opencloud/csp.yaml index 01028fdb63..a5ec3a5ed1 100644 --- a/dev/docker/opencloud/csp.yaml +++ b/dev/docker/opencloud/csp.yaml @@ -23,6 +23,7 @@ directives: - 'data:' - 'blob:' - 'https://raw.githubusercontent.com/opencloud-eu/awesome-apps/' + - 'https://tile.openstreetmap.org/' # In contrast to bash and docker the default is given after the | character - 'https://${ONLYOFFICE_DOMAIN|host.docker.internal:9981}/' - 'https://${COLLABORA_DOMAIN|host.docker.internal:9980}/' From a23b765761a1a3474b7577660d83d1e98f530426 Mon Sep 17 00:00:00 2001 From: Benedikt Kulmann Date: Mon, 4 Aug 2025 11:09:33 +0200 Subject: [PATCH 2/5] fix: display all root panels if on root level --- packages/web-pkg/src/components/SideBar/SideBar.vue | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/web-pkg/src/components/SideBar/SideBar.vue b/packages/web-pkg/src/components/SideBar/SideBar.vue index a717c702ec..327bd58177 100644 --- a/packages/web-pkg/src/components/SideBar/SideBar.vue +++ b/packages/web-pkg/src/components/SideBar/SideBar.vue @@ -71,8 +71,13 @@ > From 4830613e5f0d2f636cb106abdf6941b452271a98 Mon Sep 17 00:00:00 2001 From: Benedikt Kulmann Date: Mon, 4 Aug 2025 11:10:02 +0200 Subject: [PATCH 3/5] fix: error title wording --- .../actions/files/useFileActionsToggleHideShare.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/web-pkg/src/composables/actions/files/useFileActionsToggleHideShare.ts b/packages/web-pkg/src/composables/actions/files/useFileActionsToggleHideShare.ts index 57c18ad324..83fc7ff8eb 100644 --- a/packages/web-pkg/src/composables/actions/files/useFileActionsToggleHideShare.ts +++ b/packages/web-pkg/src/composables/actions/files/useFileActionsToggleHideShare.ts @@ -64,9 +64,7 @@ export const useFileActionsToggleHideShare = () => { } showErrorMessage({ - title: hidden - ? $gettext('Failed to hide the share') - : $gettext('Failed to unhide share share'), + title: hidden ? $gettext('Failed to hide the share') : $gettext('Failed to unhide the share'), errors }) } From 62e06e7c81ef59ea19f1cc85008bf94575e204b3 Mon Sep 17 00:00:00 2001 From: Benedikt Kulmann Date: Mon, 4 Aug 2025 11:10:51 +0200 Subject: [PATCH 4/5] fix: enforce stable order for application loading --- .../src/container/application/index.ts | 52 +++++++++--- .../web-runtime/src/container/bootstrap.ts | 84 +++++++++++++------ .../tests/unit/container/bootstrap.spec.ts | 30 +++++-- 3 files changed, 121 insertions(+), 45 deletions(-) diff --git a/packages/web-runtime/src/container/application/index.ts b/packages/web-runtime/src/container/application/index.ts index c7d4bb1cbc..82abce245e 100644 --- a/packages/web-runtime/src/container/application/index.ts +++ b/packages/web-runtime/src/container/application/index.ts @@ -62,26 +62,26 @@ const loadScriptRequireJS = (moduleUri: string) => { ) ) } -/** - * sniffs arguments and decides if given manifest is of next or current application style. - */ -export const buildApplication = async ({ - app, + +export const loadApplication = async ({ appName, applicationKey, applicationPath, applicationConfig, - router, configStore }: { - app: App appName?: string applicationKey: string applicationPath: string applicationConfig: AppConfigObject - router: Router configStore: ConfigStore -}) => { +}): Promise<{ + appName?: string + applicationKey: string + applicationPath: string + applicationConfig: AppConfigObject + applicationScript: ClassicApplicationScript +}> => { if (applicationStore.has(applicationKey)) { throw new RuntimeError('application already announced', applicationKey, applicationPath) } @@ -118,8 +118,40 @@ export const buildApplication = async ({ throw new RuntimeError('cannot load application', applicationPath, e) } - let application: NextApplication + return { + appName, + applicationKey, + applicationPath, + applicationConfig, + applicationScript + } +} + +/** + * sniffs arguments and decides if given manifest is of next or current application style. + */ +export const buildApplication = ({ + app, + appName, + applicationKey, + applicationPath, + applicationConfig, + applicationScript, + router +}: { + app: App + appName?: string + applicationKey: string + applicationPath: string + applicationConfig: AppConfigObject + applicationScript: ClassicApplicationScript + router: Router +}) => { + if (applicationStore.has(applicationKey)) { + throw new RuntimeError('application already announced', applicationKey, applicationPath) + } + let application: NextApplication try { /** add valuable sniffer to detect next applications, then implement next application factory */ if (!isObject(applicationScript.appInfo) && !applicationScript.setup) { diff --git a/packages/web-runtime/src/container/bootstrap.ts b/packages/web-runtime/src/container/bootstrap.ts index 5ad5609cc2..5f55941c16 100644 --- a/packages/web-runtime/src/container/bootstrap.ts +++ b/packages/web-runtime/src/container/bootstrap.ts @@ -1,5 +1,5 @@ import { registerClient } from '../services/clientRegistration' -import { buildApplication, NextApplication } from './application' +import { buildApplication, loadApplication, NextApplication } from './application' import { RouteLocationRaw, Router, RouteRecordNormalized } from 'vue-router' import { App, computed, watch } from 'vue' import { loadTheme } from '../helpers/theme' @@ -42,7 +42,8 @@ import { UppyService, AppConfigObject, resourceIconMappingInjectionKey, - ResourceIconMapping + ResourceIconMapping, + ClassicApplicationScript } from '@opencloud-eu/web-pkg' import { authService } from '../services/auth' import { init as sentryInit } from '@sentry/vue' @@ -190,8 +191,9 @@ export const announceAuthClient = async (configStore: ConfigStore): Promise[] = [] + let applicationKeys: string[] = [] + let applicationResponses: PromiseSettledResult[] = [] if (appProviderApps) { - applicationResults = await Promise.allSettled( + applicationKeys = appProviderService.appNames + applicationResponses = await Promise.allSettled( appProviderService.appNames.map((appName) => - buildApplication({ - app, + loadApplication({ appName, applicationKey: `web-app-external-${appName}`, applicationPath: 'web-app-external', applicationConfig: {}, - router, configStore }) ) @@ -233,30 +242,53 @@ export const initializeApplications = async ({ })), ...configStore.externalApps ] - applicationResults = await Promise.allSettled( - rawApplications.map((rawApplication) => - buildApplication({ - app, - applicationKey: rawApplication.path, - applicationPath: rawApplication.path, - applicationConfig: rawApplication.config, - router, + applicationKeys = rawApplications.map((rawApplication) => rawApplication.path) + applicationResponses = await Promise.allSettled( + rawApplications.map((rawApplications) => + loadApplication({ + applicationKey: rawApplications.path, + applicationPath: rawApplications.path, + applicationConfig: rawApplications.config || {}, configStore }) ) ) } + const applicationScripts = applicationResponses.reduce( + (acc, applicationResponse) => { + // we don't want to fail hard with the full system when one specific application can't get loaded. only log the error. + if (applicationResponse.status !== 'fulfilled') { + console.error(applicationResponse.reason) + } else { + acc.push(applicationResponse.value) + } - const applications = applicationResults.reduce((acc, applicationResult) => { - // we don't want to fail hard with the full system when one specific application can't get loaded. only log the error. - if (applicationResult.status !== 'fulfilled') { - console.error(applicationResult.reason) - } else { - acc.push(applicationResult.value) - } + return acc + }, + [] + ) - return acc - }, []) + // create applications in a stable order, one by one, so that apps and extensions get registered in the same order every time. + const applications: NextApplication[] = [] + for (const applicationKey of applicationKeys) { + const applicationResponse = applicationScripts.find( + (appResponse) => appResponse.applicationKey === applicationKey + ) + if (!applicationResponse) { + continue + } + try { + applications.push( + buildApplication({ + ...applicationResponse, + app, + router + }) + ) + } catch (error) { + console.error(error) + } + } await Promise.all(applications.map((application) => application.initialize())) diff --git a/packages/web-runtime/tests/unit/container/bootstrap.spec.ts b/packages/web-runtime/tests/unit/container/bootstrap.spec.ts index 1bebeee66a..20bc9416c5 100644 --- a/packages/web-runtime/tests/unit/container/bootstrap.spec.ts +++ b/packages/web-runtime/tests/unit/container/bootstrap.spec.ts @@ -8,7 +8,7 @@ import { announceCustomStyles, announceConfiguration } from '../../../src/container/bootstrap' -import { buildApplication } from '../../../src/container/application' +import { buildApplication, loadApplication } from '../../../src/container/application' import { createTestingPinia } from '@opencloud-eu/web-test-helpers' vi.mock('../../../src/container/application') @@ -24,16 +24,27 @@ describe('initialize applications', () => { const initialize = vi.fn() const ready = vi.fn() const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined) - const buildApplicationMock = vi + + const loadApplicationMock = vi .fn() - .mockImplementation(({ applicationPath }: { applicationPath: string }) => { - if (applicationPath.includes('Valid')) { - return Promise.resolve({ initialize, ready }) + .mockImplementation( + ({ + applicationKey, + applicationPath + }: { + applicationKey: string + applicationPath: string + }) => { + if (applicationPath.includes('Valid')) { + return Promise.resolve({ applicationKey }) + } + + return Promise.reject(fishyError) } + ) + vi.mocked(loadApplication).mockImplementation(loadApplicationMock) - return Promise.reject(fishyError) - }) - + const buildApplicationMock = vi.fn().mockReturnValue({ initialize, ready }) vi.mocked(buildApplication).mockImplementation(buildApplicationMock) const configStore = useConfigStore() @@ -50,7 +61,8 @@ describe('initialize applications', () => { appProviderService: undefined }) - expect(buildApplicationMock).toHaveBeenCalledTimes(4) + expect(loadApplicationMock).toHaveBeenCalledTimes(4) + expect(buildApplicationMock).toHaveBeenCalledTimes(2) expect(initialize).toHaveBeenCalledTimes(2) expect(errorSpy).toHaveBeenCalledTimes(2) expect(errorSpy.mock.calls[0][0]).toMatchObject(fishyError) From 1f99215edad7c0d3f04980e51ff15030db6cd200 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Tue, 5 Aug 2025 13:41:06 +0200 Subject: [PATCH 5/5] fix: app provider key mapping --- packages/web-runtime/src/container/bootstrap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web-runtime/src/container/bootstrap.ts b/packages/web-runtime/src/container/bootstrap.ts index 5f55941c16..9bedb2d1b4 100644 --- a/packages/web-runtime/src/container/bootstrap.ts +++ b/packages/web-runtime/src/container/bootstrap.ts @@ -223,7 +223,7 @@ export const initializeApplications = async ({ let applicationKeys: string[] = [] let applicationResponses: PromiseSettledResult[] = [] if (appProviderApps) { - applicationKeys = appProviderService.appNames + applicationKeys = appProviderService.appNames.map((appName) => `web-app-external-${appName}`) applicationResponses = await Promise.allSettled( appProviderService.appNames.map((appName) => loadApplication({