Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Enhance Quick action list to add custom actions #2381

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions .changeset/weak-seals-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@sap-ux/adp-tooling": patch
"@sap-ux-private/preview-middleware-client": patch
---

feat: Enhance Quick action list to add custom actions
12 changes: 12 additions & 0 deletions packages/adp-tooling/src/preview/change-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { render } from 'ejs';
import { randomBytes } from 'crypto';

const OBJECT_PAGE_CUSTOM_SECTION = 'OBJECT_PAGE_CUSTOM_SECTION';
const CUSTOM_ACTION = 'CUSTOM_ACTION';

interface FragmentTemplateConfig<T = { [key: string]: any }> {
/**
Expand All @@ -29,6 +30,17 @@ const fragmentTemplateDefinitions: Record<string, FragmentTemplateConfig> = {
}
};
}
},
[CUSTOM_ACTION]: {
path: 'common/custom-action.xml',
getData: () => {
const uuid = randomBytes(4).toString('hex');
return {
ids: {
toolbarActionButton: `btn-${uuid}`
}
};
}
}
};

Expand Down
5 changes: 5 additions & 0 deletions packages/adp-tooling/templates/rta/common/custom-action.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- Use stable and unique IDs!-->
<core:FragmentDefinition xmlns:core='sap.ui.core' xmlns='sap.m'>
<!-- add your xml here -->
<Button text="New Button" id="<%- ids.toolbarActionButton %>"></Button>
</core:FragmentDefinition>
32 changes: 32 additions & 0 deletions packages/adp-tooling/test/unit/preview/change-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,38 @@ id="<%- ids.hBox %>"`);

expect(mockLogger.info).toHaveBeenCalledWith(`XML Fragment "${fragmentName}.fragment.xml" was created`);
});

it('should create custom action fragment', () => {
mockFs.exists.mockReturnValue(false);
const updatedChange = {
...change,
content: {
...change.content,
templateName: `CUSTOM_ACTION`
}
} as unknown as AddXMLChange;
mockFs.read.mockReturnValue(`
id="<%- ids.toolbarActionButton %>`);
addXmlFragment(path, updatedChange, mockFs as unknown as Editor, mockLogger as unknown as Logger);

expect(mockFs.read).toHaveBeenCalled();
expect(
(mockFs.read.mock.calls[0][0] as string)
.replace(/\\/g, '/')
.endsWith('templates/rta/common/custom-action.xml')
).toBe(true);

expect(mockFs.write).toHaveBeenCalled();
expect(mockFs.write.mock.calls[0][0].replace(/\\/g, '/')).toMatchInlineSnapshot(
`"project/path/changes/Share.fragment.xml"`
);
expect(mockFs.write.mock.calls[0][1]).toMatchInlineSnapshot(`
"
id=\\"btn-30303030"
`);

expect(mockLogger.info).toHaveBeenCalledWith(`XML Fragment "${fragmentName}.fragment.xml" was created`);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import OverlayRegistry from 'sap/ui/dt/OverlayRegistry';
import type ElementOverlay from 'sap/ui/dt/ElementOverlay';

/** sap.ui.fl */
import {type AddFragmentChangeContentType} from 'sap/ui/fl/Change';
import { type AddFragmentChangeContentType } from 'sap/ui/fl/Change';

import ControlUtils from '../control-utils';
import CommandExecutor from '../command-executor';
Expand Down Expand Up @@ -296,8 +296,16 @@ export default class AddFragment extends BaseDialog<AddFragmentModel> {

private getFragmentTemplateName(targetAggregation: string): string {
const currentControlName = this.runtimeControl.getMetadata().getName();
return currentControlName === 'sap.uxap.ObjectPageLayout' && targetAggregation === 'sections'
? 'OBJECT_PAGE_CUSTOM_SECTION'
: '';
if (currentControlName === 'sap.uxap.ObjectPageLayout' && targetAggregation === 'sections') {
return 'OBJECT_PAGE_CUSTOM_SECTION';
} else if (
((currentControlName === 'sap.f.DynamicPageTitle' || currentControlName === 'sap.uxap.ObjectPageHeader') &&
targetAggregation === 'actions') ||
(currentControlName === 'sap.m.OverflowToolbar' && targetAggregation === 'content')
) {
return 'CUSTOM_ACTION';
} else {
return '';
}
}
}
Original file line number Diff line number Diff line change
@@ -1,237 +1,24 @@
import OverlayUtil from 'sap/ui/dt/OverlayUtil';
import FlexCommand from 'sap/ui/rta/command/FlexCommand';
import UI5Element from 'sap/ui/core/Element';
import type IconTabBar from 'sap/m/IconTabBar';
import type IconTabFilter from 'sap/m/IconTabFilter';
import type Table from 'sap/m/Table';
import type SmartTable from 'sap/ui/comp/smarttable/SmartTable';

import type { NestedQuickAction, NestedQuickActionChild } from '@sap-ux-private/control-property-editor-common';
import { NESTED_QUICK_ACTION_KIND } from '@sap-ux-private/control-property-editor-common';

import { QuickActionContext, NestedQuickActionDefinition } from '../../../cpe/quick-actions/quick-action-definition';
import { getParentContainer, getRelevantControlFromActivePage } from '../../../cpe/quick-actions/utils';
import { getControlById, isA, isManagedObject } from '../../../utils/core';
import { getUi5Version, isLowerThanMinimalUi5Version } from '../../../utils/version';
import ObjectPageSection from 'sap/uxap/ObjectPageSection';
import ObjectPageSubSection from 'sap/uxap/ObjectPageSubSection';
import TreeTable from 'sap/ui/table/TreeTable';
import ObjectPageLayout from 'sap/uxap/ObjectPageLayout';
import { getControlById, isA } from '../../../utils/core';
import ManagedObject from 'sap/ui/base/ManagedObject';
import { TableQuickActionDefinitionBase } from './table-quick-action-base';

export const CHANGE_TABLE_COLUMNS = 'change-table-columns';
const SMART_TABLE_ACTION_ID = 'CTX_COMP_VARIANT_CONTENT';
const M_TABLE_ACTION_ID = 'CTX_ADD_ELEMENTS_AS_CHILD';
const SETTINGS_ID = 'CTX_SETTINGS';
const ICON_TAB_BAR_TYPE = 'sap.m.IconTabBar';
const SMART_TABLE_TYPE = 'sap.ui.comp.smarttable.SmartTable';
const M_TABLE_TYPE = 'sap.m.Table';
// maintain order if action id
const CONTROL_TYPES = [SMART_TABLE_TYPE, M_TABLE_TYPE, 'sap.ui.table.TreeTable', 'sap.ui.table.Table'];

async function getActionId(table: UI5Element): Promise<string[]> {
const { major, minor } = await getUi5Version();

if (isA(SMART_TABLE_TYPE, table)) {
if (major === 1 && minor === 96) {
return [SETTINGS_ID];
} else {
return [SMART_TABLE_ACTION_ID];
}
}

return [M_TABLE_ACTION_ID, SETTINGS_ID];
}

export class ChangeTableColumnsQuickAction implements NestedQuickActionDefinition {
readonly kind = NESTED_QUICK_ACTION_KIND;
readonly type = CHANGE_TABLE_COLUMNS;
public get id(): string {
return `${this.context.key}-${this.type}`;
}
isActive = false;
isClearButtonEnabled = false;
children: NestedQuickActionChild[] = [];
tableMap: Record<
string,
{
table: UI5Element;
tableUpdateEventAttachedOnce: boolean;
iconTabBarFilterKey?: string;
changeColumnActionId: string;
sectionInfo?: {
section: ObjectPageSection;
subSection: ObjectPageSubSection;
layout?: ObjectPageLayout;
};
}
> = {};
private iconTabBar: IconTabBar | undefined;
constructor(private context: QuickActionContext) {}

async initialize(): Promise<void> {
// No action found in control design time for version < 1.96
const version = await getUi5Version();
if (isLowerThanMinimalUi5Version(version, { major: 1, minor: 96 })) {
this.isActive = false;
return;
}
const iconTabBarfilterMap = this.buildIconTabBarFilterMap();
for (const table of getRelevantControlFromActivePage(
this.context.controlIndex,
this.context.view,
CONTROL_TYPES
)) {
const actions = await this.context.actionService.get(table.getId());
const actionsIds = await getActionId(table);
const changeColumnAction = actionsIds.find(
(actionId) => actions.findIndex((action) => action.id === actionId) > -1
);
const tabKey = Object.keys(iconTabBarfilterMap).find((key) => table.getId().endsWith(key));
if (changeColumnAction) {
const section = getParentContainer<ObjectPageSection>(table, 'sap.uxap.ObjectPageSection');
if (section) {
this.collectChildrenInSection(section, table, changeColumnAction);
} else if (this.iconTabBar && tabKey) {
this.children.push({
label: `'${iconTabBarfilterMap[tabKey]}' table`,
children: []
});
this.tableMap[`${this.children.length - 1}`] = {
table,
iconTabBarFilterKey: tabKey,
changeColumnActionId: changeColumnAction,
tableUpdateEventAttachedOnce: false
};
} else {
this.processTable(table, changeColumnAction);
}
}
}
if (this.children.length > 0) {
this.isActive = true;
}
}

private getTableLabel(table: UI5Element): string {
if (isA<SmartTable>(SMART_TABLE_TYPE, table)) {
const header = table.getHeader();
if (header) {
return `'${header}' table`;
}
}
if (isA<Table>(M_TABLE_TYPE, table)) {
const tilte = table?.getHeaderToolbar()?.getTitleControl()?.getText();
if (tilte) {
return `'${tilte}' table`;
}
}

return 'Unnamed table';
}

private buildIconTabBarFilterMap(): { [key: string]: string } {
const iconTabBarfilterMap: { [key: string]: string } = {};

// Assumption only a tab bar control per page.
const tabBar = getRelevantControlFromActivePage(this.context.controlIndex, this.context.view, [
ICON_TAB_BAR_TYPE
])[0];
if (tabBar) {
const control = getControlById(tabBar.getId());
if (isA<IconTabBar>(ICON_TAB_BAR_TYPE, control)) {
this.iconTabBar = control;
for (const item of control.getItems()) {
if (isManagedObject(item) && isA<IconTabFilter>('sap.m.IconTabFilter', item)) {
iconTabBarfilterMap[item.getKey()] = item.getText();
}
}
}
}

return iconTabBarfilterMap;
}

private collectChildrenInSection(section: ObjectPageSection, table: UI5Element, changeColumnAction: string): void {
const layout = getParentContainer<ObjectPageLayout>(table, 'sap.uxap.ObjectPageLayout');
const subSections = section.getSubSections();
const subSection = getParentContainer<ObjectPageSubSection>(table, 'sap.uxap.ObjectPageSubSection');
if (subSection) {
if (subSections?.length === 1) {
this.processTable(table, changeColumnAction, { section, subSection: subSections[0], layout });
} else if (subSections.length > 1) {
const sectionChild = this.children.find((val) => val.label === `${section.getTitle()} section`);
let tableMapIndex = `${this.children.length - 1}`;
if (!sectionChild) {
tableMapIndex = `${tableMapIndex}/0`;
this.children.push({
label: `'${section?.getTitle()}' section`,
children: [
{
label: this.getTableLabel(table),
children: []
}
]
});
} else {
tableMapIndex = `${tableMapIndex}/${sectionChild.children.length - 1}`;
sectionChild.children.push({
label: this.getTableLabel(table),
children: []
});
}

this.tableMap[tableMapIndex] = {
table,
changeColumnActionId: changeColumnAction,
sectionInfo: { section, subSection, layout },
tableUpdateEventAttachedOnce: false
};
}
}
}

private processTable(
table: UI5Element,
changeColumnActionId: string,
sectionInfo?: { section: ObjectPageSection; subSection: ObjectPageSubSection; layout?: ObjectPageLayout }
): void {
if (isA<SmartTable>(SMART_TABLE_TYPE, table) || isA<TreeTable>('sap.ui.table.TreeTable', table)) {
this.children.push({
label: this.getTableLabel(table),
children: []
});
}
if (isA<Table>(M_TABLE_TYPE, table)) {
this.children.push({
label: this.getTableLabel(table),
children: []
});
}
this.tableMap[`${this.children.length - 1}`] = {
table,
changeColumnActionId,
sectionInfo: sectionInfo,
tableUpdateEventAttachedOnce: false
};
}

getActionObject(): NestedQuickAction {
return {
kind: NESTED_QUICK_ACTION_KIND,
id: this.id,
enabled: this.isActive,
title:
this.context.resourceBundle.getText('V2_QUICK_ACTION_CHANGE_TABLE_COLUMNS') ?? 'Change table columns',
children: this.children
};
}

private selectOverlay(table: UI5Element): void {
const controlOverlay = OverlayUtil.getClosestOverlayFor(table);
if (controlOverlay) {
controlOverlay.setSelected(true);
}
export class ChangeTableColumnsQuickAction
extends TableQuickActionDefinitionBase
implements NestedQuickActionDefinition
{
constructor(context: QuickActionContext) {
super(CHANGE_TABLE_COLUMNS, CONTROL_TYPES, 'V2_QUICK_ACTION_CHANGE_TABLE_COLUMNS', context, true);
}

async execute(path: string): Promise<FlexCommand[]> {
Expand All @@ -253,17 +40,19 @@ export class ChangeTableColumnsQuickAction implements NestedQuickActionDefinitio
if (this.iconTabBar && iconTabBarFilterKey) {
this.iconTabBar.setSelectedKey(iconTabBarFilterKey);
}

const executeAction = async () => await this.context.actionService.execute(table.getId(), changeColumnActionId);
if (isA<SmartTable>(SMART_TABLE_TYPE, table)) {
await executeAction();
} else if (isA<Table>(M_TABLE_TYPE, table)) {
// if table is busy, i.e. lazy loading, then we subscribe to 'updateFinished' event and call action service when loading is done
// to avoid reopening the dialog after close
if (this.isTableLoaded(table)) {
if (changeColumnActionId) {
const executeAction = async () =>
await this.context.actionService.execute(table.getId(), changeColumnActionId);
if (isA<SmartTable>(SMART_TABLE_TYPE, table)) {
await executeAction();
} else {
table.attachEventOnce('updateFinished', executeAction, this);
} else if (isA<Table>(M_TABLE_TYPE, table)) {
// if table is busy, i.e. lazy loading, then we subscribe to 'updateFinished' event and call action service when loading is done
// to avoid reopening the dialog after close
if (this.isTableLoaded(table)) {
await executeAction();
} else {
table.attachEventOnce('updateFinished', executeAction, this);
}
}
}

Expand Down
Loading
Loading