Skip to content

Commit 48c678e

Browse files
authored
fix: keep CSS classes when calling the "Update Style" API (#2709)
Previously, calling the "Update Style" API after adding the classes with the CSS API removed the CSS classes from a given BPMN element. This is now fixed by setting the classes in the model instead of updating directly the state. The state is refreshed when calling the "Update Style" API and the manually added classes were removed. Basic integration tests have been added to check that the CSS classes are present in the style property of the mxGraph model. They only check a few use cases to help detecting regression They complement the existing tests checking that the classes are correctly set in the SVG nodes. The existing tests cover all use cases of the CSS API.
1 parent 7f5ac76 commit 48c678e

6 files changed

Lines changed: 86 additions & 27 deletions

File tree

src/component/mxgraph/GraphCellUpdater.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,14 @@ export default class GraphCellUpdater {
4545
}
4646

4747
private updateAndRefreshCssClassesOfElement(elementId: string, cssClasses: string[]): void {
48-
const mxCell = this.graph.getModel().getCell(elementId);
49-
if (!mxCell) {
48+
const cell = this.graph.getModel().getCell(elementId);
49+
if (!cell) {
5050
return;
5151
}
52-
const view = this.graph.getView();
53-
const state = view.getState(mxCell);
54-
state.style[BpmnStyleIdentifier.EXTRA_CSS_CLASSES] = cssClasses;
55-
state.shape.redraw();
56-
// Ensure that label classes are also updated. When there is no label, state.text is null
57-
state.text?.redraw();
52+
53+
let cellStyle = cell.getStyle();
54+
cellStyle = setStyle(cellStyle, BpmnStyleIdentifier.EXTRA_CSS_CLASSES, cssClasses.join(','));
55+
this.graph.model.setStyle(cell, cellStyle);
5856
}
5957

6058
addOverlays(bpmnElementId: string, overlays: Overlay | Overlay[]): void {

src/component/mxgraph/config/ShapeConfigurator.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,11 @@ export default class ShapeConfigurator {
150150
// 'this.state.style' = the style definition associated with the cell
151151
// 'this.state.cell.style' = the style applied to the cell: 1st element: style name = bpmn shape name
152152
const cell = this.state.cell;
153-
// dialect = strictHtml is set means that current node holds an html label
153+
// dialect = strictHtml is set means that current node holds an HTML label
154154
let allBpmnClassNames = computeAllBpmnClassNamesOfCell(cell, this.dialect === mxgraph.mxConstants.DIALECT_STRICTHTML);
155155
const extraCssClasses = this.state.style[BpmnStyleIdentifier.EXTRA_CSS_CLASSES];
156-
if (extraCssClasses) {
157-
allBpmnClassNames = allBpmnClassNames.concat(extraCssClasses);
156+
if (typeof extraCssClasses == 'string') {
157+
allBpmnClassNames = allBpmnClassNames.concat(extraCssClasses.split(','));
158158
}
159159

160160
this.node.setAttribute('class', allBpmnClassNames.join(' '));

test/integration/helpers/model-expect.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ type ExpectedModelElement = {
144144
stroke?: Stroke;
145145
verticalAlign?: string;
146146
opacity?: number;
147+
// custom bpmn-visualization
148+
extraCssClasses?: string[];
147149
};
148150

149151
export interface ExpectedShapeModelElement extends ExpectedModelElement {

test/integration/matchers/matcher-utils.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type { mxCell, mxGeometry, StyleMap } from 'mxgraph';
2020
import type { Opacity } from '@lib/component/registry';
2121
import type { MxGraphCustomOverlay, MxGraphCustomOverlayStyle } from '@lib/component/mxgraph/overlay/custom-overlay';
2222
import { getFontStyleValue as computeFontStyleValue } from '@lib/component/mxgraph/renderer/StyleComputer';
23+
import { BpmnStyleIdentifier } from '@lib/component/mxgraph/style';
2324
import { Font } from '@lib/model/bpmn/internal/Label';
2425
import MatcherContext = jest.MatcherContext;
2526
import CustomMatcherResult = jest.CustomMatcherResult;
@@ -45,6 +46,8 @@ export interface BpmnCellStyle extends StyleMap {
4546
endSize?: number;
4647
shape?: string;
4748
horizontal?: number;
49+
// custom bpmn-visualization
50+
extraCssClasses?: string[];
4851
}
4952

5053
export interface ExpectedCell {
@@ -142,6 +145,8 @@ export function buildExpectedCellStyleWithCommonAttributes(expectedModelElt: Exp
142145
fontColor: font?.color ?? 'Black',
143146
fontStyle: getFontStyleValue(font),
144147
fontOpacity: expectedModelElt.font?.opacity,
148+
// custom bpmn-visualization
149+
extraCssClasses: expectedModelElt.extraCssClasses,
145150
};
146151
}
147152

@@ -165,9 +170,10 @@ export function buildReceivedViewStateStyle(cell: mxCell, bv = bpmnVisualization
165170
* It returns the style + properties resolved from the referenced styleNames (generally at the beginning of the "cell.style" string) as computed by mxStylesheet.prototype.getCellStyle.
166171
*
167172
* @param cell The Cell to consider for the computation of the resolved style.
173+
* @param bv The instance of BpmnVisualization under test
168174
*/
169-
function buildReceivedResolvedModelCellStyle(cell: mxCell): BpmnCellStyle {
170-
return toBpmnStyle(bpmnVisualization.graph.getCellStyle(cell), cell.edge);
175+
export function buildReceivedResolvedModelCellStyle(cell: mxCell, bv = bpmnVisualization): BpmnCellStyle {
176+
return toBpmnStyle(bv.graph.getCellStyle(cell), cell.edge);
171177
}
172178

173179
function toBpmnStyle(rawStyle: StyleMap, isEdge: boolean): BpmnCellStyle {
@@ -184,6 +190,8 @@ function toBpmnStyle(rawStyle: StyleMap, isEdge: boolean): BpmnCellStyle {
184190
fontColor: rawStyle.fontColor,
185191
fontStyle: rawStyle.fontStyle,
186192
fontOpacity: rawStyle.textOpacity,
193+
// custom bpmn-visualization
194+
extraCssClasses: rawStyle[BpmnStyleIdentifier.EXTRA_CSS_CLASSES]?.split(','),
187195
};
188196

189197
if (isEdge) {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
Copyright 2023 Bonitasoft S.A.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { bpmnVisualization } from './helpers/model-expect';
18+
import { readFileSync } from '@test/shared/file-helper';
19+
20+
// Most of the test are done in dom.css.classes.test.ts
21+
// The tests here check that the style of the cell in the mxGraph model includes the CSS classes
22+
describe('mxGraph model - CSS API', () => {
23+
beforeEach(() => {
24+
bpmnVisualization.load(readFileSync('../fixtures/bpmn/registry/1-pool-3-lanes-message-start-end-intermediate-events.bpmn'));
25+
});
26+
27+
test('Add CSS classes on Shape', () => {
28+
bpmnVisualization.bpmnElementsRegistry.addCssClasses('userTask_2_2', ['class#1', 'class#2']);
29+
expect('userTask_2_2').toBeUserTask({
30+
extraCssClasses: ['class#1', 'class#2'],
31+
// not under test
32+
parentId: 'lane_02',
33+
label: 'User Task 2.2',
34+
});
35+
});
36+
37+
test('Add CSS classes on Edge', () => {
38+
bpmnVisualization.bpmnElementsRegistry.addCssClasses('sequenceFlow_lane_3_elt_3', ['class-1', 'class-2', 'class-3']);
39+
expect('sequenceFlow_lane_3_elt_3').toBeSequenceFlow({
40+
extraCssClasses: ['class-1', 'class-2', 'class-3'],
41+
// not under test
42+
parentId: 'lane_03',
43+
verticalAlign: 'bottom',
44+
});
45+
});
46+
});

test/integration/mxGraph.model.style.api.test.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ import { initializeBpmnVisualizationWithContainerId } from './helpers/bpmn-visua
1818
import { HtmlElementLookup } from './helpers/html-utils';
1919
import type { ExpectedShapeModelElement } from './helpers/model-expect';
2020
import { bpmnVisualization } from './helpers/model-expect';
21-
import { buildReceivedViewStateStyle } from './matchers/matcher-utils';
21+
import { buildReceivedResolvedModelCellStyle, buildReceivedViewStateStyle } from './matchers/matcher-utils';
2222
import { buildExpectedShapeCellStyle } from './matchers/toBeShape';
2323
import { readFileSync } from '@test/shared/file-helper';
2424
import { ShapeBpmnElementKind, ShapeBpmnEventDefinitionKind } from '@lib/model/bpmn/internal';
2525
import type { EdgeStyleUpdate, Fill, Font, Stroke, StyleUpdate } from '@lib/component/registry';
26+
import type { mxCell } from 'mxgraph';
2627

2728
describe('mxGraph model - update style', () => {
2829
describe('Shapes', () => {
@@ -580,25 +581,26 @@ describe('mxGraph model - update style', () => {
580581
const bv = initializeBpmnVisualizationWithContainerId('bpmn-container-style-css-cross-tests');
581582
const htmlElementLookup = new HtmlElementLookup(bv);
582583

583-
// we cannot reuse the model expect functions here. They are using the shared bpmnVisualization that we cannot use here.
584-
// So use the minimal expect function. We only need to check a part of the data, the rest is already checked in details in other tests.
585-
const checkViewStateStyle = (bpmnElementId: string, expectedModel: ExpectedShapeModelElement): void => {
584+
const getCell = (bpmnElementId: string): mxCell => {
586585
const graph = bv.graph;
587586
const cell = graph.model.getCell(bpmnElementId);
588587
if (!cell) {
589588
throw new Error(`Unable to find cell in the model with id ${bpmnElementId}`);
590589
}
590+
return cell;
591+
};
591592

592-
const receivedViewStateStyle = buildReceivedViewStateStyle(cell, bv);
593-
expect(receivedViewStateStyle).toEqual(buildExpectedShapeCellStyle(expectedModel));
593+
// we cannot reuse the model expect functions here. They are using the shared bpmnVisualization that we cannot use here.
594+
// So use the minimal expect function. We only need to check a part of the data, the rest is already checked in details in other tests.
595+
const checkViewStateStyle = (bpmnElementId: string, expectedModel: ExpectedShapeModelElement): void => {
596+
expect(buildReceivedViewStateStyle(getCell(bpmnElementId), bv)).toEqual(buildExpectedShapeCellStyle(expectedModel));
597+
};
598+
599+
const checkModelStyle = (bpmnElementId: string, expectedModel: ExpectedShapeModelElement): void => {
600+
expect(buildReceivedResolvedModelCellStyle(getCell(bpmnElementId), bv)).toEqual(buildExpectedShapeCellStyle(expectedModel));
594601
};
595602

596-
it.each(
597-
// We have a bug when the CSS classes are applied first, they are dropped after the call of the updateStyle method
598-
// See https://github.com/process-analytics/bpmn-visualization-js/issues/2561
599-
// When fixed, it.each should use [true, false]
600-
[true],
601-
)('Apply style update first %s', (isStyleUpdateAppliedFirst: boolean) => {
603+
it.each([true, false])('Apply style update first %s', (isStyleUpdateAppliedFirst: boolean) => {
602604
bv.load(readFileSync('../fixtures/bpmn/registry/1-pool-3-lanes-message-start-end-intermediate-events.bpmn'));
603605

604606
const bpmnElementId = 'endEvent_message_1';
@@ -612,11 +614,14 @@ describe('mxGraph model - update style', () => {
612614
bv.bpmnElementsRegistry.updateStyle(bpmnElementId, { stroke: { color: strokeColor } });
613615
}
614616

615-
checkViewStateStyle(bpmnElementId, {
617+
const expectedModel = {
618+
extraCssClasses: ['class-1', 'class-2'],
616619
kind: ShapeBpmnElementKind.EVENT_END,
617620
stroke: { color: strokeColor },
618621
verticalAlign: 'top', // when events have a label
619-
});
622+
};
623+
checkModelStyle(bpmnElementId, expectedModel);
624+
checkViewStateStyle(bpmnElementId, expectedModel);
620625
htmlElementLookup.expectEndEvent(bpmnElementId, ShapeBpmnEventDefinitionKind.MESSAGE, { label: 'message end 2', additionalClasses: ['class-1', 'class-2'] });
621626
});
622627
});

0 commit comments

Comments
 (0)