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(cloud-templates): add linter plugin #3

Merged
merged 5 commits into from
Jul 10, 2023
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
396 changes: 237 additions & 159 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"dependencies": {
"@bpmn-io/element-templates-validator": "^0.14.0",
"@bpmn-io/extract-process-variables": "^0.8.0",
"bpmnlint": "^8.3.2",
"classnames": "^2.3.1",
"ids": "^1.0.0",
"min-dash": "^4.0.0",
Expand All @@ -62,20 +63,21 @@
"@bpmn-io/element-templates-icons-renderer": "^0.3.0",
"@bpmn-io/properties-panel": "^2.2.0",
"@bpmn-io/variable-resolver": "1.0.1",
"@camunda/linting": "^1.3.0",
"@camunda/linting": "^2.0.0",
"@rollup/plugin-alias": "^5.0.0",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"@testing-library/preact": "^2.0.1",
"@testing-library/preact-hooks": "^1.1.0",
"assert": "^2.0.0",
"axe-core": "^4.4.2",
"babel-loader": "^9.0.0",
"babel-plugin-istanbul": "^6.1.1",
"bpmn-js": "^13.0.0",
"bpmn-js-create-append-anything": "^0.2.1",
"bpmn-js-properties-panel": "^1.26.0",
"bpmn-js-properties-panel": "^2.0.0",
"bpmn-moddle": "^8.0.0",
"camunda-bpmn-js-behaviors": "^0.6.0",
"camunda-bpmn-moddle": "^7.0.1",
Expand Down Expand Up @@ -113,8 +115,8 @@
},
"peerDependencies": {
"@bpmn-io/properties-panel": ">= 2.2",
"bpmn-js-properties-panel": ">= 1",
"bpmn-js": ">= 11.5",
"bpmn-js-properties-panel": ">= 2",
"camunda-bpmn-js-behaviors": ">= 0.4",
"diagram-js": ">= 11.9"
}
Expand Down
116 changes: 116 additions & 0 deletions src/cloud-element-templates/LinterPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
* under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright
* ownership.
*
* Camunda licenses this file to you under the MIT; you may not use this file
* except in compliance with the MIT License.
*/

import StaticResolver from 'bpmnlint/lib/resolver/static-resolver';
import ElementTemplates from './ElementTemplates';
import { getPropertyValue, validateProperty } from './util/propertyUtil';

import { applyConditions } from './Condition';

import BpmnModdle from 'bpmn-moddle';
import zeebeModdle from 'zeebe-bpmn-moddle/resources/zeebe';

import { Validator } from './Validator';

export const elementTemplateLintRule = ({ templates = [] }) => {
const moddle = new BpmnModdle({ zeebe: zeebeModdle });

const validator = new Validator(moddle).addAll(templates);
const validTemplates = validator.getValidTemplates();

// We use the ElementTemplates Module without the required bpmn-js modules
// As we only use it to facilitate template ID and version lookup,
// access to commandstack etc. is not required
const elementTemplates = new ElementTemplates();
Copy link
Member

Choose a reason for hiding this comment

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

What can go wrong in the future here? 🙈

elementTemplates.set(validTemplates);

function check(node, reporter) {

let template = elementTemplates.get(node);

const templateId = elementTemplates._getTemplateId(node);

// Handle missing template
if (templateId && !template) {
reporter.report(
node.id,
'Linked element template not found',
{
name: node.name
}
);
return;
}

if (!template) {
return;
}

template = applyConditions(node, template);

// Check attributes
template.properties.forEach((property) => {
const value = getPropertyValue(node, property);
const error = validateProperty(value, property);

if (!error) {
return;
}

reporter.report(
node.id,
error,
{
propertiesPanel: {
entryIds: [ getEntryId(property, template) ]
},
name: node.name
}
);
});
}

return {
check
};

};


export const ElementTemplateLinterPlugin = function(templates) {
return {
config: {
rules: {
'element-templates/validate': [ 'error', { templates } ]
}
},
resolver: new StaticResolver({
'rule:bpmnlint-plugin-element-templates/validate': elementTemplateLintRule
})
};
};


// helpers //////////////////////

function getEntryId(property, template) {
const index = template.properties
.filter(p => p.group === property.group)
.indexOf(property);

const path = [ 'custom-entry', template.id ];

if (property.group) {
path.push(property.group);
}

path.push(index);
return path.join('-');
}
54 changes: 4 additions & 50 deletions src/cloud-element-templates/properties/CustomProperties.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import {
find,
forEach,
groupBy,
isString
groupBy
} from 'min-dash';

import { useService } from 'bpmn-js-properties-panel';

import { PropertyDescription } from '../../element-templates/components/PropertyDescription';

import { getPropertyValue, setPropertyValue } from '../util/propertyUtil';
import { getPropertyValue, setPropertyValue, validateProperty } from '../util/propertyUtil';

import {
Group,
Expand Down Expand Up @@ -426,61 +425,16 @@ function propertyGetter(element, property) {
}

function propertySetter(bpmnFactory, commandStack, element, property) {
return function getValue(value) {
return function setValue(value) {
return setPropertyValue(bpmnFactory, commandStack, element, property, value);
};
}

function propertyValidator(translate, property) {
return function validate(value) {
const { constraints = {} } = property;

const {
maxLength,
minLength,
notEmpty
} = constraints;

if (notEmpty && isEmpty(value)) {
return translate('Must not be empty.');
}

if (maxLength && (value || '').length > maxLength) {
return translate('Must have max length {maxLength}.', { maxLength });
}

if (minLength && (value || '').length < minLength) {
return translate('Must have min length {minLength}.', { minLength });
}

let { pattern } = constraints;

if (pattern) {
let message;

if (!isString(pattern)) {
message = pattern.message;
pattern = pattern.value;
}

if (!matchesPattern(value, pattern)) {
return message || translate('Must match pattern {pattern}.', { pattern });
}
}
};
return value => validateProperty(value, property, translate);
}

function isEmpty(value) {
if (typeof value === 'string') {
return !value.trim().length;
}

return value === undefined;
}

function matchesPattern(string, pattern) {
return new RegExp(pattern).test(string);
}

function groupByGroupId(properties) {
return groupBy(properties, 'group');
Expand Down
60 changes: 60 additions & 0 deletions src/cloud-element-templates/util/propertyUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import {
} from 'bpmn-js/lib/util/ModelUtil';

import {
default as defaultTranslate
} from 'diagram-js/lib/i18n/translate/translate';

import {
isString,
isUndefined, without
} from 'min-dash';

Expand Down Expand Up @@ -755,6 +760,49 @@ export function unsetProperty(commandStack, element, property) {
}
}

export function validateProperty(value, property, translate = defaultTranslate) {
const {
constraints = {},
label
} = property;

const {
maxLength,
minLength,
notEmpty
} = constraints;

if (notEmpty && isEmpty(value)) {
return `${label} ${translate('must not be empty.')}`;
}

if (maxLength && (value || '').length > maxLength) {
return `${label} ${translate('must have max length {maxLength}.', { maxLength })}`;
}

if (minLength && (value || '').length < minLength) {
return `${label} ${translate('must have min length {minLength}.', { minLength })}`;
}

let { pattern } = constraints;

if (pattern) {
let message;

if (!isString(pattern)) {
message = pattern.message;
pattern = pattern.value;
}

if (!matchesPattern(value, pattern)) {
if (message) {
return `${label} ${translate(message)}`;
}

return `${label} ${translate('must match pattern {pattern}.', { pattern })}`;
}
}
}

// helpers
function unknownBindingError(element, property) {
Expand All @@ -768,3 +816,15 @@ function unknownBindingError(element, property) {

return new Error(`unknown binding <${ type }> for element <${ id }>, this should never happen`);
}

function isEmpty(value) {
if (typeof value === 'string') {
return !value.trim().length;
}

return value === undefined;
}

function matchesPattern(string, pattern) {
return new RegExp(pattern).test(string);
}
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { default as ElementTemplatesPropertiesProviderModule } from './element-t

// utils
export { Validator as CloudElementTemplatesValidator } from './cloud-element-templates/Validator';
export { ElementTemplateLinterPlugin as CloudElementTemplatesLinterPlugin } from './cloud-element-templates/LinterPlugin';
57 changes: 56 additions & 1 deletion test/TestHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import Modeler from 'bpmn-js/lib/Modeler';

import axe from 'axe-core';

import BPMNModdle from 'bpmn-moddle';
import zeebeModdle from 'zeebe-bpmn-moddle/resources/zeebe';

/**
* https://www.deque.com/axe/core-documentation/api-documentation/#axe-core-tags
*/
Expand Down Expand Up @@ -255,4 +258,56 @@ document.addEventListener('keydown', function(event) {
bpmnJS.saveXML({ format: true }).then(function(result) {
download(result.xml, 'test.bpmn', 'application/xml');
});
});
});

// Moddle helpers //////////////////////
export async function createModdle(xml) {
const moddle = new BPMNModdle({
zeebe: zeebeModdle
});

let root, warnings;

try {
({
rootElement: root,
warnings = []
} = await moddle.fromXML(xml, 'bpmn:Definitions', { lax: true }));
} catch (err) {
console.log(err);
}

return {
root,
moddle,
context: {
warnings
},
warnings
};
}

export function createDefinitions(xml = '') {
return `
<bpmn:definitions
xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
xmlns:modeler="http://camunda.org/schema/modeler/1.0"
xmlns:zeebe="http://camunda.org/schema/zeebe/1.0"
id="Definitions_1">
${ xml }
</bpmn:definitions>
`;
}


export function createProcess(bpmn = '', bpmndi = '') {
return createDefinitions(`
<bpmn:process id="Process_1" isExecutable="true">
${ bpmn }
</bpmn:process>
${ bpmndi }
`);
}
Loading