Skip to content
Merged
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: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ The central architectural concept is the **plugin system**. This package extends
- Each plugin is constructed with `(bpmnVisualization, options)` parameters
- Plugins must implement `getPluginId()` to return a unique identifier
- Plugins can optionally implement the `onConfigure(options)` lifecycle hook for post-construction setup (called by `BpmnVisualization`, not by client code)
- Retrieve plugins using `bpmnVisualization.getPlugin<PluginType>(pluginId)`
- Retrieve plugins using `bpmnVisualization.getPlugin<PluginType>(pluginId)`, which returns `undefined` when no plugin is registered with this identifier

3. **Available Plugins** (in `packages/addons/src/plugins/`):
- `CssClassesPlugin`: Manipulate CSS classes on BPMN elements
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0001-plugin-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ The plugin system is implemented in `packages/addons/src/plugins-support.ts`:
- `onLoadError(error)` runs when a `load` call fails, before the error is rethrown to the caller. Implement it to roll back partial work; it does not swallow the error.
- `onDispose()` runs when the instance is disposed, before the underlying core resources are released, so the instance and the BPMN model are still accessible. Implement it to release everything the plugin acquired.
- Plugins are passed to the constructor through `options.plugins`. They are instantiated with `(bpmnVisualization, options)` and stored in a per-instance registry.
- Consumers retrieve a plugin with `getPlugin<PluginType>(pluginId)` and then call its methods.
- Consumers retrieve a plugin with `getPlugin<PluginType>(pluginId)` and then call its methods. The lookup returns `undefined` when no plugin is registered with this id.

The plugin lifecycle is therefore:

Expand Down
6 changes: 4 additions & 2 deletions packages/addons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,10 @@ const bpmnVisualization = new BpmnVisualization({
container: 'bpmn-container',
plugins: [MyPlugin]
});
// Retrieve the plugin by id. The id is defined in the plugin implementation
const myPlugin = bpmnVisualization.getPlugin<MyPlugin>('my-plugin');
// Retrieve the plugin by id. The id is defined in the plugin implementation.
// `getPlugin` returns `undefined` when no plugin is registered with this id, hence the non-null assertion here: the
// plugin has just been registered above.
const myPlugin = bpmnVisualization.getPlugin<MyPlugin>('my-plugin')!;
myPlugin.aMethod();
```

Expand Down
10 changes: 8 additions & 2 deletions packages/addons/src/plugins-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,14 @@ export class BpmnVisualization extends BaseBpmnVisualization {
this.forEachPlugin(plugin => plugin.onLoadSuccess?.());
}

getPlugin<T extends Plugin>(id: PluginIds): T {
return this.plugins.get(id) as T;
/**
* Retrieve a plugin registered on this instance.
*
* @param id The identifier of the plugin, as returned by its {@link Plugin.getPluginId} implementation.
* @returns The plugin registered with this identifier, or `undefined` when no plugin has been registered with it.
*/
getPlugin<T extends Plugin>(id: PluginIds): T | undefined {
return this.plugins.get(id) as T | undefined;
}

private readonly registerPlugins = (options: GlobalOptions): void => {
Expand Down
21 changes: 13 additions & 8 deletions packages/addons/test/spec/plugins-support.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,19 @@ class MyCustomPlugin1 implements Plugin {

test('Load a typed plugin and use it', () => {
const bpmnVisualization = new BpmnVisualization({ container: undefined!, plugins: [MyCustomPlugin1] });
const plugin = bpmnVisualization.getPlugin<MyCustomPlugin1>('custom-plugin-1');
const plugin = bpmnVisualization.getPlugin<MyCustomPlugin1>('custom-plugin-1')!;
expect(plugin).toBeInstanceOf(MyCustomPlugin1);
expect(plugin.doSomethingSpecial()).toBe(5);
});

test('Retrieve a plugin with an identifier that is not registered', () => {
const bpmnVisualization = new BpmnVisualization({ container: undefined!, plugins: [MyCustomPlugin1] });
expect(bpmnVisualization.getPlugin('unknown')).toBeUndefined();
});

test('Load a untyped plugin and use it', () => {
const bpmnVisualization = new BpmnVisualization({ container: undefined!, plugins: [MyCustomPlugin1] });
const plugin = bpmnVisualization.getPlugin('custom-plugin-1');
const plugin = bpmnVisualization.getPlugin('custom-plugin-1')!;
expect(plugin).toBeInstanceOf(MyCustomPlugin1);
expect(plugin.getPluginId()).toBe('custom-plugin-1');
expect((plugin as MyCustomPlugin1).doSomethingSpecial()).toBe(5);
Expand All @@ -67,7 +72,7 @@ test('Load several plugins and use them', () => {
const plugin1 = bpmnVisualization.getPlugin('custom-plugin-1');
expect(plugin1).toBeInstanceOf(MyCustomPlugin1);

const plugin2 = bpmnVisualization.getPlugin<MyCustomPlugin2>('custom-plugin-2');
const plugin2 = bpmnVisualization.getPlugin<MyCustomPlugin2>('custom-plugin-2')!;
expect(plugin2).toBeInstanceOf(MyCustomPlugin2);
expect(plugin2.doSomethingSpecial()).toBe('I am awesome');
});
Expand Down Expand Up @@ -141,7 +146,7 @@ describe('Ensure that plugins are configured', () => {

test('Ensure that the configurable plugin is configured after BpmnVisualization initialization', () => {
const bpmnVisualization = new BpmnVisualization({ container: undefined!, customValue: 'custom in options', plugins: [ConfigurablePlugin] } as CustomGlobalOptions);
const configurablePlugin = bpmnVisualization.getPlugin<ConfigurablePlugin>('custom-configurable-plugin');
const configurablePlugin = bpmnVisualization.getPlugin<ConfigurablePlugin>('custom-configurable-plugin')!;
expect(configurablePlugin.isConfigured).toBeTruthy();
expect(configurablePlugin.customValue).toBe('custom in options'); // ensure that the options are passed to the plugin configuration
});
Expand Down Expand Up @@ -176,8 +181,8 @@ describe('Ensure that plugins are disposed', () => {

test('Call onDispose on plugins that implement it and ignore the others when disposing BpmnVisualization', () => {
const bpmnVisualization = new BpmnVisualization({ container: undefined!, plugins: [DisposablePlugin1, PluginWithoutOptionalMethods, DisposablePlugin2] });
const disposablePlugin1 = bpmnVisualization.getPlugin<DisposablePlugin1>('custom-disposable-plugin-1');
const disposablePlugin2 = bpmnVisualization.getPlugin<DisposablePlugin2>('custom-disposable-plugin-2');
const disposablePlugin1 = bpmnVisualization.getPlugin<DisposablePlugin1>('custom-disposable-plugin-1')!;
const disposablePlugin2 = bpmnVisualization.getPlugin<DisposablePlugin2>('custom-disposable-plugin-2')!;

expect(() => bpmnVisualization.dispose()).not.toThrow();
expect(disposablePlugin1.onDispose).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -212,8 +217,8 @@ const setupLoadAwareVisualization = (): { bpmnVisualization: BpmnVisualization;
const bpmnVisualization = new BpmnVisualization({ container: insertBpmnContainerWithoutId(), plugins: [LoadAwarePlugin1, PluginWithoutOptionalMethods, LoadAwarePlugin2] });
return {
bpmnVisualization,
loadAwarePlugin1: bpmnVisualization.getPlugin<LoadAwarePlugin1>('custom-load-aware-plugin-1'),
loadAwarePlugin2: bpmnVisualization.getPlugin<LoadAwarePlugin2>('custom-load-aware-plugin-2'),
loadAwarePlugin1: bpmnVisualization.getPlugin<LoadAwarePlugin1>('custom-load-aware-plugin-1')!,
loadAwarePlugin2: bpmnVisualization.getPlugin<LoadAwarePlugin2>('custom-load-aware-plugin-2')!,
};
};

Expand Down
2 changes: 1 addition & 1 deletion packages/addons/test/spec/plugins/css-classes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ beforeEach(() => {
// The actual implementation is in `bpmn-visualization`. Here, we only validate that the `bpmn-visualization` code is called.
describe('CssClassesPlugin', () => {
const bpmnVisualization = new BpmnVisualization({ container: insertBpmnContainerWithoutId(), plugins: [CssClassesPlugin] });
const cssClassesPlugin = bpmnVisualization.getPlugin<CssClassesPlugin>('css');
const cssClassesPlugin = bpmnVisualization.getPlugin<CssClassesPlugin>('css')!;

describe('addCssClasses', () => {
test('Pass a single id', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/addons/test/spec/plugins/elements.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { readFileSync } from '../../shared/io-utilities.js';
describe('Check ElementsPlugin methods', () => {
const bpmnVisualization = new BpmnVisualization({ container: insertBpmnContainerWithoutId(), plugins: [ElementsPlugin] });
bpmnVisualization.load(readFileSync('./fixtures/bpmn/search-elements.bpmn'));
const elementsPlugin = bpmnVisualization.getPlugin<ElementsPlugin>('elements');
const elementsPlugin = bpmnVisualization.getPlugin<ElementsPlugin>('elements')!;

test('getElementsByIds', () => {
const bpmnElements = elementsPlugin.getElementsByIds('Gateway_0t7d2lu');
Expand Down
18 changes: 9 additions & 9 deletions packages/addons/test/spec/plugins/overlays.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe('setVisible', () => {
const bpmnVisualization = new BpmnVisualization({ container: insertBpmnContainerWithoutId(), plugins: [OverlaysPlugin] });
bpmnVisualization.load(readFileSync('./fixtures/bpmn/1_pool_custom_colors_with_1_text-annotation.bpmn'));

const plugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays');
const plugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays')!;
plugin.setVisible(false);
expect(new ContainersRetriever(bpmnVisualization).getOverlaysContainer()).not.toBeVisible();
});
Expand All @@ -98,7 +98,7 @@ describe('setVisible', () => {
const bpmnVisualization = new BpmnVisualization({ container: insertBpmnContainerWithoutId(), plugins: [OverlaysPlugin] });
bpmnVisualization.load(readFileSync('./fixtures/bpmn/1_pool_custom_colors_with_1_text-annotation.bpmn'));

const plugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays');
const plugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays')!;
plugin.setVisible(false);
plugin.setVisible();
const overlaysContainer = new ContainersRetriever(bpmnVisualization).getOverlaysContainer();
Expand All @@ -112,7 +112,7 @@ describe('setVisible', () => {
overlaysContainer.style.display = 'inherit';
expect(overlaysContainer).toHaveStyle('display: inherit');

const plugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays');
const plugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays')!;
plugin.setVisible(false);
plugin.setVisible();
expect(overlaysContainer).toHaveStyle('display: inherit');
Expand All @@ -124,7 +124,7 @@ describe('setVisible', () => {
overlaysContainer.style.display = 'inherit';
expect(overlaysContainer).toHaveStyle('display: inherit');

const plugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays');
const plugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays')!;
plugin.setVisible(false);
plugin.setVisible(false);
plugin.setVisible();
Expand All @@ -137,7 +137,7 @@ describe('setVisible', () => {
overlaysContainer.style.display = 'inherit';
expect(overlaysContainer).toHaveStyle('display: inherit');

const plugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays');
const plugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays')!;
plugin.setVisible();
expect(overlaysContainer).toHaveStyle('display: inherit');
});
Expand All @@ -146,7 +146,7 @@ describe('setVisible', () => {
const bpmnVisualization = new BpmnVisualization({ container: insertBpmnContainerWithoutId(), plugins: [OverlaysPlugin] });
bpmnVisualization.load(readFileSync('./fixtures/bpmn/1_pool_custom_colors_with_1_text-annotation.bpmn'));

const plugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays');
const plugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays')!;
plugin.setVisible(false);
plugin.setVisible(false);
expect(new ContainersRetriever(bpmnVisualization).getOverlaysContainer()).not.toBeVisible();
Expand All @@ -156,7 +156,7 @@ describe('setVisible', () => {
const bpmnVisualization = new BpmnVisualization({ container: insertBpmnContainerWithoutId(), plugins: [OverlaysPlugin] });
bpmnVisualization.load(readFileSync('./fixtures/bpmn/1_pool_custom_colors_with_1_text-annotation.bpmn'));

const plugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays');
const plugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays')!;
plugin.setVisible();
plugin.setVisible(true);
plugin.setVisible();
Expand All @@ -167,7 +167,7 @@ describe('setVisible', () => {
const bpmnVisualization = new BpmnVisualization({ container: insertBpmnContainerWithoutId(), plugins: [OverlaysPlugin] });
bpmnVisualization.load(readFileSync('./fixtures/bpmn/1_pool_custom_colors_with_1_text-annotation.bpmn'));

const plugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays');
const plugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays')!;
plugin.setVisible(false);
plugin.setVisible();
plugin.setVisible(false);
Expand Down Expand Up @@ -216,7 +216,7 @@ function createOverlay(label: string): Overlay {

describe('Add and remove Overlays', () => {
const bpmnVisualization = new BpmnVisualization({ container: insertBpmnContainerWithoutId(), plugins: [OverlaysPlugin] });
const overlaysPlugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays');
const overlaysPlugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays')!;
const overlaysExpectation = new OverlaysExpectation(bpmnVisualization);

beforeEach(() => {
Expand Down
4 changes: 2 additions & 2 deletions packages/addons/test/spec/plugins/style.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ beforeEach(() => {
// The actual implementation is in `bpmn-visualization`. Here, we only validate that the `bpmn-visualization` code is called.
describe('StylePlugin', () => {
const bpmnVisualization = new BpmnVisualization({ container: insertBpmnContainerWithoutId(), plugins: [StylePlugin] });
const stylePlugin = bpmnVisualization.getPlugin<StylePlugin>('style');
const stylePlugin = bpmnVisualization.getPlugin<StylePlugin>('style')!;

describe('updateStyle', () => {
test('Pass a single id', () => {
Expand Down Expand Up @@ -79,7 +79,7 @@ describe('StylePlugin', () => {
describe('StyleByNamePlugin', () => {
const bpmnVisualization = new BpmnVisualization({ container: insertBpmnContainerWithoutId(), plugins: [StyleByNamePlugin] });
bpmnVisualization.load(readFileSync('./fixtures/bpmn/search-elements.bpmn'));
const styleByNamePlugin = bpmnVisualization.getPlugin<StyleByNamePlugin>('style-by-name');
const styleByNamePlugin = bpmnVisualization.getPlugin<StyleByNamePlugin>('style-by-name')!;

describe('updateStyle', () => {
test('Pass a single name related to an existing element', () => {
Expand Down
15 changes: 11 additions & 4 deletions packages/check-ts-support/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { BpmnElementsIdentifier, PathResolver } from '@process-analytics/bpmn-visualization-addons';
import { BpmnVisualization } from 'bpmn-visualization';
import { BpmnElementsIdentifier, BpmnVisualization, CssClassesPlugin, PathResolver } from '@process-analytics/bpmn-visualization-addons';

// bpmn-visualization
const bpmnVisualization = new BpmnVisualization({ container: 'bpmn-container' });
// bpmn-visualization, through the BpmnVisualization subclass provided by the addons. Importing it from
// `bpmn-visualization` would also compile, but would not provide plugin support.
// The `plugins` property comes from the module augmentation of `GlobalOptions`. The demo checks it too, but only
// here is it checked against the lowest supported TypeScript version.
const bpmnVisualization = new BpmnVisualization({ container: 'bpmn-container', plugins: [CssClassesPlugin] });
bpmnVisualization.load(`fake BPMN content`);
const bpmnElementsRegistry = bpmnVisualization.bpmnElementsRegistry;

// addons: plugin retrieval, in both forms documented in the README
const cssClassesPlugin = bpmnVisualization.getPlugin<CssClassesPlugin>('css');
cssClassesPlugin?.addCssClasses('id_1', 'class_1');
bpmnVisualization.getPlugin<CssClassesPlugin>('css')!.addCssClasses('id_2', 'class_2');

// addons
const bpmnElementsIdentifier = new BpmnElementsIdentifier(bpmnElementsRegistry);
bpmnElementsIdentifier.isActivity('id_1');
Expand Down
2 changes: 1 addition & 1 deletion packages/demo/src/overlays.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const fitOptions: FitOptions = { type: FitType.Center, margin: 20 };
bpmnVisualization.load(diagram, { fit: fitOptions });

// Add overlays
const overlaysPlugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays');
const overlaysPlugin = bpmnVisualization.getPlugin<OverlaysPlugin>('overlays')!;
const overlayStyle = { stroke: { color: 'chartreuse' }, fill: { color: 'chartreuse' }, font: { color: 'white', size: 18 } };
// SRM subprocess
overlaysPlugin.addOverlays('Activity_0ec8azh', { label: '123', position: 'top-center', style: overlayStyle });
Expand Down
4 changes: 2 additions & 2 deletions packages/demo/src/path-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ const bpmnVisualization = new BpmnVisualization({
// Load the BPMN diagram defined above
const diagram = await fetchDiagram();
bpmnVisualization.load(diagram, { fit: { type: FitType.Center, margin: 20 } });
const elementsPlugin = bpmnVisualization.getPlugin<ElementsPlugin>('elements');
const stylePlugin = bpmnVisualization.getPlugin<StylePlugin>('style');
const elementsPlugin = bpmnVisualization.getPlugin<ElementsPlugin>('elements')!;
const stylePlugin = bpmnVisualization.getPlugin<StylePlugin>('style')!;

const pathResolver = new PathResolver(elementsPlugin);

Expand Down
2 changes: 1 addition & 1 deletion packages/demo/src/plugins-by-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ bpmnVisualization.load(diagram, { fit: fitOptions });
new ZoomComponent(bpmnVisualization, fitOptions).render();

// Use style by name plugin to update the style of the elements
const styleRegistryByName = bpmnVisualization.getPlugin<StyleByNamePlugin>('style-by-name');
const styleRegistryByName = bpmnVisualization.getPlugin<StyleByNamePlugin>('style-by-name')!;

function clearAllStyles(): void {
styleRegistryByName.resetStyle();
Expand Down
Loading