From 7e7495fbebcfa9d7b5bdca5dd6024c81c22873f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= Date: Thu, 6 Jul 2023 14:23:11 +0200 Subject: [PATCH 01/20] Update the Fill type --- src/component/registry/types.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/component/registry/types.ts b/src/component/registry/types.ts index eaf7de5dfd..2d759b928f 100644 --- a/src/component/registry/types.ts +++ b/src/component/registry/types.ts @@ -265,9 +265,17 @@ export type Fill = StyleWithOpacity & { * - 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. */ - color?: 'default' | 'inherit' | 'none' | 'swimlane' | string; + color?: FillColorGradient | 'default' | 'inherit' | 'none' | 'swimlane' | string; +}; + +type FillColorGradient = { + startColor: string; + endColor: string; + direction: Direction; }; +type Direction = 'left-to-right' | 'right-to-left' | 'bottom-to-top' | 'top-to-bottom'; + /** * @category Element Style */ From af27ed9d2b123b657a3730ce0ee11d956353458a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= Date: Thu, 6 Jul 2023 15:27:09 +0200 Subject: [PATCH 02/20] Add new types for the gradient fill color --- src/component/mxgraph/style/utils.ts | 15 +++++++-- src/component/registry/types.ts | 46 +++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/component/mxgraph/style/utils.ts b/src/component/mxgraph/style/utils.ts index 38f720a51c..c36b09e0ec 100644 --- a/src/component/mxgraph/style/utils.ts +++ b/src/component/mxgraph/style/utils.ts @@ -111,11 +111,20 @@ export const updateFont = (cellStyle: string, font: Font): string => { }; 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 = typeof color !== 'string'; + + const fillColor = isGradient ? color.startColor : color; + cellStyle = setStyle(cellStyle, mxConstants.STYLE_FILLCOLOR, fillColor, convertDefaultValue); + + if (isGradient) { + cellStyle = setStyle(cellStyle, mxConstants.STYLE_GRADIENTCOLOR, color.endColor, convertDefaultValue); + cellStyle = setStyle(cellStyle, mxConstants.STYLE_GRADIENT_DIRECTION, color.direction, convertDefaultValue); + } 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); } } diff --git a/src/component/registry/types.ts b/src/component/registry/types.ts index 2d759b928f..8aac8e8d54 100644 --- a/src/component/registry/types.ts +++ b/src/component/registry/types.ts @@ -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. @@ -268,13 +268,49 @@ export type Fill = StyleWithOpacity & { color?: FillColorGradient | 'default' | 'inherit' | 'none' | 'swimlane' | string; }; -type FillColorGradient = { - startColor: 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. + * + * @category Element Style + */ +export type FillColorGradient = { + /** + * It can be any HTML color name or HEX code, 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. + * - `swimlane` to apply the fill color of the nearest parent element with the type {@link ShapeBpmnElementKind.LANE} or {@link ShapeBpmnElementKind.POOL}. + */ + startColor: 'default' | 'inherit' | 'none' | 'swimlane' | string; + + /** + * It can be any HTML color name or HEX code, 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. + * - `swimlane` to apply the fill color of the nearest parent element with the type {@link ShapeBpmnElementKind.LANE} or {@link ShapeBpmnElementKind.POOL}. + */ endColor: string; - direction: Direction; + + /** + * 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; }; -type Direction = 'left-to-right' | 'right-to-left' | 'bottom-to-top' | 'top-to-bottom'; +/** + * @category Element Style + */ +export type GradientDirection = 'left-to-right' | 'right-to-left' | 'bottom-to-top' | 'top-to-bottom'; /** * @category Element Style From cb585b1e886570402c74a73a2edecbfb652d4525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= Date: Mon, 10 Jul 2023 14:11:23 +0200 Subject: [PATCH 03/20] Update the 'default' reset when a gradient is already set --- src/component/mxgraph/style/utils.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/component/mxgraph/style/utils.ts b/src/component/mxgraph/style/utils.ts index c36b09e0ec..290dc560f5 100644 --- a/src/component/mxgraph/style/utils.ts +++ b/src/component/mxgraph/style/utils.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { ensureOpacityValue, ensureStrokeWidthValue } from '../../helpers/validators'; +import type { FillColorGradient } from '../../registry'; import type { Fill, Font, ShapeStyleUpdate, Stroke, StyleUpdate } from '../../registry'; import { ShapeBpmnElementKind } from '../../../model/bpmn/internal'; import { mxConstants, mxUtils } from '../initializer'; @@ -113,14 +114,18 @@ export const updateFont = (cellStyle: string, font: Font): string => { export const updateFill = (cellStyle: string, fill: Fill): string => { const color = fill.color; if (color) { - const isGradient = typeof color !== 'string'; + const isGradient = isFillColorGradient(color); const fillColor = isGradient ? color.startColor : color; cellStyle = setStyle(cellStyle, mxConstants.STYLE_FILLCOLOR, fillColor, convertDefaultValue); if (isGradient) { - cellStyle = setStyle(cellStyle, mxConstants.STYLE_GRADIENTCOLOR, color.endColor, convertDefaultValue); - cellStyle = setStyle(cellStyle, mxConstants.STYLE_GRADIENT_DIRECTION, color.direction, convertDefaultValue); + // 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, 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)) { @@ -136,3 +141,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; }; + +export const isFillColorGradient = (color: string | FillColorGradient): color is FillColorGradient => { + return color && typeof color !== 'string'; +}; From bbc9b1e062b70aad2a1877004962f87d1fb124bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= Date: Mon, 10 Jul 2023 16:18:07 +0200 Subject: [PATCH 04/20] Add integration tests --- test/integration/matchers/matcher-utils.ts | 4 +++ test/integration/matchers/toBeShape/index.ts | 30 ++++++++++++----- .../mxGraph.model.style.api.test.ts | 32 ++++++++++++++++++- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/test/integration/matchers/matcher-utils.ts b/test/integration/matchers/matcher-utils.ts index b86241c315..b86f977f70 100644 --- a/test/integration/matchers/matcher-utils.ts +++ b/test/integration/matchers/matcher-utils.ts @@ -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; @@ -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; } diff --git a/test/integration/matchers/toBeShape/index.ts b/test/integration/matchers/toBeShape/index.ts index 4e1cf5acf2..eec69d8f0f 100644 --- a/test/integration/matchers/toBeShape/index.ts +++ b/test/integration/matchers/toBeShape/index.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { isFillColorGradient } from '@lib/component/mxgraph/style/utils'; import type { BpmnCellStyle, ExpectedCell } from '../matcher-utils'; import { buildCellMatcher, buildExpectedCellStyleWithCommonAttributes, buildReceivedCellWithCommonAttributes } from '../matcher-utils'; import type { @@ -66,18 +67,31 @@ 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) { + if (fill.color) { + if (isFillColorGradient(fill.color)) { + style.fillColor = fill.color.startColor; + style.gradientColor = fill.color.endColor; + style.gradientDirection = fill.color.direction; + } else { + style.fillColor = fill.color; + } + } + + style.fillOpacity = fill.opacity; + } + + if (!fill?.color && [ShapeBpmnElementKind.LANE, ShapeBpmnElementKind.POOL, ShapeBpmnElementKind.TEXT_ANNOTATION, ShapeBpmnElementKind.GROUP].includes(expectedModel.kind)) { + style.fillColor = 'none'; + } + 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(); return style; } diff --git a/test/integration/mxGraph.model.style.api.test.ts b/test/integration/mxGraph.model.style.api.test.ts index debf3693b9..04d48cb32e 100644 --- a/test/integration/mxGraph.model.style.api.test.ts +++ b/test/integration/mxGraph.model.style.api.test.ts @@ -22,7 +22,7 @@ import { buildReceivedResolvedModelCellStyle, buildReceivedViewStateStyle } from import { buildExpectedShapeCellStyle } from './matchers/toBeShape'; import { readFileSync } from '@test/shared/file-helper'; import { MessageVisibleKind, ShapeBpmnElementKind, ShapeBpmnEventDefinitionKind } from '@lib/model/bpmn/internal'; -import type { EdgeStyleUpdate, Fill, Font, Stroke, StyleUpdate } from '@lib/component/registry'; +import type { EdgeStyleUpdate, Fill, Font, GradientDirection, Stroke, StyleUpdate } from '@lib/component/registry'; import type { mxCell } from 'mxgraph'; // Create a dedicated instance with a DOM container as it is required by the CSS API. @@ -196,6 +196,18 @@ describe('mxGraph model - update style', () => { }); }); + it('Update the fill color as gradient', () => { + const fill = { color: { startColor: 'gold', endColor: 'pink', direction: 'top-to-bottom' } }; + bpmnVisualization.bpmnElementsRegistry.updateStyle('userTask_2_2', { fill }); + + expect('userTask_2_2').toBeUserTask({ + fill, + // not under test + parentId: 'lane_02', + label: 'User Task 2.2', + }); + }); + it('Update all opacity properties with wrong value', () => { bpmnVisualization.bpmnElementsRegistry.updateStyle('userTask_2_2', { stroke: { opacity: -72 }, @@ -884,6 +896,24 @@ describe('mxGraph model - reset style', () => { label: 'Pool 1', }); }); + + it('Reset the fill color as gradient', () => { + const elementId = 'userTask_2_2'; + + // Apply custom style + const fill = { color: { startColor: 'gold', endColor: 'pink', direction: 'top-to-bottom' } }; + bpmnVisualization.bpmnElementsRegistry.updateStyle(elementId, { fill }); + + // Reset style + bpmnVisualization.bpmnElementsRegistry.resetStyle(elementId); + + // Check that the style has been reset to default values + expect(elementId).toBeUserTask({ + // not under test + parentId: 'lane_02', + label: 'User Task 2.2', + }); + }); }); describe('Edges', () => { From 3220ce7eec3e4ce4ea7d693837f0ba26eb4d973f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= Date: Mon, 10 Jul 2023 17:21:08 +0200 Subject: [PATCH 05/20] Add e2e --- dev/ts/main.ts | 14 +++++++++- .../style/fill.color.gradient.png | Bin 0 -> 45938 bytes .../{fill.color.png => fill.color.string.png} | Bin test/e2e/style.api.test.ts | 24 ++++++++++++++++-- test/shared/visu/bpmn-page-utils.ts | 13 +++++++--- 5 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 test/e2e/__image_snapshots__/style/fill.color.gradient.png rename test/e2e/__image_snapshots__/style/{fill.color.png => fill.color.string.png} (100%) diff --git a/dev/ts/main.ts b/dev/ts/main.ts index f0310ed7f7..cc0ada21a2 100644 --- a/dev/ts/main.ts +++ b/dev/ts/main.ts @@ -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'; @@ -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); diff --git a/test/e2e/__image_snapshots__/style/fill.color.gradient.png b/test/e2e/__image_snapshots__/style/fill.color.gradient.png new file mode 100644 index 0000000000000000000000000000000000000000..67d442ce16514a3123022c6f00d3242989efa8af GIT binary patch literal 45938 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 { 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' } }, @@ -120,7 +120,27 @@ describe('Style API', () => { }); 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); }); diff --git a/test/shared/visu/bpmn-page-utils.ts b/test/shared/visu/bpmn-page-utils.ts index 6b3da31077..38a409e3fb 100644 --- a/test/shared/visu/bpmn-page-utils.ts +++ b/test/shared/visu/bpmn-page-utils.ts @@ -23,8 +23,7 @@ import 'expect-playwright'; import type { PageWaitForSelectorOptions } from 'expect-playwright'; import type { ElementHandle, Page } from 'playwright'; import { type LoadOptions, FitType, ZoomType } from '@lib/component/options'; -import type { ShapeStyleUpdate } from '@lib/component/registry'; -import type { StyleUpdate } from '@lib/component/registry'; +import type { ShapeStyleUpdate, StyleUpdate } from '@lib/component/registry'; import { BpmnQuerySelectorsForTests } from '@test/shared/query-selectors'; import { delay } from './test-utils'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -224,7 +223,15 @@ export class PageTester { if ('fill' in styleUpdate) { const fill = (styleUpdate).fill; - fill.color && (url += `&style.api.fill.color=${fill.color}`); + + if (typeof fill.color !== 'string') { + fill.color.startColor && (url += `&style.api.fill.color.startColor=${fill.color.startColor}`); + fill.color.endColor && (url += `&style.api.fill.color.endColor=${fill.color.endColor}`); + fill.color.direction && (url += `&style.api.fill.color.direction=${fill.color.direction}`); + } else { + fill.color && (url += `&style.api.fill.color=${fill.color}`); + } + fill.opacity && (url += `&style.api.fill.opacity=${fill.opacity}`); } } From e9c6f294e70290405aaeb233ba4f94ceee9d283f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= Date: Mon, 10 Jul 2023 17:22:02 +0200 Subject: [PATCH 06/20] Convert the API direction in mxgraph constants --- src/component/mxgraph/style/utils.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/component/mxgraph/style/utils.ts b/src/component/mxgraph/style/utils.ts index 290dc560f5..9f901f60bb 100644 --- a/src/component/mxgraph/style/utils.ts +++ b/src/component/mxgraph/style/utils.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { ensureOpacityValue, ensureStrokeWidthValue } from '../../helpers/validators'; +import type { GradientDirection } from '../../registry'; import type { FillColorGradient } from '../../registry'; import type { Fill, Font, ShapeStyleUpdate, Stroke, StyleUpdate } from '../../registry'; import { ShapeBpmnElementKind } from '../../../model/bpmn/internal'; @@ -111,6 +112,19 @@ export const updateFont = (cellStyle: string, font: Font): string => { return cellStyle; }; +const convertDirection = (direction: GradientDirection): string => { + switch (direction) { + case 'left-to-right': + return mxConstants.DIRECTION_WEST; + case 'right-to-left': + return mxConstants.DIRECTION_EAST; + case 'bottom-to-top': + return mxConstants.DIRECTION_NORTH; + case 'top-to-bottom': + return mxConstants.DIRECTION_SOUTH; + } +}; + export const updateFill = (cellStyle: string, fill: Fill): string => { const color = fill.color; if (color) { @@ -122,7 +136,7 @@ export const updateFill = (cellStyle: string, fill: Fill): string => { 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, color.direction); + 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); From b1c8756e4b1e745956a9f6e9689af3e8c996f94d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= Date: Mon, 10 Jul 2023 17:44:43 +0200 Subject: [PATCH 07/20] Fix integration tests --- src/component/mxgraph/style/utils.ts | 4 +- test/integration/helpers/model-expect.ts | 14 ++++- test/integration/matchers/toBeShape/index.ts | 17 ++---- .../mxGraph.model.style.api.test.ts | 59 +++++++++++++++++-- 4 files changed, 74 insertions(+), 20 deletions(-) diff --git a/src/component/mxgraph/style/utils.ts b/src/component/mxgraph/style/utils.ts index 9f901f60bb..572e9a713b 100644 --- a/src/component/mxgraph/style/utils.ts +++ b/src/component/mxgraph/style/utils.ts @@ -115,9 +115,9 @@ export const updateFont = (cellStyle: string, font: Font): string => { const convertDirection = (direction: GradientDirection): string => { switch (direction) { case 'left-to-right': - return mxConstants.DIRECTION_WEST; - case 'right-to-left': return mxConstants.DIRECTION_EAST; + case 'right-to-left': + return mxConstants.DIRECTION_WEST; case 'bottom-to-top': return mxConstants.DIRECTION_NORTH; case 'top-to-bottom': diff --git a/test/integration/helpers/model-expect.ts b/test/integration/helpers/model-expect.ts index 5f2b6b3e9b..ffe4c8c974 100644 --- a/test/integration/helpers/model-expect.ts +++ b/test/integration/helpers/model-expect.ts @@ -28,6 +28,7 @@ import type { Stroke, } from '@lib/bpmn-visualization'; import { BpmnVisualization, ShapeBpmnElementKind } from '@lib/bpmn-visualization'; +import { mxConstants } from '@lib/component/mxgraph/initializer'; import { toBeAssociationFlow, toBeBoundaryEvent, @@ -167,6 +168,16 @@ type ExpectedModelElement = { extraCssClasses?: string[]; }; +export interface ExpectedFill { + color?: string; + opacity?: Opacity; +} + +export interface ExpectedGradient { + color: string; + direction?: 'west' | 'east' | 'north' | 'south'; +} + 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) */ @@ -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 { diff --git a/test/integration/matchers/toBeShape/index.ts b/test/integration/matchers/toBeShape/index.ts index eec69d8f0f..5ef05b230c 100644 --- a/test/integration/matchers/toBeShape/index.ts +++ b/test/integration/matchers/toBeShape/index.ts @@ -69,23 +69,18 @@ export function buildExpectedShapeCellStyle(expectedModel: ExpectedShapeModelEle const fill = expectedModel.fill; if (fill) { - if (fill.color) { - if (isFillColorGradient(fill.color)) { - style.fillColor = fill.color.startColor; - style.gradientColor = fill.color.endColor; - style.gradientDirection = fill.color.direction; - } else { - style.fillColor = fill.color; - } - } - + 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'; } + 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; expectedModel.isSwimLaneLabelHorizontal && (style.horizontal = Number(expectedModel.isSwimLaneLabelHorizontal)); diff --git a/test/integration/mxGraph.model.style.api.test.ts b/test/integration/mxGraph.model.style.api.test.ts index 04d48cb32e..75b5ca0704 100644 --- a/test/integration/mxGraph.model.style.api.test.ts +++ b/test/integration/mxGraph.model.style.api.test.ts @@ -16,7 +16,7 @@ limitations under the License. import { initializeBpmnVisualizationWithContainerId } from './helpers/bpmn-visualization-initialization'; import { HtmlElementLookup } from './helpers/html-utils'; -import type { ExpectedShapeModelElement, VerticalAlign } from './helpers/model-expect'; +import type { ExpectedFill, ExpectedShapeModelElement, VerticalAlign } from './helpers/model-expect'; import { bpmnVisualization } from './helpers/model-expect'; import { buildReceivedResolvedModelCellStyle, buildReceivedViewStateStyle } from './matchers/matcher-utils'; import { buildExpectedShapeCellStyle } from './matchers/toBeShape'; @@ -77,7 +77,7 @@ describe('mxGraph model - update style', () => { stroke, font, opacity, - fill, + fill: fill as ExpectedFill, // not under test parentId: 'lane_02', label: 'User Task 2.2', @@ -201,7 +201,8 @@ describe('mxGraph model - update style', () => { bpmnVisualization.bpmnElementsRegistry.updateStyle('userTask_2_2', { fill }); expect('userTask_2_2').toBeUserTask({ - fill, + fill: { color: 'gold' }, + gradient: { color: 'pink', direction: 'south' }, // not under test parentId: 'lane_02', label: 'User Task 2.2', @@ -261,7 +262,7 @@ describe('mxGraph model - update style', () => { // Check that the style has been updated expect('serviceTask_1_2').toBeServiceTask({ - fill, + fill: fill as ExpectedFill, font, opacity, stroke, @@ -296,6 +297,52 @@ describe('mxGraph model - update style', () => { }); }); + it('Fill color as gradient', () => { + // Check that the element uses default values + expect('serviceTask_1_2').toBeServiceTask({ + // not under test + parentId: 'lane_01', + label: 'Service Task 1.2', + }); + + const fill: Fill = { color: { startColor: 'gold', endColor: 'pink', direction: 'right-to-left' } }; + bpmnVisualization.bpmnElementsRegistry.updateStyle('serviceTask_1_2', { fill }); + + // Check that the style has been updated + expect('serviceTask_1_2').toBeServiceTask({ + fill: { color: 'gold' }, + gradient: { color: 'pink', direction: 'west' }, + // not under test + parentId: 'lane_01', + label: 'Service Task 1.2', + }); + + // Reset the style by passing special values + bpmnVisualization.bpmnElementsRegistry.updateStyle('serviceTask_1_2', { + fill: { + color: 'default', + opacity: 'default', + }, + font: { + color: 'default', + opacity: 'default', + }, + opacity: 'default', + stroke: { + color: 'default', + opacity: 'default', + width: 'default', + }, + }); + + // The properties should have been reset to use the default values + expect('serviceTask_1_2').toBeServiceTask({ + // not under test + parentId: 'lane_01', + label: 'Service Task 1.2', + }); + }); + // For container (lane or pool), mainly check the fill property as there is a special setting in such elements it('Lane', () => { // Check that the element uses default values @@ -309,7 +356,7 @@ describe('mxGraph model - update style', () => { bpmnVisualization.bpmnElementsRegistry.updateStyle('lane_03', { fill }); // Check that the style has been updated expect('lane_03').toBeLane({ - fill, + fill: fill as ExpectedFill, // not under test parentId: 'Participant_1', label: 'Lane 3', @@ -341,7 +388,7 @@ describe('mxGraph model - update style', () => { bpmnVisualization.bpmnElementsRegistry.updateStyle('Participant_1', { fill }); // Check that the style has been updated expect('Participant_1').toBePool({ - fill, + fill: fill as ExpectedFill, // not under test label: 'Pool 1', }); From 8ede51bac2f5e4b56dff215cfb3db516c3f5b413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= Date: Mon, 10 Jul 2023 17:53:41 +0200 Subject: [PATCH 08/20] Update TS doc --- src/component/registry/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/component/registry/types.ts b/src/component/registry/types.ts index 8aac8e8d54..ef41e0b66f 100644 --- a/src/component/registry/types.ts +++ b/src/component/registry/types.ts @@ -256,7 +256,7 @@ export type Font = StyleWithOpacity & { export type Fill = StyleWithOpacity & { /** * 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. + * - `default` to use the color defined in the BPMN element default style. If a gradient was set, it will be completely reverted. * - `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}. From 11da090bbd84d60d4c68cf0885cc0c66800ba0f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= Date: Mon, 10 Jul 2023 17:57:05 +0200 Subject: [PATCH 09/20] update type & TS doc of gradient type --- src/component/registry/types.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/component/registry/types.ts b/src/component/registry/types.ts index ef41e0b66f..9c46830756 100644 --- a/src/component/registry/types.ts +++ b/src/component/registry/types.ts @@ -281,21 +281,19 @@ export type Fill = StyleWithOpacity & { export type FillColorGradient = { /** * It can be any HTML color name or HEX code, 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. * - `swimlane` to apply the fill color of the nearest parent element with the type {@link ShapeBpmnElementKind.LANE} or {@link ShapeBpmnElementKind.POOL}. */ - startColor: 'default' | 'inherit' | 'none' | 'swimlane' | string; + startColor: 'inherit' | 'none' | 'swimlane' | string; /** * It can be any HTML color name or HEX code, 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. * - `swimlane` to apply the fill color of the nearest parent element with the type {@link ShapeBpmnElementKind.LANE} or {@link ShapeBpmnElementKind.POOL}. */ - endColor: string; + endColor: 'inherit' | 'none' | 'swimlane' | string; /** * Specifies how the colors transition within the gradient. From 69dd2f2e86954817159b9e24b5d845f6b7764264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= Date: Mon, 10 Jul 2023 18:02:29 +0200 Subject: [PATCH 10/20] Update TS Doc --- src/component/registry/types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/component/registry/types.ts b/src/component/registry/types.ts index 9c46830756..8580d6d961 100644 --- a/src/component/registry/types.ts +++ b/src/component/registry/types.ts @@ -256,7 +256,7 @@ export type Font = StyleWithOpacity & { export type Fill = StyleWithOpacity & { /** * 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. If a gradient was set, it will be completely reverted. + * - `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. * - `swimlane` to apply the fill color of the nearest parent element with the type {@link ShapeBpmnElementKind.LANE} or {@link ShapeBpmnElementKind.POOL}. @@ -264,6 +264,7 @@ 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?: FillColorGradient | 'default' | 'inherit' | 'none' | 'swimlane' | string; }; From e618e24f7e6da0a5218478d026836af813383f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= Date: Mon, 10 Jul 2023 18:03:37 +0200 Subject: [PATCH 11/20] lint --- test/integration/helpers/model-expect.ts | 2 -- test/integration/matchers/toBeShape/index.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/test/integration/helpers/model-expect.ts b/test/integration/helpers/model-expect.ts index ffe4c8c974..ce214ec96b 100644 --- a/test/integration/helpers/model-expect.ts +++ b/test/integration/helpers/model-expect.ts @@ -15,7 +15,6 @@ limitations under the License. */ import type { - Fill, FlowKind, GlobalTaskKind, MessageVisibleKind, @@ -28,7 +27,6 @@ import type { Stroke, } from '@lib/bpmn-visualization'; import { BpmnVisualization, ShapeBpmnElementKind } from '@lib/bpmn-visualization'; -import { mxConstants } from '@lib/component/mxgraph/initializer'; import { toBeAssociationFlow, toBeBoundaryEvent, diff --git a/test/integration/matchers/toBeShape/index.ts b/test/integration/matchers/toBeShape/index.ts index 5ef05b230c..7736802983 100644 --- a/test/integration/matchers/toBeShape/index.ts +++ b/test/integration/matchers/toBeShape/index.ts @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { isFillColorGradient } from '@lib/component/mxgraph/style/utils'; import type { BpmnCellStyle, ExpectedCell } from '../matcher-utils'; import { buildCellMatcher, buildExpectedCellStyleWithCommonAttributes, buildReceivedCellWithCommonAttributes } from '../matcher-utils'; import type { From 7c09a06f23ce8050d994ec4eb54576951eb958e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= Date: Tue, 11 Jul 2023 09:26:02 +0200 Subject: [PATCH 12/20] Fix Thresholds --- test/e2e/style.api.test.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/test/e2e/style.api.test.ts b/test/e2e/style.api.test.ts index 0cfa7562d2..605d7116d5 100644 --- a/test/e2e/style.api.test.ts +++ b/test/e2e/style.api.test.ts @@ -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% + }, + ], ]); } @@ -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.09 / 100, // 0.1477596574325335% + }, + ], [ 'fill.color.opacity.group', { From 5ee19f48180432f65b44343b37ff1c7292964871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= Date: Tue, 11 Jul 2023 09:27:30 +0200 Subject: [PATCH 13/20] Fix duplicated type import --- src/component/mxgraph/style/utils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/component/mxgraph/style/utils.ts b/src/component/mxgraph/style/utils.ts index 572e9a713b..e884a9aeae 100644 --- a/src/component/mxgraph/style/utils.ts +++ b/src/component/mxgraph/style/utils.ts @@ -15,9 +15,7 @@ limitations under the License. */ import { ensureOpacityValue, ensureStrokeWidthValue } from '../../helpers/validators'; -import type { GradientDirection } from '../../registry'; -import type { FillColorGradient } from '../../registry'; -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'; From f6a00cdbf578cb16b030bb8c8a4c83f406bb5fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= Date: Tue, 11 Jul 2023 09:43:18 +0200 Subject: [PATCH 14/20] Add integration tests for all gradient directions --- test/integration/helpers/model-expect.ts | 4 +++- test/integration/mxGraph.model.style.api.test.ts | 13 +++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/test/integration/helpers/model-expect.ts b/test/integration/helpers/model-expect.ts index ce214ec96b..59c64fb3cc 100644 --- a/test/integration/helpers/model-expect.ts +++ b/test/integration/helpers/model-expect.ts @@ -171,9 +171,11 @@ export interface ExpectedFill { opacity?: Opacity; } +export type ExpectedDirection = 'west' | 'east' | 'north' | 'south'; + export interface ExpectedGradient { color: string; - direction?: 'west' | 'east' | 'north' | 'south'; + direction?: ExpectedDirection; } export interface ExpectedShapeModelElement extends ExpectedModelElement { diff --git a/test/integration/mxGraph.model.style.api.test.ts b/test/integration/mxGraph.model.style.api.test.ts index 75b5ca0704..7d8e1a8b62 100644 --- a/test/integration/mxGraph.model.style.api.test.ts +++ b/test/integration/mxGraph.model.style.api.test.ts @@ -16,7 +16,7 @@ limitations under the License. import { initializeBpmnVisualizationWithContainerId } from './helpers/bpmn-visualization-initialization'; import { HtmlElementLookup } from './helpers/html-utils'; -import type { ExpectedFill, ExpectedShapeModelElement, VerticalAlign } from './helpers/model-expect'; +import type { ExpectedDirection, ExpectedFill, ExpectedShapeModelElement, VerticalAlign } from './helpers/model-expect'; import { bpmnVisualization } from './helpers/model-expect'; import { buildReceivedResolvedModelCellStyle, buildReceivedViewStateStyle } from './matchers/matcher-utils'; import { buildExpectedShapeCellStyle } from './matchers/toBeShape'; @@ -196,13 +196,18 @@ describe('mxGraph model - update style', () => { }); }); - it('Update the fill color as gradient', () => { - const fill = { color: { startColor: 'gold', endColor: 'pink', direction: 'top-to-bottom' } }; + it.each([ + ['top-to-bottom', 'south'], + ['bottom-to-top', 'north'], + ['left-to-right', 'east'], + ['right-to-left', 'west'], + ])('Update the fill color as gradient with direction %s', (direction: GradientDirection, expectedDirection: ExpectedDirection) => { + const fill = { color: { startColor: 'gold', endColor: 'pink', direction } }; bpmnVisualization.bpmnElementsRegistry.updateStyle('userTask_2_2', { fill }); expect('userTask_2_2').toBeUserTask({ fill: { color: 'gold' }, - gradient: { color: 'pink', direction: 'south' }, + gradient: { color: 'pink', direction: expectedDirection }, // not under test parentId: 'lane_02', label: 'User Task 2.2', From 51c5f5142b00451bbf5a638f2ee2ee5415c9d396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= Date: Tue, 11 Jul 2023 12:29:38 +0200 Subject: [PATCH 15/20] Fix last threshold --- test/e2e/style.api.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/style.api.test.ts b/test/e2e/style.api.test.ts index 605d7116d5..1e42d462c0 100644 --- a/test/e2e/style.api.test.ts +++ b/test/e2e/style.api.test.ts @@ -71,7 +71,7 @@ class StyleImageSnapshotThresholds extends MultiBrowserImageSnapshotThresholds { { linux: 0.15 / 100, // 0.1477596574325335% macos: 0.15 / 100, // 0.1477596574325335% - windows: 0.09 / 100, // 0.1477596574325335% + windows: 0.15 / 100, // 0.1477596574325335% }, ], [ From 91b90b3e33079e82aea04035b3f929f0f1a6927c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= Date: Tue, 11 Jul 2023 15:39:43 +0200 Subject: [PATCH 16/20] Modify Elements Identification Demo to use gradient on fill color of Call Activities --- dev/public/static/js/elements-identification.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/public/static/js/elements-identification.js b/dev/public/static/js/elements-identification.js index c25f59c959..0fa20b4259 100644 --- a/dev/public/static/js/elements-identification.js +++ b/dev/public/static/js/elements-identification.js @@ -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; From 1f77e36188c8ea6c253ae64de1e027f20c824537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= <4921914+csouchet@users.noreply.github.com> Date: Wed, 12 Jul 2023 15:39:05 +0200 Subject: [PATCH 17/20] Apply suggestions from code review --- src/component/mxgraph/style/utils.ts | 6 ++++-- test/integration/mxGraph.model.style.api.test.ts | 4 ++-- test/shared/visu/bpmn-page-utils.ts | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/component/mxgraph/style/utils.ts b/src/component/mxgraph/style/utils.ts index e884a9aeae..37a7dbe267 100644 --- a/src/component/mxgraph/style/utils.ts +++ b/src/component/mxgraph/style/utils.ts @@ -120,6 +120,8 @@ const convertDirection = (direction: GradientDirection): string => { return mxConstants.DIRECTION_NORTH; case 'top-to-bottom': return mxConstants.DIRECTION_SOUTH; + default: + return mxConstants.DIRECTION_EAST; } }; @@ -154,6 +156,6 @@ export const isShapeStyleUpdate = (style: StyleUpdate): style is ShapeStyleUpdat return style && typeof style === 'object' && 'fill' in style; }; -export const isFillColorGradient = (color: string | FillColorGradient): color is FillColorGradient => { - return color && typeof color !== 'string'; +const isFillColorGradient = (color: string | FillColorGradient): color is FillColorGradient => { + return color && typeof color === 'object'; }; diff --git a/test/integration/mxGraph.model.style.api.test.ts b/test/integration/mxGraph.model.style.api.test.ts index 7d8e1a8b62..4d866a608d 100644 --- a/test/integration/mxGraph.model.style.api.test.ts +++ b/test/integration/mxGraph.model.style.api.test.ts @@ -310,7 +310,7 @@ describe('mxGraph model - update style', () => { label: 'Service Task 1.2', }); - const fill: Fill = { color: { startColor: 'gold', endColor: 'pink', direction: 'right-to-left' } }; + const fill: Fill = { color: { startColor: 'gold', endColor: 'pink', direction: 'right-to-left' } }; bpmnVisualization.bpmnElementsRegistry.updateStyle('serviceTask_1_2', { fill }); // Check that the style has been updated @@ -953,7 +953,7 @@ describe('mxGraph model - reset style', () => { const elementId = 'userTask_2_2'; // Apply custom style - const fill = { color: { startColor: 'gold', endColor: 'pink', direction: 'top-to-bottom' } }; + const fill = { color: { startColor: 'gold', endColor: 'pink', direction: 'top-to-bottom' } }; bpmnVisualization.bpmnElementsRegistry.updateStyle(elementId, { fill }); // Reset style diff --git a/test/shared/visu/bpmn-page-utils.ts b/test/shared/visu/bpmn-page-utils.ts index 38a409e3fb..af89477f05 100644 --- a/test/shared/visu/bpmn-page-utils.ts +++ b/test/shared/visu/bpmn-page-utils.ts @@ -224,7 +224,7 @@ export class PageTester { if ('fill' in styleUpdate) { const fill = (styleUpdate).fill; - if (typeof fill.color !== 'string') { + if (typeof fill.color === 'object') { fill.color.startColor && (url += `&style.api.fill.color.startColor=${fill.color.startColor}`); fill.color.endColor && (url += `&style.api.fill.color.endColor=${fill.color.endColor}`); fill.color.direction && (url += `&style.api.fill.color.direction=${fill.color.direction}`); From dd0921209d935cd7220879748096ca33d30fe152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= Date: Wed, 12 Jul 2023 15:41:50 +0200 Subject: [PATCH 18/20] introduce variable fillColor --- test/shared/visu/bpmn-page-utils.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/shared/visu/bpmn-page-utils.ts b/test/shared/visu/bpmn-page-utils.ts index af89477f05..30080a8351 100644 --- a/test/shared/visu/bpmn-page-utils.ts +++ b/test/shared/visu/bpmn-page-utils.ts @@ -224,12 +224,13 @@ export class PageTester { if ('fill' in styleUpdate) { const fill = (styleUpdate).fill; - if (typeof fill.color === 'object') { - fill.color.startColor && (url += `&style.api.fill.color.startColor=${fill.color.startColor}`); - fill.color.endColor && (url += `&style.api.fill.color.endColor=${fill.color.endColor}`); - fill.color.direction && (url += `&style.api.fill.color.direction=${fill.color.direction}`); + const fillColor = fill.color; + if (typeof fillColor === 'object') { + fillColor.startColor && (url += `&style.api.fill.color.startColor=${fillColor.startColor}`); + fillColor.endColor && (url += `&style.api.fill.color.endColor=${fillColor.endColor}`); + fillColor.direction && (url += `&style.api.fill.color.direction=${fillColor.direction}`); } else { - fill.color && (url += `&style.api.fill.color=${fill.color}`); + fillColor && (url += `&style.api.fill.color=${fillColor}`); } fill.opacity && (url += `&style.api.fill.opacity=${fill.opacity}`); From a5b61e014ae7362e545ad697ba0cc3372aa129af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= Date: Wed, 12 Jul 2023 15:47:19 +0200 Subject: [PATCH 19/20] fix type --- test/integration/mxGraph.model.style.api.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/integration/mxGraph.model.style.api.test.ts b/test/integration/mxGraph.model.style.api.test.ts index 4d866a608d..c5320b1f9f 100644 --- a/test/integration/mxGraph.model.style.api.test.ts +++ b/test/integration/mxGraph.model.style.api.test.ts @@ -953,8 +953,7 @@ describe('mxGraph model - reset style', () => { const elementId = 'userTask_2_2'; // Apply custom style - const fill = { color: { startColor: 'gold', endColor: 'pink', direction: 'top-to-bottom' } }; - bpmnVisualization.bpmnElementsRegistry.updateStyle(elementId, { fill }); + bpmnVisualization.bpmnElementsRegistry.updateStyle(elementId, { fill: { color: { startColor: 'gold', endColor: 'pink', direction: 'top-to-bottom' } } }); // Reset style bpmnVisualization.bpmnElementsRegistry.resetStyle(elementId); From 542e307571bec62f95e356f03b0501f960ed9168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souchet=20C=C3=A9line?= Date: Wed, 12 Jul 2023 16:28:21 +0200 Subject: [PATCH 20/20] remove line --- src/component/mxgraph/style/utils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/component/mxgraph/style/utils.ts b/src/component/mxgraph/style/utils.ts index 37a7dbe267..9324cb9af7 100644 --- a/src/component/mxgraph/style/utils.ts +++ b/src/component/mxgraph/style/utils.ts @@ -112,8 +112,6 @@ export const updateFont = (cellStyle: string, font: Font): string => { const convertDirection = (direction: GradientDirection): string => { switch (direction) { - case 'left-to-right': - return mxConstants.DIRECTION_EAST; case 'right-to-left': return mxConstants.DIRECTION_WEST; case 'bottom-to-top':