@servicetitan/dte-unlayer 0.94.0 → 0.96.0

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.
Files changed (68) hide show
  1. package/dist/display-conditions/ConditionGroup.d.ts +12 -0
  2. package/dist/display-conditions/ConditionGroup.d.ts.map +1 -0
  3. package/dist/display-conditions/ConditionGroup.js +181 -0
  4. package/dist/display-conditions/ConditionGroup.js.map +1 -0
  5. package/dist/display-conditions/ConditionGroupsSection.d.ts +11 -0
  6. package/dist/display-conditions/ConditionGroupsSection.d.ts.map +1 -0
  7. package/dist/display-conditions/ConditionGroupsSection.js +71 -0
  8. package/dist/display-conditions/ConditionGroupsSection.js.map +1 -0
  9. package/dist/display-conditions/ConditionRow.d.ts +11 -0
  10. package/dist/display-conditions/ConditionRow.d.ts.map +1 -0
  11. package/dist/display-conditions/ConditionRow.js +206 -0
  12. package/dist/display-conditions/ConditionRow.js.map +1 -0
  13. package/dist/display-conditions/DisplayConditionModal.d.ts +5 -0
  14. package/dist/display-conditions/DisplayConditionModal.d.ts.map +1 -0
  15. package/dist/display-conditions/DisplayConditionModal.js +282 -0
  16. package/dist/display-conditions/DisplayConditionModal.js.map +1 -0
  17. package/dist/display-conditions/SeparatorWithChip.d.ts +6 -0
  18. package/dist/display-conditions/SeparatorWithChip.d.ts.map +1 -0
  19. package/dist/display-conditions/SeparatorWithChip.js +15 -0
  20. package/dist/display-conditions/SeparatorWithChip.js.map +1 -0
  21. package/dist/display-conditions/constants.d.ts +7 -0
  22. package/dist/display-conditions/constants.d.ts.map +1 -0
  23. package/dist/display-conditions/constants.js +22 -0
  24. package/dist/display-conditions/constants.js.map +1 -0
  25. package/dist/display-conditions/displayConditionController.d.ts +9 -0
  26. package/dist/display-conditions/displayConditionController.d.ts.map +1 -0
  27. package/dist/display-conditions/displayConditionController.js +29 -0
  28. package/dist/display-conditions/displayConditionController.js.map +1 -0
  29. package/dist/display-conditions/nunjucks.d.ts +8 -0
  30. package/dist/display-conditions/nunjucks.d.ts.map +1 -0
  31. package/dist/display-conditions/nunjucks.js +448 -0
  32. package/dist/display-conditions/nunjucks.js.map +1 -0
  33. package/dist/display-conditions/schemaDataPoints.d.ts +4 -0
  34. package/dist/display-conditions/schemaDataPoints.d.ts.map +1 -0
  35. package/dist/display-conditions/schemaDataPoints.js +18 -0
  36. package/dist/display-conditions/schemaDataPoints.js.map +1 -0
  37. package/dist/display-conditions/types.d.ts +130 -0
  38. package/dist/display-conditions/types.d.ts.map +1 -0
  39. package/dist/display-conditions/types.js +72 -0
  40. package/dist/display-conditions/types.js.map +1 -0
  41. package/dist/editor-core-source.d.ts +1 -1
  42. package/dist/editor-core-source.d.ts.map +1 -1
  43. package/dist/editor-core-source.js +1 -1
  44. package/dist/editor-core-source.js.map +1 -1
  45. package/dist/editor.d.ts.map +1 -1
  46. package/dist/editor.js +4 -0
  47. package/dist/editor.js.map +1 -1
  48. package/dist/shared/schema.d.ts +2 -0
  49. package/dist/shared/schema.d.ts.map +1 -1
  50. package/dist/shared/schema.js.map +1 -1
  51. package/dist/unlayer.d.ts.map +1 -1
  52. package/dist/unlayer.js +7 -0
  53. package/dist/unlayer.js.map +1 -1
  54. package/package.json +4 -2
  55. package/src/display-conditions/ConditionGroup.tsx +145 -0
  56. package/src/display-conditions/ConditionGroupsSection.tsx +64 -0
  57. package/src/display-conditions/ConditionRow.tsx +185 -0
  58. package/src/display-conditions/DisplayConditionModal.tsx +231 -0
  59. package/src/display-conditions/SeparatorWithChip.tsx +14 -0
  60. package/src/display-conditions/constants.ts +22 -0
  61. package/src/display-conditions/displayConditionController.ts +42 -0
  62. package/src/display-conditions/nunjucks.ts +503 -0
  63. package/src/display-conditions/schemaDataPoints.ts +33 -0
  64. package/src/display-conditions/types.ts +75 -0
  65. package/src/editor-core-source.ts +1 -1
  66. package/src/editor.tsx +2 -0
  67. package/src/shared/schema.ts +2 -0
  68. package/src/unlayer.tsx +9 -0
@@ -0,0 +1,503 @@
1
+ import { generateId } from './constants';
2
+ import {
3
+ ConditionGroup,
4
+ ConditionOperator,
5
+ DisplayBehavior,
6
+ DisplayConditionState,
7
+ LogicalOperator,
8
+ SingleCondition,
9
+ UnlayerDisplayCondition,
10
+ VALUE_LESS_OPERATORS,
11
+ } from './types';
12
+
13
+ function escapeNunjucksString(s: string): string {
14
+ return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
15
+ }
16
+
17
+ function nunjucksValueLiteral(value: string): string {
18
+ return `'${escapeNunjucksString(value)}'`;
19
+ }
20
+
21
+ function isEscapedAt(s: string, index: number): boolean {
22
+ let slashCount = 0;
23
+ for (let i = index - 1; i >= 0 && s[i] === '\\'; i--) {
24
+ slashCount++;
25
+ }
26
+ return slashCount % 2 === 1;
27
+ }
28
+
29
+ function isValueLessOperator(operator: string): boolean {
30
+ return VALUE_LESS_OPERATORS.includes(operator as ConditionOperator);
31
+ }
32
+
33
+ /**
34
+ * Build a single condition expression in Nunjucks.
35
+ * Wrapped in () so splitting never breaks composite expressions.
36
+ */
37
+ function buildSingleConditionExpression(
38
+ dataPointKey: string,
39
+ operator: string,
40
+ value: string,
41
+ ): string {
42
+ const path = dataPointKey;
43
+ const defaulted = `(${path} | default(''))`;
44
+ const numDefaulted = `(${path} | default(0))`;
45
+ const normalizedValue = value.trim();
46
+ const literal = normalizedValue ? nunjucksValueLiteral(normalizedValue) : "''";
47
+ const numLiteral =
48
+ /^-?(?:\d+\.?\d*|\.\d+)$/.test(normalizedValue) && normalizedValue !== '-'
49
+ ? normalizedValue
50
+ : '0';
51
+ let inner: string;
52
+
53
+ switch (operator) {
54
+ case 'is_equal_to':
55
+ inner = `${defaulted} == ${literal}`;
56
+ break;
57
+ case 'is_not_equal_to':
58
+ inner = `${defaulted} != ${literal}`;
59
+ break;
60
+ case 'contains':
61
+ inner = `${literal} in ${defaulted}`;
62
+ break;
63
+ case 'does_not_contain':
64
+ inner = `not (${literal} in ${defaulted})`;
65
+ break;
66
+ case 'starts_with':
67
+ inner = `(${defaulted} | startswith(${literal}))`;
68
+ break;
69
+ case 'ends_with':
70
+ inner = `(${defaulted} | endswith(${literal}))`;
71
+ break;
72
+ case 'is_empty':
73
+ inner = `${defaulted} == '' or (${defaulted} | length) == 0`;
74
+ break;
75
+ case 'is_not_empty':
76
+ inner = `${defaulted} != '' and (${defaulted} | length) > 0`;
77
+ break;
78
+ case 'num_eq':
79
+ inner = `${numDefaulted} == ${numLiteral}`;
80
+ break;
81
+ case 'num_neq':
82
+ inner = `${numDefaulted} != ${numLiteral}`;
83
+ break;
84
+ case 'num_gt':
85
+ inner = `${numDefaulted} > ${numLiteral}`;
86
+ break;
87
+ case 'num_lt':
88
+ inner = `${numDefaulted} < ${numLiteral}`;
89
+ break;
90
+ case 'num_gte':
91
+ inner = `${numDefaulted} >= ${numLiteral}`;
92
+ break;
93
+ case 'num_lte':
94
+ inner = `${numDefaulted} <= ${numLiteral}`;
95
+ break;
96
+ default:
97
+ inner = `${defaulted} == ${literal}`;
98
+ }
99
+ return `(${inner})`;
100
+ }
101
+
102
+ /**
103
+ * Build group expression. Each condition is connected by its own logical operator.
104
+ * E.g. (cond1) and (cond2) or (cond3)
105
+ */
106
+ function buildGroupExpression(group: ConditionGroup): string {
107
+ const valid = group.conditions.filter(
108
+ c => c.dataPointKey && (isValueLessOperator(c.operator) || c.value.trim() !== ''),
109
+ );
110
+ if (valid.length === 0) {
111
+ return '';
112
+ }
113
+ let result = buildSingleConditionExpression(
114
+ valid[0].dataPointKey,
115
+ valid[0].operator,
116
+ valid[0].value,
117
+ );
118
+ for (let i = 1; i < valid.length; i++) {
119
+ const joiner = valid[i].logicalOperator === 'or' ? ' or ' : ' and ';
120
+ result +=
121
+ joiner +
122
+ buildSingleConditionExpression(
123
+ valid[i].dataPointKey,
124
+ valid[i].operator,
125
+ valid[i].value,
126
+ );
127
+ }
128
+ // Wrap group in parens so top-level rule AND split works cleanly
129
+ return valid.length > 1 ? `(${result})` : result;
130
+ }
131
+
132
+ /**
133
+ * Build full condition expression.
134
+ * Rules (groups) are connected by each group's logical operator.
135
+ * hide behavior wraps everything in not().
136
+ */
137
+ function buildFullConditionExpression(state: DisplayConditionState): string {
138
+ const validGroups = state.groups
139
+ .map(group => ({ expression: buildGroupExpression(group), group }))
140
+ .filter(item => !!item.expression);
141
+ if (validGroups.length === 0) {
142
+ return '';
143
+ }
144
+ let combined = validGroups[0].expression;
145
+ for (let i = 1; i < validGroups.length; i++) {
146
+ const joiner = validGroups[i].group.logicalOperator === 'or' ? ' or ' : ' and ';
147
+ combined += `${joiner}${validGroups[i].expression}`;
148
+ }
149
+ if (state.behavior === 'hide') {
150
+ return `not (${combined})`;
151
+ }
152
+ return combined;
153
+ }
154
+
155
+ function generateTypeAndLabel(state: DisplayConditionState): { type: string; label: string } {
156
+ const parts: string[] = [];
157
+ for (const group of state.groups) {
158
+ for (const c of group.conditions) {
159
+ if (!c.dataPointKey) {
160
+ continue;
161
+ }
162
+ if (isValueLessOperator(c.operator)) {
163
+ parts.push(`${c.dataPointKey} ${c.operator}`);
164
+ } else if (c.value.trim()) {
165
+ parts.push(`${c.dataPointKey} ${c.operator} ${c.value.trim()}`);
166
+ }
167
+ }
168
+ }
169
+ const summary = parts.slice(0, 2).join('; ') + (parts.length > 2 ? '…' : '');
170
+ const type = state.behavior === 'show' ? 'Display when' : 'Hide when';
171
+ const label = summary || (state.behavior === 'show' ? 'Show when' : 'Hide when');
172
+ return { type, label };
173
+ }
174
+
175
+ export function buildUnlayerDisplayCondition(
176
+ state: DisplayConditionState,
177
+ ): UnlayerDisplayCondition | null {
178
+ const expr = buildFullConditionExpression(state);
179
+ if (!expr) {
180
+ return null;
181
+ }
182
+ const before = `{% if ${expr} %}`;
183
+ const after = '{% endif %}';
184
+ const { label, type } = generateTypeAndLabel(state);
185
+ const description = state.groups.length > 1 ? `${state.groups.length} rules` : undefined;
186
+ return { after, before, description, label, type };
187
+ }
188
+
189
+ /*
190
+ * ---------------------------------------------------------------------------
191
+ * Parse helpers
192
+ * ---------------------------------------------------------------------------
193
+ */
194
+
195
+ function stripOuterParens(s: string): string {
196
+ const t = s.trim();
197
+ if (t.length < 2 || !t.startsWith('(') || t.at(-1) !== ')') {
198
+ return t;
199
+ }
200
+ let depth = 1;
201
+ for (let i = 1; i < t.length - 1; i++) {
202
+ if (t[i] === '(') {
203
+ depth++;
204
+ } else if (t[i] === ')') {
205
+ depth--;
206
+ if (depth === 0) {
207
+ return t;
208
+ }
209
+ }
210
+ }
211
+ return depth === 1 ? t.slice(1, -1).trim() : t;
212
+ }
213
+
214
+ /**
215
+ * Tokenize an expression into alternating tokens of (expr, operator).
216
+ * Returns: [expr, 'and'|'or', expr, 'and'|'or', expr, ...]
217
+ *
218
+ * This scans for top-level " and " / " or " while respecting parens,
219
+ * and records which separator was found between each pair.
220
+ */
221
+ function tokenizeGroupExpression(expr: string): {
222
+ conditionExprs: string[];
223
+ operators: LogicalOperator[];
224
+ } {
225
+ const conditionExprs: string[] = [];
226
+ const operators: LogicalOperator[] = [];
227
+ let depth = 0;
228
+ let inSingleQuote = false;
229
+ let start = 0;
230
+
231
+ for (let i = 0; i < expr.length; i++) {
232
+ const c = expr[i];
233
+ if (c === "'" && !isEscapedAt(expr, i)) {
234
+ inSingleQuote = !inSingleQuote;
235
+ } else if (!inSingleQuote && c === '(') {
236
+ depth++;
237
+ } else if (!inSingleQuote && c === ')') {
238
+ depth = Math.max(0, depth - 1);
239
+ } else if (!inSingleQuote && depth === 0) {
240
+ // Check for " and " or " or " at this position
241
+ if (expr.slice(i, i + 5) === ' and ') {
242
+ conditionExprs.push(expr.slice(start, i).trim());
243
+ operators.push('and');
244
+ start = i + 5;
245
+ i += 4;
246
+ continue;
247
+ }
248
+ if (expr.slice(i, i + 4) === ' or ') {
249
+ conditionExprs.push(expr.slice(start, i).trim());
250
+ operators.push('or');
251
+ start = i + 4;
252
+ i += 3;
253
+ continue;
254
+ }
255
+ }
256
+ }
257
+ conditionExprs.push(expr.slice(start).trim());
258
+ return { conditionExprs, operators };
259
+ }
260
+
261
+ function unescapeNunjucksString(s: string): string {
262
+ return s.replaceAll('\\\\', '\\').replaceAll(String.raw`\'`, "'");
263
+ }
264
+
265
+ const RE_STR_DEFAULT = String.raw`\(([\w.]+)\s*\|\s*default\s*\(\s*''\s*\)\)`;
266
+ const RE_NUM_DEFAULT = String.raw`\(([\w.]+)\s*\|\s*default\s*\(\s*0\s*\)\)`;
267
+ const QUOTED_CONTENT = String.raw`((?:[^'\\]|\\.)*)`;
268
+
269
+ function toSingleCondition(
270
+ operator: ConditionOperator,
271
+ dataPointKey: string,
272
+ value: string,
273
+ logicalOp?: LogicalOperator,
274
+ ): SingleCondition {
275
+ const condition: SingleCondition = {
276
+ dataPointKey: dataPointKey.trim(),
277
+ id: generateId(),
278
+ operator,
279
+ value,
280
+ };
281
+ if (logicalOp) {
282
+ condition.logicalOperator = logicalOp;
283
+ }
284
+ return condition;
285
+ }
286
+
287
+ function parseQuotedValue(s: string): { value: string; rest: string } | null {
288
+ if (!s.startsWith("'")) {
289
+ return null;
290
+ }
291
+ let i = 1;
292
+ let value = '';
293
+ while (i < s.length) {
294
+ if (s[i] === '\\' && i + 1 < s.length) {
295
+ value += s[i + 1] === "'" ? "'" : s[i + 1];
296
+ i += 2;
297
+ continue;
298
+ }
299
+ if (s[i] === "'") {
300
+ return { rest: s.slice(i + 1).trim(), value: unescapeNunjucksString(value) };
301
+ }
302
+ value += s[i];
303
+ i++;
304
+ }
305
+ return null;
306
+ }
307
+
308
+ interface ParsePattern {
309
+ getValue: (match: RegExpMatchArray) => string | null;
310
+ operator: ConditionOperator;
311
+ pathGroup: number;
312
+ pattern: RegExp;
313
+ }
314
+
315
+ const PARSE_PATTERNS: ParsePattern[] = [
316
+ // value-less
317
+ {
318
+ getValue: () => '',
319
+ operator: 'is_empty',
320
+ pathGroup: 1,
321
+ pattern: new RegExp(
322
+ String.raw`^${RE_STR_DEFAULT}\s*==\s*''\s+or\s+\(\([\w.]+\s*\|\s*default\s*\(\s*''\s*\)\)\s*\|\s*length\)\s*==\s*0$`,
323
+ ),
324
+ },
325
+ {
326
+ getValue: () => '',
327
+ operator: 'is_not_empty',
328
+ pathGroup: 1,
329
+ pattern: new RegExp(
330
+ String.raw`^${RE_STR_DEFAULT}\s*!=\s*''\s+and\s+\(\([\w.]+\s*\|\s*default\s*\(\s*''\s*\)\)\s*\|\s*length\)\s*>\s*0$`,
331
+ ),
332
+ },
333
+ // string with value
334
+ {
335
+ getValue: m => parseQuotedValue(m[2].trim())?.value ?? null,
336
+ operator: 'is_equal_to',
337
+ pathGroup: 1,
338
+ pattern: new RegExp(String.raw`^${RE_STR_DEFAULT}\s*==\s+(.+)$`),
339
+ },
340
+ {
341
+ getValue: m => parseQuotedValue(m[2].trim())?.value ?? null,
342
+ operator: 'is_not_equal_to',
343
+ pathGroup: 1,
344
+ pattern: new RegExp(String.raw`^${RE_STR_DEFAULT}\s*!=\s+(.+)$`),
345
+ },
346
+ {
347
+ getValue: m => unescapeNunjucksString(m[1]),
348
+ operator: 'contains',
349
+ pathGroup: 2,
350
+ pattern: new RegExp(String.raw`^'${QUOTED_CONTENT}'\s+in\s+${RE_STR_DEFAULT}$`),
351
+ },
352
+ {
353
+ getValue: m => unescapeNunjucksString(m[1]),
354
+ operator: 'does_not_contain',
355
+ pathGroup: 2,
356
+ pattern: new RegExp(
357
+ String.raw`^not\s*\(\s*'${QUOTED_CONTENT}'\s+in\s+${RE_STR_DEFAULT}\s*\)\s*$`,
358
+ ),
359
+ },
360
+ {
361
+ getValue: m => parseQuotedValue(m[2].trim())?.value ?? null,
362
+ operator: 'starts_with',
363
+ pathGroup: 1,
364
+ pattern: new RegExp(String.raw`^${RE_STR_DEFAULT}\s*\|\s*startswith\s*\(\s*(.+)\s*\)\s*$`),
365
+ },
366
+ {
367
+ getValue: m => parseQuotedValue(m[2].trim())?.value ?? null,
368
+ operator: 'ends_with',
369
+ pathGroup: 1,
370
+ pattern: new RegExp(String.raw`^${RE_STR_DEFAULT}\s*\|\s*endswith\s*\(\s*(.+)\s*\)\s*$`),
371
+ },
372
+ // number operators (>= and <= before > and < to avoid partial match)
373
+ {
374
+ getValue: m => m[2].trim(),
375
+ operator: 'num_gte',
376
+ pathGroup: 1,
377
+ pattern: new RegExp(String.raw`^${RE_NUM_DEFAULT}\s*>=\s*(.+)$`),
378
+ },
379
+ {
380
+ getValue: m => m[2].trim(),
381
+ operator: 'num_lte',
382
+ pathGroup: 1,
383
+ pattern: new RegExp(String.raw`^${RE_NUM_DEFAULT}\s*<=\s*(.+)$`),
384
+ },
385
+ {
386
+ getValue: m => m[2].trim(),
387
+ operator: 'num_eq',
388
+ pathGroup: 1,
389
+ pattern: new RegExp(String.raw`^${RE_NUM_DEFAULT}\s*==\s*(.+)$`),
390
+ },
391
+ {
392
+ getValue: m => m[2].trim(),
393
+ operator: 'num_neq',
394
+ pathGroup: 1,
395
+ pattern: new RegExp(String.raw`^${RE_NUM_DEFAULT}\s*!=\s*(.+)$`),
396
+ },
397
+ {
398
+ getValue: m => m[2].trim(),
399
+ operator: 'num_gt',
400
+ pathGroup: 1,
401
+ pattern: new RegExp(String.raw`^${RE_NUM_DEFAULT}\s*>\s*(.+)$`),
402
+ },
403
+ {
404
+ getValue: m => m[2].trim(),
405
+ operator: 'num_lt',
406
+ pathGroup: 1,
407
+ pattern: new RegExp(String.raw`^${RE_NUM_DEFAULT}\s*<\s*(.+)$`),
408
+ },
409
+ ];
410
+
411
+ function parseSingleConditionExpression(
412
+ expr: string,
413
+ logicalOp?: LogicalOperator,
414
+ ): SingleCondition | null {
415
+ const trimmed = expr.trim();
416
+ for (const { getValue, operator, pathGroup, pattern } of PARSE_PATTERNS) {
417
+ const match = new RegExp(pattern).exec(trimmed);
418
+ if (!match) {
419
+ continue;
420
+ }
421
+ const value = getValue(match);
422
+ if (value === null) {
423
+ continue;
424
+ }
425
+ return toSingleCondition(operator, match[pathGroup], value, logicalOp);
426
+ }
427
+ return null;
428
+ }
429
+
430
+ /**
431
+ * Parse a group expression into conditions with per-condition logical operators.
432
+ */
433
+ function parseGroupExpression(groupExpr: string): SingleCondition[] | null {
434
+ const { conditionExprs, operators } = tokenizeGroupExpression(groupExpr);
435
+ const conditions: SingleCondition[] = [];
436
+
437
+ for (let i = 0; i < conditionExprs.length; i++) {
438
+ const cs = conditionExprs[i];
439
+ if (!cs) {
440
+ continue;
441
+ }
442
+ const unwrapped = stripOuterParens(cs);
443
+ const logicalOp = i > 0 ? operators[i - 1] : undefined;
444
+ const single = parseSingleConditionExpression(unwrapped, logicalOp);
445
+ if (single) {
446
+ conditions.push(single);
447
+ }
448
+ }
449
+
450
+ return conditions.length > 0 ? conditions : null;
451
+ }
452
+
453
+ /**
454
+ * Parse a saved Unlayer display condition back into modal state for prefilling.
455
+ * Returns null if the condition is missing or cannot be parsed.
456
+ */
457
+ export function parseUnlayerDisplayCondition(
458
+ condition: UnlayerDisplayCondition | null | undefined,
459
+ ): DisplayConditionState | null {
460
+ if (!condition?.before) {
461
+ return null;
462
+ }
463
+ const ifMatch = new RegExp(/\{%\s*if\s+(.+)\s*%\}/s).exec(condition.before);
464
+ if (!ifMatch) {
465
+ return null;
466
+ }
467
+ let expr = ifMatch[1].trim();
468
+
469
+ let behavior: DisplayBehavior = 'show';
470
+ if (expr.startsWith('not (') && expr.endsWith(')')) {
471
+ behavior = 'hide';
472
+ expr = expr.slice(5, -1).trim();
473
+ }
474
+
475
+ const { conditionExprs: ruleStrings, operators: groupOperators } =
476
+ tokenizeGroupExpression(expr);
477
+ const groups: ConditionGroup[] = [];
478
+
479
+ for (let i = 0; i < ruleStrings.length; i++) {
480
+ const rs = ruleStrings[i];
481
+ if (!rs) {
482
+ continue;
483
+ }
484
+ const unwrapped = stripOuterParens(rs);
485
+ const conditions = parseGroupExpression(unwrapped);
486
+ if (conditions) {
487
+ const group: ConditionGroup = {
488
+ conditions,
489
+ id: generateId(),
490
+ };
491
+ if (i > 0) {
492
+ group.logicalOperator = groupOperators[i - 1] ?? 'and';
493
+ }
494
+ groups.push(group);
495
+ }
496
+ }
497
+
498
+ if (groups.length === 0) {
499
+ return null;
500
+ }
501
+
502
+ return { behavior, groups };
503
+ }
@@ -0,0 +1,33 @@
1
+ import {
2
+ SchemaFieldBaseOptions,
3
+ SchemaObject,
4
+ schemaBuildMap,
5
+ schemaIsNumber,
6
+ } from '../shared/schema';
7
+ import { DataPointOption } from './types';
8
+
9
+ function isUseInConditionals(node: { options?: SchemaFieldBaseOptions } | undefined): boolean {
10
+ return (
11
+ node?.options?.useInConditionals === true || node?.options?.useInCalculatedFields === true
12
+ );
13
+ }
14
+
15
+ export function getSchemaDataPointOptions(schema: SchemaObject | undefined): DataPointOption[] {
16
+ if (!schema?.properties) {
17
+ return [];
18
+ }
19
+ const { map } = schemaBuildMap(schema);
20
+ return Object.values(map)
21
+ .filter(
22
+ f =>
23
+ f.isValue &&
24
+ !f.isArrayed &&
25
+ isUseInConditionals(f.node as { options?: SchemaFieldBaseOptions }),
26
+ )
27
+ .map(f => ({
28
+ fieldType: schemaIsNumber(f.node) ? ('number' as const) : ('string' as const),
29
+ fullKey: f.fullKey,
30
+ title: f.fullTitle || f.title || f.fullKey,
31
+ }))
32
+ .sort((a, b) => a.title.localeCompare(b.title));
33
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Display condition types for the modal and Nunjucks generation.
3
+ */
4
+
5
+ export type DisplayBehavior = 'show' | 'hide';
6
+
7
+ export type LogicalOperator = 'and' | 'or';
8
+
9
+ /** Operators for string-type fields. */
10
+ export const STRING_OPERATORS = [
11
+ { label: 'Contains', value: 'contains' },
12
+ { label: 'Does not contain', value: 'does_not_contain' },
13
+ { label: 'Is equal to', value: 'is_equal_to' },
14
+ { label: 'Is not equal to', value: 'is_not_equal_to' },
15
+ { label: 'Starts with', value: 'starts_with' },
16
+ { label: 'Ends with', value: 'ends_with' },
17
+ { label: 'Is empty', value: 'is_empty' },
18
+ { label: 'Is not empty', value: 'is_not_empty' },
19
+ ] as const;
20
+
21
+ /** Operators for number-type fields. */
22
+ export const NUMBER_OPERATORS = [
23
+ { label: '== (equal to)', value: 'num_eq' },
24
+ { label: '!= (not equal to)', value: 'num_neq' },
25
+ { label: '> (greater than)', value: 'num_gt' },
26
+ { label: '< (less than)', value: 'num_lt' },
27
+ { label: '>= (greater than or equal to)', value: 'num_gte' },
28
+ { label: '<= (less than or equal to)', value: 'num_lte' },
29
+ ] as const;
30
+
31
+ /** All operators combined for type definitions. */
32
+ export const CONDITION_OPERATORS = [...STRING_OPERATORS, ...NUMBER_OPERATORS] as const;
33
+
34
+ export type ConditionOperator =
35
+ | (typeof STRING_OPERATORS)[number]['value']
36
+ | (typeof NUMBER_OPERATORS)[number]['value'];
37
+
38
+ /** Operators that do not require a value (empty/is not empty) */
39
+ export const VALUE_LESS_OPERATORS: ConditionOperator[] = ['is_empty', 'is_not_empty'];
40
+
41
+ export interface SingleCondition {
42
+ id: string;
43
+ dataPointKey: string;
44
+ /** How this condition connects to the PREVIOUS condition. Absent on the first condition. */
45
+ logicalOperator?: LogicalOperator;
46
+ operator: ConditionOperator;
47
+ value: string;
48
+ }
49
+
50
+ export interface ConditionGroup {
51
+ id: string;
52
+ /** How this group connects to the PREVIOUS group. Absent on the first group. */
53
+ logicalOperator?: LogicalOperator;
54
+ conditions: SingleCondition[];
55
+ }
56
+
57
+ export interface DisplayConditionState {
58
+ behavior: DisplayBehavior;
59
+ groups: ConditionGroup[];
60
+ }
61
+
62
+ export interface DataPointOption {
63
+ fieldType: 'number' | 'string';
64
+ fullKey: string;
65
+ title: string;
66
+ }
67
+
68
+ /** Unlayer display condition shape passed to done() */
69
+ export interface UnlayerDisplayCondition {
70
+ type: string;
71
+ label: string;
72
+ description?: string;
73
+ before: string;
74
+ after: string;
75
+ }