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

Allow the usage of React components as UI extensions #733

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion packages/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
"@axonivy/process-editor-protocol": "~13.1.0-next",
"@eclipse-glsp/client": "2.3.0",
"marked": "^15.0.6",
"toastify-js": "1.12.0"
"toastify-js": "1.12.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/lodash": "4.17.14",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { JumpAction } from '@axonivy/process-editor-protocol';
import { IvyIcons } from '@axonivy/ui-icons';
import {
Action,
EditorContextService,
GLSPAbstractUIExtension,
IActionDispatcher,
IActionHandler,
SelectionService,
Expand All @@ -13,15 +11,18 @@ import {
UpdateModelAction
} from '@eclipse-glsp/client';
import { inject, injectable } from 'inversify';
import { createElement, createIcon } from '../utils/ui-utils';
import { ReactUIExtension } from '../utils/react-ui-extension';
import React from 'react';
import IvyIcon from '../utils/ui-components';

const JSX = { createElement: React.createElement };

@injectable()
export class JumpOutUi extends GLSPAbstractUIExtension implements IActionHandler {
export class JumpOutUi extends ReactUIExtension implements IActionHandler {
static readonly ID = 'jumpOutUi';

@inject(TYPES.IActionDispatcher) protected readonly actionDispatcher: IActionDispatcher;
@inject(SelectionService) protected selectionService: SelectionService;
@inject(EditorContextService) protected readonly editorContext: EditorContextService;

id(): string {
return JumpOutUi.ID;
Expand All @@ -31,17 +32,17 @@ export class JumpOutUi extends GLSPAbstractUIExtension implements IActionHandler
return 'jump-out-container';
}

override initializeContents(containerElement: HTMLElement): void {
containerElement.style.position = 'absolute';
protected initializeContainer(container: HTMLElement): void {
super.initializeContainer(container);
container.style.position = 'absolute';
}

override onBeforeShow(containerElement: HTMLElement) {
containerElement.innerHTML = '';
const button = createElement('button', ['jump-out-btn']);
button.title = 'Jump out (J)';
button.appendChild(createIcon(IvyIcons.JumpOut));
button.onclick = () => this.actionDispatcher.dispatch(JumpAction.create({ elementId: '' }));
containerElement.appendChild(button);
protected render(): React.ReactNode {
return (
<button className={'jump-out-btn'} onClick={() => this.actionDispatcher.dispatch(JumpAction.create({ elementId: '' }))}>
<IvyIcon icon={IvyIcons.JumpOut} />
</button>
);
}

handle(action: Action): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {
GLSPAbstractUIExtension,
Action,
DisposableCollection,
EditorContextService,
EnableDefaultToolsAction,
EnableToolPaletteAction,
IActionHandler,
Expand Down Expand Up @@ -36,16 +34,21 @@ import { ShowToolBarOptionsMenuAction } from './options/action';
import { ToolBarOptionsMenu } from './options/options-menu-ui';
import { ShowToolBarMenuAction, ToolBarMenu } from './tool-bar-menu';
import { UpdatePaletteItems } from '@axonivy/process-editor-protocol';
import { ReactUIExtension } from '../../utils/react-ui-extension';
import { SModelRootImpl } from 'sprotty';
import React from 'react';
import IvyIcon from '../../utils/ui-components';

const JSX = { createElement: React.createElement };

const CLICKED_CSS_CLASS = 'clicked';

@injectable()
export class ToolBar extends GLSPAbstractUIExtension implements IActionHandler, IEditModeListener, ISelectionListener {
export class ToolBar extends ReactUIExtension implements IActionHandler, IEditModeListener, ISelectionListener {
static readonly ID = 'ivy-tool-bar';

@inject(TYPES.IActionDispatcher) protected readonly actionDispatcher: IActionDispatcher;
@inject(SelectionService) protected readonly selectionService: SelectionService;
@inject(EditorContextService) protected readonly editorContext: EditorContextService;
@multiInject(IVY_TYPES.ToolBarButtonProvider) protected toolBarButtonProvider: ToolBarButtonProvider[];

protected lastActivebutton?: HTMLElement;
Expand All @@ -72,12 +75,14 @@ export class ToolBar extends GLSPAbstractUIExtension implements IActionHandler,
}

protected initializeContents(containerElement: HTMLElement) {
super.initializeContents(containerElement);
this.createHeader();
this.lastActivebutton = this.defaultToolsButton;
containerElement.onwheel = ev => (ev.ctrlKey ? ev.preventDefault() : true);
}

protected onBeforeShow() {
protected onBeforeShow(containerElement: HTMLElement, root: Readonly<SModelRootImpl>, ...contextElementIds: string[]): void {
super.onBeforeShow(containerElement, root, ...contextElementIds);
this.toDisposeOnHide.push(this.selectionService.onSelectionChanged(() => this.selectionChanged()));
}

Expand All @@ -86,6 +91,46 @@ export class ToolBar extends GLSPAbstractUIExtension implements IActionHandler,
this.toDisposeOnHide.dispose();
}

protected render(): React.ReactNode {
return (
<div className='tool-bar-header'>
<div className='left-buttons'>
<button className='tool-bar-button' onClick={() => this.dispatchAction([DefaultSelectButton.action()])}>
<IvyIcon icon={DefaultSelectButton.icon} />
<span>{DefaultSelectButton.title}</span>
</button>
<button className='tool-bar-button' onClick={() => this.dispatchAction([MarqueeToolButton.action()])}>
<IvyIcon icon={MarqueeToolButton.icon} />
<span>{MarqueeToolButton.title}</span>
</button>
</div>
<div className='middle-buttons'>
{this.toolBarButtonProvider
.map(provider => provider.button())
.filter(isNotUndefined)
.filter(button => button.location === ToolBarButtonLocation.Middle)
.filter(button => !this.editorContext.isReadonly || button.readonly)
.sort(compareButtons)
.map(button => (
<button
key={button.id}
className='tool-bar-button'
onClick={() => {
this.dispatchAction([button.action()]);
if (button.switchFocus) {
this.changeActiveButton();
}
}}
>
<IvyIcon icon={button.icon} />
<span>{button.title}</span>
</button>
))}
</div>
</div>
);
}

protected createHeader(): void {
const headerCompartment = createElement('div', ['tool-bar-header']);
headerCompartment.appendChild(this.createLeftButtons());
Expand Down
103 changes: 0 additions & 103 deletions packages/editor/src/ui-tools/viewport/viewport-bar.ts

This file was deleted.

102 changes: 102 additions & 0 deletions packages/editor/src/ui-tools/viewport/viewport-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
Action,
IActionHandler,
isViewport,
IToolManager,
SetUIExtensionVisibilityAction,
SetViewportAction,
TYPES,
SelectionService,
type IActionDispatcher
} from '@eclipse-glsp/client';
import { inject, injectable } from 'inversify';
import { CenterButton, FitToScreenButton, OriginScreenButton, ViewportBarButton } from './button';

import { QuickActionUI } from '../quick-action/quick-action-ui';
import { EnableViewportAction, SetViewportZoomAction } from '@axonivy/process-editor-protocol';
import { ReactUIExtension } from '../../utils/react-ui-extension';
import React from 'react';
import IvyIcon from '../../utils/ui-components';

const JSX = { createElement: React.createElement };

@injectable()
export class ViewportBar extends ReactUIExtension implements IActionHandler {
static readonly ID = 'ivy-viewport-bar';

@inject(TYPES.IActionDispatcher) protected readonly actionDispatcher: IActionDispatcher;
@inject(TYPES.IToolManager) protected readonly toolManager: IToolManager;
@inject(SelectionService) protected selectionService: SelectionService;

protected zoomLevel = '100%';
protected zoomLevelElement?: HTMLElement;

id(): string {
return ViewportBar.ID;
}
containerClass(): string {
return ViewportBar.ID;
}

protected initializeContainer(container: HTMLElement): void {
super.initializeContainer(container);
container.onwheel = ev => (ev.ctrlKey ? ev.preventDefault() : true);
}

protected render(): React.ReactNode {
return (
<div className='viewport-bar'>
<div className='viewport-bar-tools'>
{this.createViewportButton(new OriginScreenButton())}
{this.createViewportButton(new FitToScreenButton())}
{this.createViewportButton(new CenterButton(() => [...this.selectionService.getSelectedElementIDs()]))}
<label>{this.zoomLevel}</label>
</div>
</div>
);
}

protected createViewportButton(toolButton: ViewportBarButton): React.ReactNode {
return (
<button
id={toolButton.id}
title={toolButton.title}
onClick={() => {
this.actionDispatcher.dispatch(toolButton.action()).then(() => {
const model = this.editorContext.modelRoot;
if (isViewport(model)) {
this.actionDispatcher.dispatchAll([
SetUIExtensionVisibilityAction.create({
extensionId: QuickActionUI.ID,
visible: true,
contextElementsId: [...this.selectionService.getSelectedElementIDs()]
}),
SetViewportAction.create(model.id, model, {})
]);
}
});
}}
>
<IvyIcon icon={toolButton.icon} />
</button>
);
}

handle(action: Action) {
if (EnableViewportAction.is(action)) {
this.actionDispatcher.dispatch(SetUIExtensionVisibilityAction.create({ extensionId: ViewportBar.ID, visible: true }));
}
if (SetViewportAction.is(action)) {
this.updateZoomLevel(action.newViewport.zoom);
} else if (SetViewportZoomAction.is(action)) {
this.updateZoomLevel(action.zoom);
}
}

private updateZoomLevel(zoom: number): void {
this.zoomLevel = (zoom * 100).toFixed(0).toString() + '%';
if (this.zoomLevelElement) {
this.zoomLevelElement.textContent = this.zoomLevel;
}
}
}
Loading