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

Add initial pool and swimlane implementation #314

Merged
merged 17 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from 12 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
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ module.exports = {
plugins: ['@typescript-eslint'],
root: true,
rules: {
'no-case-declarations': 'off',
'no-console': ['error', { allow: ['warn', 'error'] }],
'no-constant-condition': 'off',
'no-empty': 'off',
'no-extra-boolean-cast': 'off',
'no-prototype-builtins': 'off',
'no-useless-escape': 'off',
'prefer-const': 'off',
'no-case-declarations': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
Expand Down
3 changes: 3 additions & 0 deletions src/main/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@
"BPMNMessageFlow": "Nachricht",
"BPMNAssociationFlow": "Assoziation",
"BPMNDataObject": "Datenobjekt",
"BPMNPool": "Pool",
"BPMNSwimlane": "Bahn",
"BPMNCreateSwimlane": "+ Bahn",
"BPMNGroup": "Gruppe"
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/main/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@
"BPMNMessageFlow": "Message",
"BPMNAssociationFlow": "Association",
"BPMNDataObject": "Data Object",
"BPMNPool": "Pool",
"BPMNSwimlane": "Lane",
"BPMNCreateSwimlane": "+ Lane",
"BPMNGroup": "Group"
}
}
Expand Down
15 changes: 10 additions & 5 deletions src/main/packages/bpmn/bpmn-diagram-preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { BPMNTransaction } from './bpmn-transaction/bpmn-transaction';
import { BPMNCallActivity } from './bpmn-call-activity/bpmn-call-activity';
import { BPMNAnnotation } from './bpmn-annotation/bpmn-annotation';
import { BPMNConversation } from './bpmn-conversation/bpmn-conversation';
import { BPMNPool } from './bpmn-pool/bpmn-pool';
import { BPMNSwimlane } from './bpmn-swimlane/bpmn-swimlane';
import { BPMNDataObject } from './bpmn-data-object/bpmn-data-object';
import { BPMNGroup } from './bpmn-group/bpmn-group';

Expand All @@ -19,7 +21,7 @@ export const composeBPMNPreview: ComposePreview = (
translate: (id: string) => string,
): PreviewElement[] => {
const elements: PreviewElement[] = [];
const defaultBounds: IBoundary = { x: 0, y: 0, width: 150, height: computeDimension(1.0, 60) };
const defaultBounds: IBoundary = { x: 0, y: 0, width: 150, height: 60 };

elements.push(
new BPMNTask({
Expand Down Expand Up @@ -64,28 +66,24 @@ export const composeBPMNPreview: ComposePreview = (

elements.push(
new BPMNStartEvent({
name: translate('packages.BPMN.BPMNStartEvent'),
bounds: { x: 0, y: 0, width: 40, height: 40 },
}),
);

elements.push(
new BPMNIntermediateEvent({
name: translate('packages.BPMN.BPMNIntermediateEvent'),
bounds: { x: 0, y: 0, width: 40, height: 40 },
}),
);

elements.push(
new BPMNEndEvent({
name: translate('packages.BPMN.BPMNEndEvent'),
bounds: { x: 0, y: 0, width: 40, height: 40 },
}),
);

elements.push(
new BPMNGateway({
name: translate('packages.BPMN.BPMNGateway'),
bounds: { x: 0, y: 0, width: 40, height: 40 },
}),
);
Expand All @@ -96,5 +94,12 @@ export const composeBPMNPreview: ComposePreview = (
}),
);

elements.push(
new BPMNPool({
name: translate('packages.BPMN.BPMNPool'),
bounds: { x: 0, y: 0, width: 160, height: 80 },
}),
);

return elements;
};
1 change: 0 additions & 1 deletion src/main/packages/bpmn/bpmn-end-event/bpmn-end-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export class BPMNEndEvent extends UMLContainer {

type: UMLElementType = BPMNElementType.BPMNEndEvent;
bounds: IBoundary = { ...this.bounds, width: 40, height: 40 };
name = 'End Event';

constructor(values?: DeepPartial<UMLContainer>) {
super(values);
Expand Down
42 changes: 42 additions & 0 deletions src/main/packages/bpmn/bpmn-pool/bpmn-pool-component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, { FunctionComponent } from 'react';
import { BPMNPool } from './bpmn-pool';
import { ThemedRect } from '../../../components/theme/themedComponents';

export const BPMNPoolComponent: FunctionComponent<Props> = ({ element, children }) => {
return (
<g>
<ThemedRect
y={0}
width={BPMNPool.HEADER_WIDTH}
height={element.bounds.height}
strokeColor={element.strokeColor}
fillColor={element.fillColor}
/>
<ThemedRect
y={0}
x={BPMNPool.HEADER_WIDTH}
width={element.bounds.width - BPMNPool.HEADER_WIDTH}
height={element.bounds.height}
strokeColor={element.strokeColor}
fillColor={element.fillColor}
/>
<text
y={20}
x={-(element.bounds.height / 2)}
textAnchor="middle"
alignmentBaseline="middle"
transform="rotate(270)"
fontWeight="bold"
pointerEvents="none"
>
{element.name}
</text>
{children}
</g>
);
};

interface Props {
element: BPMNPool;
children?: React.ReactNode;
}
138 changes: 138 additions & 0 deletions src/main/packages/bpmn/bpmn-pool/bpmn-pool-update.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React, { Component, ComponentClass } from 'react';
import { connect } from 'react-redux';
import { compose } from 'redux';
import { Button } from '../../../components/controls/button/button';
import { Divider } from '../../../components/controls/divider/divider';
import { TrashIcon } from '../../../components/controls/icon/trash';
import { Textfield } from '../../../components/controls/textfield/textfield';
import { I18nContext } from '../../../components/i18n/i18n-context';
import { localized } from '../../../components/i18n/localized';
import { ModelState } from '../../../components/store/model-state';
import { styled } from '../../../components/theme/styles';
import { UMLElementRepository } from '../../../services/uml-element/uml-element-repository';
import { BPMNSwimlane } from '../bpmn-swimlane/bpmn-swimlane';
import { BPMNPool } from './bpmn-pool';
import { uuid } from '../../../utils/uuid';
import { notEmpty } from '../../../utils/not-empty';
import { UMLElement } from '../../../services/uml-element/uml-element';
import { AsyncDispatch } from '../../../utils/actions/actions';
import { BPMNElementType } from '../index';

interface OwnProps {
element: BPMNPool;
}

type StateProps = {};

interface DispatchProps {
create: typeof UMLElementRepository.create;
update: typeof UMLElementRepository.update;
delete: typeof UMLElementRepository.delete;
getById: (id: string) => UMLElement | null;
}

type Props = OwnProps & StateProps & DispatchProps & I18nContext;

const enhance = compose<ComponentClass<OwnProps>>(
localized,
connect<StateProps, DispatchProps, OwnProps, ModelState>(null, {
create: UMLElementRepository.create,
update: UMLElementRepository.update,
delete: UMLElementRepository.delete,
getById: UMLElementRepository.getById as any as AsyncDispatch<typeof UMLElementRepository.getById>,
}),
);

const Flex = styled.div`
display: flex;
align-items: baseline;
justify-content: space-between;
`;

class BPMNPoolUpdateComponent extends Component<Props> {
render() {
const { element } = this.props;

return (
<div>
<section>
<Flex>
<Textfield value={element.name} onChange={this.rename(element.id)} autoFocus />
<Button color="link" tabIndex={-1} onClick={this.delete(element.id)}>
<TrashIcon />
</Button>
</Flex>
<Divider />
</section>
<section>
<Button color="link" tabIndex={-1} onClick={this.insertSwimlane(element.id)}>
{this.props.translate('packages.BPMN.BPMNCreateSwimlane')}
</Button>
</section>
</div>
);
}

/**
* Rename the gateway
* @param id The ID of the gateway that should be renamed
*/
private rename = (id: string) => (value: string) => {
this.props.update(id, { name: value });
};

/**
* Delete a gateway
* @param id The ID of the gateway that should be deleted
*/
private delete = (id: string) => () => {
this.props.delete(id);
};

/**
* Insert a new lane into the pool. If there are already elements in the pool other than swimlanes, all existing
* elements will be moved to the newly created swimlane.
*
* @param id The ID of the pool into which a new swimlane should be inserted in.
*/
private insertSwimlane = (id: string) => () => {
// We resolve all non-empty children of the current pool from the redux store
const children = this.props.element.ownedElements.map((id) => this.props.getById(id)).filter(notEmpty);

// We then check if there is currently any direct children within the pool to determine whether we need to convert
// the pool to a swimlane-based pool or if we just need to insert a new swimlane.
const convertToSwimlaneBased = children.every((child) => child.type !== BPMNElementType.BPMNSwimlane);

// We then create a new swimlane object. If the pool is converted, the transfer the pools children to the swimlane
// and size the swimlane accordingly to fit all child elements.
const swimlane = new BPMNSwimlane({
id: uuid(),
name: this.props.translate('packages.BPMN.BPMNSwimlane'),
bounds: {
width: this.props.element.bounds.width - BPMNPool.HEADER_WIDTH,
height: convertToSwimlaneBased ? this.props.element.bounds.height : BPMNSwimlane.DEFAULT_HEIGHT,
},
owner: id,
//ownedElements: convertToSwimlaneBased ? this.props.element.ownedElements : [],
});

this.props.create(swimlane);

// We then update the pool element and remove the child elements that have been transferred to the newly created
// swim lane
const pool = new BPMNPool({
...this.props.element,
ownedElements: convertToSwimlaneBased ? [swimlane.id] : [swimlane.id, ...this.props.element.ownedElements],
});

this.props.update(id, pool);

// As the last step, all child elements that were transferred from the pool to a swimlane then have their owner
// field set to the new swimlane element.
if (convertToSwimlaneBased) {
children.forEach((child) => this.props.update(child.id, { owner: swimlane.id }));
}
};
}

export const BPMNPoolUpdate = enhance(BPMNPoolUpdateComponent);
70 changes: 70 additions & 0 deletions src/main/packages/bpmn/bpmn-pool/bpmn-pool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { BPMNElementType } from '..';
import { UMLElementType } from '../../uml-element-type';
import { ILayer } from '../../../services/layouter/layer';
import { ILayoutable } from '../../../services/layouter/layoutable';
import { UMLElementFeatures } from '../../../services/uml-element/uml-element-features';
import { UMLElement } from '../../../services/uml-element/uml-element';
import { UMLContainer } from '../../../services/uml-container/uml-container';
import { UMLModelElement } from '../../../typings';
import { UMLPackage } from '../../common/uml-package/uml-package';

export class BPMNPool extends UMLPackage {
static MIN_WIDTH = 80;
static MIN_HEIGHT = 80;
static HEADER_WIDTH = 40;

static features: UMLElementFeatures = {
...UMLElement.features,
droppable: true,
movable: true,
resizable: true,
connectable: false,
};

type: UMLElementType = BPMNElementType.BPMNPool;

hasSwimlanes = (children: ILayoutable[]) =>

Check notice on line 26 in src/main/packages/bpmn/bpmn-pool/bpmn-pool.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/main/packages/bpmn/bpmn-pool/bpmn-pool.ts#L26

Missing return type on function.
children.length > 0 &&
children.every((child: ILayoutable & { type?: UMLElementType }) => child.type === BPMNElementType.BPMNSwimlane);

render(layer: ILayer, children: ILayoutable[] = []): ILayoutable[] {
if (this.bounds.width < BPMNPool.MIN_WIDTH) {
this.bounds.width = BPMNPool.MIN_WIDTH;
}

// We determine if the current pool has swimlanes as a pool with lanes behaves different in regard to resizing
// compared to a pool without lanes
const hasSwimlanes = this.hasSwimlanes(children);

if (!hasSwimlanes) {
// If the pool does not have lanes, we simply return the pool and its child elements
return [this, ...children];
}

const calculatedContainerPoolHeight = children.reduce((acc, element) => acc + element.bounds.height, 0);

// We reverse the swim lane array to ensure that the lanes are rendered bottom to top, ensuring that
// the resize handles are not overlapped by the following lane.
const repositionedChildren = children.reverse().map((element, index) => ({
...element,
bounds: {
x: BPMNPool.HEADER_WIDTH,
y: index > 0 ? children[index - 1].bounds.y + children[index - 1].bounds.height : 0,
width: this.bounds.width - BPMNPool.HEADER_WIDTH - 10,
height: element.bounds.height,
},
}));

// If the pool has swimlanes, we set its height to the sum of the heights of the contained swimlanes
if (hasSwimlanes) {
this.bounds.height =
calculatedContainerPoolHeight < BPMNPool.MIN_HEIGHT ? BPMNPool.MIN_HEIGHT : calculatedContainerPoolHeight;
}

return [this, ...repositionedChildren];
}

serialize(children?: UMLElement[]): UMLModelElement {
return super.serialize(children);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export class BPMNStartEvent extends UMLContainer {

type: UMLElementType = BPMNElementType.BPMNStartEvent;
bounds: IBoundary = { ...this.bounds, width: 40, height: 40 };
name = 'Start Event';

constructor(values?: DeepPartial<UMLContainer>) {
super(values);
Expand Down
27 changes: 27 additions & 0 deletions src/main/packages/bpmn/bpmn-swimlane/bpmn-swimlane-component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { FunctionComponent } from 'react';
import { ThemedRect } from '../../../components/theme/themedComponents';
import { BPMNSwimlane } from './bpmn-swimlane';

export const BPMNSwimlaneComponent: FunctionComponent<Props> = ({ element, children }) => {
return (
<g>
<ThemedRect width={element.bounds.width} height={element.bounds.height} fillColor="transparent" />
<text
y={20}
x={-(element.bounds.height / 2)}
transform="rotate(270)"
textAnchor="middle"
alignmentBaseline="middle"
pointerEvents="none"
>
{element.name}
</text>
{children}
</g>
);
};

interface Props {
element: BPMNSwimlane;
children?: React.ReactNode;
}
Loading
Loading