Skip to content

Commit

Permalink
impl, refactoring
Browse files Browse the repository at this point in the history
Issue imixs#221
  • Loading branch information
rsoika committed Mar 21, 2023
1 parent 420b850 commit 52e48b6
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 52 deletions.
55 changes: 47 additions & 8 deletions doc/BPMN_PROPERTIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,53 @@ The schemata is generated server side by the corresponding BPMN Extension by imp
....
}



### Custom Renderer (deprecated)
# Action Events

Most of the data to be displayed can be handled by JsonForms out of the box, so we only need to provide the corresponding schemata. But in some cases - e.g. the BPMN Event definitions - the corresponding input form is more complex. For this we implement a [custom renderer](https://jsonforms.io/docs/tutorial/custom-renderers) to provide an optimized input form.
The BPMN Property Panel reacts and produces different Action events to updated data in both directions.

The custom renderer for Event definitions splits into tree parts:
## The SelectionListener

* EventDefinitionForm - shows the HTML part implemented in React
* EventDefinitionControl - the JsonForms Control element
* EventDefinitionTester - the tester class deciding if the control should be used.
Mainly on the BPMN PropertyPanel reacts on the GLSP SelectionChanged event. If the selection changed and onyl one BPMN Element was selected In this case the panel refreshes the content for the Form Panel with the UISchema and Data provided by the selected element. Each BPMN Element provides the following arguments holding the corresponding JsonForms schema:

* JSONFormsData - the data holding key/value pairs
* JSONFormsSchema - the data schema
* JSONFormsUISchema - the UI schema

In addition the panel also computes the last selected category. This is used in the setState method to restore the last category if the element type has not changed.

## The BPMNPropertyPanelToggleAction

The `BPMNPropertyPanelToggleAction` is a custom action used by Open-BPMN to toggle the display state of the Property Panel. The panel either expands tis height to 50% or minimizes the panel. This is a client side action event triggered by custom menu commands defined for the Theia and VSCode integration.

## The BPMNApplyPropertiesUpdateOperation

The `BPMNApplyPropertiesUpdateOperation` is a client side custom action, that directly manipulates the BPMN model representation on server side. The action is send from the client property panel to the server each time the data in an input field changed (onchange).

The action is handled on the server by the `BPMNApplyPropertiesUpdateOperationHandler` which is responsible for updating the model representation accordingly.

The Action is providing the Element ID, a JSON data structure with the new/updated data and an optional category. The category can be used to update only parts on an BPMN element. This is to optimize the update performance as the data structure can become very complex or various BPMN elements.

The update process is handled by the [Extension Mechanism](./BPMN_EXTENSIONS.md) in a transparent way.

## The BPMNPropertyPanelUpdateAction

In some cases the `BPMNApplyPropertiesUpdateOperation` causes on the server side a more complex data update that requires an update of the property panel on the client side too. For example adding a new Signal or Diagram Definition is such a situation where the complete panel need to be updated on the client side.

In this cases the server sends a `BPMNPropertyPanelUpdateAction` to the client. The BPMN Property Panel reacts on this kind of action and updates the panel content.

# Custom Renderer (Imixs-Workflow)

Most of the data to be displayed can be handled by JsonForms out of the box, so we only need to provide the corresponding schemata. But in some cases - e.g. the Imixs-Workflow extension - the corresponding input form is more complex. For this we implement a [custom renderer](https://jsonforms.io/docs/tutorial/custom-renderers) to provide an optimized input form.

## SelectItemControl

The `SelectItemControl` is a custom renderer for selectItems represented as RadioButtons, CheckBoxes or ComboBoxes.
The control can handle single String values (represented as a Radio Button or ComboBox) or Arrays of Strings (represented as Checkboxes).

In addition the renderer support label|value pairs separated by a | char.

`My Value | val-1`

This allows to separate the label and data value in one string.

In addition the layout for Checkboxes and RadioButtons can be customized by the option `orientation=horizontal|vertical`.
138 changes: 99 additions & 39 deletions open-bpmn.glsp-client/open-bpmn-properties/src/bpmn-property-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
IActionHandler,
ICommand,
SetUIExtensionVisibilityAction,
SModelElement,
SModelRoot,
TYPES
} from 'sprotty';
Expand Down Expand Up @@ -56,6 +57,7 @@ export class BPMNPropertyPanel extends AbstractUIExtension implements SelectionL
protected bodyDiv: HTMLElement;
protected headerDiv: HTMLElement;
modelRootId: string;
modelRoot: Readonly<SModelRoot>;
selectedElementId: string;
initForm: boolean;
lastCategory: string;
Expand Down Expand Up @@ -185,6 +187,13 @@ export class BPMNPropertyPanel extends AbstractUIExtension implements SelectionL
return headerTools;
}

/**
* Helper method to resize the property panel
*
* @param button
* @param toolId
* @returns
*/
protected onClickResizePanel(button: HTMLElement, toolId?: string) {
return (_ev: MouseEvent) => {
if (!this.editorContext.isReadonly) {
Expand All @@ -203,12 +212,17 @@ export class BPMNPropertyPanel extends AbstractUIExtension implements SelectionL
}

/*
* We react on the EnableToolPaletteAction to make the property panel visible
* We react on different event actions.
*
* The EnableToolPaletteAction is used to make the property panel visible
* If we already have the bodyDiv defined, than we skip this event.
* See Discussion: https://github.com/eclipse-glsp/glsp/discussions/910
*
* In addition we also handle the BPMNPropertyPanelToggleAction to show/hide the
* propertyPanel.
* The BPMNPropertyPanelToggleAction is used to show/hide the
* propertyPanel. This action is called by context menus.
*
* The BPMNPropertyPanelUpdateAction is send by the server during an update
* of the data state and indicates that the property panel need to be refreshed.
*/
handle(action: Action): ICommand | Action | void {
// check enable state
Expand Down Expand Up @@ -241,25 +255,30 @@ export class BPMNPropertyPanel extends AbstractUIExtension implements SelectionL
}
this.panelToggle=!this.panelToggle;
}

// Update content of the property panel. Action is triggered by the server
if (action.kind === BPMNPropertyPanelUpdateAction.KIND) {
this.updatePropertyPanel(this.selectedElementId);
}
}

/*
* This method reacts on the selection of a BPMN element
* and updates the property panel input fields
* This method reacts on the selection of a BPMN element. It can handel different
* situations of a selection and updates the property panel input fields.
*
* The method also computes the last selected category. This is used in the setState method
* to restore the last category if the element type has not changed.
*/
selectionChanged(root: Readonly<SModelRoot>, selectedElements: string[]): void {
this.modelRoot=root;
// return if we do not yet have a body DIV.
if (!this.bodyDiv) {
return;
}

if (selectedElements && selectedElements.length>0) {
// first we need to verify if a Symbol/BPMNLabel combination was selected.
// In this case we are only interested in the BPMNFlowElement and not in the label
if (selectedElements.length > 1) {
if (selectedElements.length > 1) {
const filteredArr = selectedElements.filter(val => !val.endsWith('_bpmnlabel'));
selectedElements = filteredArr;
}
Expand All @@ -275,45 +294,74 @@ export class BPMNPropertyPanel extends AbstractUIExtension implements SelectionL
}

// if no element is selected we default to the root element (diagram plane)
let element;
let _id;
if (!selectedElements || selectedElements.length===0) {
element=root;
_id=root.id;
} else if (selectedElements.length === 1 ) {
// we have exactly one element selected.
element = root.index.getById(selectedElements[0]);
_id=selectedElements[0];
}

// avoid message loop...
if (!_id || _id === this.selectedElementId || _id === 'EMPTY') {
// skip this event!
return;
}

// now if we have an element we show teh panel..
if (element) {
if (_id) {
// did we have a change?
// avoid message loop...
if (element.id === this.selectedElementId) {
// skip this event!
return;
}
if (element.id === 'EMPTY') {
// skip this event!
return;
}

// compute the current category....
this.updateLastCategory();

// set new selectionId
this.selectedElementId = element.id;
this.selectedElementId = _id;
// because the jsonForms send a onchange event after init we mark this state here
this.initForm = true;
// update header
if (element===root) {
this.headerTitle.textContent = 'Default Process';
} else {
this.headerTitle.textContent = element.type;
}

// init the react container only once....
if (!this.panelContainer) {
this.panelContainer = createRoot(this.bodyDiv);
}
// finally update the panel
this.updatePropertyPanel(_id);
} else {
// no single element selected!
this.selectedElementId = '';
if (this.bodyDiv && this.panelContainer) {
this.headerTitle.textContent = 'BPMN Properties';
// multi selection - we can not show a property panel
this.panelContainer.render(<React.Fragment>Please select a single element </React.Fragment>);
}
}
}

/**
* This helper method is responsible to refresh teh property panel.
* The method loads the element from the root model context and updates
* the JsonForms schemata.
*
* @param _elementID
*/
updatePropertyPanel(_elementID: string): void {
// return if we do not yet have a body DIV.
if (!this.bodyDiv) {
return;
}
// compute the current category....
this.updateLastCategory();

let element: SModelElement | undefined;
// first we load the element from the model root
if (this.modelRoot.id===_elementID) {
element=this.modelRoot;
this.headerTitle.textContent = 'Default Process';
} else {
// load element from index#
element=this.modelRoot.index.getById(_elementID);
if (element) {
this.headerTitle.textContent = element.type;
}
}

// update the property panel if we have a valid element
if (element) {
// BPMN Node selected, collect JSONForms schemata....
let bpmnPropertiesData;
let bpmnPropertiesSchema;
Expand Down Expand Up @@ -351,13 +399,9 @@ export class BPMNPropertyPanel extends AbstractUIExtension implements SelectionL
this.headerTitle.textContent = 'BPMN Properties';
}
} else {
// no single element selected!
// we have no UISchema!
this.selectedElementId = '';
if (this.bodyDiv && this.panelContainer) {
this.headerTitle.textContent = 'BPMN Properties';
// multi selection - we can not show a property panel
this.panelContainer.render(<React.Fragment>Please select a single element </React.Fragment>);
}
this.headerTitle.textContent = 'BPMN Properties';
}
}

Expand Down Expand Up @@ -426,7 +470,7 @@ export interface BPMNPropertyPanelToggleAction extends Action {
}

export namespace BPMNPropertyPanelToggleAction {
export const KIND = 'properties';
export const KIND = 'propertyPanelToggle';

export function is(object: any): object is BPMNPropertyPanelToggleAction {
return Action.hasKind(object, KIND);
Expand All @@ -436,3 +480,19 @@ export namespace BPMNPropertyPanelToggleAction {
return { kind: KIND };
}
}

export interface BPMNPropertyPanelUpdateAction extends Action {
kind: typeof BPMNPropertyPanelUpdateAction.KIND;
// selectedElementID: string;
// category: string;
}

export namespace BPMNPropertyPanelUpdateAction {
export const KIND = 'propertyPanelUpdate';
export function is(object: any): object is BPMNPropertyPanelUpdateAction {
return Action.hasKind(object, KIND);
}
export function create(): BPMNPropertyPanelUpdateAction {
return { kind: KIND };
}
}
6 changes: 4 additions & 2 deletions open-bpmn.glsp-client/open-bpmn-properties/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import { EnableToolPaletteAction, TYPES } from '@eclipse-glsp/client';
import { ContainerModule } from 'inversify';
import { configureActionHandler } from 'sprotty';
import { BPMNPropertyPanel, BPMNPropertyPanelToggleAction } from './bpmn-property-panel';
import { BPMNPropertyPanel, BPMNPropertyPanelToggleAction, BPMNPropertyPanelUpdateAction } from './bpmn-property-panel';
// css styles
import '../css/bpmn-properties.css';
import '../css/jsonforms-theia.css';
Expand All @@ -26,5 +26,7 @@ export const BPMNPropertyModule = new ContainerModule((bind, _unbind, isBound, r
bind(TYPES.IUIExtension).toService(BPMNPropertyPanel);
configureActionHandler({ bind, isBound }, EnableToolPaletteAction.KIND, BPMNPropertyPanel);
configureActionHandler({ bind, isBound }, BPMNPropertyPanelToggleAction.KIND, BPMNPropertyPanel);
configureActionHandler({ bind, isBound }, BPMNPropertyPanelUpdateAction.KIND, BPMNPropertyPanel);
});
export { BPMNPropertyPanelToggleAction } from './bpmn-property-panel';
export { BPMNPropertyPanel, BPMNPropertyPanelToggleAction, BPMNPropertyPanelUpdateAction } from './bpmn-property-panel';

Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.glsp.graph.GModelElement;
import org.eclipse.glsp.server.actions.ActionDispatcher;
import org.openbpmn.bpmn.BPMNModel;
import org.openbpmn.bpmn.BPMNTypes;
import org.openbpmn.bpmn.elements.BPMNProcess;
Expand All @@ -39,6 +40,7 @@
import org.openbpmn.glsp.jsonforms.UISchemaBuilder;
import org.openbpmn.glsp.jsonforms.UISchemaBuilder.Layout;
import org.openbpmn.glsp.model.BPMNGModelState;
import org.openbpmn.glsp.operations.BPMNPropertyPanelUpdateAction;
import org.w3c.dom.Element;

import com.google.inject.Inject;
Expand All @@ -55,6 +57,9 @@ public class DefaultBPMNDefinitionsExtension extends AbstractBPMNElementExtensio

private static Logger logger = LogManager.getLogger(DefaultBPMNDefinitionsExtension.class);

@Inject
protected ActionDispatcher actionDispatcher;

@Inject
protected BPMNGModelState modelState;

Expand Down Expand Up @@ -146,17 +151,19 @@ public void updatePropertiesData(final JsonObject json, final String category, f
JsonArray signalSetValues = json.getJsonArray("signals");

if (signalSetValues != null) {
for (JsonValue laneValue : signalSetValues) {
JsonObject signalData = (JsonObject) laneValue;
for (JsonValue signalValue : signalSetValues) {
JsonObject signalData = (JsonObject) signalValue;
String id = signalData.getString("id", null);
Signal signal = (Signal) modelState.getBpmnModel().findElementById(id);
if (signal != null) {
signal.setName(signalData.getString("name"));
} else {
// signal did not yet exist in definition list - so we create a new one
int i = modelState.getBpmnModel().getSignals().size() + 1;
String newSignalID = "signal_" + i;
String newSignalName = "Signal " + i;
try {
modelState.getBpmnModel().addSignal("signal_" + i, "Signal " + i);
modelState.getBpmnModel().addSignal(newSignalID, newSignalName);
} catch (BPMNModelException e) {
logger.warn("Unable to add new signal: " + e.getMessage());
}
Expand All @@ -168,6 +175,10 @@ public void updatePropertiesData(final JsonObject json, final String category, f
}
if (update) {
modelState.reset();
// send an update for the property panel to the client...
actionDispatcher
.dispatchAfterNextUpdate(new BPMNPropertyPanelUpdateAction());

}
}
}
Expand Down
Loading

0 comments on commit 52e48b6

Please sign in to comment.