diff --git a/.changeset/lazy-schools-wait.md b/.changeset/lazy-schools-wait.md new file mode 100644 index 0000000000..5797606c0e --- /dev/null +++ b/.changeset/lazy-schools-wait.md @@ -0,0 +1,6 @@ +--- +'@sap-ux-private/preview-middleware-client': patch +'@sap-ux/preview-middleware': patch +--- + +handle higher layer changes diff --git a/packages/preview-middleware-client/src/flp/init.ts b/packages/preview-middleware-client/src/flp/init.ts index 2c2b830248..c6a1508958 100644 --- a/packages/preview-middleware-client/src/flp/init.ts +++ b/packages/preview-middleware-client/src/flp/init.ts @@ -1,7 +1,7 @@ import Log from 'sap/base/Log'; import type AppLifeCycle from 'sap/ushell/services/AppLifeCycle'; import type { InitRtaScript, RTAPlugin, StartAdaptation } from 'sap/ui/rta/api/startAdaptation'; -import { SCENARIO, type Scenario } from '@sap-ux-private/control-property-editor-common'; +import { SCENARIO, showMessage, type Scenario } from '@sap-ux-private/control-property-editor-common'; import type { FlexSettings, RTAOptions } from 'sap/ui/rta/RuntimeAuthoring'; import IconPool from 'sap/ui/core/IconPool'; import ResourceBundle from 'sap/base/i18n/ResourceBundle'; @@ -9,7 +9,9 @@ import AppState from 'sap/ushell/services/AppState'; import { getManifestAppdescr } from '../adp/api-handler'; import { getError } from '../utils/error'; import initConnectors from './initConnectors'; -import { getUi5Version, isLowerThanMinimalUi5Version } from '../utils/version'; +import { getUi5Version, isLowerThanMinimalUi5Version, Ui5VersionInfo } from '../utils/version'; +import { CommunicationService } from '../cpe/communication-service'; +import { getTextBundle } from '../i18n'; /** * SAPUI5 delivered namespaces from https://ui5.sap.com/#/api/sap @@ -304,7 +306,11 @@ export async function init({ libs, // eslint-disable-next-line no-shadow async function (startAdaptation: StartAdaptation | InitRtaScript, pluginScript: RTAPlugin) { - await startAdaptation(options, pluginScript); + try { + await startAdaptation(options, pluginScript); + } catch (error) { + await handleHigherLayerChanges(error, ui5VersionInfo); + } } ); }); @@ -318,7 +324,7 @@ export async function init({ // Load custom library paths if configured if (appUrls) { - await registerComponentDependencyPaths(JSON.parse(appUrls) as string[] ?? [], urlParams); + await registerComponentDependencyPaths((JSON.parse(appUrls) as string[]) ?? [], urlParams); } // Load rta connector @@ -351,3 +357,28 @@ if (bootstrapConfig) { Log.error('Sandbox initialization failed: ' + error.message); }); } + +/** + * Handle higher layer changes when starting UI Adaptation. + * When RTA detects higher layer changes an error with Reload triggered text is thrown, the RTA instance is destroyed and the application is reloaded. + * For UI5 version lower than 1.84.0 RTA is showing a popup with notification text about the detection of higher layer changes. + * + * @param error the error thrown when there are higher layer changes when starting UI Adaptation. + * @param ui5VersionInfo ui5 version info + */ +export async function handleHigherLayerChanges(error: unknown, ui5VersionInfo: Ui5VersionInfo): Promise { + const err = getError(error); + if (err.message.includes('Reload triggered')) { + if (!isLowerThanMinimalUi5Version(ui5VersionInfo, { major: 1, minor: 84 })) { + const bundle = await getTextBundle(); + const action = showMessage({ + message: bundle.getText('HIGHER_LAYER_CHANGES_INFO_MESSAGE'), + shouldHideIframe: false + }); + CommunicationService.sendAction(action); + } + + // eslint-disable-next-line fiori-custom/sap-no-location-reload + window.location.reload(); + } +} diff --git a/packages/preview-middleware-client/src/messagebundle.properties b/packages/preview-middleware-client/src/messagebundle.properties index 699b762277..5fc620e6c8 100644 --- a/packages/preview-middleware-client/src/messagebundle.properties +++ b/packages/preview-middleware-client/src/messagebundle.properties @@ -37,3 +37,5 @@ ADP_REUSE_COMPONENTS_MESSAGE = Reuse components are detected for some views in t CPE_CHANGES_VISIBLE_AFTER_SAVE_AND_RELOAD_MESSAGE = Note: The change will be visible after save and reload. TABLE_ROWS_NEEDED_TO_CREATE_CUSTOM_COLUMN=At least one table row is required to create new custom column. Make sure the table data is loaded and try again. + +HIGHER_LAYER_CHANGES_INFO_MESSAGE=The preview of the project was reloaded due to detected changes in higher layer than the one used in your project. diff --git a/packages/preview-middleware-client/test/unit/flp/init.test.ts b/packages/preview-middleware-client/test/unit/flp/init.test.ts index bd4037dc7e..c9d02f7666 100644 --- a/packages/preview-middleware-client/test/unit/flp/init.test.ts +++ b/packages/preview-middleware-client/test/unit/flp/init.test.ts @@ -13,6 +13,29 @@ import { fetchMock, sapMock } from 'mock/window'; import type { InitRtaScript, RTAPlugin, StartAdaptation } from 'sap/ui/rta/api/startAdaptation'; import type { Scenario } from '@sap-ux-private/control-property-editor-common'; import VersionInfo from 'mock/sap/ui/VersionInfo'; +import { CommunicationService } from '../../../src/cpe/communication-service'; + +jest.mock('../../../src/i18n', () => { + return { + ...jest.requireActual('../../../src/i18n'), + getTextBundle: async () => { + return { + hasText: jest.fn().mockReturnValueOnce(true), + getText: jest + .fn() + .mockReturnValueOnce('The application was reloaded because of changes in a higher layer.') + }; + } + }; +}); + +Object.defineProperty(window, 'location', { + value: { + ...window.location, + reload: jest.fn() + }, + writable: true +}); describe('flp/init', () => { test('registerSAPFonts', () => { @@ -190,10 +213,24 @@ describe('flp/init', () => { }); describe('init', () => { + const reloadSpy = jest.fn(); + const location = window.location; beforeEach(() => { sapMock.ushell.Container.attachRendererCreatedEvent.mockReset(); sapMock.ui.require.mockReset(); jest.clearAllMocks(); + + Object.defineProperty(window, 'location', { + value: { + reload: reloadSpy + } + }); + }); + + afterEach(() => { + Object.defineProperty(window, 'location', { + value: location + }); }); test('nothing configured', async () => { @@ -298,5 +335,47 @@ describe('flp/init', () => { expect(sapMock.ui.require).toBeCalledWith([customInit]); }); + + test('handle higher layer changes', async () => { + const flexSettings = { + layer: 'VENDOR', + pluginScript: 'my/script' + }; + + VersionInfo.load.mockResolvedValueOnce({ name: 'sap.ui.core', version: '1.84.50' }); + + // Mocking `sap.ui.require` to throw the correct error structure + sapMock.ui.require.mockImplementationOnce((libs, callback) => { + callback(async () => { + throw 'Reload triggered'; + }, {}); + }); + + const sendActionSpy = jest.spyOn(CommunicationService, 'sendAction'); + await init({ flex: JSON.stringify(flexSettings) }); + const rendererCb = sapMock.ushell.Container.attachRendererCreatedEvent.mock + .calls[0][0] as () => Promise; + const mockService = { + attachAppLoaded: jest.fn().mockImplementation((callback) => { + callback({ getParameter: jest.fn() }); + }) + }; + sapMock.ushell.Container.getServiceAsync.mockResolvedValueOnce(mockService); + + await rendererCb(); + + const loadedCb = mockService.attachAppLoaded.mock.calls[0][0] as (event: unknown) => Promise; + await loadedCb({ getParameter: () => {} }); + + expect(sendActionSpy).toHaveBeenCalled(); + expect(sendActionSpy).toHaveBeenNthCalledWith(1, { + type: '[ext] show-dialog-message', + payload: { + message: 'The application was reloaded because of changes in a higher layer.', + shouldHideIframe: false + } + }); + expect(reloadSpy).toHaveBeenCalled(); + }); }); });