Skip to content

Commit 950da0c

Browse files
committed
feat: custom validation properties panel
Related to #1162
1 parent a3222e9 commit 950da0c

File tree

6 files changed

+194
-1
lines changed

6 files changed

+194
-1
lines changed

packages/form-js-editor/src/features/properties-panel/PropertiesProvider.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
SerializationGroup,
77
ConstraintsGroup,
88
ValidationGroup,
9+
CustomValidationsGroup,
910
OptionsGroups,
1011
TableHeaderGroups,
1112
LayoutGroup,
@@ -66,6 +67,7 @@ export class PropertiesProvider {
6667
SerializationGroup(field, editField),
6768
ConstraintsGroup(field, editField),
6869
ValidationGroup(field, editField),
70+
CustomValidationsGroup(field, editField, getService),
6971
CustomPropertiesGroup(field, editField),
7072
].filter((group) => group != null);
7173

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { get, set } from 'min-dash';
2+
3+
import { useService } from '../hooks';
4+
5+
import { FeelEntry, isFeelEntryEdited } from '@bpmn-io/properties-panel';
6+
import { useCallback } from 'preact/hooks';
7+
8+
export function CustomValidationEntry(props) {
9+
const { editField, field, idPrefix, index } = props;
10+
11+
const entries = [
12+
{
13+
component: Condition,
14+
editField,
15+
field,
16+
id: idPrefix + '-condition',
17+
idPrefix,
18+
index,
19+
},
20+
{
21+
component: Message,
22+
editField,
23+
field,
24+
id: idPrefix + '-message',
25+
idPrefix,
26+
index,
27+
},
28+
];
29+
30+
return entries;
31+
}
32+
33+
function Condition(props) {
34+
const { editField, field, id, index } = props;
35+
36+
const debounce = useService('debounce');
37+
38+
const setValue = (value, error) => {
39+
if (error) {
40+
return;
41+
}
42+
43+
const validate = get(field, ['validate']);
44+
const newValidate = set(validate, ['custom', index, 'condition'], value);
45+
46+
return editField(field, 'validate', newValidate);
47+
};
48+
49+
const getValue = () => {
50+
return get(field, ['validate', 'custom', index, 'condition']);
51+
};
52+
53+
const conditionEntryValidate = useCallback((value) => {
54+
if (typeof value !== 'string' || value.length === 0) {
55+
return 'Must not be empty.';
56+
}
57+
}, []);
58+
59+
return FeelEntry({
60+
feel: 'required',
61+
isEdited: isFeelEntryEdited,
62+
debounce,
63+
element: field,
64+
getValue,
65+
id,
66+
label: 'Condition',
67+
setValue,
68+
validate: conditionEntryValidate,
69+
});
70+
}
71+
72+
function Message(props) {
73+
const { editField, field, id, index } = props;
74+
75+
const debounce = useService('debounce');
76+
77+
const setValue = (value, error) => {
78+
if (error) {
79+
return;
80+
}
81+
82+
const validate = get(field, ['validate']);
83+
const newValidate = set(validate, ['custom', index, 'message'], value);
84+
85+
return editField(field, 'validate', newValidate);
86+
};
87+
88+
const getValue = () => {
89+
return get(field, ['validate', 'custom', index, 'message']);
90+
};
91+
92+
const messageEntryValidate = useCallback((value) => {
93+
if (typeof value !== 'string' || value.length === 0) {
94+
return 'Must not be empty.';
95+
}
96+
}, []);
97+
98+
return FeelEntry({
99+
feel: 'optional',
100+
isEdited: isFeelEntryEdited,
101+
debounce,
102+
element: field,
103+
getValue,
104+
id,
105+
label: 'Message if condition not met',
106+
setValue,
107+
validate: messageEntryValidate,
108+
});
109+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { get, set, without } from 'min-dash';
2+
3+
import { arrayAdd } from '../Util';
4+
5+
import { ListGroup } from '@bpmn-io/properties-panel';
6+
7+
import { VALIDATION_TYPE_OPTIONS } from './ValidationGroup';
8+
import { CustomValidationEntry } from '../entries/CustomValidationEntry';
9+
10+
export function CustomValidationsGroup(field, editField, getService) {
11+
const validate = get(field, ['validate'], {});
12+
const { validatable, keyed } = getService('formFields').get(field.type).config;
13+
14+
const isValidatable = validatable !== undefined ? validatable : keyed;
15+
const isCustomValidation = [undefined, VALIDATION_TYPE_OPTIONS.custom.value].includes(validate.validationType);
16+
const shouldRender = isValidatable && isCustomValidation;
17+
18+
if (!shouldRender) {
19+
return;
20+
}
21+
22+
return {
23+
id: 'custom-validation',
24+
label: 'Custom validations',
25+
tooltip: "Define custom validation rules for this field. Use 'value' to reference the field value.",
26+
component: ListGroup,
27+
...CustomValidationsEntry({ editField, field, id: 'custom-validation-list' }),
28+
};
29+
}
30+
31+
export function CustomValidationsEntry(props) {
32+
const { editField, field, id: idPrefix } = props;
33+
34+
const addEntry = (e) => {
35+
e.stopPropagation();
36+
37+
const customValidations = get(field, ['validate', 'custom'], []);
38+
const newIndex = customValidations.length + 1;
39+
40+
const newValue = {
41+
condition: '=false',
42+
message: 'Error message.',
43+
};
44+
45+
const newArray = arrayAdd(customValidations, newIndex, newValue);
46+
const newValidate = set(field.validate || {}, ['custom'], newArray);
47+
48+
editField(field, ['validate'], newValidate);
49+
};
50+
51+
const removeEntry = (entry) => {
52+
const customValidations = get(field, ['validate', 'custom'], []);
53+
const newArray = without(customValidations, entry);
54+
const newValidate = set(field.validate, ['custom'], newArray);
55+
56+
editField(field, ['validate'], newValidate);
57+
};
58+
59+
const items = get(field, ['validate', 'custom'], []).map((entry, index) => {
60+
const id = idPrefix + '-' + index;
61+
62+
return {
63+
id,
64+
entries: CustomValidationEntry({
65+
editField,
66+
field,
67+
idPrefix,
68+
index,
69+
}),
70+
label: 'Rule ' + (index + 1),
71+
remove: () => removeEntry(entry),
72+
};
73+
});
74+
75+
return {
76+
items,
77+
add: addEntry,
78+
shouldSort: false,
79+
};
80+
}

packages/form-js-editor/src/features/properties-panel/groups/ValidationGroup.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { useService, useVariables } from '../hooks';
1414

1515
import { INPUTS } from '../Util';
1616

17-
const VALIDATION_TYPE_OPTIONS = {
17+
export const VALIDATION_TYPE_OPTIONS = {
1818
custom: {
1919
value: '',
2020
label: 'Custom',

packages/form-js-editor/src/features/properties-panel/groups/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export { GeneralGroup } from './GeneralGroup';
22
export { SerializationGroup } from './SerializationGroup';
33
export { ConstraintsGroup } from './ConstraintsGroup';
44
export { ValidationGroup } from './ValidationGroup';
5+
export { CustomValidationsGroup } from './CustomValidationsGroup';
56
export { OptionsGroups } from './OptionsGroups';
67
export { CustomPropertiesGroup } from './CustomPropertiesGroup';
78
export { AppearanceGroup } from './AppearanceGroup';

packages/form-js-viewer/src/render/components/form-fields/ExpressionField.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ ExpressionField.config = {
4545
label: 'Expression',
4646
group: 'basic-input',
4747
keyed: true,
48+
validatable: false,
4849
emptyValue: null,
4950
escapeGridRender: true,
5051
create: (options = {}) => ({

0 commit comments

Comments
 (0)