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

feat(style): add support for fill color gradient in the Update Style API #2760

Merged
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 dev/public/static/js/elements-identification.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ function computeStyleUpdateByKind(bpmnKind) {
style.font.isItalic = true;
style.font.isStrikeThrough = true;

style.fill.color = 'LimeGreen';
style.fill.color = { startColor: 'LightYellow', endColor: 'LimeGreen', direction: 'left-to-right' };
} else if (ShapeUtil.isSubProcess(bpmnKind)) {
style.font.color = 'white';
style.font.size = 14;
Expand Down
14 changes: 13 additions & 1 deletion dev/ts/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import type {
StyleUpdate,
Version,
ZoomType,
FillColorGradient,
GradientDirection,
} from '../../src/bpmn-visualization';
import { FlowKind, ShapeBpmnElementKind } from '../../src/bpmn-visualization';
import { fetchBpmnContent, logDownload, logError, logErrorAndOpenAlert, logStartup } from './utils/internal-helpers';
Expand Down Expand Up @@ -262,9 +264,19 @@ function configureStyleFromParameters(parameters: URLSearchParams): void {
style = { stroke: {}, font: {}, fill: {} };

parameters.get('style.api.stroke.color') && (style.stroke.color = parameters.get('style.api.stroke.color'));

parameters.get('style.api.font.color') && (style.font.color = parameters.get('style.api.font.color'));
parameters.get('style.api.font.opacity') && (style.font.opacity = Number(parameters.get('style.api.font.opacity')));
parameters.get('style.api.fill.color') && (style.fill.color = parameters.get('style.api.fill.color'));

if (parameters.get('style.api.fill.color')) {
style.fill.color = parameters.get('style.api.fill.color');
} else if (parameters.get('style.api.fill.color.startColor') && parameters.get('style.api.fill.color.endColor') && parameters.get('style.api.fill.color.direction')) {
style.fill.color = {
startColor: parameters.get('style.api.fill.color.startColor'),
endColor: parameters.get('style.api.fill.color.endColor'),
direction: parameters.get('style.api.fill.color.direction') as GradientDirection,
} as FillColorGradient;
}
parameters.get('style.api.fill.opacity') && (style.fill.opacity = Number(parameters.get('style.api.fill.opacity')));

logStartup(`Prepared "Update Style" API object`, style);
Expand Down
38 changes: 34 additions & 4 deletions src/component/mxgraph/style/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ limitations under the License.
*/

import { ensureOpacityValue, ensureStrokeWidthValue } from '../../helpers/validators';
import type { Fill, Font, ShapeStyleUpdate, Stroke, StyleUpdate } from '../../registry';
import type { Fill, Font, ShapeStyleUpdate, Stroke, StyleUpdate, GradientDirection, FillColorGradient } from '../../registry';
import { ShapeBpmnElementKind } from '../../../model/bpmn/internal';
import { mxConstants, mxUtils } from '../initializer';
import { BpmnStyleIdentifier } from './identifiers';
Expand Down Expand Up @@ -110,12 +110,38 @@ export const updateFont = (cellStyle: string, font: Font): string => {
return cellStyle;
};

const convertDirection = (direction: GradientDirection): string => {
switch (direction) {
case 'right-to-left':
return mxConstants.DIRECTION_WEST;
case 'bottom-to-top':
return mxConstants.DIRECTION_NORTH;
case 'top-to-bottom':
return mxConstants.DIRECTION_SOUTH;
csouchet marked this conversation as resolved.
Show resolved Hide resolved
default:
return mxConstants.DIRECTION_EAST;
csouchet marked this conversation as resolved.
Show resolved Hide resolved
}
csouchet marked this conversation as resolved.
Show resolved Hide resolved
};

export const updateFill = (cellStyle: string, fill: Fill): string => {
if (fill.color) {
cellStyle = setStyle(cellStyle, mxConstants.STYLE_FILLCOLOR, fill.color, convertDefaultValue);
const color = fill.color;
if (color) {
const isGradient = isFillColorGradient(color);

const fillColor = isGradient ? color.startColor : color;
cellStyle = setStyle(cellStyle, mxConstants.STYLE_FILLCOLOR, fillColor, convertDefaultValue);

if (isGradient) {
// The values of the color are mandatory. So, no need to check if it's undefined.
cellStyle = mxUtils.setStyle(cellStyle, mxConstants.STYLE_GRADIENTCOLOR, color.endColor);
cellStyle = mxUtils.setStyle(cellStyle, mxConstants.STYLE_GRADIENT_DIRECTION, convertDirection(color.direction));
} else if (color === 'default') {
cellStyle = mxUtils.setStyle(cellStyle, mxConstants.STYLE_GRADIENTCOLOR, undefined);
cellStyle = mxUtils.setStyle(cellStyle, mxConstants.STYLE_GRADIENT_DIRECTION, undefined);
}

if (cellStyle.includes(ShapeBpmnElementKind.POOL) || cellStyle.includes(ShapeBpmnElementKind.LANE)) {
cellStyle = setStyle(cellStyle, mxConstants.STYLE_SWIMLANE_FILLCOLOR, fill.color, convertDefaultValue);
cellStyle = setStyle(cellStyle, mxConstants.STYLE_SWIMLANE_FILLCOLOR, fillColor, convertDefaultValue);
}
}

Expand All @@ -127,3 +153,7 @@ export const updateFill = (cellStyle: string, fill: Fill): string => {
export const isShapeStyleUpdate = (style: StyleUpdate): style is ShapeStyleUpdate => {
return style && typeof style === 'object' && 'fill' in style;
};

const isFillColorGradient = (color: string | FillColorGradient): color is FillColorGradient => {
return color && typeof color === 'object';
};
47 changes: 45 additions & 2 deletions src/component/registry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ export type Font = StyleWithOpacity & {
*/
export type Fill = StyleWithOpacity & {
/**
* Possible values are all HTML color names or HEX codes, as well as special keywords such as:
* Possible values are all HTML color names, HEX codes, {@link FillColorGradient}, as well as special keywords such as:
* - `default` to use the color defined in the BPMN element default style.
* - `inherit` to apply the fill color of the direct parent element.
* - `none` for no color.
Expand All @@ -264,10 +264,53 @@ export type Fill = StyleWithOpacity & {
* **Notes about the `default` special keyword**:
* - It can be used when the style is first updated and then needs to be reset to its initial value.
* - It doesn't use the color set in the BPMN source when the "BPMN in Color" support is enabled. It uses the color defined in the BPMN element default style.
* - If a gradient was set, it will be completely reverted.
*/
color?: 'default' | 'inherit' | 'none' | 'swimlane' | string;
color?: FillColorGradient | 'default' | 'inherit' | 'none' | 'swimlane' | string;
};

/**
* It is a linear gradient managed by `mxGraph`.
* For more information about mxGraph, refer to the documentation at:
* {@link https://jgraph.github.io/mxgraph/docs/js-api/files/util/mxConstants-js.html#mxConstants.STYLE_GRADIENTCOLOR}
*
* **Notes**:
* Using the same color for the start color and end color will have the same effect as setting only the fill color with an HTML color name, HEX code or special keyword.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: 👍🏿

*
* @category Element Style
*/
export type FillColorGradient = {
/**
* It can be any HTML color name or HEX code, as well as special keywords such as:
* - `inherit` to apply the fill color of the direct parent element.
* - `none` for no color.
* - `swimlane` to apply the fill color of the nearest parent element with the type {@link ShapeBpmnElementKind.LANE} or {@link ShapeBpmnElementKind.POOL}.
*/
startColor: 'inherit' | 'none' | 'swimlane' | string;

/**
* It can be any HTML color name or HEX code, as well as special keywords such as:
* - `inherit` to apply the fill color of the direct parent element.
* - `none` for no color.
* - `swimlane` to apply the fill color of the nearest parent element with the type {@link ShapeBpmnElementKind.LANE} or {@link ShapeBpmnElementKind.POOL}.
*/
endColor: 'inherit' | 'none' | 'swimlane' | string;

/**
* Specifies how the colors transition within the gradient.
*
* Taking the example of `bottom-to-top`, this means that the start color is at the bottom of the paint pattern and the end color is at the top, with a gradient between them.
*
* @see {@link GradientDirection}
*/
direction: GradientDirection;
};

/**
* @category Element Style
*/
export type GradientDirection = 'left-to-right' | 'right-to-left' | 'bottom-to-top' | 'top-to-bottom';

/**
* @category Element Style
*/
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 39 additions & 3 deletions test/e2e/style.api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ class StyleImageSnapshotThresholds extends MultiBrowserImageSnapshotThresholds {
windows: 0.12 / 100, // 0.11000117996341485%
},
],
[
'fill.color.gradient',
{
linux: 0.11 / 100, // 0.10872082550010823%
macos: 0.11 / 100, // 0.10872082550010823%
windows: 0.11 / 100, // 0.10872082550010823%
},
],
]);
}

Expand All @@ -51,13 +59,21 @@ class StyleImageSnapshotThresholds extends MultiBrowserImageSnapshotThresholds {
},
],
[
'fill.color',
'fill.color.string',
{
linux: 0.09 / 100, // 0.08216175057789155%
macos: 0.09 / 100, // 0.08216175057789155%
windows: 0.09 / 100, // 0.08216175057789155%
},
],
[
'fill.color.gradient',
{
linux: 0.15 / 100, // 0.1477596574325335%
macos: 0.15 / 100, // 0.1477596574325335%
windows: 0.15 / 100, // 0.1477596574325335%
},
],
[
'fill.color.opacity.group',
{
Expand Down Expand Up @@ -112,15 +128,35 @@ describe('Style API', () => {
expect(image).toMatchImageSnapshot(config);
});

it(`Update 'fill.color'`, async () => {
it(`Update 'fill.color' as string`, async () => {
await pageTester.gotoPageAndLoadBpmnDiagram('01.most.bpmn.types.without.label', {
styleOptions: {
styleUpdate: { fill: { color: 'chartreuse' } },
},
});

const image = await page.screenshot({ fullPage: true });
const config = imageSnapshotConfigurator.getConfig('fill.color');
const config = imageSnapshotConfigurator.getConfig('fill.color.string');
expect(image).toMatchImageSnapshot(config);
});

it(`Update 'fill.color' as gradient`, async () => {
await pageTester.gotoPageAndLoadBpmnDiagram('01.most.bpmn.types.without.label', {
styleOptions: {
styleUpdate: {
fill: {
color: {
startColor: 'pink',
endColor: 'lime',
direction: 'top-to-bottom',
},
},
},
},
});

const image = await page.screenshot({ fullPage: true });
const config = imageSnapshotConfigurator.getConfig('fill.color.gradient');
expect(image).toMatchImageSnapshot(config);
});

Expand Down
16 changes: 14 additions & 2 deletions test/integration/helpers/model-expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ limitations under the License.
*/

import type {
Fill,
FlowKind,
GlobalTaskKind,
MessageVisibleKind,
Expand Down Expand Up @@ -167,6 +166,18 @@ type ExpectedModelElement = {
extraCssClasses?: string[];
};

export interface ExpectedFill {
color?: string;
opacity?: Opacity;
}
Comment on lines +169 to +172
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: needed as in the Fill type, the color can be an object and here, the checks expect it to be a string.

Copy link
Member Author

@csouchet csouchet Jul 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you look my first implementation for the integration tests, I used the same implementation for the update style API.
But, the calculation of what we really test was hidden from the reading of the tests.

bbc9b1e

As the direction is not the same value between the API and mxgraph, I needed to change it.


export type ExpectedDirection = 'west' | 'east' | 'north' | 'south';

export interface ExpectedGradient {
color: string;
direction?: ExpectedDirection;
}

export interface ExpectedShapeModelElement extends ExpectedModelElement {
kind?: ShapeBpmnElementKind;
/** Generally needed when the BPMN shape doesn't exist yet (use an arbitrary shape until the final render is implemented) */
Expand All @@ -179,7 +190,8 @@ export interface ExpectedShapeModelElement extends ExpectedModelElement {
* - Vertical pool/lane --> true (the label is horizontal)
**/
isSwimLaneLabelHorizontal?: boolean;
fill?: Fill;
fill?: ExpectedFill;
gradient?: ExpectedGradient;
}

export interface ExpectedEventModelElement extends ExpectedShapeModelElement {
Expand Down
4 changes: 4 additions & 0 deletions test/integration/matchers/matcher-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export interface BpmnCellStyle extends StyleMap {
fillColor: string;
fillOpacity?: Opacity;
swimlaneFillColor?: string;
gradientColor?: string;
gradientDirection?: string;
fontColor: string;
fontFamily: string;
fontSize: number;
Expand Down Expand Up @@ -208,6 +210,8 @@ function toBpmnStyle(rawStyle: StyleMap, isEdge: boolean): BpmnCellStyle {
style.horizontal = rawStyle.horizontal;
style.swimlaneFillColor = rawStyle.swimlaneFillColor;
style.fillOpacity = rawStyle.fillOpacity;
style.gradientColor = rawStyle.gradientColor;
style.gradientDirection = rawStyle.gradientDirection;
}
return style;
}
Expand Down
24 changes: 16 additions & 8 deletions test/integration/matchers/toBeShape/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,26 @@ export function buildExpectedShapeCellStyle(expectedModel: ExpectedShapeModelEle
style.align = expectedModel.align ?? 'center';
style.strokeWidth = style.strokeWidth ?? expectedStrokeWidth(expectedModel.kind);

style.fillColor =
expectedModel.fill?.color ??
([ShapeBpmnElementKind.LANE, ShapeBpmnElementKind.POOL, ShapeBpmnElementKind.TEXT_ANNOTATION, ShapeBpmnElementKind.GROUP].includes(expectedModel.kind)
? 'none'
: style.fillColor);
const fill = expectedModel.fill;
if (fill) {
style.fillColor = fill.color ?? style.fillColor;
style.fillOpacity = fill.opacity;
}
if (!fill?.color && [ShapeBpmnElementKind.LANE, ShapeBpmnElementKind.POOL, ShapeBpmnElementKind.TEXT_ANNOTATION, ShapeBpmnElementKind.GROUP].includes(expectedModel.kind)) {
style.fillColor = 'none';
}
Comment on lines +69 to +76
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: this seems to be a refactoring that is not related to the new feature.
If so, please revert and eventually create a dedicated PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a rest of bbc9b1e.

The old code wasn't clear for me for the real test.
I can move in another PR, but I was lazy 😆


if (expectedModel.gradient) {
style.gradientColor = expectedModel.gradient.color;
style.gradientDirection = expectedModel.gradient.direction;
}

style.swimlaneFillColor = [ShapeBpmnElementKind.POOL, ShapeBpmnElementKind.LANE].includes(expectedModel.kind) && style.fillColor !== 'none' ? style.fillColor : undefined;

style.fillOpacity = expectedModel.fill?.opacity;
'isSwimLaneLabelHorizontal' in expectedModel && (style.horizontal = Number(expectedModel.isSwimLaneLabelHorizontal));
expectedModel.isSwimLaneLabelHorizontal && (style.horizontal = Number(expectedModel.isSwimLaneLabelHorizontal));

// ignore marker order, which is only relevant when rendering the shape (it has its own order algorithm)
'markers' in expectedModel && (style.markers = expectedModel.markers.sort());
style.markers = expectedModel.markers?.sort();
Comment on lines +85 to +88
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: I don't understand the need for this refactoring and it changes the resulting style object.
Previously the style object didn't contain the properties, not they are set to undefined.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The object returned by buildExpectedShapeCellStyle will have the markers property set to undefined if we either don't set it or explicitly set it to undefined. Since it is the same returned object, this way of expressing it is easier to read 🙂


return style;
}
Expand Down
Loading