diff --git a/.github/workflows/TASKLIST_CARBONISATION.yml b/.github/workflows/TASKLIST_CARBONISATION.yml index 42a3ebd17..fddbb176a 100644 --- a/.github/workflows/TASKLIST_CARBONISATION.yml +++ b/.github/workflows/TASKLIST_CARBONISATION.yml @@ -15,11 +15,11 @@ jobs: options: --user 1001:1000 steps: - name: Checkout form-js - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + uses: actions/checkout@9a9194f87191a7e9055e3e9b95b8cfb13023bb08 with: path: form-js - name: Checkout Tasklist - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + uses: actions/checkout@9a9194f87191a7e9055e3e9b95b8cfb13023bb08 with: repository: camunda/zeebe token: ${{ secrets.ADD_TO_HTO_PROJECT_PAT }} @@ -67,7 +67,7 @@ jobs: - name: Run Playwright tests working-directory: ./zeebe/tasklist/client run: yarn playwright form-js-integration - - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a + - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 if: always() with: name: playwright-report diff --git a/packages/form-js-editor/src/features/modeling/behavior/ValidateBehavior.js b/packages/form-js-editor/src/features/modeling/behavior/ValidateBehavior.js index d412e27da..698f9a71c 100644 --- a/packages/form-js-editor/src/features/modeling/behavior/ValidateBehavior.js +++ b/packages/form-js-editor/src/features/modeling/behavior/ValidateBehavior.js @@ -21,6 +21,7 @@ export class ValidateBehavior extends CommandInterceptor { delete newValidate.minLength; delete newValidate.maxLength; + delete newValidate.custom; delete newValidate.pattern; properties['validate'] = newValidate; diff --git a/packages/form-js-editor/src/features/properties-panel/PropertiesProvider.js b/packages/form-js-editor/src/features/properties-panel/PropertiesProvider.js index 95a5a267b..45fb43d1e 100644 --- a/packages/form-js-editor/src/features/properties-panel/PropertiesProvider.js +++ b/packages/form-js-editor/src/features/properties-panel/PropertiesProvider.js @@ -6,6 +6,7 @@ import { SerializationGroup, ConstraintsGroup, ValidationGroup, + CustomValidationsGroup, OptionsGroups, TableHeaderGroups, LayoutGroup, @@ -66,6 +67,7 @@ export class PropertiesProvider { SerializationGroup(field, editField), ConstraintsGroup(field, editField), ValidationGroup(field, editField), + CustomValidationsGroup(field, editField, getService), CustomPropertiesGroup(field, editField), ].filter((group) => group != null); diff --git a/packages/form-js-editor/src/features/properties-panel/entries/AdornerEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/AdornerEntry.js index 7a555f8de..0646ca249 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/AdornerEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/AdornerEntry.js @@ -52,7 +52,7 @@ function PrefixAdorner(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); return FeelTemplatingEntry({ debounce, @@ -72,7 +72,7 @@ function SuffixAdorner(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); return FeelTemplatingEntry({ debounce, diff --git a/packages/form-js-editor/src/features/properties-panel/entries/AltTextEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/AltTextEntry.js index 868944b21..3cc56437e 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/AltTextEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/AltTextEntry.js @@ -26,7 +26,7 @@ function AltText(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); const path = ['alt']; diff --git a/packages/form-js-editor/src/features/properties-panel/entries/ColumnsExpressionEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/ColumnsExpressionEntry.js index 7609b3c47..cc9eb72e9 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/ColumnsExpressionEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/ColumnsExpressionEntry.js @@ -27,7 +27,7 @@ function ColumnsExpression(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); const getValue = () => { return get(field, PATH); diff --git a/packages/form-js-editor/src/features/properties-panel/entries/ConditionEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/ConditionEntry.js index fb49a8820..a30ee107f 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/ConditionEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/ConditionEntry.js @@ -22,7 +22,7 @@ function Condition(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); const path = ['conditional', 'hide']; diff --git a/packages/form-js-editor/src/features/properties-panel/entries/CustomValidationEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/CustomValidationEntry.js new file mode 100644 index 000000000..0251235e4 --- /dev/null +++ b/packages/form-js-editor/src/features/properties-panel/entries/CustomValidationEntry.js @@ -0,0 +1,123 @@ +import { get, set } from 'min-dash'; + +import { useService, useVariables } from '../hooks'; + +import { FeelEntry, isFeelEntryEdited } from '@bpmn-io/properties-panel'; +import { useCallback, useMemo } from 'preact/hooks'; + +export function CustomValidationEntry(props) { + const { editField, field, idPrefix, index } = props; + + const entries = [ + { + component: Condition, + editField, + field, + id: idPrefix + '-condition', + idPrefix, + index, + }, + { + component: Message, + editField, + field, + id: idPrefix + '-message', + idPrefix, + index, + }, + ]; + + return entries; +} + +function Condition(props) { + const { editField, field, id, index } = props; + + const debounce = useService('debounce'); + const _variables = useVariables(); + + const variables = useMemo( + () => [{ name: 'value', type: 'keyword', info: 'Returns the current field value.' }, ..._variables], + [_variables], + ); + + const setValue = (value, error) => { + if (error) { + return; + } + + const validate = get(field, ['validate']); + const newValidate = set(validate, ['custom', index, 'condition'], value); + + return editField(field, 'validate', newValidate); + }; + + const getValue = () => { + return get(field, ['validate', 'custom', index, 'condition']); + }; + + const conditionEntryValidate = useCallback((value) => { + if (typeof value !== 'string' || value.length === 0) { + return 'Must not be empty.'; + } + }, []); + + return FeelEntry({ + feel: 'required', + isEdited: isFeelEntryEdited, + debounce, + element: field, + getValue, + id, + label: 'Condition', + setValue, + validate: conditionEntryValidate, + variables, + }); +} + +function Message(props) { + const { editField, field, id, index } = props; + + const debounce = useService('debounce'); + const _variables = useVariables(); + + const variables = useMemo( + () => [{ name: 'value', type: 'keyword', info: 'Returns the current field value.' }, ..._variables], + [_variables], + ); + + const setValue = (value, error) => { + if (error) { + return; + } + + const validate = get(field, ['validate']); + const newValidate = set(validate, ['custom', index, 'message'], value); + + return editField(field, 'validate', newValidate); + }; + + const getValue = () => { + return get(field, ['validate', 'custom', index, 'message']); + }; + + const messageEntryValidate = useCallback((value) => { + if (typeof value !== 'string' || value.length === 0) { + return 'Must not be empty.'; + } + }, []); + + return FeelEntry({ + feel: 'optional', + isEdited: isFeelEntryEdited, + debounce, + element: field, + getValue, + id, + label: 'Message if condition not met', + setValue, + validate: messageEntryValidate, + variables, + }); +} diff --git a/packages/form-js-editor/src/features/properties-panel/entries/DescriptionEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/DescriptionEntry.js index c5b0f1909..e58e6fcf8 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/DescriptionEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/DescriptionEntry.js @@ -28,7 +28,7 @@ function Description(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); const path = ['description']; diff --git a/packages/form-js-editor/src/features/properties-panel/entries/ExpressionFieldEntries.js b/packages/form-js-editor/src/features/properties-panel/entries/ExpressionFieldEntries.js index cdc71825b..efc4d6c0a 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/ExpressionFieldEntries.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/ExpressionFieldEntries.js @@ -31,7 +31,7 @@ function ExpressionFieldExpression(props) { const { editField, field, id } = props; const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); const getValue = () => field.expression || ''; diff --git a/packages/form-js-editor/src/features/properties-panel/entries/HtmlEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/HtmlEntry.js index bf1f81a38..d525c1326 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/HtmlEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/HtmlEntry.js @@ -26,7 +26,7 @@ function Content(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); const path = ['content']; diff --git a/packages/form-js-editor/src/features/properties-panel/entries/IFrameUrlEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/IFrameUrlEntry.js index 118bdd9b3..ea67baefd 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/IFrameUrlEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/IFrameUrlEntry.js @@ -27,7 +27,7 @@ function Url(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); const path = ['url']; diff --git a/packages/form-js-editor/src/features/properties-panel/entries/ImageSourceEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/ImageSourceEntry.js index 30b61c867..89da70770 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/ImageSourceEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/ImageSourceEntry.js @@ -25,7 +25,7 @@ function Source(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); const path = ['source']; diff --git a/packages/form-js-editor/src/features/properties-panel/entries/LabelEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/LabelEntry.js index 884ac308a..17daf8780 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/LabelEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/LabelEntry.js @@ -58,7 +58,7 @@ function Label(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); const path = ['label']; @@ -89,7 +89,7 @@ function DateLabel(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); const path = DATE_LABEL_PATH; @@ -118,7 +118,7 @@ function TimeLabel(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); const path = TIME_LABEL_PATH; diff --git a/packages/form-js-editor/src/features/properties-panel/entries/OptionsExpressionEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/OptionsExpressionEntry.js index 25547ced8..a0ef47e03 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/OptionsExpressionEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/OptionsExpressionEntry.js @@ -22,7 +22,7 @@ function OptionsExpression(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); const path = OPTIONS_SOURCES_PATHS[OPTIONS_SOURCES.EXPRESSION]; diff --git a/packages/form-js-editor/src/features/properties-panel/entries/ReadonlyEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/ReadonlyEntry.js index 20cadb40b..f2e8980ed 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/ReadonlyEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/ReadonlyEntry.js @@ -32,7 +32,7 @@ function Readonly(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); const path = ['readonly']; diff --git a/packages/form-js-editor/src/features/properties-panel/entries/TableDataSourceEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/TableDataSourceEntry.js index 30d6dd772..808bbd4e0 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/TableDataSourceEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/TableDataSourceEntry.js @@ -27,7 +27,7 @@ function Source(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); const path = ['dataSource']; diff --git a/packages/form-js-editor/src/features/properties-panel/entries/TextEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/TextEntry.js index 166cbf1c3..8d6f730ed 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/TextEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/TextEntry.js @@ -26,7 +26,7 @@ function Text(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); const path = ['text']; diff --git a/packages/form-js-editor/src/features/properties-panel/groups/CustomValidationsGroup.js b/packages/form-js-editor/src/features/properties-panel/groups/CustomValidationsGroup.js new file mode 100644 index 000000000..da33a26f6 --- /dev/null +++ b/packages/form-js-editor/src/features/properties-panel/groups/CustomValidationsGroup.js @@ -0,0 +1,80 @@ +import { get, set, without } from 'min-dash'; + +import { arrayAdd } from '../Util'; + +import { ListGroup } from '@bpmn-io/properties-panel'; + +import { VALIDATION_TYPE_OPTIONS } from './ValidationGroup'; +import { CustomValidationEntry } from '../entries/CustomValidationEntry'; + +export function CustomValidationsGroup(field, editField, getService) { + const validate = get(field, ['validate'], {}); + const { validatable, keyed } = getService('formFields').get(field.type).config; + + const isValidatable = validatable !== undefined ? validatable : keyed; + const isCustomValidation = [undefined, VALIDATION_TYPE_OPTIONS.custom.value].includes(validate.validationType); + const shouldRender = isValidatable && isCustomValidation; + + if (!shouldRender) { + return; + } + + return { + id: 'custom-validation', + label: 'Custom validations', + tooltip: "Define custom validation rules for this field. Use 'value' to reference the field value.", + component: ListGroup, + ...CustomValidationsEntry({ editField, field, id: 'custom-validation-list' }), + }; +} + +export function CustomValidationsEntry(props) { + const { editField, field, id: idPrefix } = props; + + const addEntry = (e) => { + e.stopPropagation(); + + const customValidations = get(field, ['validate', 'custom'], []); + const newIndex = customValidations.length + 1; + + const newValue = { + condition: '=false', + message: 'Error message.', + }; + + const newArray = arrayAdd(customValidations, newIndex, newValue); + const newValidate = set(field.validate || {}, ['custom'], newArray); + + editField(field, ['validate'], newValidate); + }; + + const removeEntry = (entry) => { + const customValidations = get(field, ['validate', 'custom'], []); + const newArray = without(customValidations, entry); + const newValidate = set(field.validate, ['custom'], newArray); + + editField(field, ['validate'], newValidate); + }; + + const items = get(field, ['validate', 'custom'], []).map((entry, index) => { + const id = idPrefix + '-' + index; + + return { + id, + entries: CustomValidationEntry({ + editField, + field, + idPrefix, + index, + }), + label: 'Rule ' + (index + 1), + remove: () => removeEntry(entry), + }; + }); + + return { + items, + add: addEntry, + shouldSort: false, + }; +} diff --git a/packages/form-js-editor/src/features/properties-panel/groups/ValidationGroup.js b/packages/form-js-editor/src/features/properties-panel/groups/ValidationGroup.js index 017d2f473..f599c27d5 100644 --- a/packages/form-js-editor/src/features/properties-panel/groups/ValidationGroup.js +++ b/packages/form-js-editor/src/features/properties-panel/groups/ValidationGroup.js @@ -14,7 +14,7 @@ import { useService, useVariables } from '../hooks'; import { INPUTS } from '../Util'; -const VALIDATION_TYPE_OPTIONS = { +export const VALIDATION_TYPE_OPTIONS = { custom: { value: '', label: 'Custom', @@ -149,7 +149,7 @@ function MinLength(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); return FeelNumberEntry({ debounce, @@ -169,7 +169,7 @@ function MaxLength(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); return FeelNumberEntry({ debounce, @@ -204,7 +204,7 @@ function Min(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); return FeelNumberEntry({ debounce, @@ -224,7 +224,7 @@ function Max(props) { const debounce = useService('debounce'); - const variables = useVariables().map((name) => ({ name })); + const variables = useVariables(); return FeelNumberEntry({ debounce, diff --git a/packages/form-js-editor/src/features/properties-panel/groups/index.js b/packages/form-js-editor/src/features/properties-panel/groups/index.js index 648be7a52..46feb510b 100644 --- a/packages/form-js-editor/src/features/properties-panel/groups/index.js +++ b/packages/form-js-editor/src/features/properties-panel/groups/index.js @@ -2,6 +2,7 @@ export { GeneralGroup } from './GeneralGroup'; export { SerializationGroup } from './SerializationGroup'; export { ConstraintsGroup } from './ConstraintsGroup'; export { ValidationGroup } from './ValidationGroup'; +export { CustomValidationsGroup } from './CustomValidationsGroup'; export { OptionsGroups } from './OptionsGroups'; export { CustomPropertiesGroup } from './CustomPropertiesGroup'; export { AppearanceGroup } from './AppearanceGroup'; diff --git a/packages/form-js-editor/src/features/properties-panel/hooks/useVariables.js b/packages/form-js-editor/src/features/properties-panel/hooks/useVariables.js index ed85bff07..24da50188 100644 --- a/packages/form-js-editor/src/features/properties-panel/hooks/useVariables.js +++ b/packages/form-js-editor/src/features/properties-panel/hooks/useVariables.js @@ -1,14 +1,21 @@ -import { getSchemaVariables } from '@bpmn-io/form-js-viewer'; +import { clone, getSchemaVariables } from '@bpmn-io/form-js-viewer'; import { useService } from './usePropertiesPanelService'; +import { useMemo, useState } from 'preact/hooks'; /** - * Retrieve list of variables from the form schema. + * Retrieve list of variables from the form schema in the structure expected by FEEL entries. * - * @returns { string[] } list of variables used in form schema + * @returns { { name: string; }[] } list of variables used in form schema */ export function useVariables() { const form = useService('formEditor'); - const schema = form.getSchema(); + const [schema, setSchema] = useState(clone(form.getSchema())); - return getSchemaVariables(schema); + form.on('changed', ({ schema }) => { + if (schema !== undefined) { + setSchema(clone(schema)); + } + }); + + return useMemo(() => getSchemaVariables(schema).map((name) => ({ name, type: 'variable' })), [schema]); } diff --git a/packages/form-js-playground/rollup.config.js b/packages/form-js-playground/rollup.config.js index 2aef04da8..199d96ae7 100644 --- a/packages/form-js-playground/rollup.config.js +++ b/packages/form-js-playground/rollup.config.js @@ -64,6 +64,7 @@ export default [ 'classnames', 'min-dom', 'min-dash', + 'lodash', ], onwarn, }, diff --git a/packages/form-js-viewer/rollup.config.js b/packages/form-js-viewer/rollup.config.js index 4a16355ae..2f3dccb65 100644 --- a/packages/form-js-viewer/rollup.config.js +++ b/packages/form-js-viewer/rollup.config.js @@ -50,6 +50,7 @@ export default [ ], external: [ 'min-dash', + 'lodash', 'big.js', 'preact', 'preact/jsx-runtime', diff --git a/packages/form-js-viewer/src/core/Validator.js b/packages/form-js-viewer/src/core/Validator.js index 272490537..9a37c5d75 100644 --- a/packages/form-js-viewer/src/core/Validator.js +++ b/packages/form-js-viewer/src/core/Validator.js @@ -81,6 +81,18 @@ export class Validator { errors = [...errors, ...runPresetValidation(field, evaluatedValidation, value)]; + const evaluateExpression = (expression) => + runExpressionEvaluation(this._expressionLanguage, expression, { ...expressionContextInfo, value }); + + if ('custom' in evaluatedValidation && value && evaluatedValidation.custom.length) { + const { custom } = evaluatedValidation; + custom.forEach(({ condition, message }) => { + if (condition && message && !evaluateExpression(condition)) { + errors = [...errors, evaluateExpression(message)]; + } + }); + } + return errors; } } diff --git a/packages/form-js-viewer/src/render/components/form-fields/ExpressionField.js b/packages/form-js-viewer/src/render/components/form-fields/ExpressionField.js index 5ac067847..79aaceddb 100644 --- a/packages/form-js-viewer/src/render/components/form-fields/ExpressionField.js +++ b/packages/form-js-viewer/src/render/components/form-fields/ExpressionField.js @@ -45,6 +45,7 @@ ExpressionField.config = { label: 'Expression', group: 'basic-input', keyed: true, + validatable: false, emptyValue: null, escapeGridRender: true, create: (options = {}) => ({ diff --git a/packages/form-js-viewer/src/util/expressions.js b/packages/form-js-viewer/src/util/expressions.js index 81640b281..858c72d40 100644 --- a/packages/form-js-viewer/src/util/expressions.js +++ b/packages/form-js-viewer/src/util/expressions.js @@ -22,13 +22,13 @@ export function buildExpressionContext(context) { * If the string is not an expression, it is returned as is. * * @param {any} expressionLanguage - The expression language to use. - * @param {string} value - The string to evaluate. + * @param {string} expression - The string expression to evaluate. * @param {Object} expressionContextInfo - The context information to use. * @returns {any} - Evaluated value or the original value if not an expression. */ -export function runExpressionEvaluation(expressionLanguage, value, expressionContextInfo) { - if (expressionLanguage && expressionLanguage.isExpression(value)) { - return expressionLanguage.evaluate(value, buildExpressionContext(expressionContextInfo)); +export function runExpressionEvaluation(expressionLanguage, expression, expressionContextInfo) { + if (expressionLanguage && expressionLanguage.isExpression(expression)) { + return expressionLanguage.evaluate(expression, buildExpressionContext(expressionContextInfo)); } - return value; + return expression; } diff --git a/packages/form-js-viewer/test/spec/core/Validator.spec.js b/packages/form-js-viewer/test/spec/core/Validator.spec.js index 590974286..60f78cf7c 100644 --- a/packages/form-js-viewer/test/spec/core/Validator.spec.js +++ b/packages/form-js-viewer/test/spec/core/Validator.spec.js @@ -1,9 +1,12 @@ import EventBus from 'diagram-js/lib/core/EventBus'; import { Validator } from '../../../src/core/Validator'; import { FeelExpressionLanguage } from '../../../src/features/expressionLanguage'; +import { set } from 'min-dash'; describe('Validator', function () { - const validator = createValidator(); + const formFields = { foo: {} }; + const validator = createValidator({ formFields }); + const setValidate = (validate) => set(formFields.foo, ['validate'], validate); describe('#validateField', function () { it('should return no errors', function () { @@ -564,162 +567,310 @@ describe('Validator', function () { expect(errors[0]).to.equal('Field must be a valid international phone number. (e.g. +4930664040900)'); }); }); - }); - describe('minLength', function () { - it('should be valid', function () { - // given - const field = { - validate: { - minLength: 5, - }, - }; + describe('minLength', function () { + it('should be valid', function () { + // given + const field = { + validate: { + minLength: 5, + }, + }; - // when - const errors = validator.validateField(field, 'foobar'); + // when + const errors = validator.validateField(field, 'foobar'); - // then - expect(errors).to.have.length(0); - }); + // then + expect(errors).to.have.length(0); + }); - it('should be invalid', function () { - // given - const field = { - validate: { - minLength: 5, - }, - }; + it('should be invalid', function () { + // given + const field = { + validate: { + minLength: 5, + }, + }; - // when - const errors = validator.validateField(field, 'foo'); + // when + const errors = validator.validateField(field, 'foo'); - // then - expect(errors).to.have.length(1); - expect(errors[0]).to.equal('Field must have minimum length of 5.'); + // then + expect(errors).to.have.length(1); + expect(errors[0]).to.equal('Field must have minimum length of 5.'); + }); }); - }); - describe('minLength (expression)', function () { - it('should be valid', function () { - // given - const field = { - validate: { - minLength: '=5', - }, - }; + describe('minLength (expression)', function () { + it('should be valid', function () { + // given + const field = { + validate: { + minLength: '=5', + }, + }; - // when - const errors = validator.validateField(field, 'foobar'); + // when + const errors = validator.validateField(field, 'foobar'); - // then - expect(errors).to.have.length(0); - }); + // then + expect(errors).to.have.length(0); + }); - it('should be invalid', function () { - // given - const field = { - validate: { - minLength: '=5', - }, - }; + it('should be invalid', function () { + // given + const field = { + validate: { + minLength: '=5', + }, + }; - // when - const errors = validator.validateField(field, 'foo'); + // when + const errors = validator.validateField(field, 'foo'); - // then - expect(errors).to.have.length(1); - expect(errors[0]).to.equal('Field must have minimum length of 5.'); + // then + expect(errors).to.have.length(1); + expect(errors[0]).to.equal('Field must have minimum length of 5.'); + }); }); - }); - describe('maxLength', function () { - it('should be valid', function () { - // given - const field = { - validate: { - maxLength: 5, - }, - }; + describe('maxLength', function () { + it('should be valid', function () { + // given + const field = { + validate: { + maxLength: 5, + }, + }; - // when - const errors = validator.validateField(field, 'foo'); + // when + const errors = validator.validateField(field, 'foo'); - // then - expect(errors).to.have.length(0); + // then + expect(errors).to.have.length(0); + }); + + it('should be invalid', function () { + // given + const field = { + validate: { + maxLength: 5, + }, + }; + + // when + const errors = validator.validateField(field, 'foobar'); + + // then + expect(errors).to.have.length(1); + expect(errors[0]).to.equal('Field must have maximum length of 5.'); + }); }); - it('should be invalid', function () { - // given - const field = { - validate: { - maxLength: 5, - }, - }; + describe('maxLength (expression)', function () { + it('should be valid', function () { + // given + const field = { + validate: { + maxLength: '=5', + }, + }; - // when - const errors = validator.validateField(field, 'foobar'); + // when + const errors = validator.validateField(field, 'foo'); - // then - expect(errors).to.have.length(1); - expect(errors[0]).to.equal('Field must have maximum length of 5.'); + // then + expect(errors).to.have.length(0); + }); + + it('should be invalid', function () { + // given + const field = { + validate: { + maxLength: '=5', + }, + }; + + // when + const errors = validator.validateField(field, 'foobar'); + + // then + expect(errors).to.have.length(1); + expect(errors[0]).to.equal('Field must have maximum length of 5.'); + }); }); }); - describe('maxLength (expression)', function () { - it('should be valid', function () { - // given - const field = { - validate: { - maxLength: '=5', - }, - }; + describe('#validateFieldInstance', function () { + describe('custom validator', function () { + it('should be valid', function () { + // given + setValidate({ + custom: [ + { + condition: '=1 > 0', + message: 'No errors expected', + }, + ], + }); - // when - const errors = validator.validateField(field, 'foo'); + const fieldInstance = { + id: 'foo', + expressionContextInfo: {}, + }; - // then - expect(errors).to.have.length(0); - }); + // when + const errors = validator.validateFieldInstance(fieldInstance, 'value'); - it('should be invalid', function () { - // given - const field = { - validate: { - maxLength: '=5', - }, - }; + // then + expect(errors).to.have.length(0); + }); - // when - const errors = validator.validateField(field, 'foobar'); + it('should be invalid', function () { + // given + setValidate({ + custom: [ + { + condition: '=1 < 0', + message: 'Error expected', + }, + ], + }); - // then - expect(errors).to.have.length(1); - expect(errors[0]).to.equal('Field must have maximum length of 5.'); + const fieldInstance = { + id: 'foo', + expressionContextInfo: {}, + }; + + // when + const errors = validator.validateFieldInstance(fieldInstance, 'value'); + + // then + expect(errors).to.deep.equal(['Error expected']); + }); + + it('should handle multiple custom validators', function () { + // given + setValidate({ + custom: [ + { + condition: '=2 < 0', + message: 'Errors expected', + }, + { + condition: '=1 < 0', + message: 'Error expected 2', + }, + ], + }); + + const fieldInstance = { + id: 'foo', + expressionContextInfo: {}, + }; + + // when + const errors = validator.validateFieldInstance(fieldInstance, 'value'); + + // then + expect(errors).to.deep.equal(['Errors expected', 'Error expected 2']); + }); + + it('should be able to use variables from expression context', function () { + // given + setValidate({ + custom: [ + { + condition: '=foo = "bar"', + message: 'Error expected', + }, + ], + }); + + const fieldInstance = { + id: 'foo', + expressionContextInfo: { + foo: 'bar', + }, + }; + + // when + const errors = validator.validateFieldInstance(fieldInstance, 'value'); + + // then + expect(errors).to.deep.equal([]); + + // but given + fieldInstance.expressionContextInfo.foo = 'baz'; + + // when + const errors2 = validator.validateFieldInstance(fieldInstance, 'value'); + + // then + expect(errors2).to.deep.equal(['Error expected']); + }); + + it('should be able to use value in expression', function () { + // given + setValidate({ + custom: [ + { + condition: '=value = "bar"', + message: 'Error expected', + }, + ], + }); + + const fieldInstance = { + id: 'foo', + expressionContextInfo: {}, + }; + + // when + const errors = validator.validateFieldInstance(fieldInstance, 'bar'); + + // then + expect(errors).to.deep.equal([]); + + // but when when + const errors2 = validator.validateFieldInstance(fieldInstance, 'baz'); + + // then + expect(errors2).to.deep.equal(['Error expected']); + }); }); }); -}); -// helpers ////////// - -function createValidator() { - const eventBus = new EventBus(); - const expressionLanguage = new FeelExpressionLanguage(eventBus); - - const conditionChecker = { - applyConditions() {}, - check() {}, - }; - - const form = { - _getState() { - return { - data: {}, - errors: {}, - initialData: {}, - properties: {}, - }; - }, - }; - - return new Validator(expressionLanguage, conditionChecker, form); -} + // helpers ////////// + + function createValidator(options = {}) { + const { formFields } = options; + + const eventBus = new EventBus(); + const expressionLanguage = new FeelExpressionLanguage(eventBus); + + const conditionChecker = { + applyConditions() {}, + check() {}, + }; + + const form = { + _getState() { + return { + data: {}, + errors: {}, + initialData: {}, + properties: {}, + }; + }, + }; + + const formFieldRegistry = { + get(id) { + return formFields[id]; + }, + }; + + return new Validator(expressionLanguage, conditionChecker, form, formFieldRegistry); + } +});