diff --git a/.changeset/modern-phones-help.md b/.changeset/modern-phones-help.md new file mode 100644 index 0000000000..91a4de49d4 --- /dev/null +++ b/.changeset/modern-phones-help.md @@ -0,0 +1,7 @@ +--- +'@sap-ux-private/preview-middleware-client': patch +'@sap-ux/control-property-editor': patch +'@sap-ux/preview-middleware': patch +--- + +Fixed Quick Actions not working after trying to open multiple dialogs and Quick Actions that create manifest changes in SAP Fiori Elements for OData V2 applications not showing correct state when there are unsaved manifest changes. diff --git a/packages/control-property-editor/src/panels/quick-actions/NestedQuickAction.tsx b/packages/control-property-editor/src/panels/quick-actions/NestedQuickAction.tsx index 71ec2c1908..8a3ba4e0f9 100644 --- a/packages/control-property-editor/src/panels/quick-actions/NestedQuickAction.tsx +++ b/packages/control-property-editor/src/panels/quick-actions/NestedQuickAction.tsx @@ -37,7 +37,7 @@ export function NestedQuickActionListItem({ actionIndex }: Readonly): ReactElement { const dispatch = useDispatch(); - const isDisabled = useSelector((state) => state.appMode === 'navigation'); + const isDisabled = useSelector((state) => state.appMode === 'navigation') || !action.enabled; const [showContextualMenu, setShowContextualMenu] = useState(false); const [target, setTarget] = useState<(EventTarget & (HTMLAnchorElement | HTMLElement | HTMLButtonElement)) | null>( null diff --git a/packages/preview-middleware-client/src/adp/controllers/AddFragment.controller.ts b/packages/preview-middleware-client/src/adp/controllers/AddFragment.controller.ts index e470204d16..392a0b69d0 100644 --- a/packages/preview-middleware-client/src/adp/controllers/AddFragment.controller.ts +++ b/packages/preview-middleware-client/src/adp/controllers/AddFragment.controller.ts @@ -66,7 +66,6 @@ export default class AddFragment extends BaseDialog { title: options.title, completeView: options.aggregation === undefined }); - this.ui5Version = sap.ui.version; this.commandExecutor = new CommandExecutor(this.rta); } diff --git a/packages/preview-middleware-client/src/adp/controllers/AddTableColumnFragments.controller.ts b/packages/preview-middleware-client/src/adp/controllers/AddTableColumnFragments.controller.ts index ea7fee688c..b0cc2c24c2 100644 --- a/packages/preview-middleware-client/src/adp/controllers/AddTableColumnFragments.controller.ts +++ b/packages/preview-middleware-client/src/adp/controllers/AddTableColumnFragments.controller.ts @@ -66,7 +66,6 @@ export default class AddTableColumnFragments extends BaseDialog = {} + ): Promise { + if (this.isDialogOpen) { + return; + } + let controller: Controller; + const resources = await getTextBundle(); + + switch (dialogName) { + case DialogNames.ADD_FRAGMENT: + controller = new AddFragment(`open.ux.preview.client.adp.controllers.${dialogName}`, overlay, rta, { + aggregation: options.aggregation, + title: resources.getText(options.title ?? 'ADP_ADD_FRAGMENT_DIALOG_TITLE') + }); + break; + case DialogNames.ADD_TABLE_COLUMN_FRAGMENTS: + controller = new AddTableColumnFragments( + `open.ux.preview.client.adp.controllers.${dialogName}`, + overlay, + rta, + { + aggregation: options.aggregation, + title: resources.getText(options.title ?? 'ADP_ADD_FRAGMENT_DIALOG_TITLE') + } + ); + break; + case DialogNames.CONTROLLER_EXTENSION: + controller = new ControllerExtension( + `open.ux.preview.client.adp.controllers.${dialogName}`, + overlay, + rta + ); + break; + case DialogNames.ADD_FRAGMENT_AT_EXTENSION_POINT: + controller = new ExtensionPoint( + `open.ux.preview.client.adp.controllers.${dialogName}`, + overlay, + rta, + extensionPointData! + ); + break; + } + + const id = dialogName === DialogNames.ADD_FRAGMENT_AT_EXTENSION_POINT ? `dialog--${dialogName}` : undefined; + + const dialog = (await Fragment.load({ + name: `open.ux.preview.client.adp.ui.${dialogName}`, + controller, + id + })) as Dialog; + + this.isDialogOpen = true; + dialog.attachBeforeClose(() => { + this.updateStatus(false); + }); + + await controller.setup(dialog); + this.updateStatus(true); + } + + /** + * Updates open dialog status. + * + * @param isDialogOpen Flag indicating if there is an open dialog. + */ + private static updateStatus(isDialogOpen: boolean) { + this.isDialogOpen = isDialogOpen; + const event = new CustomEvent(OPEN_DIALOG_STATUS_CHANGED); + this.eventTarget.dispatchEvent(event); + } + + /** + * Attach event handler for OPEN_DIALOG_STATUS_CHANGED event. + * + * @param handler Event handler. + * @returns Function that removes listener. + */ + public static onOpenDialogStatusChange(handler: (event: CustomEvent) => void | Promise): () => void { + this.eventTarget.addEventListener(OPEN_DIALOG_STATUS_CHANGED, handler as EventListener); + return () => { + this.eventTarget.removeEventListener(OPEN_DIALOG_STATUS_CHANGED, handler as EventListener); + }; + } +} diff --git a/packages/preview-middleware-client/src/adp/extension-point.ts b/packages/preview-middleware-client/src/adp/extension-point.ts index 8a45a91d7f..97875b336b 100644 --- a/packages/preview-middleware-client/src/adp/extension-point.ts +++ b/packages/preview-middleware-client/src/adp/extension-point.ts @@ -7,9 +7,9 @@ import CommandFactory from 'sap/ui/rta/command/CommandFactory'; import { ExternalAction, addExtensionPoint } from '@sap-ux-private/control-property-editor-common'; import { Deferred, createDeferred } from './utils'; +import { DialogFactory, DialogNames } from './dialog-factory'; import { CommunicationService } from '../cpe/communication-service'; -import { DialogNames, handler } from './init-dialogs'; type ActionService = { execute: (controlId: string, actionId: string) => void; @@ -100,7 +100,7 @@ export default class ExtensionPointService { let deferred = createDeferred(); const name = this.selectedExtensionPointName; - await handler(overlay, this.rta, DialogNames.ADD_FRAGMENT_AT_EXTENSION_POINT, { + await DialogFactory.createDialog(overlay, this.rta, DialogNames.ADD_FRAGMENT_AT_EXTENSION_POINT, { name, info, deferred diff --git a/packages/preview-middleware-client/src/adp/init-dialogs.ts b/packages/preview-middleware-client/src/adp/init-dialogs.ts index 7106488891..043077f9b7 100644 --- a/packages/preview-middleware-client/src/adp/init-dialogs.ts +++ b/packages/preview-middleware-client/src/adp/init-dialogs.ts @@ -1,8 +1,4 @@ -/** sap.m */ -import type Dialog from 'sap/m/Dialog'; - /** sap.ui.core */ -import Fragment from 'sap/ui/core/Fragment'; import UI5Element from 'sap/ui/core/Element'; /** sap.ui.rta */ @@ -15,24 +11,10 @@ import FlUtils from 'sap/ui/fl/Utils'; /** sap.ui.dt */ import type ElementOverlay from 'sap/ui/dt/ElementOverlay'; -import AddFragment, { AddFragmentOptions } from './controllers/AddFragment.controller'; -import ControllerExtension from './controllers/ControllerExtension.controller'; -import { ExtensionPointData } from './extension-point'; -import ExtensionPoint from './controllers/ExtensionPoint.controller'; import ManagedObject from 'sap/ui/base/ManagedObject'; import { isReuseComponent } from '../cpe/utils'; import { Ui5VersionInfo } from '../utils/version'; -import { getTextBundle } from '../i18n'; -import AddTableColumnFragments from './controllers/AddTableColumnFragments.controller'; - -export const enum DialogNames { - ADD_FRAGMENT = 'AddFragment', - ADD_TABLE_COLUMN_FRAGMENTS = 'AddTableColumnFragments', - CONTROLLER_EXTENSION = 'ControllerExtension', - ADD_FRAGMENT_AT_EXTENSION_POINT = 'ExtensionPoint' -} - -type Controller = AddFragment | AddTableColumnFragments | ControllerExtension | ExtensionPoint; +import { DialogFactory, DialogNames } from './dialog-factory'; /** * Handler for enablement of Extend With Controller context menu entry @@ -105,62 +87,6 @@ export const getAddFragmentItemText = (overlay: ElementOverlay) => { return 'Add: Fragment'; }; -/** - * Handler for new context menu entry - * - * @param overlay Control overlays - * @param rta Runtime Authoring - * @param dialogName Dialog name - * @param extensionPointData Control ID - * @param options Dialog options - */ -export async function handler( - overlay: UI5Element, - rta: RuntimeAuthoring, - dialogName: DialogNames, - extensionPointData?: ExtensionPointData, - options: Partial = {} -): Promise { - let controller: Controller; - const resources = await getTextBundle(); - - switch (dialogName) { - case DialogNames.ADD_FRAGMENT: - controller = new AddFragment(`open.ux.preview.client.adp.controllers.${dialogName}`, overlay, rta, { - aggregation: options.aggregation, - title: resources.getText(options.title ?? 'ADP_ADD_FRAGMENT_DIALOG_TITLE') - }); - break; - case DialogNames.ADD_TABLE_COLUMN_FRAGMENTS: - controller = new AddTableColumnFragments(`open.ux.preview.client.adp.controllers.${dialogName}`, overlay, rta, { - aggregation: options.aggregation, - title: resources.getText(options.title ?? 'ADP_ADD_FRAGMENT_DIALOG_TITLE') - }); - break; - case DialogNames.CONTROLLER_EXTENSION: - controller = new ControllerExtension(`open.ux.preview.client.adp.controllers.${dialogName}`, overlay, rta); - break; - case DialogNames.ADD_FRAGMENT_AT_EXTENSION_POINT: - controller = new ExtensionPoint( - `open.ux.preview.client.adp.controllers.${dialogName}`, - overlay, - rta, - extensionPointData! - ); - break; - } - - const id = dialogName === DialogNames.ADD_FRAGMENT_AT_EXTENSION_POINT ? `dialog--${dialogName}` : undefined; - - const dialog = await Fragment.load({ - name: `open.ux.preview.client.adp.ui.${dialogName}`, - controller, - id - }); - - await controller.setup(dialog as Dialog); -} - /** * Adds a new item to the context menu * @@ -174,7 +100,8 @@ export const initDialogs = (rta: RuntimeAuthoring, syncViewsIds: string[], ui5Ve contextMenu.addMenuItem({ id: 'ADD_FRAGMENT', text: getAddFragmentItemText, - handler: async (overlays: UI5Element[]) => await handler(overlays[0], rta, DialogNames.ADD_FRAGMENT), + handler: async (overlays: UI5Element[]) => + await DialogFactory.createDialog(overlays[0], rta, DialogNames.ADD_FRAGMENT), icon: 'sap-icon://attachment-html', enabled: (overlays: ElementOverlay[]) => isFragmentCommandEnabled(overlays, ui5VersionInfo) }); @@ -182,7 +109,8 @@ export const initDialogs = (rta: RuntimeAuthoring, syncViewsIds: string[], ui5Ve contextMenu.addMenuItem({ id: 'EXTEND_CONTROLLER', text: 'Extend With Controller', - handler: async (overlays: UI5Element[]) => await handler(overlays[0], rta, DialogNames.CONTROLLER_EXTENSION), + handler: async (overlays: UI5Element[]) => + await DialogFactory.createDialog(overlays[0], rta, DialogNames.CONTROLLER_EXTENSION), icon: 'sap-icon://create-form', enabled: (overlays: ElementOverlay[]) => isControllerExtensionEnabled(overlays, syncViewsIds, ui5VersionInfo) }); diff --git a/packages/preview-middleware-client/src/adp/quick-actions/common/add-controller-to-page.ts b/packages/preview-middleware-client/src/adp/quick-actions/common/add-controller-to-page.ts index 5dd4c9770b..67467c6a38 100644 --- a/packages/preview-middleware-client/src/adp/quick-actions/common/add-controller-to-page.ts +++ b/packages/preview-middleware-client/src/adp/quick-actions/common/add-controller-to-page.ts @@ -8,9 +8,11 @@ import type { QuickActionContext, SimpleQuickActionDefinition } from '../../../cpe/quick-actions/quick-action-definition'; -import { DialogNames, handler, isControllerExtensionEnabledForControl } from '../../init-dialogs'; +import { DialogFactory, DialogNames } from '../../dialog-factory'; +import { isControllerExtensionEnabledForControl } from '../../init-dialogs'; import { getExistingController } from '../../api-handler'; import { SimpleQuickActionDefinitionBase } from '../simple-quick-action-base'; +import { DIALOG_ENABLEMENT_VALIDATOR } from '../dialog-enablement-validator'; export const ADD_CONTROLLER_TO_PAGE_TYPE = 'add-controller-to-page'; const CONTROL_TYPES = ['sap.f.DynamicPage', 'sap.uxap.ObjectPageLayout']; @@ -23,7 +25,7 @@ export class AddControllerToPageQuickAction implements SimpleQuickActionDefinition { constructor(context: QuickActionContext) { - super(ADD_CONTROLLER_TO_PAGE_TYPE, CONTROL_TYPES, '', context); + super(ADD_CONTROLLER_TO_PAGE_TYPE, CONTROL_TYPES, '', context, [DIALOG_ENABLEMENT_VALIDATOR]); } private controllerExists = false; @@ -52,7 +54,7 @@ export class AddControllerToPageQuickAction async execute(): Promise { if (this.control) { const overlay = OverlayRegistry.getOverlay(this.control) || []; - await handler(overlay, this.context.rta, DialogNames.CONTROLLER_EXTENSION); + await DialogFactory.createDialog(overlay, this.context.rta, DialogNames.CONTROLLER_EXTENSION); } return []; } diff --git a/packages/preview-middleware-client/src/adp/quick-actions/common/create-page-action.ts b/packages/preview-middleware-client/src/adp/quick-actions/common/create-page-action.ts index 2edaa52fe7..3a7054bba9 100644 --- a/packages/preview-middleware-client/src/adp/quick-actions/common/create-page-action.ts +++ b/packages/preview-middleware-client/src/adp/quick-actions/common/create-page-action.ts @@ -1,11 +1,12 @@ import OverlayRegistry from 'sap/ui/dt/OverlayRegistry'; import FlexCommand from 'sap/ui/rta/command/FlexCommand'; -import { DialogNames, handler } from '../../init-dialogs'; +import { DialogFactory, DialogNames } from '../../dialog-factory'; import { QuickActionContext, SimpleQuickActionDefinition } from '../../../cpe/quick-actions/quick-action-definition'; import { SimpleQuickActionDefinitionBase } from '../simple-quick-action-base'; import { getApplicationType } from '../../../utils/application'; import { getUi5Version, isLowerThanMinimalUi5Version } from '../../../utils/version'; +import { DIALOG_ENABLEMENT_VALIDATOR } from '../dialog-enablement-validator'; export const ADD_PAGE_ACTION = 'add-page-action'; const CONTROL_TYPES = ['sap.f.DynamicPageTitle', 'sap.uxap.ObjectPageHeader', 'sap.uxap.ObjectPageDynamicHeaderTitle']; @@ -15,7 +16,9 @@ const CONTROL_TYPES = ['sap.f.DynamicPageTitle', 'sap.uxap.ObjectPageHeader', 's */ export class AddPageActionQuickAction extends SimpleQuickActionDefinitionBase implements SimpleQuickActionDefinition { constructor(context: QuickActionContext) { - super(ADD_PAGE_ACTION, CONTROL_TYPES, 'QUICK_ACTION_ADD_CUSTOM_PAGE_ACTION', context); + super(ADD_PAGE_ACTION, CONTROL_TYPES, 'QUICK_ACTION_ADD_CUSTOM_PAGE_ACTION', context, [ + DIALOG_ENABLEMENT_VALIDATOR + ]); } async initialize(): Promise { @@ -24,13 +27,13 @@ export class AddPageActionQuickAction extends SimpleQuickActionDefinitionBase im if (appType === 'fe-v4' && isLowerThanMinimalUi5Version(version, { major: 1, minor: 130 })) { return; } - return super.initialize(); + super.initialize(); } async execute(): Promise { if (this.control) { const overlay = OverlayRegistry.getOverlay(this.control) || []; - await handler(overlay, this.context.rta, DialogNames.ADD_FRAGMENT, undefined, { + await DialogFactory.createDialog(overlay, this.context.rta, DialogNames.ADD_FRAGMENT, undefined, { aggregation: 'actions', title: 'QUICK_ACTION_ADD_CUSTOM_PAGE_ACTION' }); diff --git a/packages/preview-middleware-client/src/adp/quick-actions/common/op-add-custom-section.ts b/packages/preview-middleware-client/src/adp/quick-actions/common/op-add-custom-section.ts index 4764c7e598..2f0490454c 100644 --- a/packages/preview-middleware-client/src/adp/quick-actions/common/op-add-custom-section.ts +++ b/packages/preview-middleware-client/src/adp/quick-actions/common/op-add-custom-section.ts @@ -2,10 +2,11 @@ import OverlayRegistry from 'sap/ui/dt/OverlayRegistry'; import FlexCommand from 'sap/ui/rta/command/FlexCommand'; import ObjectPageLayout from 'sap/uxap/ObjectPageLayout'; -import { DialogNames, handler } from '../../init-dialogs'; +import { DialogFactory, DialogNames } from '../../dialog-factory'; import { getRelevantControlFromActivePage } from '../../../cpe/quick-actions/utils'; import { QuickActionContext, SimpleQuickActionDefinition } from '../../../cpe/quick-actions/quick-action-definition'; import { SimpleQuickActionDefinitionBase } from '../simple-quick-action-base'; +import { DIALOG_ENABLEMENT_VALIDATOR } from '../dialog-enablement-validator'; export const OP_ADD_CUSTOM_SECTION = 'op-add-custom-section'; const CONTROL_TYPES = ['sap.uxap.ObjectPageLayout']; @@ -18,7 +19,9 @@ export class AddCustomSectionQuickAction implements SimpleQuickActionDefinition { constructor(context: QuickActionContext) { - super(OP_ADD_CUSTOM_SECTION, CONTROL_TYPES, 'QUICK_ACTION_OP_ADD_CUSTOM_SECTION', context); + super(OP_ADD_CUSTOM_SECTION, CONTROL_TYPES, 'QUICK_ACTION_OP_ADD_CUSTOM_SECTION', context, [ + DIALOG_ENABLEMENT_VALIDATOR + ]); } async execute(): Promise { @@ -29,7 +32,7 @@ export class AddCustomSectionQuickAction )[0] as ObjectPageLayout; const overlay = OverlayRegistry.getOverlay(objectPageLayout) || []; - await handler(overlay, this.context.rta, DialogNames.ADD_FRAGMENT, undefined, { + await DialogFactory.createDialog(overlay, this.context.rta, DialogNames.ADD_FRAGMENT, undefined, { aggregation: 'sections', title: 'QUICK_ACTION_OP_ADD_CUSTOM_SECTION' }); diff --git a/packages/preview-middleware-client/src/adp/quick-actions/common/op-add-header-field.ts b/packages/preview-middleware-client/src/adp/quick-actions/common/op-add-header-field.ts index 7059f38738..77182d0c3b 100644 --- a/packages/preview-middleware-client/src/adp/quick-actions/common/op-add-header-field.ts +++ b/packages/preview-middleware-client/src/adp/quick-actions/common/op-add-header-field.ts @@ -3,11 +3,12 @@ import FlexCommand from 'sap/ui/rta/command/FlexCommand'; import ObjectPageLayout from 'sap/uxap/ObjectPageLayout'; import FlexBox from 'sap/m/FlexBox'; -import { DialogNames, handler } from '../../../adp/init-dialogs'; +import { DialogFactory, DialogNames } from '../../dialog-factory'; import { getRelevantControlFromActivePage } from '../../../cpe/quick-actions/utils'; import { QuickActionContext, SimpleQuickActionDefinition } from '../../../cpe/quick-actions/quick-action-definition'; import { isA } from '../../../utils/core'; import { SimpleQuickActionDefinitionBase } from '../simple-quick-action-base'; +import { DIALOG_ENABLEMENT_VALIDATOR } from '../dialog-enablement-validator'; export const OP_ADD_HEADER_FIELD_TYPE = 'op-add-header-field'; const CONTROL_TYPES = ['sap.uxap.ObjectPageLayout']; @@ -17,7 +18,9 @@ const CONTROL_TYPES = ['sap.uxap.ObjectPageLayout']; */ export class AddHeaderFieldQuickAction extends SimpleQuickActionDefinitionBase implements SimpleQuickActionDefinition { constructor(context: QuickActionContext) { - super(OP_ADD_HEADER_FIELD_TYPE, CONTROL_TYPES, 'QUICK_ACTION_OP_ADD_HEADER_FIELD', context); + super(OP_ADD_HEADER_FIELD_TYPE, CONTROL_TYPES, 'QUICK_ACTION_OP_ADD_HEADER_FIELD', context, [ + DIALOG_ENABLEMENT_VALIDATOR + ]); } async execute(): Promise { @@ -32,13 +35,13 @@ export class AddHeaderFieldQuickAction extends SimpleQuickActionDefinitionBase i // check if only flex box exist in the headerContent. if (headerContent.length === 1 && isA('sap.m.FlexBox', headerContent[0])) { const overlay = OverlayRegistry.getOverlay(headerContent[0]) || []; - await handler(overlay, this.context.rta, DialogNames.ADD_FRAGMENT, undefined, { + await DialogFactory.createDialog(overlay, this.context.rta, DialogNames.ADD_FRAGMENT, undefined, { aggregation: 'items', title: 'QUICK_ACTION_OP_ADD_HEADER_FIELD' }); } else if (this.control) { const overlay = OverlayRegistry.getOverlay(this.control) || []; - await handler(overlay, this.context.rta, DialogNames.ADD_FRAGMENT, undefined, { + await DialogFactory.createDialog(overlay, this.context.rta, DialogNames.ADD_FRAGMENT, undefined, { aggregation: 'headerContent', title: 'QUICK_ACTION_OP_ADD_HEADER_FIELD' }); diff --git a/packages/preview-middleware-client/src/adp/quick-actions/dialog-enablement-validator.ts b/packages/preview-middleware-client/src/adp/quick-actions/dialog-enablement-validator.ts new file mode 100644 index 0000000000..4935e3a2c5 --- /dev/null +++ b/packages/preview-middleware-client/src/adp/quick-actions/dialog-enablement-validator.ts @@ -0,0 +1,18 @@ +import { getTextBundle } from '../../i18n'; + +import { DialogFactory } from '../dialog-factory'; + +import type { EnablementValidator, EnablementValidatorResult } from './enablement-validator'; + +export const DIALOG_ENABLEMENT_VALIDATOR: EnablementValidator = { + run: async (): Promise => { + const i18n = await getTextBundle(); + if (!DialogFactory.canOpenDialog) { + return { + type: 'error', + message: i18n.getText('ADP_QUICK_ACTION_DIALOG_OPEN_MESSAGE') + }; + } + return undefined; + } +}; diff --git a/packages/preview-middleware-client/src/adp/quick-actions/enablement-validator.ts b/packages/preview-middleware-client/src/adp/quick-actions/enablement-validator.ts new file mode 100644 index 0000000000..a636f689bb --- /dev/null +++ b/packages/preview-middleware-client/src/adp/quick-actions/enablement-validator.ts @@ -0,0 +1,18 @@ +export interface EnablementValidatorSuccess { + type: 'success'; +} + +export interface EnablementValidatorError { + type: 'error'; + message: string; +} + +export type EnablementValidatorResult = undefined | EnablementValidatorError; + +export interface EnablementValidator { + /** + * Checks if action can be executed. + * @returns Validation result. + */ + run: () => EnablementValidatorResult | Promise; +} diff --git a/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/change-table-columns.ts b/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/change-table-columns.ts index 2175822517..7c97369461 100644 --- a/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/change-table-columns.ts +++ b/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/change-table-columns.ts @@ -6,6 +6,7 @@ import ManagedObject from 'sap/ui/base/ManagedObject'; import { QuickActionContext, NestedQuickActionDefinition } from '../../../cpe/quick-actions/quick-action-definition'; import { getControlById, isA } from '../../../utils/core'; import { TableQuickActionDefinitionBase } from '../table-quick-action-base'; +import { DIALOG_ENABLEMENT_VALIDATOR } from '../dialog-enablement-validator'; export const CHANGE_TABLE_COLUMNS = 'change-table-columns'; const SMART_TABLE_TYPE = 'sap.ui.comp.smarttable.SmartTable'; @@ -18,9 +19,16 @@ export class ChangeTableColumnsQuickAction implements NestedQuickActionDefinition { constructor(context: QuickActionContext) { - super(CHANGE_TABLE_COLUMNS, CONTROL_TYPES, 'V2_QUICK_ACTION_CHANGE_TABLE_COLUMNS', context, { - includeServiceAction: true - }); + super( + CHANGE_TABLE_COLUMNS, + CONTROL_TYPES, + 'V2_QUICK_ACTION_CHANGE_TABLE_COLUMNS', + context, + { + includeServiceAction: true + }, + [DIALOG_ENABLEMENT_VALIDATOR] + ); } async execute(path: string): Promise { const { table, iconTabBarFilterKey, changeColumnActionId, sectionInfo } = this.tableMap[path]; diff --git a/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/create-table-action.ts b/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/create-table-action.ts index 30addfe003..9214da0032 100644 --- a/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/create-table-action.ts +++ b/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/create-table-action.ts @@ -7,8 +7,9 @@ import UI5Element from 'sap/ui/core/Element'; import { QuickActionContext, NestedQuickActionDefinition } from '../../../cpe/quick-actions/quick-action-definition'; import { getControlById, isA } from '../../../utils/core'; -import { DialogNames, handler } from '../../init-dialogs'; +import { DialogFactory, DialogNames } from '../../dialog-factory'; import { TableQuickActionDefinitionBase } from '../table-quick-action-base'; +import { DIALOG_ENABLEMENT_VALIDATOR } from '../dialog-enablement-validator'; export const CREATE_TABLE_ACTION = 'create-table-action'; const SMART_TABLE_TYPE = 'sap.ui.comp.smarttable.SmartTable'; @@ -19,7 +20,9 @@ const CONTROL_TYPES = [SMART_TABLE_TYPE, M_TABLE_TYPE, 'sap.ui.table.TreeTable', export class AddTableActionQuickAction extends TableQuickActionDefinitionBase implements NestedQuickActionDefinition { constructor(context: QuickActionContext) { - super(CREATE_TABLE_ACTION, CONTROL_TYPES, 'QUICK_ACTION_ADD_CUSTOM_TABLE_ACTION', context); + super(CREATE_TABLE_ACTION, CONTROL_TYPES, 'QUICK_ACTION_ADD_CUSTOM_TABLE_ACTION', context, undefined, [ + DIALOG_ENABLEMENT_VALIDATOR + ]); } async execute(path: string): Promise { @@ -52,7 +55,7 @@ export class AddTableActionQuickAction extends TableQuickActionDefinitionBase im // open dialogBox to add, and content is selected ByDefault if (headerToolbar) { const overlay = OverlayRegistry.getOverlay(headerToolbar as UI5Element) || []; - await handler(overlay, this.context.rta, DialogNames.ADD_FRAGMENT, undefined, { + await DialogFactory.createDialog(overlay, this.context.rta, DialogNames.ADD_FRAGMENT, undefined, { aggregation: 'content', title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_ACTION' }); diff --git a/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/create-table-custom-column.ts b/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/create-table-custom-column.ts index e1243a810b..1a66a086fa 100644 --- a/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/create-table-custom-column.ts +++ b/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/create-table-custom-column.ts @@ -1,20 +1,25 @@ -import FlexCommand from 'sap/ui/rta/command/FlexCommand'; +import ManagedObject from 'sap/ui/base/ManagedObject'; +import UI5Element from 'sap/ui/core/Element'; + +import type FlexCommand from 'sap/ui/rta/command/FlexCommand'; +import OverlayRegistry from 'sap/ui/dt/OverlayRegistry'; + +import ObjectPageSection from 'sap/uxap/ObjectPageSection'; +import ObjectPageSubSection from 'sap/uxap/ObjectPageSubSection'; +import ObjectPageLayout from 'sap/uxap/ObjectPageLayout'; + +import IconTabBar from 'sap/m/IconTabBar'; + import type SmartTable from 'sap/ui/comp/smarttable/SmartTable'; import { QuickActionContext, NestedQuickActionDefinition } from '../../../cpe/quick-actions/quick-action-definition'; import { getControlById, isA } from '../../../utils/core'; -import OverlayRegistry from 'sap/ui/dt/OverlayRegistry'; -import { DialogNames, handler } from '../../init-dialogs'; +import { DialogNames, DialogFactory } from '../../dialog-factory'; import { ANALYTICAL_TABLE_TYPE, GRID_TABLE_TYPE, M_TABLE_TYPE, SMART_TABLE_TYPE, TREE_TABLE_TYPE } from '../control-types'; import { TableQuickActionDefinitionBase } from '../table-quick-action-base'; -import ManagedObject from 'sap/ui/base/ManagedObject'; -import UI5Element from 'sap/ui/core/Element'; import { notifyUser } from '../../utils'; import { getTextBundle } from '../../../i18n'; -import ObjectPageSection from 'sap/uxap/ObjectPageSection'; -import ObjectPageSubSection from 'sap/uxap/ObjectPageSubSection'; -import ObjectPageLayout from 'sap/uxap/ObjectPageLayout'; -import IconTabBar from 'sap/m/IconTabBar'; +import { DIALOG_ENABLEMENT_VALIDATOR } from '../dialog-enablement-validator'; export const CREATE_TABLE_CUSTOM_COLUMN = 'create-table-custom-column'; @@ -58,9 +63,16 @@ export class AddTableCustomColumnQuickAction implements NestedQuickActionDefinition { constructor(context: QuickActionContext) { - super(CREATE_TABLE_CUSTOM_COLUMN, CONTROL_TYPES, 'QUICK_ACTION_ADD_CUSTOM_TABLE_COLUMN', context, { - areTableRowsRequired: true - }); + super( + CREATE_TABLE_CUSTOM_COLUMN, + CONTROL_TYPES, + 'QUICK_ACTION_ADD_CUSTOM_TABLE_COLUMN', + context, + { + areTableRowsRequired: true + }, + [DIALOG_ENABLEMENT_VALIDATOR] + ); } async execute(path: string): Promise { @@ -103,7 +115,7 @@ export class AddTableCustomColumnQuickAction ) ? DialogNames.ADD_FRAGMENT : DialogNames.ADD_TABLE_COLUMN_FRAGMENTS; - await handler(overlay, this.context.rta, dialog, undefined, { + await DialogFactory.createDialog(overlay, this.context.rta, dialog, undefined, { aggregation: 'columns', title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_COLUMN' }); diff --git a/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/lr-enable-semantic-date-range-filter-bar.ts b/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/lr-enable-semantic-date-range-filter-bar.ts index ee39773f2b..55d145da43 100644 --- a/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/lr-enable-semantic-date-range-filter-bar.ts +++ b/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/lr-enable-semantic-date-range-filter-bar.ts @@ -33,8 +33,15 @@ export class ToggleSemanticDateRangeFilterBar const isActionApplicable = pageHasControlId(this.context.view, control.controlId); const modifiedControl = getControlById(control.controlId); if (isActionApplicable && modifiedControl) { - this.isUseDateRangeTypeEnabled = modifiedControl.getProperty('useDateRangeType'); this.control = modifiedControl; + + const id = (this.control.getProperty('persistencyKey') as unknown) ?? this.control.getId(); + if (typeof id !== 'string') { + throw new Error('Could not retrieve configuration property because control id is not valid!'); + } + const value = this.context.changeService.getConfigurationPropertyValue(id, 'useDateRange'); + this.isUseDateRangeTypeEnabled = + value === undefined ? (this.control.data('useDateRangeType') as boolean) : (value as boolean); } } } @@ -58,8 +65,6 @@ export class ToggleSemanticDateRangeFilterBar } ); - this.isUseDateRangeTypeEnabled = !this.isUseDateRangeTypeEnabled; - return command; } } diff --git a/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/lr-enable-table-filtering.ts b/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/lr-enable-table-filtering.ts index 871ad0769c..5a8d6a79ce 100644 --- a/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/lr-enable-table-filtering.ts +++ b/packages/preview-middleware-client/src/adp/quick-actions/fe-v2/lr-enable-table-filtering.ts @@ -1,4 +1,7 @@ -import FlexCommand from 'sap/ui/rta/command/FlexCommand'; +import SmartTable from 'sap/ui/comp/smarttable/SmartTable'; + +import type FlexCommand from 'sap/ui/rta/command/FlexCommand'; + import { QuickActionContext, SimpleQuickActionDefinition } from '../../../cpe/quick-actions/quick-action-definition'; import { getRelevantControlFromActivePage, pageHasControlId } from '../../../cpe/quick-actions/utils'; import { GRID_TABLE_TYPE, M_TABLE_TYPE, SMART_TABLE_TYPE, TREE_TABLE_TYPE } from '../control-types'; @@ -6,7 +9,7 @@ import { areManifestChangesSupported, prepareManifestChange } from './utils'; import { SimpleQuickActionDefinitionBase } from '../simple-quick-action-base'; import { isA } from '../../../utils/core'; -import SmartTable from 'sap/ui/comp/smarttable/SmartTable'; + const COMPONENT = 'sap.suite.ui.generic.template.ListReport'; export const ENABLE_TABLE_FILTERING = 'enable-table-filtering'; @@ -21,15 +24,39 @@ export class EnableTableFilteringQuickAction implements SimpleQuickActionDefinition { constructor(context: QuickActionContext) { - super(ENABLE_TABLE_FILTERING, CONTROL_TYPES, 'QUICK_ACTION_ENABLE_TABLE_FILTERING', context); + super(ENABLE_TABLE_FILTERING, CONTROL_TYPES, 'QUICK_ACTION_ENABLE_TABLE_FILTERING', context, [ + { + run: () => { + if (this.control) { + const id = (this.control.getProperty('persistencyKey') as unknown) ?? this.control.getId(); + if (typeof id !== 'string') { + throw new Error( + 'Could not retrieve configuration property because control id is not valid!' + ); + } + const value = this.context.changeService.getConfigurationPropertyValue( + id, + 'enableTableFilterInPageVariant' + ); + const isFilterEnabled: boolean = + value === undefined + ? (this.control.data('p13nDialogSettings')?.filter?.visible as boolean) + : (value as boolean); + if (isFilterEnabled) { + return { + type: 'error', + message: this.context.resourceBundle.getText( + 'TABLE_FILTERING_CHANGE_HAS_ALREADY_BEEN_MADE' + ) + }; + } + } + return undefined; + } + } + ]); } - isActive: boolean; readonly forceRefreshAfterExecution = true; - lsTableMap: Record = {}; - public get tooltip(): string | undefined { - return this.isDisabled ? this.context.resourceBundle.getText('TABLE_FILTERING_CHANGE_HAS_ALREADY_BEEN_MADE') : undefined; - } - async initialize(): Promise { const manifestChangesSupported = await areManifestChangesSupported(this.context.manifest); if (!manifestChangesSupported) { @@ -41,17 +68,13 @@ export class EnableTableFilteringQuickAction CONTROL_TYPES )) { if (table) { - const isFilterEnabled = table.data('p13nDialogSettings').filter.visible; const isActionApplicable = pageHasControlId(this.context.view, table.getId()); if (isActionApplicable) { - this.isDisabled = isFilterEnabled; this.control = table; break; } } } - - return Promise.resolve(); } async execute(): Promise { @@ -71,8 +94,6 @@ export class EnableTableFilteringQuickAction } ); - this.isDisabled = !this.isDisabled; - return command; } } diff --git a/packages/preview-middleware-client/src/adp/quick-actions/fe-v4/change-table-columns.ts b/packages/preview-middleware-client/src/adp/quick-actions/fe-v4/change-table-columns.ts index 4183eb060b..1fd9826019 100644 --- a/packages/preview-middleware-client/src/adp/quick-actions/fe-v4/change-table-columns.ts +++ b/packages/preview-middleware-client/src/adp/quick-actions/fe-v4/change-table-columns.ts @@ -6,6 +6,7 @@ import { getRelevantControlFromActivePage } from '../../../cpe/quick-actions/uti import { getControlById } from '../../../utils/core'; import { TableQuickActionDefinitionBase } from './table-quick-action-base'; import { MDC_TABLE_TYPE } from '../control-types'; +import { DIALOG_ENABLEMENT_VALIDATOR } from '../dialog-enablement-validator'; export const CHANGE_TABLE_COLUMNS = 'change-table-columns'; const ACTION_ID = 'CTX_SETTINGS0'; @@ -19,7 +20,9 @@ export class ChangeTableColumnsQuickAction implements NestedQuickActionDefinition { constructor(context: QuickActionContext) { - super(CHANGE_TABLE_COLUMNS, [MDC_TABLE_TYPE], 'V4_QUICK_ACTION_CHANGE_TABLE_COLUMNS', context); + super(CHANGE_TABLE_COLUMNS, [MDC_TABLE_TYPE], 'V4_QUICK_ACTION_CHANGE_TABLE_COLUMNS', context, undefined, [ + DIALOG_ENABLEMENT_VALIDATOR + ]); } async execute(path: string): Promise { diff --git a/packages/preview-middleware-client/src/adp/quick-actions/fe-v4/create-table-action.ts b/packages/preview-middleware-client/src/adp/quick-actions/fe-v4/create-table-action.ts index 4e53cf03d2..2efa385da3 100644 --- a/packages/preview-middleware-client/src/adp/quick-actions/fe-v4/create-table-action.ts +++ b/packages/preview-middleware-client/src/adp/quick-actions/fe-v4/create-table-action.ts @@ -1,10 +1,11 @@ import OverlayUtil from 'sap/ui/dt/OverlayUtil'; -import FlexCommand from 'sap/ui/rta/command/FlexCommand'; +import type FlexCommand from 'sap/ui/rta/command/FlexCommand'; import { QuickActionContext, NestedQuickActionDefinition } from '../../../cpe/quick-actions/quick-action-definition'; import { getRelevantControlFromActivePage } from '../../../cpe/quick-actions/utils'; import { getControlById } from '../../../utils/core'; -import { DialogNames, handler } from '../../init-dialogs'; +import { DialogFactory, DialogNames } from '../../dialog-factory'; +import { DIALOG_ENABLEMENT_VALIDATOR } from '../dialog-enablement-validator'; import { TableQuickActionDefinitionBase } from './table-quick-action-base'; import { MDC_TABLE_TYPE } from '../control-types'; @@ -16,7 +17,9 @@ const TOOLBAR_ACTION = 'sap.ui.mdc.ActionToolbar'; */ export class AddTableActionQuickAction extends TableQuickActionDefinitionBase implements NestedQuickActionDefinition { constructor(context: QuickActionContext) { - super(CREATE_TABLE_ACTION, [MDC_TABLE_TYPE], 'QUICK_ACTION_ADD_CUSTOM_TABLE_ACTION', context, true); + super(CREATE_TABLE_ACTION, [MDC_TABLE_TYPE], 'QUICK_ACTION_ADD_CUSTOM_TABLE_ACTION', context, true, [ + DIALOG_ENABLEMENT_VALIDATOR + ]); } async execute(path: string): Promise { @@ -32,10 +35,16 @@ export class AddTableActionQuickAction extends TableQuickActionDefinitionBase im const controlOverlay = OverlayUtil.getClosestOverlayFor(section); if (controlOverlay) { controlOverlay.setSelected(true); - await handler(controlOverlay, this.context.rta, DialogNames.ADD_FRAGMENT, undefined, { - aggregation: 'actions', - title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_ACTION' - }); + await DialogFactory.createDialog( + controlOverlay, + this.context.rta, + DialogNames.ADD_FRAGMENT, + undefined, + { + aggregation: 'actions', + title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_ACTION' + } + ); } } } diff --git a/packages/preview-middleware-client/src/adp/quick-actions/fe-v4/create-table-custom-column.ts b/packages/preview-middleware-client/src/adp/quick-actions/fe-v4/create-table-custom-column.ts index 514ac24dce..d6e950c247 100644 --- a/packages/preview-middleware-client/src/adp/quick-actions/fe-v4/create-table-custom-column.ts +++ b/packages/preview-middleware-client/src/adp/quick-actions/fe-v4/create-table-custom-column.ts @@ -1,12 +1,13 @@ -import FlexCommand from 'sap/ui/rta/command/FlexCommand'; +import type FlexCommand from 'sap/ui/rta/command/FlexCommand'; +import OverlayRegistry from 'sap/ui/dt/OverlayRegistry'; import { QuickActionContext, NestedQuickActionDefinition } from '../../../cpe/quick-actions/quick-action-definition'; -import OverlayRegistry from 'sap/ui/dt/OverlayRegistry'; -import { DialogNames, handler } from '../../init-dialogs'; +import { DialogFactory, DialogNames } from '../../dialog-factory'; import { SMART_TABLE_TYPE, GRID_TABLE_TYPE, MDC_TABLE_TYPE, TREE_TABLE_TYPE } from '../control-types'; import { TableQuickActionDefinitionBase } from '../table-quick-action-base'; import { preprocessActionExecution } from '../fe-v2/create-table-custom-column'; +import { DIALOG_ENABLEMENT_VALIDATOR } from '../dialog-enablement-validator'; export const CREATE_TABLE_CUSTOM_COLUMN = 'create-table-custom-column'; @@ -17,7 +18,9 @@ export class AddTableCustomColumnQuickAction implements NestedQuickActionDefinition { constructor(context: QuickActionContext) { - super(CREATE_TABLE_CUSTOM_COLUMN, CONTROL_TYPES, 'QUICK_ACTION_ADD_CUSTOM_TABLE_COLUMN', context); + super(CREATE_TABLE_CUSTOM_COLUMN, CONTROL_TYPES, 'QUICK_ACTION_ADD_CUSTOM_TABLE_COLUMN', context, undefined, [ + DIALOG_ENABLEMENT_VALIDATOR + ]); } async execute(path: string): Promise { @@ -30,7 +33,7 @@ export class AddTableCustomColumnQuickAction this.selectOverlay(table); const overlay = OverlayRegistry.getOverlay(table); - await handler(overlay, this.context.rta, DialogNames.ADD_FRAGMENT, undefined, { + await DialogFactory.createDialog(overlay, this.context.rta, DialogNames.ADD_FRAGMENT, undefined, { aggregation: 'columns', title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_COLUMN' }); diff --git a/packages/preview-middleware-client/src/adp/quick-actions/fe-v4/table-quick-action-base.ts b/packages/preview-middleware-client/src/adp/quick-actions/fe-v4/table-quick-action-base.ts index 95848da3b2..c0ef815771 100644 --- a/packages/preview-middleware-client/src/adp/quick-actions/fe-v4/table-quick-action-base.ts +++ b/packages/preview-middleware-client/src/adp/quick-actions/fe-v4/table-quick-action-base.ts @@ -6,22 +6,15 @@ import { NESTED_QUICK_ACTION_KIND } from '@sap-ux-private/control-property-edito import { QuickActionContext } from '../../../cpe/quick-actions/quick-action-definition'; import { getRelevantControlFromActivePage } from '../../../cpe/quick-actions/utils'; +import { EnablementValidator } from '../enablement-validator'; +import { QuickActionDefinitionBase } from '../quick-action-base'; const ACTION_ID = 'CTX_SETTINGS0'; -export abstract class TableQuickActionDefinitionBase { - readonly kind = NESTED_QUICK_ACTION_KIND; - public get id(): string { - return `${this.context.key}-${this.type}`; - } - protected get textKey(): string { - return this.defaultTextKey; - } +export abstract class TableQuickActionDefinitionBase extends QuickActionDefinitionBase< + typeof NESTED_QUICK_ACTION_KIND +> { isApplicable = false; - protected isDisabled = false; - public get tooltip(): string | undefined { - return undefined; - } isClearButtonEnabled = false; children: NestedQuickActionChild[] = []; @@ -31,8 +24,11 @@ export abstract class TableQuickActionDefinitionBase { protected readonly controlTypes: string[], protected readonly defaultTextKey: string, protected readonly context: QuickActionContext, - protected readonly isSkipVariantManagementCheck?: boolean - ) {} + protected readonly isSkipVariantManagementCheck?: boolean, + protected readonly enablementValidators: EnablementValidator[] = [] + ) { + super(type, NESTED_QUICK_ACTION_KIND, defaultTextKey, context, enablementValidators); + } async initialize(): Promise { let index = 0; diff --git a/packages/preview-middleware-client/src/adp/quick-actions/quick-action-base.ts b/packages/preview-middleware-client/src/adp/quick-actions/quick-action-base.ts new file mode 100644 index 0000000000..3545b5297c --- /dev/null +++ b/packages/preview-middleware-client/src/adp/quick-actions/quick-action-base.ts @@ -0,0 +1,55 @@ +import { QuickActionContext } from '../../cpe/quick-actions/quick-action-definition'; + +import { EnablementValidator, EnablementValidatorError, EnablementValidatorResult } from './enablement-validator'; + +/** + * Base class for all quick actions. + */ +export abstract class QuickActionDefinitionBase { + public get id(): string { + return `${this.context.key}-${this.type}`; + } + + /** + * Quick Actions tooltip. + */ + public get tooltip(): string | undefined { + if (this.validationResult) { + const validationErrors = this.validationResult.filter( + (result): result is EnablementValidatorError => result?.type === 'error' + ); + if (validationErrors.length > 0) { + const error = validationErrors[0]; + return error.message; + } + } + return undefined; + } + + protected validationResult: EnablementValidatorResult[] | undefined; + protected get isDisabled(): boolean { + if (this.validationResult === undefined) { + return false; + } + const validationErrors = this.validationResult.filter((result) => result?.type === 'error'); + return validationErrors.length > 0; + } + + protected get textKey(): string { + return this.defaultTextKey; + } + + constructor( + public readonly type: string, + public readonly kind: T, + protected readonly defaultTextKey: string, + protected readonly context: QuickActionContext, + protected readonly enablementValidators: EnablementValidator[] = [] + ) {} + + async runEnablementValidators(): Promise { + this.validationResult = await Promise.all( + this.enablementValidators.map(async (validator) => await validator.run()) + ); + } +} diff --git a/packages/preview-middleware-client/src/adp/quick-actions/simple-quick-action-base.ts b/packages/preview-middleware-client/src/adp/quick-actions/simple-quick-action-base.ts index 7841c6fcd2..a80488972c 100644 --- a/packages/preview-middleware-client/src/adp/quick-actions/simple-quick-action-base.ts +++ b/packages/preview-middleware-client/src/adp/quick-actions/simple-quick-action-base.ts @@ -4,38 +4,30 @@ import { SIMPLE_QUICK_ACTION_KIND, SimpleQuickAction } from '@sap-ux-private/con import { getRelevantControlFromActivePage } from '../../cpe/quick-actions/utils'; import { QuickActionContext } from '../../cpe/quick-actions/quick-action-definition'; +import { EnablementValidator } from './enablement-validator'; +import { QuickActionDefinitionBase } from './quick-action-base'; /** * Base class for all simple quick actions. */ -export abstract class SimpleQuickActionDefinitionBase { - readonly kind = SIMPLE_QUICK_ACTION_KIND; - public get id(): string { - return `${this.context.key}-${this.type}`; - } - +export abstract class SimpleQuickActionDefinitionBase extends QuickActionDefinitionBase< + typeof SIMPLE_QUICK_ACTION_KIND +> { public get isApplicable(): boolean { return this.control !== undefined; } - protected isDisabled: boolean | undefined; - - public get tooltip(): string | undefined { - return undefined; - } - - protected get textKey(): string { - return this.defaultTextKey; - } - protected control: UI5Element | undefined; constructor( public readonly type: string, protected readonly controlTypes: string[], protected readonly defaultTextKey: string, - protected readonly context: QuickActionContext - ) {} + protected readonly context: QuickActionContext, + protected readonly enablementValidators: EnablementValidator[] = [] + ) { + super(type, SIMPLE_QUICK_ACTION_KIND, defaultTextKey, context, enablementValidators); + } initialize(): void { for (const control of getRelevantControlFromActivePage( diff --git a/packages/preview-middleware-client/src/adp/quick-actions/table-quick-action-base.ts b/packages/preview-middleware-client/src/adp/quick-actions/table-quick-action-base.ts index b38467f5ac..eed733b949 100644 --- a/packages/preview-middleware-client/src/adp/quick-actions/table-quick-action-base.ts +++ b/packages/preview-middleware-client/src/adp/quick-actions/table-quick-action-base.ts @@ -15,6 +15,8 @@ import ObjectPageSection from 'sap/uxap/ObjectPageSection'; import ObjectPageSubSection from 'sap/uxap/ObjectPageSubSection'; import ObjectPageLayout from 'sap/uxap/ObjectPageLayout'; import ManagedObject from 'sap/ui/base/ManagedObject'; +import { EnablementValidator } from './enablement-validator'; +import { QuickActionDefinitionBase } from './quick-action-base'; import { ANALYTICAL_TABLE_TYPE, GRID_TABLE_TYPE, @@ -50,20 +52,11 @@ export type TableQuickActionsOptions = { /** * Base class for table quick actions. */ -export abstract class TableQuickActionDefinitionBase { - readonly kind = NESTED_QUICK_ACTION_KIND; - public get id(): string { - return `${this.context.key}-${this.type}`; - } - +export abstract class TableQuickActionDefinitionBase extends QuickActionDefinitionBase< + typeof NESTED_QUICK_ACTION_KIND +> { public isApplicable = false; - protected isDisabled: boolean | undefined; - - public get tooltip(): string | undefined { - return undefined; - } - public children: NestedQuickActionChild[] = []; public tableMap: Record< string, @@ -92,8 +85,11 @@ export abstract class TableQuickActionDefinitionBase { protected readonly controlTypes: string[], protected readonly defaultTextKey: string, protected readonly context: QuickActionContext, - protected options: TableQuickActionsOptions = {} - ) {} + protected options: TableQuickActionsOptions = {}, + protected readonly enablementValidators: EnablementValidator[] = [] + ) { + super(type, NESTED_QUICK_ACTION_KIND, defaultTextKey, context, enablementValidators); + } /** * Initializes action object instance diff --git a/packages/preview-middleware-client/src/cpe/changes/service.ts b/packages/preview-middleware-client/src/cpe/changes/service.ts index 7dbd609a2d..31576306e7 100644 --- a/packages/preview-middleware-client/src/cpe/changes/service.ts +++ b/packages/preview-middleware-client/src/cpe/changes/service.ts @@ -558,6 +558,50 @@ export class ChangeService extends EventTarget { return result; } + private prepareV2ConfigurationChange( + command: FlexCommand, + fileName: string, + index: number, + inactiveCommandCount: number + ): PendingConfigurationChange { + const { entityPropertyChange, page } = command.getProperty('parameters') as { + entityPropertyChange: { + propertyPath: string; + propertyValue: Record; + }; + page: string; + }; + const propertyName = Object.keys(entityPropertyChange.propertyValue)[0]; + const propertyValue = entityPropertyChange.propertyValue[propertyName]; + const controlId = this.getCommandSelectorId(command) ?? ''; + const propertyPathSegments = entityPropertyChange.propertyPath.split('/'); + + const key = getConfigMapControlIdMap(page, propertyPathSegments); + + const isActive = index >= inactiveCommandCount; + const controlIds = this.configPropertyControlIdMap?.get(key) || [controlId]; + + const result: PendingConfigurationChange = { + type: PENDING_CHANGE_TYPE, + kind: CONFIGURATION_CHANGE_KIND, + controlIds, + propertyPath: getCompactV4ConfigPath(propertyPathSegments) || page, + propertyName, + isActive, + value: propertyValue, + fileName + }; + for (const id of result.controlIds) { + if (!this.pendingConfigChangeMap.get(id)) { + this.pendingConfigChangeMap.set(id, []); + } + const pendingChanges = this.pendingConfigChangeMap.get(id); + pendingChanges?.push(result); + } + + return result; + } + /** * Prepares the type of change based on the command and other parameters. * @@ -613,6 +657,8 @@ export class ChangeService extends EventTarget { command.getProperty('parameters') as { entityPropertyChange: { propertyValue: ConfigurationValue } } ).entityPropertyChange.propertyValue; return this.prepareV4ConfigurationChange(command, value, fileName, index, inactiveCommandCount); + } else if (changeType === 'appdescr_ui_generic_app_changePageConfiguration') { + return this.prepareV2ConfigurationChange(command, fileName, index, inactiveCommandCount); } else { let result: PendingChange = { type: PENDING_CHANGE_TYPE, diff --git a/packages/preview-middleware-client/src/cpe/quick-actions/quick-action-definition.ts b/packages/preview-middleware-client/src/cpe/quick-actions/quick-action-definition.ts index 5fb69f22c7..29a2349a10 100644 --- a/packages/preview-middleware-client/src/cpe/quick-actions/quick-action-definition.ts +++ b/packages/preview-middleware-client/src/cpe/quick-actions/quick-action-definition.ts @@ -62,9 +62,13 @@ interface QuickActionDefinitionBase { */ isApplicable: boolean; /** - * Initializes the action and checks if it should be enabled in current context. + * Initializes the action and checks if it should be displayed in current context. */ initialize: () => void | Promise; + /** + * Runs enablement validators to check if the action should be enabled. + */ + runEnablementValidators: () => void | Promise; } export interface SimpleQuickActionDefinition extends QuickActionDefinitionBase { diff --git a/packages/preview-middleware-client/src/cpe/quick-actions/quick-action-service.ts b/packages/preview-middleware-client/src/cpe/quick-actions/quick-action-service.ts index b48812ba57..8855f72f97 100644 --- a/packages/preview-middleware-client/src/cpe/quick-actions/quick-action-service.ts +++ b/packages/preview-middleware-client/src/cpe/quick-actions/quick-action-service.ts @@ -20,6 +20,7 @@ import { QuickActionDefinitionRegistry } from './registry'; import { OutlineService } from '../outline/service'; import { getTextBundle, TextBundle } from '../../i18n'; import { ChangeService } from '../changes'; +import { DialogFactory } from '../../adp/dialog-factory'; /** * Service providing Quick Actions. @@ -33,7 +34,7 @@ export class QuickActionService implements Service { private texts: TextBundle; /** - * Qucik action service constructor.zrf + * Quick action service constructor. * * @param rta - RTA object. * @param outlineService - Outline service instance. @@ -85,6 +86,10 @@ export class QuickActionService implements Service { this.changeService.onStackChange(async () => { await this.reloadQuickActions(this.controlTreeIndex); }); + + DialogFactory.onOpenDialogStatusChange(async () => { + await this.reloadQuickActions(this.controlTreeIndex); + }); } /** @@ -119,7 +124,7 @@ export class QuickActionService implements Service { try { const instance = new Definition(actionContext); await instance.initialize(); - this.addAction(group, instance); + await this.addAction(group, instance); } catch { Log.warning(`Failed to initialize ${Definition.name} quick action.`); } @@ -131,8 +136,9 @@ export class QuickActionService implements Service { this.sendAction(quickActionListChanged(groups)); } - private addAction(group: QuickActionGroup, instance: QuickActionDefinition): void { + private async addAction(group: QuickActionGroup, instance: QuickActionDefinition): Promise { if (instance.isApplicable) { + await instance.runEnablementValidators(); const quickAction = instance.getActionObject(); group.actions.push(quickAction); this.actions.push(instance); diff --git a/packages/preview-middleware-client/src/messagebundle.properties b/packages/preview-middleware-client/src/messagebundle.properties index e7c747a4b5..fb97dab963 100644 --- a/packages/preview-middleware-client/src/messagebundle.properties +++ b/packages/preview-middleware-client/src/messagebundle.properties @@ -31,6 +31,7 @@ ADP_ADD_FRAGMENT_WITH_TEMPLATE_NOTIFICATION = Note: The "{0}.fragment.xml" fragm ADP_ADD_TWO_FRAGMENTS_WITH_TEMPLATE_NOTIFICATION = Note: The "{0}.fragment.xml" and "{1}.fragment.xml" fragments will be created once you save the changes. ADP_SYNC_VIEWS_MESSAGE = Synchronous views are detected for this application. Controller extensions are not supported for such views and will be disabled. ADP_REUSE_COMPONENTS_MESSAGE = Reuse components are detected for some views in this application. Controller extensions, adding fragments and manifest changes are not supported for such views and will be disabled. +ADP_QUICK_ACTION_DIALOG_OPEN_MESSAGE = This action is disabled because a dialog is already open CPE_CHANGES_VISIBLE_AFTER_SAVE_AND_RELOAD_MESSAGE = Note: The change will be visible after save and reload. diff --git a/packages/preview-middleware-client/test/__mock__/sap/ui/core/Fragment.ts b/packages/preview-middleware-client/test/__mock__/sap/ui/core/Fragment.ts index 5f8d4d1a54..1201611973 100644 --- a/packages/preview-middleware-client/test/__mock__/sap/ui/core/Fragment.ts +++ b/packages/preview-middleware-client/test/__mock__/sap/ui/core/Fragment.ts @@ -1,4 +1,8 @@ // add required functionality for testing here +export const attachBeforeClose = jest.fn(); export default { - load: jest.fn() + load: jest.fn().mockReturnValue({ + attachBeforeClose, + setEscapeHandler: jest.fn() + }) }; diff --git a/packages/preview-middleware-client/test/unit/adp/dialog-factory.test.ts b/packages/preview-middleware-client/test/unit/adp/dialog-factory.test.ts new file mode 100644 index 0000000000..fc5b910fdf --- /dev/null +++ b/packages/preview-middleware-client/test/unit/adp/dialog-factory.test.ts @@ -0,0 +1,139 @@ +import type UI5Element from 'sap/ui/core/Element'; +import { RTAOptions } from 'sap/ui/rta/RuntimeAuthoring'; +import type RuntimeAuthoring from 'sap/ui/rta/RuntimeAuthoring'; + +import Fragment, { attachBeforeClose } from 'mock/sap/ui/core/Fragment'; +import Controller from 'mock/sap/ui/core/mvc/Controller'; +import RuntimeAuthoringMock from 'mock/sap/ui/rta/RuntimeAuthoring'; + +import { DialogFactory, DialogNames } from 'open/ux/preview/client/adp/dialog-factory'; +import AddFragment from '../../../src/adp/controllers/AddFragment.controller'; +import ControllerExtension from '../../../src/adp/controllers/ControllerExtension.controller'; +import ExtensionPoint from '../../../src/adp/controllers/ExtensionPoint.controller'; +import AddTableColumnFragments from 'open/ux/preview/client/adp/controllers/AddTableColumnFragments.controller'; + +describe('DialogFactory', () => { + afterEach(() => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const closeDialogFunction = attachBeforeClose.mock.calls[0]?.[0]; + if (typeof closeDialogFunction === 'function') { + // make sure that dialog factory is in clean state after each test + closeDialogFunction(); + } + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + test('ensure that only one dialog is open at a time', async () => { + const controller = { overlays: {}, rta: { 'yes': 'no' } }; + Controller.create.mockResolvedValue(controller); + const rtaMock = new RuntimeAuthoringMock({} as RTAOptions); + + AddFragment.prototype.setup = jest.fn(); + + await DialogFactory.createDialog( + {} as unknown as UI5Element, + rtaMock as unknown as RuntimeAuthoring, + DialogNames.ADD_FRAGMENT + ); + + await DialogFactory.createDialog( + {} as unknown as UI5Element, + rtaMock as unknown as RuntimeAuthoring, + DialogNames.ADD_FRAGMENT + ); + + expect(Fragment.load).toHaveBeenCalledTimes(1); + expect(DialogFactory.canOpenDialog).toBe(false); + }); + + test('create Add Fragment dialog', async () => { + const controller = { overlays: {}, rta: { 'yes': 'no' } }; + Controller.create.mockResolvedValue(controller); + const rtaMock = new RuntimeAuthoringMock({} as RTAOptions); + + AddFragment.prototype.setup = jest.fn(); + + await DialogFactory.createDialog( + {} as unknown as UI5Element, + rtaMock as unknown as RuntimeAuthoring, + DialogNames.ADD_FRAGMENT + ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(Fragment.load.mock.calls[0][0].name).toStrictEqual('open.ux.preview.client.adp.ui.AddFragment'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(Fragment.load.mock.calls[0][0].id).toStrictEqual(undefined); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(Fragment.load.mock.calls[0][0].controller).toBeInstanceOf(AddFragment); + + expect(DialogFactory.canOpenDialog).toBe(false); + }); + + test('create Add Table Column Fragment dialog', async () => { + const controller = { overlays: {}, rta: { 'yes': 'no' } }; + Controller.create.mockResolvedValue(controller); + const rtaMock = new RuntimeAuthoringMock({} as RTAOptions); + + AddTableColumnFragments.prototype.setup = jest.fn(); + + await DialogFactory.createDialog( + {} as unknown as UI5Element, + rtaMock as unknown as RuntimeAuthoring, + DialogNames.ADD_TABLE_COLUMN_FRAGMENTS + ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(Fragment.load.mock.calls[0][0].name).toStrictEqual( + 'open.ux.preview.client.adp.ui.AddTableColumnFragments' + ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(Fragment.load.mock.calls[0][0].id).toStrictEqual(undefined); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(Fragment.load.mock.calls[0][0].controller).toBeInstanceOf(AddTableColumnFragments); + + expect(DialogFactory.canOpenDialog).toBe(false); + }); + + test('create Add Controller dialog', async () => { + const controller = { overlays: {}, rta: { 'yes': 'no' } }; + Controller.create.mockResolvedValue(controller); + const rtaMock = new RuntimeAuthoringMock({} as RTAOptions); + + ControllerExtension.prototype.setup = jest.fn(); + + await DialogFactory.createDialog( + {} as unknown as UI5Element, + rtaMock as unknown as RuntimeAuthoring, + DialogNames.CONTROLLER_EXTENSION + ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(Fragment.load.mock.calls[0][0].name).toStrictEqual('open.ux.preview.client.adp.ui.ControllerExtension'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(Fragment.load.mock.calls[0][0].id).toStrictEqual(undefined); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(Fragment.load.mock.calls[0][0].controller).toBeInstanceOf(ControllerExtension); + + expect(DialogFactory.canOpenDialog).toBe(false); + }); + + test('create Add Fragment at Extension Point dialog', async () => { + const controller = { overlays: {}, rta: { 'yes': 'no' } }; + Controller.create.mockResolvedValue(controller); + const rtaMock = new RuntimeAuthoringMock({} as RTAOptions); + + ExtensionPoint.prototype.setup = jest.fn(); + + await DialogFactory.createDialog( + {} as unknown as UI5Element, + rtaMock as unknown as RuntimeAuthoring, + DialogNames.ADD_FRAGMENT_AT_EXTENSION_POINT + ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(Fragment.load.mock.calls[0][0].name).toStrictEqual('open.ux.preview.client.adp.ui.ExtensionPoint'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(Fragment.load.mock.calls[0][0].id).toStrictEqual('dialog--ExtensionPoint'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(Fragment.load.mock.calls[0][0].controller).toBeInstanceOf(ExtensionPoint); + + expect(DialogFactory.canOpenDialog).toBe(false); + }); +}); diff --git a/packages/preview-middleware-client/test/unit/adp/init-dialogs.test.ts b/packages/preview-middleware-client/test/unit/adp/init-dialogs.test.ts index 322fd76cf9..e0c97565ab 100644 --- a/packages/preview-middleware-client/test/unit/adp/init-dialogs.test.ts +++ b/packages/preview-middleware-client/test/unit/adp/init-dialogs.test.ts @@ -1,29 +1,18 @@ -import type UI5Element from 'sap/ui/core/Element'; import { RTAOptions } from 'sap/ui/rta/RuntimeAuthoring'; import type ElementOverlay from 'sap/ui/dt/ElementOverlay'; import type RuntimeAuthoring from 'sap/ui/rta/RuntimeAuthoring'; import hasStableId from 'mock/sap/ui/rta/util/hasStableId'; import FlUtils from 'sap/ui/fl/Utils'; -import { sapMock } from 'mock/window'; - -import Fragment from 'mock/sap/ui/core/Fragment'; -import Controller from 'mock/sap/ui/core/mvc/Controller'; import RuntimeAuthoringMock from 'mock/sap/ui/rta/RuntimeAuthoring'; import { - DialogNames, - handler, initDialogs, isControllerExtensionEnabled, isFragmentCommandEnabled, getAddFragmentItemText } from '../../../src/adp/init-dialogs'; -import AddFragment from '../../../src/adp/controllers/AddFragment.controller'; -import ControllerExtension from '../../../src/adp/controllers/ControllerExtension.controller'; -import ExtensionPoint from '../../../src/adp/controllers/ExtensionPoint.controller'; import * as cpeUtils from '../../../src/cpe/utils'; -import AddTableColumnFragments from 'open/ux/preview/client/adp/controllers/AddTableColumnFragments.controller'; describe('Dialogs', () => { describe('initDialogs', () => { @@ -42,40 +31,6 @@ describe('Dialogs', () => { initDialogs(rtaMock as unknown as RuntimeAuthoring, [], { major: 1, minor: 118 }); expect(addMenuItemSpy).toHaveBeenCalledTimes(2); }); - - test('addMenuItem handler function', async () => { - sapMock.ui.version = '1.120.4'; - Controller.create.mockResolvedValue({ overlays: {}, rta: { 'yes': 'no' } }); - const rtaMock = new RuntimeAuthoringMock({} as RTAOptions); - - AddFragment.prototype.setup = jest.fn(); - AddTableColumnFragments.prototype.setup = jest.fn(); - ControllerExtension.prototype.setup = jest.fn(); - ExtensionPoint.prototype.setup = jest.fn(); - - await handler( - {} as unknown as UI5Element, - rtaMock as unknown as RuntimeAuthoring, - DialogNames.ADD_FRAGMENT - ); - await handler( - {} as unknown as UI5Element, - rtaMock as unknown as RuntimeAuthoring, - DialogNames.CONTROLLER_EXTENSION - ); - await handler( - {} as unknown as UI5Element, - rtaMock as unknown as RuntimeAuthoring, - DialogNames.ADD_FRAGMENT_AT_EXTENSION_POINT - ); - await handler( - {} as unknown as UI5Element, - rtaMock as unknown as RuntimeAuthoring, - DialogNames.ADD_TABLE_COLUMN_FRAGMENTS - ); - - expect(Fragment.load).toHaveBeenCalledTimes(4); - }); }); describe('isFragmentCommandEnabled', () => { diff --git a/packages/preview-middleware-client/test/unit/adp/quick-actions/fe-v2.test.ts b/packages/preview-middleware-client/test/unit/adp/quick-actions/fe-v2.test.ts index fab8919d5a..a5fb71b4f4 100644 --- a/packages/preview-middleware-client/test/unit/adp/quick-actions/fe-v2.test.ts +++ b/packages/preview-middleware-client/test/unit/adp/quick-actions/fe-v2.test.ts @@ -9,18 +9,12 @@ import { QuickAction } from '@sap-ux-private/control-property-editor-common'; -jest.mock('../../../../src/adp/init-dialogs', () => { - return { - ...jest.requireActual('../../../../src/adp/init-dialogs'), - handler: jest.fn() - }; -}); - import { QuickActionService } from '../../../../src/cpe/quick-actions/quick-action-service'; import { OutlineService } from '../../../../src/cpe/outline/service'; import { FeatureService } from '../../../../src/cpe/feature-service'; import FEV2QuickActionRegistry from '../../../../src/adp/quick-actions/fe-v2/registry'; +import { attachBeforeClose } from 'mock/sap/ui/core/Fragment'; import { sapCoreMock } from 'mock/window'; import NavContainer from 'mock/sap/m/NavContainer'; import XMLView from 'mock/sap/ui/core/mvc/XMLView'; @@ -40,7 +34,7 @@ import { SMART_TABLE_TYPE, TREE_TABLE_TYPE } from 'open/ux/preview/client/adp/quick-actions/control-types'; -import { DialogNames } from 'open/ux/preview/client/adp/init-dialogs'; +import { DialogFactory, DialogNames } from 'open/ux/preview/client/adp/dialog-factory'; import * as adpUtils from 'open/ux/preview/client/adp/utils'; import type { ChangeService } from '../../../../src/cpe/changes/service'; import VersionInfo from 'mock/sap/ui/VersionInfo'; @@ -54,6 +48,7 @@ describe('FE V2 quick actions', () => { beforeEach(() => { sendActionMock = jest.fn(); subscribeMock = jest.fn(); + jest.spyOn(DialogFactory, 'createDialog').mockResolvedValue(); jest.clearAllMocks(); }); @@ -71,6 +66,12 @@ describe('FE V2 quick actions', () => { }); }); afterEach(() => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const closeDialogFunction = attachBeforeClose.mock.calls[0]?.[0]; + if (typeof closeDialogFunction === 'function') { + // make sure that dialog factory is in clean state after each test + closeDialogFunction(); + } jest.clearAllMocks(); }); describe('clear filter bar button', () => { @@ -290,11 +291,8 @@ describe('FE V2 quick actions', () => { await subscribeMock.mock.calls[0][0]( executeQuickAction({ id: 'listReport0-add-controller-to-page', kind: 'simple' }) ); - const { handler } = jest.requireMock<{ handler: () => Promise }>( - '../../../../src/adp/init-dialogs' - ); - expect(handler).toHaveBeenCalledWith(mockOverlay, rtaMock, 'ControllerExtension'); + expect(DialogFactory.createDialog).toHaveBeenCalledWith(mockOverlay, rtaMock, 'ControllerExtension'); }); }); @@ -575,14 +573,16 @@ describe('FE V2 quick actions', () => { executeQuickAction({ id: 'listReport0-create-table-action', kind: 'nested', path: '0' }) ); - const { handler } = jest.requireMock<{ handler: () => Promise }>( - '../../../../src/adp/init-dialogs' + expect(DialogFactory.createDialog).toHaveBeenCalledWith( + mockOverlay, + rtaMock, + 'AddFragment', + undefined, + { + aggregation: 'content', + title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_ACTION' + } ); - - expect(handler).toHaveBeenCalledWith(mockOverlay, rtaMock, 'AddFragment', undefined, { - aggregation: 'content', - title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_ACTION' - }); }); }); @@ -702,14 +702,17 @@ describe('FE V2 quick actions', () => { await subscribeMock.mock.calls[0][0]( executeQuickAction({ id: 'listReport0-add-page-action', kind: 'simple' }) ); - const { handler } = jest.requireMock<{ handler: () => Promise }>( - '../../../../src/adp/init-dialogs' - ); - expect(handler).toHaveBeenCalledWith(mockOverlay, rtaMock, 'AddFragment', undefined, { - aggregation: 'actions', - title: 'QUICK_ACTION_ADD_CUSTOM_PAGE_ACTION' - }); + expect(DialogFactory.createDialog).toHaveBeenCalledWith( + mockOverlay, + rtaMock, + 'AddFragment', + undefined, + { + aggregation: 'actions', + title: 'QUICK_ACTION_ADD_CUSTOM_PAGE_ACTION' + } + ); }); }); @@ -858,18 +861,20 @@ describe('FE V2 quick actions', () => { executeQuickAction({ id: 'listReport0-create-table-custom-column', kind: 'nested', path: '0' }) ); - const { handler } = jest.requireMock<{ handler: () => Promise }>( - '../../../../src/adp/init-dialogs' + expect(DialogFactory.createDialog).toHaveBeenCalledWith( + mockOverlay, + rtaMock, + testCase.dialog, + undefined, + { + aggregation: 'columns', + title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_COLUMN' + } ); - - expect(handler).toHaveBeenCalledWith(mockOverlay, rtaMock, testCase.dialog, undefined, { - aggregation: 'columns', - title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_COLUMN' - }); }); }); - describe('enable table filtering for different valid UI5 versions', () => { + describe('Enable Semantic Date Range', () => { const testCases: { validVersion: boolean; versionInfo: string; @@ -916,137 +921,346 @@ describe('FE V2 quick actions', () => { isManifestPagesAsArray: true } ]; - test.each(testCases)( - 'initialize and execute action (%s)', - async (testCase) => { - VersionInfo.load.mockResolvedValue({ name: 'sap.ui.core', version: testCase.versionInfo }); - sapCoreMock.byId.mockImplementation((id) => { - if (id == 'SmartFilterBar') { + test.each(testCases)('initialize and execute action (%s)', async (testCase) => { + VersionInfo.load.mockResolvedValue({ name: 'sap.ui.core', version: testCase.versionInfo }); + sapCoreMock.byId.mockImplementation((id) => { + if (id == 'SmartFilterBar') { + return { + isA: (type: string) => type === 'sap.ui.comp.smartfilterbar.SmartFilterBar', + getProperty: jest.fn().mockImplementation((name) => { + if (name === 'persistencyKey') { + return 'filterbar'; + } + return false; + }), + getDomRef: () => ({}), + getEntitySet: jest.fn().mockImplementation(() => 'testEntity'), + data: (key: string) => { + if (key === 'useDateRangeType') { + return false; + } + } + }; + } + if (id == 'NavContainer') { + const container = new NavContainer(); + const component = new UIComponentMock(); + const view = new XMLView(); + const pageView = new XMLView(); + pageView.getDomRef.mockImplementation(() => { return { - isA: (type: string) => type === 'sap.ui.comp.smartfilterbar.SmartFilterBar', - getProperty: jest.fn().mockImplementation(() => false), - getDomRef: () => ({}), - getEntitySet: jest.fn().mockImplementation(() => 'testEntity') + contains: () => true }; + }); + pageView.getViewName.mockImplementation( + () => 'sap.suite.ui.generic.template.ListReport.view.ListReport' + ); + const componentContainer = new ComponentContainer(); + const spy = jest.spyOn(componentContainer, 'getComponent'); + spy.mockImplementation(() => { + return 'component-id'; + }); + jest.spyOn(Component, 'getComponentById').mockImplementation((id: string | undefined) => { + if (id === 'component-id') { + return component; + } + }); + view.getContent.mockImplementation(() => { + return [componentContainer]; + }); + container.getCurrentPage.mockImplementation(() => { + return view; + }); + component.getRootControl.mockImplementation(() => { + return pageView; + }); + return container; + } + }); + CommandFactory.getCommandFor.mockImplementation((control, type, value, _, settings) => { + return { type, value, settings }; + }); + + const rtaMock = new RuntimeAuthoringMock({} as RTAOptions) as unknown as RuntimeAuthoring; + const pages = testCase.isManifestPagesAsArray + ? [{ name: 'test', id: 'test' }] + : { name: 'test', id: 'test' }; + jest.spyOn(rtaMock.getRootControlInstance(), 'getManifest').mockReturnValue({ + 'sap.ui.generic.app': { + pages + } + }); + const registry = new FEV2QuickActionRegistry(); + const service = new QuickActionService( + rtaMock, + new OutlineService(rtaMock, mockChangeService), + [registry], + { onStackChange: jest.fn(), getConfigurationPropertyValue: jest.fn() } as any + ); + await service.init(sendActionMock, subscribeMock); + await service.reloadQuickActions({ + 'sap.ui.comp.smartfilterbar.SmartFilterBar': [ + { + controlId: 'SmartFilterBar' + } as any + ], + 'sap.m.NavContainer': [ + { + controlId: 'NavContainer' + } as any + ] + }); + expect(sendActionMock).toHaveBeenCalledWith( + quickActionListChanged([ + { + title: 'LIST REPORT', + actions: + testCase.validVersion && !testCase.isManifestPagesAsArray + ? [ + { + 'kind': 'simple', + id: 'listReport0-enable-semantic-daterange-filterbar', + title: 'Enable Semantic Date Range in Filter Bar', + enabled: true + } + ] + : [] } - if (id == 'NavContainer') { - const container = new NavContainer(); - const component = new UIComponentMock(); - const view = new XMLView(); - const pageView = new XMLView(); - pageView.getDomRef.mockImplementation(() => { - return { - contains: () => true - }; - }); - pageView.getViewName.mockImplementation( - () => 'sap.suite.ui.generic.template.ListReport.view.ListReport' - ); - const componentContainer = new ComponentContainer(); - const spy = jest.spyOn(componentContainer, 'getComponent'); - spy.mockImplementation(() => { - return 'component-id'; - }); - jest.spyOn(Component, 'getComponentById').mockImplementation((id: string | undefined) => { - if (id === 'component-id') { - return component; + ]) + ); + if (testCase.validVersion && !testCase.isManifestPagesAsArray) { + await subscribeMock.mock.calls[0][0]( + executeQuickAction({ id: 'listReport0-enable-semantic-daterange-filterbar', kind: 'simple' }) + ); + expect(rtaMock.getCommandStack().pushAndExecute).toHaveBeenCalledWith({ + 'settings': {}, + 'type': 'appDescriptor', + 'value': { + 'changeType': 'appdescr_ui_generic_app_changePageConfiguration', + 'parameters': { + 'entityPropertyChange': { + 'operation': 'UPSERT', + 'propertyPath': 'component/settings/filterSettings/dateSettings', + 'propertyValue': { + 'useDateRange': true + } + }, + 'parentPage': { + 'component': 'sap.suite.ui.generic.template.ListReport', + 'entitySet': 'testEntity' } - }); - view.getContent.mockImplementation(() => { - return [componentContainer]; - }); - container.getCurrentPage.mockImplementation(() => { - return view; - }); - component.getRootControl.mockImplementation(() => { - return pageView; - }); - return container; + }, + 'reference': undefined } }); - CommandFactory.getCommandFor.mockImplementation((control, type, value, _, settings) => { - return { type, value, settings }; - }); + } + }); + }); - const rtaMock = new RuntimeAuthoringMock({} as RTAOptions) as unknown as RuntimeAuthoring; - const pages = testCase.isManifestPagesAsArray - ? [{ name: 'test', id: 'test' }] - : { name: 'test', id: 'test' }; - jest.spyOn(rtaMock.getRootControlInstance(), 'getManifest').mockReturnValue({ - 'sap.ui.generic.app': { - pages - } - }); - const registry = new FEV2QuickActionRegistry(); - const service = new QuickActionService( - rtaMock, - new OutlineService(rtaMock, mockChangeService), - [registry], - { onStackChange: jest.fn() } as any - ); - await service.init(sendActionMock, subscribeMock); - await service.reloadQuickActions({ - 'sap.ui.comp.smartfilterbar.SmartFilterBar': [ - { - controlId: 'SmartFilterBar' - } as any - ], - 'sap.m.NavContainer': [ - { - controlId: 'NavContainer' - } as any - ] - }); - expect(sendActionMock).toHaveBeenCalledWith( - quickActionListChanged([ - { - title: 'LIST REPORT', - actions: - testCase.validVersion && !testCase.isManifestPagesAsArray - ? [ - { - 'kind': 'simple', - id: 'listReport0-enable-semantic-daterange-filterbar', - title: 'Enable Semantic Date Range in Filter Bar', - enabled: true - } - ] - : [] - } - ]) - ); - if (testCase.validVersion && !testCase.isManifestPagesAsArray) { - await subscribeMock.mock.calls[0][0]( - executeQuickAction({ - id: 'listReport0-enable-semantic-daterange-filterbar', - kind: 'simple' + describe('Enable Table Filtering', () => { + const testCases: { + visible: boolean; + ui5version: string; + expectedIsEnabled: boolean; + isValidUI5Version: boolean; + expectedTooltip?: string; + }[] = [ + { + visible: false, + ui5version: '1.124.0', + expectedIsEnabled: true, + isValidUI5Version: false + }, + { + visible: false, + ui5version: '1.96.0', + expectedIsEnabled: true, + isValidUI5Version: false + }, + { + visible: false, + ui5version: '1.96.37', + expectedIsEnabled: true, + isValidUI5Version: true + }, + { + visible: false, + ui5version: '1.130.0', + expectedIsEnabled: true, + isValidUI5Version: true + }, + { + visible: true, + ui5version: '1.108.38', + expectedIsEnabled: false, + isValidUI5Version: true, + expectedTooltip: + 'This option is disabled because table filtering for page variants is already enabled' + } + ]; + test.each(testCases)('initialize and execute action (%s)', async (testCase) => { + const pageView = new XMLView(); + VersionInfo.load.mockResolvedValue({ name: 'sap.ui.core', version: testCase.ui5version }); + const scrollIntoView = jest.fn(); + sapCoreMock.byId.mockImplementation((id) => { + if (id == 'mTable') { + return { + isA: (type: string) => type === 'sap.m.Table', + getId: () => id, + getDomRef: () => ({ + scrollIntoView + }), + getProperty: jest.fn().mockImplementation((name) => { + if (name === 'persistencyKey') { + return 'table'; + } + }), + getEntitySet: jest.fn().mockImplementation(() => 'testEntity'), + getAggregation: () => 'headerToolbar', + getParent: () => pageView, + getHeaderToolbar: () => { + return { + getTitleControl: () => { + return { + getText: () => 'MyTable' + }; + } + }; + }, + data: jest.fn().mockImplementation((key) => { + if (key === 'p13nDialogSettings') { + return { + filter: { + visible: testCase.visible + } + }; + } }) + }; + } + if (id == 'NavContainer') { + const container = new NavContainer(); + const component = new UIComponentMock(); + const view = new XMLView(); + pageView.getDomRef.mockImplementation(() => { + return { + contains: () => true + }; + }); + pageView.getViewName.mockImplementation( + () => 'sap.suite.ui.generic.template.ListReport.view.ListReport' ); - expect(rtaMock.getCommandStack().pushAndExecute).toHaveBeenCalledWith({ - 'settings': {}, - 'type': 'appDescriptor', - 'value': { - 'changeType': 'appdescr_ui_generic_app_changePageConfiguration', - 'parameters': { - 'entityPropertyChange': { - 'operation': 'UPSERT', - 'propertyPath': 'component/settings/filterSettings/dateSettings', - 'propertyValue': { - 'useDateRange': true - } - }, - 'parentPage': { - 'component': 'sap.suite.ui.generic.template.ListReport', - 'entitySet': 'testEntity' - } - }, - 'reference': undefined + const componentContainer = new ComponentContainer(); + const spy = jest.spyOn(componentContainer, 'getComponent'); + spy.mockImplementation(() => { + return 'component-id'; + }); + jest.spyOn(Component, 'getComponentById').mockImplementation((id: string | undefined) => { + if (id === 'component-id') { + return component; } }); + view.getContent.mockImplementation(() => { + return [componentContainer]; + }); + container.getCurrentPage.mockImplementation(() => { + return view; + }); + component.getRootControl.mockImplementation(() => { + return pageView; + }); + return container; } - }, - 100000 - ); + }); + + const rtaMock = new RuntimeAuthoringMock({} as RTAOptions) as unknown as RuntimeAuthoring; + + const pages = { name: 'test', id: 'test' }; + jest.spyOn(rtaMock.getRootControlInstance(), 'getManifest').mockReturnValue({ + 'sap.ui.generic.app': { + pages + } + }); + const registry = new FEV2QuickActionRegistry(); + const service = new QuickActionService( + rtaMock, + new OutlineService(rtaMock, mockChangeService), + [registry], + { + onStackChange: jest.fn(), + getConfigurationPropertyValue: jest.fn().mockReturnValue(undefined) + } as any + ); + await service.init(sendActionMock, subscribeMock); + + await service.reloadQuickActions({ + 'sap.m.Table': [ + { + controlId: 'mTable' + } as any + ], + 'sap.m.NavContainer': [ + { + controlId: 'NavContainer' + } as any + ] + }); + + const isActionExpected = testCase.isValidUI5Version; + + expect(sendActionMock).toHaveBeenCalledWith( + quickActionListChanged([ + { + 'actions': [ + { + 'kind': 'nested', + id: 'listReport0-create-table-action', + title: 'Add Custom Table Action', + tooltip: undefined, + enabled: true, + children: [ + { + children: [], + enabled: true, + label: `'MyTable' table` + } + ] + }, + { + 'children': [ + { + 'children': [], + enabled: true, + 'label': `'MyTable' table` + } + ], + 'enabled': true, + 'id': 'listReport0-create-table-custom-column', + 'kind': 'nested', + 'tooltip': undefined, + 'title': 'Add Custom Table Column' + }, + ...(isActionExpected + ? [ + { + 'enabled': testCase.expectedIsEnabled, + 'id': 'listReport0-enable-table-filtering', + 'kind': 'simple', + 'title': 'Enable Table Filtering for Page Variants', + 'tooltip': testCase.expectedTooltip + } as any + ] + : []) + ], + 'title': 'LIST REPORT' + } + ]) + ); + + await subscribeMock.mock.calls[0][0]( + executeQuickAction({ id: 'listReport0-enable-table-filtering', kind: 'simple' }) + ); + }); }); }); describe('ObjectPage', () => { @@ -1180,14 +1394,17 @@ describe('FE V2 quick actions', () => { await subscribeMock.mock.calls[0][0]( executeQuickAction({ id: 'objectPage0-op-add-header-field', kind: 'simple' }) ); - const { handler } = jest.requireMock<{ handler: () => Promise }>( - '../../../../src/adp/init-dialogs' - ); - expect(handler).toHaveBeenCalledWith(mockOverlay, rtaMock, 'AddFragment', undefined, { - aggregation: 'items', - title: 'QUICK_ACTION_OP_ADD_HEADER_FIELD' - }); + expect(DialogFactory.createDialog).toHaveBeenCalledWith( + mockOverlay, + rtaMock, + 'AddFragment', + undefined, + { + aggregation: 'items', + title: 'QUICK_ACTION_OP_ADD_HEADER_FIELD' + } + ); }); }); describe('add custom section', () => { @@ -1320,14 +1537,17 @@ describe('FE V2 quick actions', () => { await subscribeMock.mock.calls[0][0]( executeQuickAction({ id: 'objectPage0-op-add-custom-section', kind: 'simple' }) ); - const { handler } = jest.requireMock<{ handler: () => Promise }>( - '../../../../src/adp/init-dialogs' - ); - expect(handler).toHaveBeenCalledWith(mockOverlay, rtaMock, 'AddFragment', undefined, { - aggregation: 'sections', - title: 'QUICK_ACTION_OP_ADD_CUSTOM_SECTION' - }); + expect(DialogFactory.createDialog).toHaveBeenCalledWith( + mockOverlay, + rtaMock, + 'AddFragment', + undefined, + { + aggregation: 'sections', + title: 'QUICK_ACTION_OP_ADD_CUSTOM_SECTION' + } + ); }); }); describe('create table action', () => { @@ -1482,14 +1702,16 @@ describe('FE V2 quick actions', () => { executeQuickAction({ id: 'objectPage0-create-table-action', kind: 'nested', path: '0/0' }) ); - const { handler } = jest.requireMock<{ handler: () => Promise }>( - '../../../../src/adp/init-dialogs' + expect(DialogFactory.createDialog).toHaveBeenCalledWith( + mockOverlay, + rtaMock, + 'AddFragment', + undefined, + { + aggregation: 'content', + title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_ACTION' + } ); - - expect(handler).toHaveBeenCalledWith(mockOverlay, rtaMock, 'AddFragment', undefined, { - aggregation: 'content', - title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_ACTION' - }); }); }); @@ -1668,14 +1890,16 @@ describe('FE V2 quick actions', () => { }) ); - const { handler } = jest.requireMock<{ handler: () => Promise }>( - '../../../../src/adp/init-dialogs' + expect(DialogFactory.createDialog).toHaveBeenCalledWith( + mockOverlay, + rtaMock, + testCase.dialog, + undefined, + { + aggregation: 'columns', + title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_COLUMN' + } ); - - expect(handler).toHaveBeenCalledWith(mockOverlay, rtaMock, testCase.dialog, undefined, { - aggregation: 'columns', - title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_COLUMN' - }); }); test('displays warning when no rows loaded', async () => { const pageView = new XMLView(); @@ -2094,7 +2318,7 @@ describe('FE V2 quick actions', () => { describe('AnalyticalListPage', () => { describe('create table custom column', () => { - test('initialize and execute action (%s)', async () => { + test('initialize and execute action', async () => { const pageView = new XMLView(); const scrollIntoView = jest.fn(); jest.spyOn(QCUtils, 'getParentContainer').mockImplementation(() => { @@ -2213,14 +2437,16 @@ describe('FE V2 quick actions', () => { }) ); - const { handler } = jest.requireMock<{ handler: () => Promise }>( - '../../../../src/adp/init-dialogs' + expect(DialogFactory.createDialog).toHaveBeenCalledWith( + mockOverlay, + rtaMock, + DialogNames.ADD_FRAGMENT, + undefined, + { + aggregation: 'columns', + title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_COLUMN' + } ); - - expect(handler).toHaveBeenCalledWith(mockOverlay, rtaMock, DialogNames.ADD_FRAGMENT, undefined, { - aggregation: 'columns', - title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_COLUMN' - }); }); }); }); diff --git a/packages/preview-middleware-client/test/unit/adp/quick-actions/fe-v4.test.ts b/packages/preview-middleware-client/test/unit/adp/quick-actions/fe-v4.test.ts index 9f8e78d065..c0f8363981 100644 --- a/packages/preview-middleware-client/test/unit/adp/quick-actions/fe-v4.test.ts +++ b/packages/preview-middleware-client/test/unit/adp/quick-actions/fe-v4.test.ts @@ -1,6 +1,7 @@ import RuntimeAuthoring, { RTAOptions } from 'sap/ui/rta/RuntimeAuthoring'; import FlexBox from 'sap/m/FlexBox'; import RuntimeAuthoringMock from 'mock/sap/ui/rta/RuntimeAuthoring'; +import { attachBeforeClose } from 'mock/sap/ui/core/Fragment'; import type { ChangeService } from '../../../../src/cpe/changes/service'; const mockChangeService = { syncOutlineChanges: jest.fn() @@ -12,12 +13,6 @@ import { QuickAction } from '@sap-ux-private/control-property-editor-common'; -jest.mock('../../../../src/adp/init-dialogs', () => { - return { - ...jest.requireActual('../../../../src/adp/init-dialogs'), - handler: jest.fn() - }; -}); import { QuickActionService } from '../../../../src/cpe/quick-actions/quick-action-service'; import { OutlineService } from '../../../../src/cpe/outline/service'; import { FeatureService } from '../../../../src/cpe/feature-service'; @@ -39,7 +34,7 @@ import ComponentMock from 'mock/sap/ui/core/Component'; import UIComponent from 'sap/ui/core/UIComponent'; import AppComponentMock from 'mock/sap/fe/core/AppComponent'; import FlexRuntimeInfoAPI from 'mock/sap/ui/fl/apply/api/FlexRuntimeInfoAPI'; -import { DialogNames } from 'open/ux/preview/client/adp/init-dialogs'; +import { DialogFactory, DialogNames } from 'open/ux/preview/client/adp/dialog-factory'; import { ANALYTICAL_TABLE_TYPE, GRID_TABLE_TYPE, @@ -59,10 +54,17 @@ describe('FE V4 quick actions', () => { beforeEach(() => { sendActionMock = jest.fn(); subscribeMock = jest.fn(); + jest.spyOn(DialogFactory, 'createDialog').mockResolvedValue(); jest.clearAllMocks(); }); afterEach(() => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const closeDialogFunction = attachBeforeClose.mock.calls[0]?.[0]; + if (typeof closeDialogFunction === 'function') { + // make sure that dialog factory is in clean state after each test + closeDialogFunction(); + } fetchMock.mockRestore(); }); @@ -495,11 +497,8 @@ describe('FE V4 quick actions', () => { await subscribeMock.mock.calls[0][0]( executeQuickAction({ id: 'listReport0-add-controller-to-page', kind: 'simple' }) ); - const { handler } = jest.requireMock<{ handler: () => Promise }>( - '../../../../src/adp/init-dialogs' - ); - expect(handler).toHaveBeenCalledWith(mockOverlay, rtaMock, 'ControllerExtension'); + expect(DialogFactory.createDialog).toHaveBeenCalledWith(mockOverlay, rtaMock, 'ControllerExtension'); }); }); @@ -904,14 +903,16 @@ describe('FE V4 quick actions', () => { executeQuickAction({ id: 'listReport0-create-table-custom-column', kind: 'nested', path: '0' }) ); - const { handler } = jest.requireMock<{ handler: () => Promise }>( - '../../../../src/adp/init-dialogs' + expect(DialogFactory.createDialog).toHaveBeenCalledWith( + mockOverlay, + rtaMock, + DialogNames.ADD_FRAGMENT, + undefined, + { + aggregation: 'columns', + title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_COLUMN' + } ); - - expect(handler).toHaveBeenCalledWith(mockOverlay, rtaMock, DialogNames.ADD_FRAGMENT, undefined, { - aggregation: 'columns', - title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_COLUMN' - }); }); }); @@ -934,6 +935,7 @@ describe('FE V4 quick actions', () => { p13nMode: ['Filter'], expectedIsEnabled: false, expectedTooltip: + 'This option is disabled because table filtering for page variants is already enabled' } ]; @@ -1425,14 +1427,17 @@ describe('FE V4 quick actions', () => { await subscribeMock.mock.calls[0][0]( executeQuickAction({ id: 'objectPage0-op-add-header-field', kind: 'simple' }) ); - const { handler } = jest.requireMock<{ handler: () => Promise }>( - '../../../../src/adp/init-dialogs' - ); - expect(handler).toHaveBeenCalledWith(mockOverlay, rtaMock, 'AddFragment', undefined, { - aggregation: 'items', - title: 'QUICK_ACTION_OP_ADD_HEADER_FIELD' - }); + expect(DialogFactory.createDialog).toHaveBeenCalledWith( + mockOverlay, + rtaMock, + 'AddFragment', + undefined, + { + aggregation: 'items', + title: 'QUICK_ACTION_OP_ADD_HEADER_FIELD' + } + ); }); }); @@ -1647,14 +1652,17 @@ describe('FE V4 quick actions', () => { }) ); - const { handler } = jest.requireMock<{ handler: () => Promise }>( - '../../../../src/adp/init-dialogs' + + expect(DialogFactory.createDialog).toHaveBeenCalledWith( + mockOverlay, + rtaMock, + testCase.dialog, + undefined, + { + aggregation: 'columns', + title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_COLUMN' + } ); - - expect(handler).toHaveBeenCalledWith(mockOverlay, rtaMock, testCase.dialog, undefined, { - aggregation: 'columns', - title: 'QUICK_ACTION_ADD_CUSTOM_TABLE_COLUMN' - }); }); }); diff --git a/packages/preview-middleware-client/test/unit/cpe/changes/service.test.ts b/packages/preview-middleware-client/test/unit/cpe/changes/service.test.ts index d438251e9b..70347a2f24 100644 --- a/packages/preview-middleware-client/test/unit/cpe/changes/service.test.ts +++ b/packages/preview-middleware-client/test/unit/cpe/changes/service.test.ts @@ -1473,7 +1473,94 @@ describe('ChangeService', () => { }); }); - test('manifest change', async () => { + test('FE V2 manifest change', async () => { + fetchMock.mockResolvedValue({ json: () => Promise.resolve({}) }); + function createCommand( + properties: Map, + toggle = false + ): { + getProperty: (name: string) => any; + getElement: () => any; + getSelector: () => any; + getChangeType: () => string; + getParent: () => any; + getPreparedChange: () => { getDefinition: () => { fileName: string } }; + } { + const cache = new Map(properties); + return { + getProperty: (name: string): any => { + return cache.get(name); + }, + getElement: jest.fn().mockReturnValue({ + getMetadata: jest.fn().mockReturnValue({ getName: jest.fn().mockReturnValue('sap.m.Button') }) + }), + getSelector: jest.fn().mockReturnValue({ + id: !toggle ? 'ListReport.view.ListReport::SEPMRA_C_PD_Product--app.my-test-button' : undefined, + name: 'ExtensionPoint1' + }), + getChangeType: (): any => { + return cache.get('changeType'); + }, + getParent: jest.fn().mockReturnValue({ + getElement: jest.fn().mockReturnValue({ + getId: () => 'ListReport.view.ListReport::SEPMRA_C_PD_Product--app.my-test-button' + }) + }), + getPreparedChange: (): { getDefinition: () => { fileName: string } } => { + return { getDefinition: () => ({ fileName: 'testFileName' }) }; + } + }; + } + const commands = [ + createCommand( + new Map([ + ['changeType', 'appdescr_ui_generic_app_changePageConfiguration'], + [ + 'parameters', + { + 'entityPropertyChange': { + 'propertyPath': 'controlConfig/settings', + 'operation': 'upsert', + 'propertyValue': { + 'someSetting': true + } + } + } + ] + ]) + ) + ]; + rtaMock.getCommandStack.mockReturnValue({ + getCommands: jest.fn().mockReturnValue(commands), + getAllExecutedCommands: jest.fn().mockReturnValue(commands) + }); + const service = new ChangeService({ rta: rtaMock } as any); + + await service.init(sendActionMock, subscribeMock); + + await (rtaMock.attachUndoRedoStackModified as jest.Mock).mock.calls[0][0](); + expect(sendActionMock).toHaveBeenCalledTimes(5); + expect(sendActionMock).toHaveBeenNthCalledWith(2, setApplicationRequiresReload(true)); + expect(sendActionMock).toHaveBeenNthCalledWith(3, { + type: '[ext] change-stack-modified', + payload: { + saved: [], + pending: [ + { + controlIds: ['ListReport.view.ListReport::SEPMRA_C_PD_Product--app.my-test-button'], + isActive: true, + fileName: 'testFileName', + type: 'pending', + kind: 'configuration', + propertyName: 'someSetting', + propertyPath: 'controlConfig/settings', + value: true + } + ] + } + }); + }); + test('FE V4 manifest change', async () => { fetchMock.mockResolvedValue({ json: () => Promise.resolve({}) }); function createCommand( properties: Map, diff --git a/packages/preview-middleware-client/test/unit/cpe/quick-actions/service.test.ts b/packages/preview-middleware-client/test/unit/cpe/quick-actions/service.test.ts index 7c11437247..6238a39c26 100644 --- a/packages/preview-middleware-client/test/unit/cpe/quick-actions/service.test.ts +++ b/packages/preview-middleware-client/test/unit/cpe/quick-actions/service.test.ts @@ -46,6 +46,8 @@ class MockDefinition implements SimpleQuickActionDefinition { } as unknown as FlexCommand ]; } + + runEnablementValidators(): void | Promise {} } class MockRegistry extends QuickActionDefinitionRegistry {