@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.
@@ -93,10 +93,9 @@
93
93
  }
94
94
 
95
95
  .fjs-container .fjs-layout-row {
96
- width: 100%;
96
+ flex: auto;
97
97
  padding: 9px 0;
98
98
  position: relative;
99
- margin-left: 1px;
100
99
  }
101
100
 
102
101
  .fjs-container .fjs-column {
@@ -114,6 +113,8 @@
114
113
 
115
114
  .fjs-container .cds--grid .cds--row {
116
115
  align-items: start;
116
+ margin-left: 0;
117
+ margin-right: 0;
117
118
  }
118
119
 
119
120
  @media (max-width: 66rem) {
@@ -134,6 +135,7 @@
134
135
  color: var(--color-text);
135
136
  background-color: var(--color-background);
136
137
  position: relative;
138
+ padding: 0 4px;
137
139
  }
138
140
 
139
141
  .fjs-container .fjs-form * {
@@ -270,7 +272,6 @@
270
272
  padding: 8px;
271
273
  width: auto !important;
272
274
  min-width: 34px;
273
- max-width: 30%;
274
275
  display: flex;
275
276
  overflow: hidden;
276
277
  }
@@ -352,6 +353,7 @@
352
353
  .fjs-container .fjs-input-group .fjs-taglist,
353
354
  .fjs-container .fjs-input-group .fjs-select {
354
355
  height: unset;
356
+ min-width: min(60px, 50%);
355
357
  width: 100%;
356
358
  }
357
359
 
@@ -540,6 +542,7 @@
540
542
  .fjs-container .fjs-form-field.fjs-has-errors .fjs-input,
541
543
  .fjs-container .fjs-form-field.fjs-has-errors .fjs-select,
542
544
  .fjs-container .fjs-form-field.fjs-has-errors .fjs-textarea,
545
+ .fjs-container .fjs-form-field.fjs-has-errors .fjs-taglist,
543
546
  .fjs-container .fjs-form-field.fjs-has-errors .fjs-input-group,
544
547
  .fjs-container .fjs-form-field.fjs-has-errors .fjs-input-group .fjs-input {
545
548
  border-color: var(--color-warning);
@@ -81,10 +81,9 @@
81
81
  }
82
82
 
83
83
  .fjs-container .fjs-layout-row {
84
- width: 100%;
84
+ flex: auto;
85
85
  padding: 9px 0;
86
86
  position: relative;
87
- margin-left: 1px;
88
87
  }
89
88
 
90
89
  .fjs-container .fjs-column {
@@ -102,6 +101,8 @@
102
101
 
103
102
  .fjs-container .cds--grid .cds--row {
104
103
  align-items: start;
104
+ margin-left: 0;
105
+ margin-right: 0;
105
106
  }
106
107
 
107
108
  @media (max-width: 66rem) {
@@ -121,6 +122,7 @@
121
122
  color: var(--color-text);
122
123
  background-color: var(--color-background);
123
124
  position: relative;
125
+ padding: 0 4px;
124
126
  }
125
127
 
126
128
  .fjs-container .fjs-form * {
@@ -257,7 +259,6 @@
257
259
  padding: 8px;
258
260
  width: auto !important;
259
261
  min-width: 34px;
260
- max-width: 30%;
261
262
  display: flex;
262
263
  overflow: hidden;
263
264
  }
@@ -337,6 +338,7 @@
337
338
  .fjs-container .fjs-input-group .fjs-taglist,
338
339
  .fjs-container .fjs-input-group .fjs-select {
339
340
  height: unset;
341
+ min-width: min(60px, 50%);
340
342
  width: 100%;
341
343
  }
342
344
 
@@ -523,6 +525,7 @@
523
525
  .fjs-container .fjs-form-field.fjs-has-errors .fjs-input,
524
526
  .fjs-container .fjs-form-field.fjs-has-errors .fjs-select,
525
527
  .fjs-container .fjs-form-field.fjs-has-errors .fjs-textarea,
528
+ .fjs-container .fjs-form-field.fjs-has-errors .fjs-taglist,
526
529
  .fjs-container .fjs-form-field.fjs-has-errors .fjs-input-group,
527
530
  .fjs-container .fjs-form-field.fjs-has-errors .fjs-input-group .fjs-input {
528
531
  border-color: var(--color-warning);
package/dist/index.cjs CHANGED
@@ -15,15 +15,164 @@ var flatpickr = require('flatpickr');
15
15
  var Markup = require('preact-markup');
16
16
  var didi = require('didi');
17
17
 
18
+ const getFlavouredFeelVariableNames = (feelString, feelFlavour, options = {}) => {
19
+ const {
20
+ depth = 0,
21
+ specialDepthAccessors = {}
22
+ } = options;
23
+ if (!['expression', 'unaryTest'].includes(feelFlavour)) return [];
24
+ const tree = feelFlavour === 'expression' ? feelin.parseExpressions(feelString) : feelin.parseUnaryTests(feelString);
25
+ const simpleExpressionTree = _buildSimpleFeelStructureTree(tree, feelString);
26
+ return function _unfoldVariables(node) {
27
+ if (node.name === 'PathExpression') {
28
+ if (Object.keys(specialDepthAccessors).length === 0) {
29
+ return depth === 0 ? [_getVariableNameAtPathIndex(node, 0)] : [];
30
+ }
31
+
32
+ // if using special depth accessors, use a more complex extraction
33
+ return Array.from(_smartExtractVariableNames(node, depth, specialDepthAccessors));
34
+ }
35
+ if (depth === 0 && node.name === 'VariableName') return [node.variableName];
36
+
37
+ // for any other kind of node, traverse its children and flatten the result
38
+ if (node.children) {
39
+ return node.children.reduce((acc, child) => {
40
+ return acc.concat(_unfoldVariables(child));
41
+ }, []);
42
+ }
43
+ return [];
44
+ }(simpleExpressionTree);
45
+ };
46
+
47
+ /**
48
+ * Get the variable name at the specified index in a given path expression.
49
+ *
50
+ * @param {Object} root - The root node of the path expression tree.
51
+ * @param {number} index - The index of the variable name to retrieve.
52
+ * @returns {string|null} The variable name at the specified index or null if index is out of bounds.
53
+ */
54
+ const _getVariableNameAtPathIndex = (root, index) => {
55
+ const accessors = _deconstructPathExpression(root);
56
+ return accessors[index] || null;
57
+ };
58
+
59
+ /**
60
+ * Extracts the variables which are required of the external context for a given path expression.
61
+ * This is done by traversing the path expression tree and keeping track of the current depth relative to the external context.
62
+ *
63
+ * @param {Object} node - The root node of the path expression tree.
64
+ * @param {number} initialDepth - The depth at which the root node is located in the outer context.
65
+ * @param {Object} specialDepthAccessors - Definitions of special keywords which represent more complex accesses of the outer context.
66
+ * @returns {Set} - A set containing the extracted variable names.
67
+ */
68
+ const _smartExtractVariableNames = (node, initialDepth, specialDepthAccessors) => {
69
+ // depth info represents the previous (initialised as null) and current depth of the current accessor in the path expression
70
+ // we track multiple of these to account for the fact that a path expression may be ambiguous due to special keywords
71
+ let accessorDepthInfos = [{
72
+ previous: null,
73
+ current: initialDepth - 1
74
+ }];
75
+ const extractedVariables = new Set();
76
+ const nodeAccessors = _deconstructPathExpression(node);
77
+ for (let i = 0; i < nodeAccessors.length; i++) {
78
+ const currentAccessor = nodeAccessors[i];
79
+ if (currentAccessor in specialDepthAccessors) {
80
+ const depthOffsets = specialDepthAccessors[currentAccessor];
81
+
82
+ // if the current accessor is a special keyword, we need to expand the current depth info set
83
+ // this is done to account for the ambiguity of keywords like parent, which may be used to access
84
+ // the parent of the current node, or a child variable of the same name
85
+ accessorDepthInfos = depthOffsets.reduce((accumulator, offset) => {
86
+ return [...accumulator, ...accessorDepthInfos.map(depthInfo => ({
87
+ previous: depthInfo.current,
88
+ current: depthInfo.current + offset
89
+ }))];
90
+ }, []).filter(depthInfo => depthInfo.current >= -1); // discard all depth infos which are out of bounds
91
+ } else {
92
+ // if the current accessor is not a special keyword, we know it's simply accessing a child
93
+ // hence we are now one level deeper in the tree and simply increment
94
+ accessorDepthInfos = accessorDepthInfos.map(depthInfo => ({
95
+ previous: depthInfo.current,
96
+ current: depthInfo.current + 1
97
+ }));
98
+ }
99
+
100
+ // finally, we check if for the current accessor, there is a scenario where:
101
+ // previous it was at depth -1 (i.e. the root context), and is now at depth 0 (i.e. a variable)
102
+ // these are the variables we need to request, so we add them to the set
103
+ if (accessorDepthInfos.some(depthInfo => depthInfo.previous === -1 && depthInfo.current === 0)) {
104
+ extractedVariables.add(currentAccessor);
105
+ }
106
+ }
107
+
108
+ // we return a set to avoid duplicates
109
+ return new Set(extractedVariables);
110
+ };
111
+
112
+ /**
113
+ * Deconstructs a path expression tree into an array of components.
114
+ *
115
+ * @param {Object} root - The root node of the path expression tree.
116
+ * @returns {Array<string>} An array of components in the path expression, in the correct order.
117
+ */
118
+ const _deconstructPathExpression = root => {
119
+ let node = root;
120
+ let parts = [];
121
+
122
+ // Traverse the tree and collect path components
123
+ while (node.name === 'PathExpression') {
124
+ parts.push(node.children[1].variableName);
125
+ node = node.children[0];
126
+ }
127
+
128
+ // Add the last component to the array
129
+ parts.push(node.variableName);
130
+
131
+ // Reverse and return the array to get the correct order
132
+ return parts.reverse();
133
+ };
134
+
135
+ /**
136
+ * Builds a simplified feel structure tree from the given parse tree and feel string.
137
+ * The nodes follow this structure: `{ name: string, children: Array, variableName?: string }`
138
+ *
139
+ * @param {Object} parseTree - The parse tree generated by a parser.
140
+ * @param {string} feelString - The feel string used for parsing.
141
+ * @returns {Object} The simplified feel structure tree.
142
+ */
143
+ const _buildSimpleFeelStructureTree = (parseTree, feelString) => {
144
+ const stack = [{
145
+ children: []
146
+ }];
147
+ parseTree.iterate({
148
+ enter: node => {
149
+ const nodeRepresentation = {
150
+ name: node.type.name,
151
+ children: []
152
+ };
153
+ if (node.type.name === 'VariableName') {
154
+ nodeRepresentation.variableName = feelString.slice(node.from, node.to);
155
+ }
156
+ stack.push(nodeRepresentation);
157
+ },
158
+ leave: () => {
159
+ const result = stack.pop();
160
+ const parent = stack[stack.length - 1];
161
+ parent.children.push(result);
162
+ }
163
+ });
164
+ return stack[0].children[0];
165
+ };
166
+
18
167
  class FeelExpressionLanguage {
19
168
  constructor(eventBus) {
20
169
  this._eventBus = eventBus;
21
170
  }
22
171
 
23
172
  /**
24
- * Determines if the given string is a FEEL expression.
173
+ * Determines if the given value is a FEEL expression.
25
174
  *
26
- * @param {string} value
175
+ * @param {any} value
27
176
  * @returns {boolean}
28
177
  *
29
178
  */
@@ -47,12 +196,10 @@ class FeelExpressionLanguage {
47
196
  if (!this.isExpression(expression)) {
48
197
  return [];
49
198
  }
50
- if (type === 'unaryTest') {
51
- return this._getUnaryVariableNames(expression);
52
- } else if (type === 'expression') {
53
- return this._getExpressionVariableNames(expression);
199
+ if (!['unaryTest', 'expression'].includes(type)) {
200
+ throw new Error('Unknown expression type: ' + type);
54
201
  }
55
- throw new Error('Unknown expression type: ' + options.type);
202
+ return getFlavouredFeelVariableNames(expression, type);
56
203
  }
57
204
 
58
205
  /**
@@ -80,37 +227,52 @@ class FeelExpressionLanguage {
80
227
  return null;
81
228
  }
82
229
  }
83
- _getExpressionVariableNames(expression) {
84
- const tree = feelin.parseExpressions(expression);
85
- const cursor = tree.cursor();
86
- const variables = new Set();
87
- do {
88
- const node = cursor.node;
89
- if (node.type.name === 'VariableName') {
90
- variables.add(expression.slice(node.from, node.to));
91
- }
92
- } while (cursor.next());
93
- return Array.from(variables);
94
- }
95
- _getUnaryVariableNames(unaryTest) {
96
- const tree = feelin.parseUnaryTests(unaryTest);
97
- const cursor = tree.cursor();
98
- const variables = new Set();
99
- do {
100
- const node = cursor.node;
101
- if (node.type.name === 'VariableName') {
102
- variables.add(unaryTest.slice(node.from, node.to));
103
- }
104
- } while (cursor.next());
105
- return Array.from(variables);
106
- }
107
230
  }
108
231
  FeelExpressionLanguage.$inject = ['eventBus'];
109
232
 
110
233
  class FeelersTemplating {
111
234
  constructor() {}
235
+
236
+ /**
237
+ * Determines if the given value is a feelers template.
238
+ *
239
+ * @param {any} value
240
+ * @returns {boolean}
241
+ *
242
+ */
112
243
  isTemplate(value) {
113
- return minDash.isString(value) && (value.startsWith('=') || /{{/.test(value));
244
+ return minDash.isString(value) && (value.startsWith('=') || /{{.*?}}/.test(value));
245
+ }
246
+
247
+ /**
248
+ * Retrieve variable names from a given feelers template.
249
+ *
250
+ * @param {string} template
251
+ *
252
+ * @returns {string[]}
253
+ */
254
+ getVariableNames(template) {
255
+ if (!this.isTemplate(template)) {
256
+ return [];
257
+ }
258
+ const expressions = this._extractExpressionsWithDepth(template);
259
+
260
+ // 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)
261
+ const specialDepthAccessors = {
262
+ parent: [-1, 1],
263
+ _parent_: [-1],
264
+ this: [0, 1],
265
+ _this_: [0]
266
+ };
267
+ return expressions.reduce((variables, {
268
+ expression,
269
+ depth
270
+ }) => {
271
+ return variables.concat(getFlavouredFeelVariableNames(expression, 'expression', {
272
+ depth,
273
+ specialDepthAccessors
274
+ }));
275
+ }, []);
114
276
  }
115
277
 
116
278
  /**
@@ -137,6 +299,50 @@ class FeelersTemplating {
137
299
  buildDebugString
138
300
  });
139
301
  }
302
+
303
+ /**
304
+ * @typedef {Object} ExpressionWithDepth
305
+ * @property {number} depth - The depth of the expression in the syntax tree.
306
+ * @property {string} expression - The extracted expression
307
+ */
308
+
309
+ /**
310
+ * Extracts all feel expressions in the template along with their depth in the syntax tree.
311
+ * The depth is incremented for child expressions of loops to account for context drilling.
312
+ * @name extractExpressionsWithDepth
313
+ * @param {string} template - A feelers template string.
314
+ * @returns {Array<ExpressionWithDepth>} An array of objects, each containing the depth and the extracted expression.
315
+ *
316
+ * @example
317
+ * const template = "Hello {{user}}, you have:{{#loop items}}\n- {{amount}} {{name}}{{/loop}}.";
318
+ * const extractedExpressions = _extractExpressionsWithDepth(template);
319
+ */
320
+ _extractExpressionsWithDepth(template) {
321
+ // build simplified feelers syntax tree
322
+ const parseTree = feelers.parser.parse(template);
323
+ const tree = feelers.buildSimpleTree(parseTree, template);
324
+ return function _traverse(n, depth = 0) {
325
+ if (['Feel', 'FeelBlock'].includes(n.name)) {
326
+ return [{
327
+ depth,
328
+ expression: n.content
329
+ }];
330
+ }
331
+ if (n.name === 'LoopSpanner') {
332
+ const loopExpression = n.children[0].content;
333
+ const childResults = n.children.slice(1).reduce((acc, child) => {
334
+ return acc.concat(_traverse(child, depth + 1));
335
+ }, []);
336
+ return [{
337
+ depth,
338
+ expression: loopExpression
339
+ }, ...childResults];
340
+ }
341
+ return n.children.reduce((acc, child) => {
342
+ return acc.concat(_traverse(child, depth));
343
+ }, []);
344
+ }(tree);
345
+ }
140
346
  }
141
347
  FeelersTemplating.$inject = [];
142
348
 
@@ -762,8 +968,13 @@ class Validator {
762
968
  if (validate.pattern && value && !new RegExp(validate.pattern).test(value)) {
763
969
  errors = [...errors, `Field must match pattern ${validate.pattern}.`];
764
970
  }
765
- if (validate.required && (minDash.isNil(value) || value === '')) {
766
- errors = [...errors, 'Field is required.'];
971
+ if (validate.required) {
972
+ const isUncheckedCheckbox = type === 'checkbox' && value === false;
973
+ const isUnsetValue = minDash.isNil(value) || value === '';
974
+ const isEmptyMultiselect = Array.isArray(value) && value.length === 0;
975
+ if (isUncheckedCheckbox || isUnsetValue || isEmptyMultiselect) {
976
+ errors = [...errors, 'Field is required.'];
977
+ }
767
978
  }
768
979
  if ('min' in validate && (value || value === 0) && value < validate.min) {
769
980
  errors = [...errors, `Field must have minimum value of ${validate.min}.`];
@@ -1065,6 +1276,7 @@ function createFormContainer(prefix = 'fjs') {
1065
1276
  }
1066
1277
 
1067
1278
  const EXPRESSION_PROPERTIES = ['alt', 'source', 'text'];
1279
+ const TEMPLATE_PROPERTIES = ['text'];
1068
1280
  function findErrors(errors, path) {
1069
1281
  return errors[pathStringify(path)];
1070
1282
  }
@@ -1118,7 +1330,7 @@ function clone(data, replacer) {
1118
1330
  *
1119
1331
  * @return {string[]}
1120
1332
  */
1121
- function getSchemaVariables(schema, expressionLanguage = new FeelExpressionLanguage(null)) {
1333
+ function getSchemaVariables(schema, expressionLanguage = new FeelExpressionLanguage(null), templating = new FeelersTemplating()) {
1122
1334
  if (!schema.components) {
1123
1335
  return [];
1124
1336
  }
@@ -1153,6 +1365,13 @@ function getSchemaVariables(schema, expressionLanguage = new FeelExpressionLangu
1153
1365
  variables = [...variables, ...expressionVariables];
1154
1366
  }
1155
1367
  });
1368
+ TEMPLATE_PROPERTIES.forEach(prop => {
1369
+ const property = component[prop];
1370
+ if (property && !expressionLanguage.isExpression(property) && templating.isTemplate(property)) {
1371
+ const templateVariables = templating.getVariableNames(property);
1372
+ variables = [...variables, ...templateVariables];
1373
+ }
1374
+ });
1156
1375
  return variables;
1157
1376
  }, []);
1158
1377
 
@@ -1467,8 +1686,12 @@ function Checkbox(props) {
1467
1686
  const {
1468
1687
  description,
1469
1688
  id,
1470
- label
1689
+ label,
1690
+ validate = {}
1471
1691
  } = field;
1692
+ const {
1693
+ required
1694
+ } = validate;
1472
1695
  const onChange = ({
1473
1696
  target
1474
1697
  }) => {
@@ -1490,7 +1713,7 @@ function Checkbox(props) {
1490
1713
  children: [jsxRuntime.jsx(Label, {
1491
1714
  id: prefixId(id, formId),
1492
1715
  label: label,
1493
- required: false,
1716
+ required: required,
1494
1717
  children: jsxRuntime.jsx("input", {
1495
1718
  checked: value,
1496
1719
  class: "fjs-input",
@@ -1857,8 +2080,12 @@ function Checklist(props) {
1857
2080
  const {
1858
2081
  description,
1859
2082
  id,
1860
- label
2083
+ label,
2084
+ validate = {}
1861
2085
  } = field;
2086
+ const {
2087
+ required
2088
+ } = validate;
1862
2089
  const toggleCheckbox = v => {
1863
2090
  let newValue = [...value];
1864
2091
  if (!newValue.includes(v)) {
@@ -1884,7 +2111,8 @@ function Checklist(props) {
1884
2111
  disabled
1885
2112
  })),
1886
2113
  children: [jsxRuntime.jsx(Label, {
1887
- label: label
2114
+ label: label,
2115
+ required: required
1888
2116
  }), loadState == LOAD_STATES.LOADED && options.map((v, index) => {
1889
2117
  return jsxRuntime.jsx(Label, {
1890
2118
  id: prefixId(`${id}-${index}`, formId),
@@ -3706,6 +3934,12 @@ function Select(props) {
3706
3934
  errors,
3707
3935
  disabled
3708
3936
  }),
3937
+ onKeyDown: event => {
3938
+ if (event.key === 'Enter') {
3939
+ event.preventDefault();
3940
+ event.stopPropagation();
3941
+ }
3942
+ },
3709
3943
  children: [jsxRuntime.jsx(Label, {
3710
3944
  id: prefixId(id, formId),
3711
3945
  label: label,
@@ -3754,8 +3988,12 @@ function Taglist(props) {
3754
3988
  const {
3755
3989
  description,
3756
3990
  id,
3757
- label
3991
+ label,
3992
+ validate = {}
3758
3993
  } = field;
3994
+ const {
3995
+ required
3996
+ } = validate;
3759
3997
  const {
3760
3998
  formId
3761
3999
  } = hooks.useContext(FormContext$1);
@@ -3852,8 +4090,15 @@ function Taglist(props) {
3852
4090
  errors,
3853
4091
  disabled
3854
4092
  }),
4093
+ onKeyDown: event => {
4094
+ if (event.key === 'Enter') {
4095
+ event.stopPropagation();
4096
+ event.preventDefault();
4097
+ }
4098
+ },
3855
4099
  children: [jsxRuntime.jsx(Label, {
3856
4100
  label: label,
4101
+ required: required,
3857
4102
  id: prefixId(`${id}-search`, formId)
3858
4103
  }), jsxRuntime.jsxs("div", {
3859
4104
  class: classNames('fjs-taglist', {