@bpmn-io/form-js-viewer 0.13.1 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.es.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import Ids from 'ids';
2
2
  import { isString, isArray, isFunction, isNumber, bind, assign, isNil, groupBy, flatten, get, isUndefined, set, findIndex, isObject } from 'min-dash';
3
- import { evaluate, parseExpressions, parseUnaryTests, unaryTest } from 'feelin';
4
- import { evaluate as evaluate$1 } from 'feelers';
3
+ import { parseExpressions, parseUnaryTests, evaluate, unaryTest } from 'feelin';
4
+ import { evaluate as evaluate$1, parser, buildSimpleTree } from 'feelers';
5
5
  import showdown from 'showdown';
6
6
  import Big from 'big.js';
7
7
  import classNames from 'classnames';
@@ -13,15 +13,164 @@ import flatpickr from 'flatpickr';
13
13
  import Markup from 'preact-markup';
14
14
  import { Injector } from 'didi';
15
15
 
16
+ const getFlavouredFeelVariableNames = (feelString, feelFlavour, options = {}) => {
17
+ const {
18
+ depth = 0,
19
+ specialDepthAccessors = {}
20
+ } = options;
21
+ if (!['expression', 'unaryTest'].includes(feelFlavour)) return [];
22
+ const tree = feelFlavour === 'expression' ? parseExpressions(feelString) : parseUnaryTests(feelString);
23
+ const simpleExpressionTree = _buildSimpleFeelStructureTree(tree, feelString);
24
+ return function _unfoldVariables(node) {
25
+ if (node.name === 'PathExpression') {
26
+ if (Object.keys(specialDepthAccessors).length === 0) {
27
+ return depth === 0 ? [_getVariableNameAtPathIndex(node, 0)] : [];
28
+ }
29
+
30
+ // if using special depth accessors, use a more complex extraction
31
+ return Array.from(_smartExtractVariableNames(node, depth, specialDepthAccessors));
32
+ }
33
+ if (depth === 0 && node.name === 'VariableName') return [node.variableName];
34
+
35
+ // for any other kind of node, traverse its children and flatten the result
36
+ if (node.children) {
37
+ return node.children.reduce((acc, child) => {
38
+ return acc.concat(_unfoldVariables(child));
39
+ }, []);
40
+ }
41
+ return [];
42
+ }(simpleExpressionTree);
43
+ };
44
+
45
+ /**
46
+ * Get the variable name at the specified index in a given path expression.
47
+ *
48
+ * @param {Object} root - The root node of the path expression tree.
49
+ * @param {number} index - The index of the variable name to retrieve.
50
+ * @returns {string|null} The variable name at the specified index or null if index is out of bounds.
51
+ */
52
+ const _getVariableNameAtPathIndex = (root, index) => {
53
+ const accessors = _deconstructPathExpression(root);
54
+ return accessors[index] || null;
55
+ };
56
+
57
+ /**
58
+ * Extracts the variables which are required of the external context for a given path expression.
59
+ * This is done by traversing the path expression tree and keeping track of the current depth relative to the external context.
60
+ *
61
+ * @param {Object} node - The root node of the path expression tree.
62
+ * @param {number} initialDepth - The depth at which the root node is located in the outer context.
63
+ * @param {Object} specialDepthAccessors - Definitions of special keywords which represent more complex accesses of the outer context.
64
+ * @returns {Set} - A set containing the extracted variable names.
65
+ */
66
+ const _smartExtractVariableNames = (node, initialDepth, specialDepthAccessors) => {
67
+ // depth info represents the previous (initialised as null) and current depth of the current accessor in the path expression
68
+ // we track multiple of these to account for the fact that a path expression may be ambiguous due to special keywords
69
+ let accessorDepthInfos = [{
70
+ previous: null,
71
+ current: initialDepth - 1
72
+ }];
73
+ const extractedVariables = new Set();
74
+ const nodeAccessors = _deconstructPathExpression(node);
75
+ for (let i = 0; i < nodeAccessors.length; i++) {
76
+ const currentAccessor = nodeAccessors[i];
77
+ if (currentAccessor in specialDepthAccessors) {
78
+ const depthOffsets = specialDepthAccessors[currentAccessor];
79
+
80
+ // if the current accessor is a special keyword, we need to expand the current depth info set
81
+ // this is done to account for the ambiguity of keywords like parent, which may be used to access
82
+ // the parent of the current node, or a child variable of the same name
83
+ accessorDepthInfos = depthOffsets.reduce((accumulator, offset) => {
84
+ return [...accumulator, ...accessorDepthInfos.map(depthInfo => ({
85
+ previous: depthInfo.current,
86
+ current: depthInfo.current + offset
87
+ }))];
88
+ }, []).filter(depthInfo => depthInfo.current >= -1); // discard all depth infos which are out of bounds
89
+ } else {
90
+ // if the current accessor is not a special keyword, we know it's simply accessing a child
91
+ // hence we are now one level deeper in the tree and simply increment
92
+ accessorDepthInfos = accessorDepthInfos.map(depthInfo => ({
93
+ previous: depthInfo.current,
94
+ current: depthInfo.current + 1
95
+ }));
96
+ }
97
+
98
+ // finally, we check if for the current accessor, there is a scenario where:
99
+ // previous it was at depth -1 (i.e. the root context), and is now at depth 0 (i.e. a variable)
100
+ // these are the variables we need to request, so we add them to the set
101
+ if (accessorDepthInfos.some(depthInfo => depthInfo.previous === -1 && depthInfo.current === 0)) {
102
+ extractedVariables.add(currentAccessor);
103
+ }
104
+ }
105
+
106
+ // we return a set to avoid duplicates
107
+ return new Set(extractedVariables);
108
+ };
109
+
110
+ /**
111
+ * Deconstructs a path expression tree into an array of components.
112
+ *
113
+ * @param {Object} root - The root node of the path expression tree.
114
+ * @returns {Array<string>} An array of components in the path expression, in the correct order.
115
+ */
116
+ const _deconstructPathExpression = root => {
117
+ let node = root;
118
+ let parts = [];
119
+
120
+ // Traverse the tree and collect path components
121
+ while (node.name === 'PathExpression') {
122
+ parts.push(node.children[1].variableName);
123
+ node = node.children[0];
124
+ }
125
+
126
+ // Add the last component to the array
127
+ parts.push(node.variableName);
128
+
129
+ // Reverse and return the array to get the correct order
130
+ return parts.reverse();
131
+ };
132
+
133
+ /**
134
+ * Builds a simplified feel structure tree from the given parse tree and feel string.
135
+ * The nodes follow this structure: `{ name: string, children: Array, variableName?: string }`
136
+ *
137
+ * @param {Object} parseTree - The parse tree generated by a parser.
138
+ * @param {string} feelString - The feel string used for parsing.
139
+ * @returns {Object} The simplified feel structure tree.
140
+ */
141
+ const _buildSimpleFeelStructureTree = (parseTree, feelString) => {
142
+ const stack = [{
143
+ children: []
144
+ }];
145
+ parseTree.iterate({
146
+ enter: node => {
147
+ const nodeRepresentation = {
148
+ name: node.type.name,
149
+ children: []
150
+ };
151
+ if (node.type.name === 'VariableName') {
152
+ nodeRepresentation.variableName = feelString.slice(node.from, node.to);
153
+ }
154
+ stack.push(nodeRepresentation);
155
+ },
156
+ leave: () => {
157
+ const result = stack.pop();
158
+ const parent = stack[stack.length - 1];
159
+ parent.children.push(result);
160
+ }
161
+ });
162
+ return stack[0].children[0];
163
+ };
164
+
16
165
  class FeelExpressionLanguage {
17
166
  constructor(eventBus) {
18
167
  this._eventBus = eventBus;
19
168
  }
20
169
 
21
170
  /**
22
- * Determines if the given string is a FEEL expression.
171
+ * Determines if the given value is a FEEL expression.
23
172
  *
24
- * @param {string} value
173
+ * @param {any} value
25
174
  * @returns {boolean}
26
175
  *
27
176
  */
@@ -45,12 +194,10 @@ class FeelExpressionLanguage {
45
194
  if (!this.isExpression(expression)) {
46
195
  return [];
47
196
  }
48
- if (type === 'unaryTest') {
49
- return this._getUnaryVariableNames(expression);
50
- } else if (type === 'expression') {
51
- return this._getExpressionVariableNames(expression);
197
+ if (!['unaryTest', 'expression'].includes(type)) {
198
+ throw new Error('Unknown expression type: ' + type);
52
199
  }
53
- throw new Error('Unknown expression type: ' + options.type);
200
+ return getFlavouredFeelVariableNames(expression, type);
54
201
  }
55
202
 
56
203
  /**
@@ -78,37 +225,52 @@ class FeelExpressionLanguage {
78
225
  return null;
79
226
  }
80
227
  }
81
- _getExpressionVariableNames(expression) {
82
- const tree = parseExpressions(expression);
83
- const cursor = tree.cursor();
84
- const variables = new Set();
85
- do {
86
- const node = cursor.node;
87
- if (node.type.name === 'VariableName') {
88
- variables.add(expression.slice(node.from, node.to));
89
- }
90
- } while (cursor.next());
91
- return Array.from(variables);
92
- }
93
- _getUnaryVariableNames(unaryTest) {
94
- const tree = parseUnaryTests(unaryTest);
95
- const cursor = tree.cursor();
96
- const variables = new Set();
97
- do {
98
- const node = cursor.node;
99
- if (node.type.name === 'VariableName') {
100
- variables.add(unaryTest.slice(node.from, node.to));
101
- }
102
- } while (cursor.next());
103
- return Array.from(variables);
104
- }
105
228
  }
106
229
  FeelExpressionLanguage.$inject = ['eventBus'];
107
230
 
108
231
  class FeelersTemplating {
109
232
  constructor() {}
233
+
234
+ /**
235
+ * Determines if the given value is a feelers template.
236
+ *
237
+ * @param {any} value
238
+ * @returns {boolean}
239
+ *
240
+ */
110
241
  isTemplate(value) {
111
- return isString(value) && (value.startsWith('=') || /{{/.test(value));
242
+ return isString(value) && (value.startsWith('=') || /{{.*?}}/.test(value));
243
+ }
244
+
245
+ /**
246
+ * Retrieve variable names from a given feelers template.
247
+ *
248
+ * @param {string} template
249
+ *
250
+ * @returns {string[]}
251
+ */
252
+ getVariableNames(template) {
253
+ if (!this.isTemplate(template)) {
254
+ return [];
255
+ }
256
+ const expressions = this._extractExpressionsWithDepth(template);
257
+
258
+ // defines special accessors, and the change(s) in depth they could imply (e.g. parent can be used to access the parent context (depth - 1) or a child variable named parent (depth + 1)
259
+ const specialDepthAccessors = {
260
+ parent: [-1, 1],
261
+ _parent_: [-1],
262
+ this: [0, 1],
263
+ _this_: [0]
264
+ };
265
+ return expressions.reduce((variables, {
266
+ expression,
267
+ depth
268
+ }) => {
269
+ return variables.concat(getFlavouredFeelVariableNames(expression, 'expression', {
270
+ depth,
271
+ specialDepthAccessors
272
+ }));
273
+ }, []);
112
274
  }
113
275
 
114
276
  /**
@@ -135,6 +297,50 @@ class FeelersTemplating {
135
297
  buildDebugString
136
298
  });
137
299
  }
300
+
301
+ /**
302
+ * @typedef {Object} ExpressionWithDepth
303
+ * @property {number} depth - The depth of the expression in the syntax tree.
304
+ * @property {string} expression - The extracted expression
305
+ */
306
+
307
+ /**
308
+ * Extracts all feel expressions in the template along with their depth in the syntax tree.
309
+ * The depth is incremented for child expressions of loops to account for context drilling.
310
+ * @name extractExpressionsWithDepth
311
+ * @param {string} template - A feelers template string.
312
+ * @returns {Array<ExpressionWithDepth>} An array of objects, each containing the depth and the extracted expression.
313
+ *
314
+ * @example
315
+ * const template = "Hello {{user}}, you have:{{#loop items}}\n- {{amount}} {{name}}{{/loop}}.";
316
+ * const extractedExpressions = _extractExpressionsWithDepth(template);
317
+ */
318
+ _extractExpressionsWithDepth(template) {
319
+ // build simplified feelers syntax tree
320
+ const parseTree = parser.parse(template);
321
+ const tree = buildSimpleTree(parseTree, template);
322
+ return function _traverse(n, depth = 0) {
323
+ if (['Feel', 'FeelBlock'].includes(n.name)) {
324
+ return [{
325
+ depth,
326
+ expression: n.content
327
+ }];
328
+ }
329
+ if (n.name === 'LoopSpanner') {
330
+ const loopExpression = n.children[0].content;
331
+ const childResults = n.children.slice(1).reduce((acc, child) => {
332
+ return acc.concat(_traverse(child, depth + 1));
333
+ }, []);
334
+ return [{
335
+ depth,
336
+ expression: loopExpression
337
+ }, ...childResults];
338
+ }
339
+ return n.children.reduce((acc, child) => {
340
+ return acc.concat(_traverse(child, depth));
341
+ }, []);
342
+ }(tree);
343
+ }
138
344
  }
139
345
  FeelersTemplating.$inject = [];
140
346
 
@@ -760,8 +966,13 @@ class Validator {
760
966
  if (validate.pattern && value && !new RegExp(validate.pattern).test(value)) {
761
967
  errors = [...errors, `Field must match pattern ${validate.pattern}.`];
762
968
  }
763
- if (validate.required && (isNil(value) || value === '')) {
764
- errors = [...errors, 'Field is required.'];
969
+ if (validate.required) {
970
+ const isUncheckedCheckbox = type === 'checkbox' && value === false;
971
+ const isUnsetValue = isNil(value) || value === '';
972
+ const isEmptyMultiselect = Array.isArray(value) && value.length === 0;
973
+ if (isUncheckedCheckbox || isUnsetValue || isEmptyMultiselect) {
974
+ errors = [...errors, 'Field is required.'];
975
+ }
765
976
  }
766
977
  if ('min' in validate && (value || value === 0) && value < validate.min) {
767
978
  errors = [...errors, `Field must have minimum value of ${validate.min}.`];
@@ -1063,6 +1274,7 @@ function createFormContainer(prefix = 'fjs') {
1063
1274
  }
1064
1275
 
1065
1276
  const EXPRESSION_PROPERTIES = ['alt', 'source', 'text'];
1277
+ const TEMPLATE_PROPERTIES = ['text'];
1066
1278
  function findErrors(errors, path) {
1067
1279
  return errors[pathStringify(path)];
1068
1280
  }
@@ -1116,7 +1328,7 @@ function clone(data, replacer) {
1116
1328
  *
1117
1329
  * @return {string[]}
1118
1330
  */
1119
- function getSchemaVariables(schema, expressionLanguage = new FeelExpressionLanguage(null)) {
1331
+ function getSchemaVariables(schema, expressionLanguage = new FeelExpressionLanguage(null), templating = new FeelersTemplating()) {
1120
1332
  if (!schema.components) {
1121
1333
  return [];
1122
1334
  }
@@ -1151,6 +1363,13 @@ function getSchemaVariables(schema, expressionLanguage = new FeelExpressionLangu
1151
1363
  variables = [...variables, ...expressionVariables];
1152
1364
  }
1153
1365
  });
1366
+ TEMPLATE_PROPERTIES.forEach(prop => {
1367
+ const property = component[prop];
1368
+ if (property && !expressionLanguage.isExpression(property) && templating.isTemplate(property)) {
1369
+ const templateVariables = templating.getVariableNames(property);
1370
+ variables = [...variables, ...templateVariables];
1371
+ }
1372
+ });
1154
1373
  return variables;
1155
1374
  }, []);
1156
1375
 
@@ -1465,8 +1684,12 @@ function Checkbox(props) {
1465
1684
  const {
1466
1685
  description,
1467
1686
  id,
1468
- label
1687
+ label,
1688
+ validate = {}
1469
1689
  } = field;
1690
+ const {
1691
+ required
1692
+ } = validate;
1470
1693
  const onChange = ({
1471
1694
  target
1472
1695
  }) => {
@@ -1488,7 +1711,7 @@ function Checkbox(props) {
1488
1711
  children: [jsx(Label, {
1489
1712
  id: prefixId(id, formId),
1490
1713
  label: label,
1491
- required: false,
1714
+ required: required,
1492
1715
  children: jsx("input", {
1493
1716
  checked: value,
1494
1717
  class: "fjs-input",
@@ -1855,8 +2078,12 @@ function Checklist(props) {
1855
2078
  const {
1856
2079
  description,
1857
2080
  id,
1858
- label
2081
+ label,
2082
+ validate = {}
1859
2083
  } = field;
2084
+ const {
2085
+ required
2086
+ } = validate;
1860
2087
  const toggleCheckbox = v => {
1861
2088
  let newValue = [...value];
1862
2089
  if (!newValue.includes(v)) {
@@ -1882,7 +2109,8 @@ function Checklist(props) {
1882
2109
  disabled
1883
2110
  })),
1884
2111
  children: [jsx(Label, {
1885
- label: label
2112
+ label: label,
2113
+ required: required
1886
2114
  }), loadState == LOAD_STATES.LOADED && options.map((v, index) => {
1887
2115
  return jsx(Label, {
1888
2116
  id: prefixId(`${id}-${index}`, formId),
@@ -3704,6 +3932,12 @@ function Select(props) {
3704
3932
  errors,
3705
3933
  disabled
3706
3934
  }),
3935
+ onKeyDown: event => {
3936
+ if (event.key === 'Enter') {
3937
+ event.preventDefault();
3938
+ event.stopPropagation();
3939
+ }
3940
+ },
3707
3941
  children: [jsx(Label, {
3708
3942
  id: prefixId(id, formId),
3709
3943
  label: label,
@@ -3752,8 +3986,12 @@ function Taglist(props) {
3752
3986
  const {
3753
3987
  description,
3754
3988
  id,
3755
- label
3989
+ label,
3990
+ validate = {}
3756
3991
  } = field;
3992
+ const {
3993
+ required
3994
+ } = validate;
3757
3995
  const {
3758
3996
  formId
3759
3997
  } = useContext(FormContext$1);
@@ -3850,8 +4088,15 @@ function Taglist(props) {
3850
4088
  errors,
3851
4089
  disabled
3852
4090
  }),
4091
+ onKeyDown: event => {
4092
+ if (event.key === 'Enter') {
4093
+ event.stopPropagation();
4094
+ event.preventDefault();
4095
+ }
4096
+ },
3853
4097
  children: [jsx(Label, {
3854
4098
  label: label,
4099
+ required: required,
3855
4100
  id: prefixId(`${id}-search`, formId)
3856
4101
  }), jsxs("div", {
3857
4102
  class: classNames('fjs-taglist', {