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/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/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/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/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/locales/en.js b/src/locales/en.js index 13c5f1c00..15712c1f5 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', @@ -411,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 57fb5f719..596382a92 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', @@ -411,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/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/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 fdd4b59b2..250d5d809 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'; @@ -54,6 +55,66 @@ 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; + const header = pluginId ? [ + , + ] : []; + return [ + ...header, + ...renderDynamicItems(items, intl, userPermissions), + ]; + })} + + ); +}; + const renderCmsMenuItems = (intl, userPermissions, systemReport, currSysConfigAdvancedSearchOn) => { const hasMenuContentsAccess = hasAccess([ CRUD_CONTENTS_PERMISSION, @@ -74,7 +135,8 @@ const renderCmsMenuItems = (intl, userPermissions, systemReport, currSysConfigAd ], userPermissions); const hasMenuContentSettingsAccess = hasAccess(SUPERUSER_PERMISSION, userPermissions); - const { contentSchedulerPluginInstalled, contentWorkFlowPluginInstalled } = systemReport; + const cmsGroups = renderMenuItemsByHook(systemReport, 'cms'); + const cmsItems = cmsGroups.length > 0 ? getGroupItems(cmsGroups[0]) : []; return ( ) } - { - hasMenuContentsAccess && contentSchedulerPluginInstalled && ( - - ) - } - { - hasMenuContentsAccess && contentWorkFlowPluginInstalled && ( - - ) - } + {renderDynamicItems(cmsItems, intl, userPermissions)} { hasMenuContentTypeAccess && ( { 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 ( + + + + + + + } + > + + + + + + +