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: Adding Controller Extension only on async views in Adp projects #1874

Merged
Merged
8 changes: 8 additions & 0 deletions .changeset/six-pigs-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@sap-ux-private/control-property-editor-common': patch
'@sap-ux-private/preview-middleware-client': patch
'@sap-ux/control-property-editor': patch
'@sap-ux/preview-middleware': patch
---

Enable Adding Controller Extension only on async views for Adp Projects
7 changes: 6 additions & 1 deletion packages/control-property-editor-common/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ export interface PropertyChangeDeletionDetails {
fileName?: string;
}

export interface ShowMessage {
message: string;
shouldHideIframe: boolean;
}

/**
* ACTIONS
*/
Expand Down Expand Up @@ -226,7 +231,7 @@ export const changeProperty = createExternalAction<PropertyChange>('change-prope
export const propertyChanged = createExternalAction<PropertyChanged>('property-changed');
export const propertyChangeFailed = createExternalAction<PropertyChangeFailed>('change-property-failed');
export const changeStackModified = createExternalAction<ChangeStackModified>('change-stack-modified');
export const showMessage = createExternalAction<string>('show-dialog-message');
export const showMessage = createExternalAction<ShowMessage>('show-dialog-message');
export const reloadApplication = createExternalAction<void>('reload-application');
export const storageFileChanged = createExternalAction<string>('storage-file-changed');
export type ExternalAction =
Expand Down
36 changes: 27 additions & 9 deletions packages/control-property-editor/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useAppDispatch } from './store';
import { changePreviewScale } from './slice';
import { useWindowSize } from './use-window-size';
import { DEFAULT_DEVICE_WIDTH, DEVICE_WIDTH_MAP } from './devices';
import { ShowMessage } from '@sap-ux-private/control-property-editor-common';

import './App.scss';
import './Workarounds.scss';
Expand Down Expand Up @@ -45,18 +46,18 @@ export default function App(appProps: AppProps): ReactElement {

const [hideWarningDialog, setHideWarningDialog] = useLocalStorage('hide-warning-dialog', false);
const [isWarningDialogVisible, setWarningDialogVisibility] = useState(() => hideWarningDialog !== true);
const [shouldShowDialogMessage, setShouldShowDialogMessage] = useState(false);
const [shouldHideIframe, setShouldHideIframe] = useState(false);

const [isInitialized, setIsInitialized] = useState(false);
const [shouldShowDialogMessageForAdpProjects, setShouldShowDialogMessageForAdpProjects] = useState(false);

const previewWidth = useSelector<RootState, string>(
(state) => `${DEVICE_WIDTH_MAP.get(state.deviceType) ?? DEFAULT_DEVICE_WIDTH}px`
);
const previewScale = useSelector<RootState, number>((state) => state.scale);
const fitPreview = useSelector<RootState, boolean>((state) => state.fitPreview ?? false);
const windowSize = useWindowSize();
const dialogMessage = useSelector<RootState, string | undefined>((state) => state.dialogMessage);

const dialogMessage = useSelector<RootState, ShowMessage | undefined>((state) => state.dialogMessage);
const containerRef = useCallback(
(node) => {
if (node === null) {
Expand Down Expand Up @@ -92,11 +93,16 @@ export default function App(appProps: AppProps): ReactElement {
setWarningDialogVisibility(false);
}

const closeAdpWarningDialog = (): void => {
setShouldShowDialogMessage(false);
};

useEffect(() => {
if (dialogMessage && isAdpProject) {
setShouldShowDialogMessageForAdpProjects(true);
setShouldShowDialogMessage(true);
setShouldHideIframe(dialogMessage.shouldHideIframe);
}
}, [dialogMessage]);
}, [dialogMessage, isAdpProject]);

return (
<div className="app">
Expand All @@ -105,7 +111,7 @@ export default function App(appProps: AppProps): ReactElement {
</section>
<section ref={containerRef} className="app-content">
<div className="app-canvas">
{!shouldShowDialogMessageForAdpProjects && (
{!shouldHideIframe && (
<iframe
className="app-preview"
id="preview"
Expand All @@ -122,15 +128,27 @@ export default function App(appProps: AppProps): ReactElement {
<section className="app-panel app-panel-right">
<PropertiesPanel />
</section>
{isAdpProject && (
{isAdpProject && shouldHideIframe && (
<UIDialog
hidden={!shouldShowDialogMessage}
dialogContentProps={{
title: t('TOOL_DISCLAIMER_TITLE'),
subText: dialogMessage?.message
}}
/>
)}
{isAdpProject && !shouldHideIframe && (
<UIDialog
hidden={!shouldShowDialogMessageForAdpProjects}
hidden={!shouldShowDialogMessage}
dialogContentProps={{
title: t('TOOL_DISCLAIMER_TITLE'),
subText: dialogMessage
subText: dialogMessage?.message
}}
acceptButtonText={t('OK')}
onAccept={closeAdpWarningDialog}
/>
)}

{scenario === 'FE_FROM_SCRATCH' ? (
<UIDialog
hidden={!isWarningDialogVisible}
Expand Down
5 changes: 3 additions & 2 deletions packages/control-property-editor/src/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import type {
PendingPropertyChange,
PropertyChange,
SavedPropertyChange,
Scenario
Scenario,
ShowMessage
} from '@sap-ux-private/control-property-editor-common';
import {
changeStackModified,
Expand Down Expand Up @@ -39,7 +40,7 @@ interface SliceState {
isAdpProject: boolean;
icons: IconDetails[];
changes: ChangesSlice;
dialogMessage: string | undefined;
dialogMessage: ShowMessage | undefined;
fileChanges?: string[];
}

Expand Down
39 changes: 37 additions & 2 deletions packages/control-property-editor/test/unit/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import { render, mockDomEventListener } from './utils';
import { initI18n } from '../../src/i18n';

import App from '../../src/App';
import { controlSelected } from '@sap-ux-private/control-property-editor-common';
import { controlSelected, scenario, showMessage } from '@sap-ux-private/control-property-editor-common';
import { mockResizeObserver } from '../utils/utils';
import { InputType } from '../../src/panels/properties/types';
import { registerAppIcons } from '../../src/icons';
import { DeviceType } from '../../src/devices';
import { changePreviewScale, initialState } from '../../src/slice';
import { FilterName, SliceState, changePreviewScale, initialState } from '../../src/slice';

jest.useFakeTimers({ advanceTimers: true });
const windowEventListenerMock = mockDomEventListener(window);
Expand Down Expand Up @@ -182,6 +182,41 @@ test('renders warning dialog for "FE_FROM_SCRATCH" scenario', async () => {
fireEvent.click(okButton);
});

test('renders warning message for "ADAPTATION_PROJECT" scenario', async () => {
const initialState = {
deviceType: DeviceType.Desktop,
scale: 1.0,
selectedControl: undefined,
outline: [],
filterQuery: [
{ name: FilterName.focusEditable, value: true },
{ name: FilterName.focusCommonlyUsed, value: true },
{ name: FilterName.query, value: '' },
{ name: FilterName.changeSummaryFilterQuery, value: '' },
{ name: FilterName.showEditableProperties, value: true }
],
nikmace marked this conversation as resolved.
Show resolved Hide resolved
scenario: scenario.AdaptationProject,
isAdpProject: true,
icons: [],
changes: {
controls: {},
pending: [],
saved: [],
pendingChangeIds: []
},
dialogMessage: {
message: 'Some Text',
shouldHideIframe: false
}
};
render(<App previewUrl="" scenario="ADAPTATION_PROJECT" />, { initialState });

const warningDialog = screen.getByText(/Some Text/i);
expect(warningDialog).toBeInTheDocument();
const okButton = screen.getByText(/ok/i);
expect(okButton).toBeInTheDocument();
});

const testCases = [
{
deviceType: DeviceType.Desktop,
Expand Down
4 changes: 2 additions & 2 deletions packages/control-property-editor/test/unit/slice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,8 @@ describe('main redux slice', () => {
});

test('show message', () => {
expect(reducer({} as any, showMessage('testMessage'))).toStrictEqual({
dialogMessage: 'testMessage'
expect(reducer({} as any, showMessage({ message: 'testMessage', shouldHideIframe: false }))).toStrictEqual({
dialogMessage: { message: 'testMessage', shouldHideIframe: false }
});
});
test('reload application', () => {
Expand Down
27 changes: 24 additions & 3 deletions packages/preview-middleware-client/src/adp/init-dialogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ import type Dialog from 'sap/m/Dialog';

/** sap.ui.core */
import Fragment from 'sap/ui/core/Fragment';
import type UI5Element from 'sap/ui/core/Element';
import UI5Element from 'sap/ui/core/Element';

/** sap.ui.rta */
import type RuntimeAuthoring from 'sap/ui/rta/RuntimeAuthoring';

/** sap.ui.fl */
import FlUtils from 'sap/ui/fl/Utils';

import ElementOverlay from 'sap/ui/dt/ElementOverlay';

import AddFragment from './controllers/AddFragment.controller';
import ControllerExtension from './controllers/ControllerExtension.controller';
import { ExtensionPointData } from './extension-point';
Expand All @@ -25,8 +30,9 @@ type Controller = AddFragment | ControllerExtension | ExtensionPoint;
* Adds a new item to the context menu
*
* @param rta Runtime Authoring
* @param syncViewsIds Ids of all application sync views
*/
export const initDialogs = (rta: RuntimeAuthoring): void => {
export const initDialogs = (rta: RuntimeAuthoring, syncViewsIds: string[]): void => {
const contextMenu = rta.getDefaultPlugins().contextMenu;

contextMenu.addMenuItem({
Expand All @@ -40,10 +46,25 @@ export const initDialogs = (rta: RuntimeAuthoring): void => {
id: 'EXTEND_CONTROLLER',
text: 'Extend With Controller',
handler: async (overlays: UI5Element[]) => await handler(overlays[0], rta, DialogNames.CONTROLLER_EXTENSION),
icon: 'sap-icon://create-form'
icon: 'sap-icon://create-form',
enabled: (overlays: ElementOverlay[]) => isControllerExtensionEnabled(overlays, syncViewsIds)
});
};

/**
* Handler for enablement of Extend With Controller context menu entry
*
* @param overlays Control overlays
* @param syncViewsIds Runtime Authoring
*
* @returns boolean
*/
export const isControllerExtensionEnabled = (overlays: ElementOverlay[], syncViewsIds: string[]): boolean => {
const clickedControlId = FlUtils.getViewForControl(overlays[0].getElement()).getId();

return overlays.length <= 1 && !syncViewsIds.includes(clickedControlId);
};

/**
* Handler for new context menu entry
*
Expand Down
41 changes: 35 additions & 6 deletions packages/preview-middleware-client/src/adp/init.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import log from 'sap/base/Log';
import type RuntimeAuthoring from 'sap/ui/rta/RuntimeAuthoring';
import UI5ElementRegistry from 'sap/ui/core/ElementRegistry';

import init from '../cpe/init';
import { initDialogs } from './init-dialogs';
Expand All @@ -13,6 +14,7 @@ import {
import { ActionHandler } from '../cpe/types';
import VersionInfo from 'sap/ui/VersionInfo';
import { getUI5VersionValidationMessage } from './ui5-version-utils';
import UI5Element from 'sap/ui/dt/Element';

export default async function (rta: RuntimeAuthoring) {
const { version } = (await VersionInfo.load()) as { version: string };
Expand Down Expand Up @@ -44,23 +46,50 @@ export default async function (rta: RuntimeAuthoring) {
}
);

// initialize fragment content menu entry
initDialogs(rta);
const syncViewsIds = getAllSyncViewsIds();
initDialogs(rta, syncViewsIds);

// initialize extension point service
if (minor > 77) {
const ExtensionPointService = (await import('open/ux/preview/client/adp/extension-point')).default;
const extPointService = new ExtensionPointService(rta);
extPointService.init(subscribe);
}

// also initialize the editor
await init(rta);

const ui5VersionValidationMsg = getUI5VersionValidationMessage(version);

if (ui5VersionValidationMsg) {
sendAction(showMessage(ui5VersionValidationMsg));
sendAction(showMessage({ message: ui5VersionValidationMsg, shouldHideIframe: true }));

return;
}
if (syncViewsIds.length > 0) {
sendAction(
showMessage({
message:
'Have in mind that synchronous views are detected for this application and controller extensions are not supported for such views. Controller extension functionality on these views will be disabled.',
shouldHideIframe: false
})
);
}

log.debug('ADP init executed.');
}

/**
*
* Get Ids for all sync views
*
* @returns array of Ids for application sync views
*/
function getAllSyncViewsIds(): string[] {
const elements = UI5ElementRegistry.all() as Record<string, UI5Element>;
const syncViewIds: string[] = [];
Object.entries(elements).forEach(([key, ui5Element]) => {
if (ui5Element?.getMetadata()?.getName()?.includes('XMLView') && ui5Element?.oAsyncState === undefined) {
syncViewIds.push(key);
}
});

return syncViewIds;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
all: jest.fn().mockReturnValue([])
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ 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 } from '../../../src/adp/init-dialogs';
import { DialogNames, handler, initDialogs, isControllerExtensionEnabled } 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 ElementOverlay from 'sap/ui/dt/ElementOverlay';
import FlUtils from 'sap/ui/fl/Utils';

describe('Dialogs', () => {
describe('initDialogs', () => {
Expand All @@ -26,7 +28,7 @@ describe('Dialogs', () => {
addMenuItem: addMenuItemSpy
}
});
initDialogs(rtaMock as unknown as RuntimeAuthoring);
initDialogs(rtaMock as unknown as RuntimeAuthoring, []);
expect(addMenuItemSpy).toHaveBeenCalledTimes(2);
});

Expand Down Expand Up @@ -58,4 +60,29 @@ describe('Dialogs', () => {
expect(Fragment.load).toHaveBeenCalledTimes(3);
});
});

describe('isControllerExtensionEnabled', () => {
const syncViewsIds = ['syncViewId1', 'syncViewId2'];
const elementOverlayMock = { getElement: jest.fn() } as unknown as ElementOverlay;

it('should return true when overlays length is 1 and clickedControlId is not in syncViewsIds', () => {
FlUtils.getViewForControl = jest.fn().mockReturnValue({ getId: jest.fn().mockReturnValue('asyncViewId2') });
const overlays: ElementOverlay[] = [elementOverlayMock];
expect(isControllerExtensionEnabled(overlays, syncViewsIds)).toBe(true);
});

it('should return false when overlays length is 1 and clickedControlId is in syncViewsIds', () => {
FlUtils.getViewForControl = jest.fn().mockReturnValue({ getId: jest.fn().mockReturnValue('syncViewId1') });
const overlays: ElementOverlay[] = [elementOverlayMock];

expect(isControllerExtensionEnabled(overlays, syncViewsIds)).toBe(false);
});

it('should return false when overlays length is more than 1', () => {
FlUtils.getViewForControl = jest.fn().mockReturnValue({ getId: jest.fn().mockReturnValue('syncViewId3') });
const overlays: ElementOverlay[] = [elementOverlayMock, elementOverlayMock];
const syncViewsIds = ['syncViewId1', 'syncViewId2'];
expect(isControllerExtensionEnabled(overlays, syncViewsIds)).toBe(false);
});
});
});