From 3d30b32c0339810f91126e438d1133b528a23cb1 Mon Sep 17 00:00:00 2001 From: ffalqui Date: Mon, 9 Mar 2026 15:33:11 +0100 Subject: [PATCH 1/4] ESB-1012 - Render dynamic CMS menu items from SystemReport API response - hardcoded scheduler/workflow menu items with a dynamic loop - over the new array-based SystemReport payload, filtering by permissions. --- src/ui/internal-page/VerticalMenuContainer.js | 35 ++++++++++--------- test/ui/app/MfeContainer.test.js | 4 +-- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/ui/internal-page/VerticalMenuContainer.js b/src/ui/internal-page/VerticalMenuContainer.js index fdd4b59b2..cbc9fb3fb 100644 --- a/src/ui/internal-page/VerticalMenuContainer.js +++ b/src/ui/internal-page/VerticalMenuContainer.js @@ -74,7 +74,7 @@ const renderCmsMenuItems = (intl, userPermissions, systemReport, currSysConfigAd ], userPermissions); const hasMenuContentSettingsAccess = hasAccess(SUPERUSER_PERMISSION, userPermissions); - const { contentSchedulerPluginInstalled, contentWorkFlowPluginInstalled } = systemReport; + const dynamicMenuItems = Array.isArray(systemReport) ? systemReport : []; return ( - ) - } - { - hasMenuContentsAccess && contentWorkFlowPluginInstalled && ( - - ) + dynamicMenuItems + .filter(item => + hasAccess( + item['appBuilderMenu.requiredPermission'].split(',').map(p => p.trim()), + userPermissions, + )) + .map(item => ( + + )) } { hasMenuContentTypeAccess && ( diff --git a/test/ui/app/MfeContainer.test.js b/test/ui/app/MfeContainer.test.js index 9a6a6738a..62d5cb92c 100644 --- a/test/ui/app/MfeContainer.test.js +++ b/test/ui/app/MfeContainer.test.js @@ -26,9 +26,7 @@ const mfeConfigMock = { jest.mock('hooks/useMfe', () => jest.fn(() => ({ assetLoading: mockedLoading, mfe: mockMfe }))); jest.mock('state/system/selectors', () => ({ - getSystemReport: jest.fn().mockReturnValue({ - contentSchedulerPluginInstalled: true, - }), + getSystemReport: jest.fn().mockReturnValue([]), })); describe('MfeContainer', () => { From 9529531e6201ce1552879c6b3b23043d7af0a9d8 Mon Sep 17 00:00:00 2001 From: ffalqui Date: Mon, 9 Mar 2026 17:34:01 +0100 Subject: [PATCH 2/4] ESB-1012 - Dynamic CMS menu items from SystemReport API with boolean permission expressions --- src/helpers/permissions.js | 43 +++++++++++++++++++ src/ui/internal-page/VerticalMenuContainer.js | 6 +-- 2 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 src/helpers/permissions.js diff --git a/src/helpers/permissions.js b/src/helpers/permissions.js new file mode 100644 index 000000000..a6a84c400 --- /dev/null +++ b/src/helpers/permissions.js @@ -0,0 +1,43 @@ +/** + * Evaluates a boolean permission expression against user permissions. + * Supports & (AND), | (OR), and parentheses for grouping. + * Example: "(editContents|validateContents)&superuser" + */ +// eslint-disable-next-line import/prefer-default-export +export const checkPermission = (expr, userPermissions) => { + if (!expr) return false; + let i = 0; + + const parseOr = () => { + // eslint-disable-next-line no-use-before-define + let result = parseAnd(); + // eslint-disable-next-line no-plusplus,no-use-before-define + while (expr[i] === '|') { i++; result = parseAnd() || result; } + return result; + }; + + const parseAnd = () => { + // eslint-disable-next-line no-use-before-define + let result = parseAtom(); + // eslint-disable-next-line no-plusplus,no-use-before-define + while (expr[i] === '&') { i++; result = parseAtom() && result; } + return result; + }; + + const parseAtom = () => { + if (expr[i] === '(') { + // eslint-disable-next-line no-plusplus + i++; + const result = parseOr(); + // eslint-disable-next-line no-plusplus + i++; + return result; + } + let name = ''; + // eslint-disable-next-line no-plusplus + while (i < expr.length && /\w/.test(expr[i])) name += expr[i++]; + return userPermissions.includes(name); + }; + + return parseOr(); +}; diff --git a/src/ui/internal-page/VerticalMenuContainer.js b/src/ui/internal-page/VerticalMenuContainer.js index cbc9fb3fb..f7b84ef31 100644 --- a/src/ui/internal-page/VerticalMenuContainer.js +++ b/src/ui/internal-page/VerticalMenuContainer.js @@ -9,6 +9,7 @@ import { routeConverter, hasAccess } from '@entando/utils'; import { clearAppTourProgress, setAppTourLastStep, setWizardEnabled } from 'state/app-tour/actions'; import { adminConsoleUrl } from 'helpers/urlUtils'; +import { checkPermission } from 'helpers/permissions'; import UserMenuContainer from 'ui/internal-page/UserMenuContainer'; import LanguageSelectContainer from 'ui/internal-page/LanguageSelectContainer'; @@ -132,10 +133,7 @@ const renderCmsMenuItems = (intl, userPermissions, systemReport, currSysConfigAd { dynamicMenuItems .filter(item => - hasAccess( - item['appBuilderMenu.requiredPermission'].split(',').map(p => p.trim()), - userPermissions, - )) + checkPermission(item['appBuilderMenu.requiredPermission'], userPermissions)) .map(item => ( Date: Tue, 10 Mar 2026 14:47:37 +0100 Subject: [PATCH 3/4] ESB-1012 - Add Legacy Plugins menu with dynamic items from /legacy-components-menu API Refactor SystemReport response to use hook-based grouping with appBuilderMenu.items array structure. CMS dynamic items and new Legacy Plugins menu both consume the same item shape. --- sass/internal-page/InternalPage.scss | 35 +++++++++ src/api/system.js | 2 +- src/locales/en.js | 1 + src/locales/it.js | 1 + src/locales/pt.js | 1 + src/ui/internal-page/VerticalMenuContainer.js | 78 +++++++++++++++---- test/api/system.test.js | 2 +- 7 files changed, 101 insertions(+), 19 deletions(-) diff --git a/sass/internal-page/InternalPage.scss b/sass/internal-page/InternalPage.scss index f6147a57c..2f966b62c 100644 --- a/sass/internal-page/InternalPage.scss +++ b/sass/internal-page/InternalPage.scss @@ -307,6 +307,41 @@ // sass-lint:enable force-element-nesting class-name-format nesting-depth +// sass-lint:disable class-name-format force-element-nesting nesting-depth no-important no-qualifying-elements quotes property-sort-order +.nav-pf-secondary-nav .list-group { + li.list-group-item.LegacyPlugins__group-header { + position: relative; + margin-top: 4px; + padding-left: 25px; + pointer-events: none; + + &::before { + content: ''; + position: absolute; + top: 0; + right: 20px; + left: 20px; + height: 1px; + background-color: $color-pf-black-600; + } + + &:first-of-type { + margin-top: 0; + + &::before { + display: none; + } + } + + > a { + padding-left: 20px !important; + font-weight: 700 !important; + cursor: default; + } + } +} +// sass-lint:enable class-name-format force-element-nesting nesting-depth no-important no-qualifying-elements quotes property-sort-order + .NoAccessPage { position: fixed; top: 0; diff --git a/src/api/system.js b/src/api/system.js index 198dc2a84..8cdcc4540 100644 --- a/src/api/system.js +++ b/src/api/system.js @@ -2,7 +2,7 @@ import { makeRequest, METHODS } from '@entando/apimanager'; // eslint-disable-next-line import/prefer-default-export export const getSystemReport = () => makeRequest({ - uri: '/api/system/report', + uri: '/api/system/legacy-components-menu', method: METHODS.GET, mockResponse: {}, useAuthentication: true, diff --git a/src/locales/en.js b/src/locales/en.js index 13c5f1c00..849d95e95 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -337,6 +337,7 @@ export default { 'menu.scheduler': 'Scheduler', 'menu.versioning': 'Versioning', 'menu.apps': 'Apps', + 'menu.legacyPlugins': 'Legacy Plugins', 'menu.settings': 'Administration', 'menu.emailConfig': 'Email Configuration', 'dashboard.cards.noPermission': 'You have no permission to visualize this data', diff --git a/src/locales/it.js b/src/locales/it.js index 57fb5f719..0e4779c24 100644 --- a/src/locales/it.js +++ b/src/locales/it.js @@ -337,6 +337,7 @@ export default { 'menu.scheduler': 'Scheduler', 'menu.versioning': 'Versionamento', 'menu.apps': 'App', + 'menu.legacyPlugins': 'Plugin Legacy', 'menu.settings': 'Amministrazione', 'menu.emailConfig': 'Configurazione Email', 'dashboard.cards.noPermission': 'Non hai il permesso di visualizzare questi dati', diff --git a/src/locales/pt.js b/src/locales/pt.js index 5c9fd69bb..70adbb922 100644 --- a/src/locales/pt.js +++ b/src/locales/pt.js @@ -326,6 +326,7 @@ export default { 'menu.apiManagement.resources': 'Recursos', 'menu.apiManagement.services': 'serviços', 'menu.apiManagement.consumers': 'Consumer', + 'menu.legacyPlugins': 'Plugins Legados', 'menu.settings': 'Administração', 'menu.emailConfig': 'Configuração de Email', 'menu.apps': 'Apps', diff --git a/src/ui/internal-page/VerticalMenuContainer.js b/src/ui/internal-page/VerticalMenuContainer.js index f7b84ef31..ff098d46a 100644 --- a/src/ui/internal-page/VerticalMenuContainer.js +++ b/src/ui/internal-page/VerticalMenuContainer.js @@ -55,6 +55,63 @@ const { const publicUrl = process.env.PUBLIC_URL; +const getReportEntries = systemReport => (Array.isArray(systemReport) ? systemReport : []); + +const getGroupItems = group => + (Array.isArray(group['appBuilderMenu.items']) ? group['appBuilderMenu.items'] : []); + +const renderMenuItemsByHook = (systemReport, hook) => + getReportEntries(systemReport).filter(e => e['appBuilderMenu.hook'] === hook); + +const renderDynamicItems = (items, intl, userPermissions) => + items + .filter(item => checkPermission(item.requiredPermission, userPermissions)) + .map(item => ( + + )); + +const renderLegacyPluginsMenu = (intl, userPermissions, systemReport) => { + const pluginGroups = renderMenuItemsByHook(systemReport, 'legacyPlugins') + .filter(g => getGroupItems(g) + .some(item => checkPermission(item.requiredPermission, userPermissions))); + + if (pluginGroups.length === 0) return null; + + return ( + {}} + iconClass="fa fa-puzzle-piece" + title={intl.formatMessage({ id: 'menu.legacyPlugins', defaultMessage: 'Legacy Plugins' })} + > + {pluginGroups.flatMap((group) => { + const items = getGroupItems(group) + .filter(item => checkPermission(item.requiredPermission, userPermissions)); + const pluginId = group['appBuilderMenu.pluginId'] || ''; + const pluginLabel = group['appBuilderMenu.pluginLabel'] || pluginId; + return [ + , + ...renderDynamicItems(items, intl, userPermissions), + ]; + })} + + ); +}; + const renderCmsMenuItems = (intl, userPermissions, systemReport, currSysConfigAdvancedSearchOn) => { const hasMenuContentsAccess = hasAccess([ CRUD_CONTENTS_PERMISSION, @@ -75,7 +132,8 @@ const renderCmsMenuItems = (intl, userPermissions, systemReport, currSysConfigAd ], userPermissions); const hasMenuContentSettingsAccess = hasAccess(SUPERUSER_PERMISSION, userPermissions); - const dynamicMenuItems = Array.isArray(systemReport) ? systemReport : []; + const cmsGroups = renderMenuItemsByHook(systemReport, 'cms'); + const cmsItems = cmsGroups.length > 0 ? getGroupItems(cmsGroups[0]) : []; return ( ) } - { - dynamicMenuItems - .filter(item => - checkPermission(item['appBuilderMenu.requiredPermission'], userPermissions)) - .map(item => ( - - )) - } + {renderDynamicItems(cmsItems, intl, userPermissions)} { hasMenuContentTypeAccess && ( { it('should make a request with the correct parameters', () => { getSystemReport(); expect(makeRequest).toHaveBeenCalledWith({ - uri: '/api/system/report', + uri: '/api/system/legacy-components-menu', method: METHODS.GET, mockResponse: {}, useAuthentication: true, From 010e6ddede4169113c7e75f8475f0d5791b3e712 Mon Sep 17 00:00:00 2001 From: ffalqui Date: Wed, 11 Mar 2026 17:47:02 +0100 Subject: [PATCH 4/4] ESB-1012 - Legacy widget config iframe with postMessage resize and top Cancel/Save buttons - Render legacy LEGACY_CONFIG widgets in an iframe with headless Struts JSP - Add postMessage listener for entando.legacyConfigResize to auto-resize iframe height - Add Cancel button and disabled Save button with tooltip via WidgetConfigPortal - Make labelId optional in renderDynamicItems (falls back to item.id) - Show legacy plugin header only when pluginId is present - Fix ContentPageContainer test mock for getGroupsMap --- sass/widgets/config/WidgetConfigPage.scss | 7 ++ src/helpers/legacyWidget.js | 20 ++++++ src/locales/en.js | 1 + src/locales/it.js | 1 + src/state/page-config/actions.js | 5 +- src/state/page-config/selectors.js | 1 + src/ui/internal-page/VerticalMenuContainer.js | 7 +- src/ui/pages/config/PageConfigGridCol.js | 4 ++ src/ui/pages/config/WidgetFrame.js | 9 ++- src/ui/widgets/config/WidgetConfigPage.js | 66 ++++++++++++++++++- .../config/WidgetConfigPageContainer.js | 6 ++ .../pages/config/ContentPageContainer.test.js | 1 + 12 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 src/helpers/legacyWidget.js diff --git a/sass/widgets/config/WidgetConfigPage.scss b/sass/widgets/config/WidgetConfigPage.scss index bb7133572..cf589340c 100644 --- a/sass/widgets/config/WidgetConfigPage.scss +++ b/sass/widgets/config/WidgetConfigPage.scss @@ -56,4 +56,11 @@ & .panel-heading { background-color: $color-white; } + + &__legacy-iframe { + width: 100%; + min-height: 600px; + border: 0; + overflow: hidden; + } } diff --git a/src/helpers/legacyWidget.js b/src/helpers/legacyWidget.js new file mode 100644 index 000000000..c0007c163 --- /dev/null +++ b/src/helpers/legacyWidget.js @@ -0,0 +1,20 @@ +import getAppBuilderWidgetForm from 'helpers/getAppBuilderWidgetForm'; +import { adminConsoleUrl } from 'helpers/urlUtils'; + +export const LEGACY_CONFIG_CUSTOM_ELEMENT = 'LEGACY_CONFIG'; + +// A widget is "legacy" when its configUi.customElement is set to LEGACY_CONFIG +// and it has no internal app-builder form registered in getAppBuilderWidgetForm. +export const isLegacyWidget = (widget) => { + if (!widget || !widget.configUi || widget.configUi.customElement !== 'LEGACY_CONFIG') { + return false; + } + return !getAppBuilderWidgetForm(widget); +}; + +export const buildLegacyConfigUrl = (widget, pageCode, frameId) => { + const actionConf = widget.configUiName || 'configSimpleParameter'; + const path = + `do/Page/SpecialWidget/${actionConf}?pageCode=${pageCode}&widgetTypeCode=${widget.code}&frame=${frameId}&entandoHeadless=true`; + return adminConsoleUrl(path); +}; diff --git a/src/locales/en.js b/src/locales/en.js index 849d95e95..15712c1f5 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -412,6 +412,7 @@ export default { 'widget.page.config.error': 'Unable to load widget configuration', 'widget.page.config.goToConfig': 'Go to widget default configuration', 'widget.page.config.readOnlyMessage': 'This is the default configuration for this widget. Do you want to change it?', + 'widget.page.config.legacySaveTooltip': 'Use the Save button inside the configuration form below', 'widget.icon.description': 'Upload your Icon from the Icon Library or your Computer', 'widget.icon.iconLibrary': 'Icon Library', 'widget.icon.upload': 'Upload', diff --git a/src/locales/it.js b/src/locales/it.js index 0e4779c24..596382a92 100644 --- a/src/locales/it.js +++ b/src/locales/it.js @@ -412,6 +412,7 @@ export default { 'widget.page.config.error': 'Impossibile caricare la configurazione del widget', 'widget.page.config.goToConfig': 'Vai alla configurazione di default', 'widget.page.config.readOnlyMessage': 'Questa è la configurazione di default del widget. Vuoi modificarla?', + 'widget.page.config.legacySaveTooltip': 'Usa il pulsante Salva nel modulo di configurazione qui sotto', 'widget.icon.description': 'Carica la tua icona dalla libreria delle icone o dal tuo computer', 'widget.icon.iconLibrary': 'Icon Library', 'widget.icon.upload': 'Carica', diff --git a/src/state/page-config/actions.js b/src/state/page-config/actions.js index a32781303..e5792ab13 100644 --- a/src/state/page-config/actions.js +++ b/src/state/page-config/actions.js @@ -2,6 +2,7 @@ import { initialize } from 'redux-form'; import { routeConverter } from '@entando/utils'; import { addToast, addErrors, clearErrors, TOAST_ERROR } from '@entando/messages'; +import { isLegacyWidget } from 'helpers/legacyWidget'; import { loadSelectedPageTemplate } from 'state/page-templates/actions'; import { getSelectedPageTemplateMainFrame, getSelectedPageTemplateDefaultConfig } from 'state/page-templates/selectors'; import { loadSelectedPage, setSelectedPage } from 'state/pages/actions'; @@ -254,10 +255,12 @@ export const configOrUpdatePageWidget = (sourceWidgetId, sourceFrameId, targetFr if (widget.hasConfig && !isAlreadyConfigured) { const nextStep = widgetNextSteps[sourceWidgetId]; const appTourProgress = getAppTourProgress(getState()); + history.push(routeConverter( ROUTE_WIDGET_CONFIG, { pageCode, widgetCode: sourceWidgetId, framePos: targetFrameId }, )); + if (nextStep && appTourProgress === APP_TOUR_STARTED) { dispatch(setAppTourLastStep(nextStep)); } @@ -277,7 +280,7 @@ export const editWidgetConfig = (frameId, pageCode) => const widget = getWidgetsMap(getState())[pageConfigItem.code]; const isConfigurableWidget = widget && widget.hasConfig; - if (isConfigurableWidget || pageConfigItem.config) { + if (isLegacyWidget(widget) || isConfigurableWidget || pageConfigItem.config) { dispatch(initialize('widgetConfigForm', pageConfigItem.config)); history.push(routeConverter( ROUTE_WIDGET_CONFIG, diff --git a/src/state/page-config/selectors.js b/src/state/page-config/selectors.js index 4116f4c4a..1df14ed95 100644 --- a/src/state/page-config/selectors.js +++ b/src/state/page-config/selectors.js @@ -70,6 +70,7 @@ export const makeGetPageConfigCellMap = params => createSelector( relatedCell.widgetTitle = widget.titles[locale] || widget.name; relatedCell.widgetHasConfig = widget.hasConfig; relatedCell.configUiName = widget.configUiName; + relatedCell.configUi = widget.configUi || null; relatedCell.widgetHasConfigForm = !!isMicrofrontendWidgetForm(widget); } } diff --git a/src/ui/internal-page/VerticalMenuContainer.js b/src/ui/internal-page/VerticalMenuContainer.js index ff098d46a..250d5d809 100644 --- a/src/ui/internal-page/VerticalMenuContainer.js +++ b/src/ui/internal-page/VerticalMenuContainer.js @@ -71,7 +71,7 @@ const renderDynamicItems = (items, intl, userPermissions) => key={item.id} id={item.id} title={intl.formatMessage({ - id: item.labelId, + id: item.labelId || item.id, defaultMessage: item.defaultLabel, })} href={adminConsoleUrl(item.href)} @@ -98,13 +98,16 @@ const renderLegacyPluginsMenu = (intl, userPermissions, systemReport) => { .filter(item => checkPermission(item.requiredPermission, userPermissions)); const pluginId = group['appBuilderMenu.pluginId'] || ''; const pluginLabel = group['appBuilderMenu.pluginLabel'] || pluginId; - return [ + const header = pluginId ? [ , + ] : []; + return [ + ...header, ...renderDynamicItems(items, intl, userPermissions), ]; })} diff --git a/src/ui/pages/config/PageConfigGridCol.js b/src/ui/pages/config/PageConfigGridCol.js index 0e4ce7f6c..7869b9ec6 100644 --- a/src/ui/pages/config/PageConfigGridCol.js +++ b/src/ui/pages/config/PageConfigGridCol.js @@ -56,6 +56,7 @@ const PageConfigGridCol = ({ cellMap, cellKey, gridWidth }) => { widgetHasConfig={col.widgetHasConfig} widgetStatus={col.widgetStatus} widgetHasConfigForm={col.widgetHasConfigForm} + configUi={col.configUi} /> ); } else { @@ -92,6 +93,9 @@ PageConfigGridCol.propTypes = { widgetCode: PropTypes.string, widgetStatus: PropTypes.string, configUiName: PropTypes.string, + configUi: PropTypes.shape({ + customElement: PropTypes.string, + }), frameDescr: PropTypes.string, widgetTitle: PropTypes.string, widgetHasConfig: PropTypes.bool, diff --git a/src/ui/pages/config/WidgetFrame.js b/src/ui/pages/config/WidgetFrame.js index 7301ec951..53f93c067 100644 --- a/src/ui/pages/config/WidgetFrame.js +++ b/src/ui/pages/config/WidgetFrame.js @@ -7,6 +7,7 @@ import { routeConverter } from '@entando/utils'; import { ROUTE_WIDGET_DETAIL, ROUTE_WIDGET_EDIT } from 'app-init/router'; import { WIDGET_STATUS_MATCH, WIDGET_STATUS_DIFF, WIDGET_STATUS_REMOVED } from 'state/page-config/const'; +import { LEGACY_CONFIG_CUSTOM_ELEMENT } from 'helpers/legacyWidget'; import WidgetIcon from 'ui/widgets/common/WidgetIcon'; @@ -15,12 +16,14 @@ class WidgetFrame extends Component { const { widgetId, widgetName, widgetHasConfig, widgetStatus, frameId, frameName, frameIsMainFrame, onClickDelete, connectDragSource, connectDropTarget, isOver, onClickSettings, onClickSaveAs, - configUiName, widgetHasConfigForm, + configUiName, widgetHasConfigForm, configUi, } = this.props; + const isLegacy = configUi && configUi.customElement === LEGACY_CONFIG_CUSTOM_ELEMENT; + let actionsMenu = null; if (widgetStatus !== WIDGET_STATUS_REMOVED) { - const configMenuItems = widgetHasConfig && (configUiName || widgetHasConfigForm) ? + const configMenuItems = widgetHasConfig && (configUiName || widgetHasConfigForm || isLegacy) ? [ ( ); } + if (isLegacyWidget(widget)) { + const legacyUrl = buildLegacyConfigUrl(widget, pageCode, framePos); + return ( + + + + + + + } + > + + + + + + +