@servicetitan/dte-pdf-editor 1.21.0 → 1.23.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.
- package/dist/components/display-conditions/condition-group.d.ts +11 -0
- package/dist/components/display-conditions/condition-group.d.ts.map +1 -0
- package/dist/components/display-conditions/condition-group.js +52 -0
- package/dist/components/display-conditions/condition-group.js.map +1 -0
- package/dist/components/display-conditions/condition-groups-section.d.ts +10 -0
- package/dist/components/display-conditions/condition-groups-section.d.ts.map +1 -0
- package/dist/components/display-conditions/condition-groups-section.js +14 -0
- package/dist/components/display-conditions/condition-groups-section.js.map +1 -0
- package/dist/components/display-conditions/condition-row.d.ts +10 -0
- package/dist/components/display-conditions/condition-row.d.ts.map +1 -0
- package/dist/components/display-conditions/condition-row.js +60 -0
- package/dist/components/display-conditions/condition-row.js.map +1 -0
- package/dist/components/display-conditions/display-condition-modal.d.ts +12 -0
- package/dist/components/display-conditions/display-condition-modal.d.ts.map +1 -0
- package/dist/components/display-conditions/display-condition-modal.js +89 -0
- package/dist/components/display-conditions/display-condition-modal.js.map +1 -0
- package/dist/components/display-conditions/display-condition.d.ts +9 -0
- package/dist/components/display-conditions/display-condition.d.ts.map +1 -0
- package/dist/components/display-conditions/display-condition.js +10 -0
- package/dist/components/display-conditions/display-condition.js.map +1 -0
- package/dist/components/field-config-panel/advanced-settings.d.ts.map +1 -1
- package/dist/components/field-config-panel/advanced-settings.js +10 -8
- package/dist/components/field-config-panel/advanced-settings.js.map +1 -1
- package/dist/components/field-config-panel/field-config-panel.d.ts +1 -1
- package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
- package/dist/components/field-config-panel/field-config-panel.js +5 -3
- package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
- package/dist/components/field-config-panel/formula-generator.d.ts.map +1 -1
- package/dist/components/field-config-panel/formula-generator.js +3 -20
- package/dist/components/field-config-panel/formula-generator.js.map +1 -1
- package/dist/components/field-config-panel/formula-modal.d.ts.map +1 -1
- package/dist/components/field-config-panel/formula-modal.js +9 -3
- package/dist/components/field-config-panel/formula-modal.js.map +1 -1
- package/dist/components/field-config-panel/formula-workspace.d.ts.map +1 -1
- package/dist/components/field-config-panel/formula-workspace.js +4 -3
- package/dist/components/field-config-panel/formula-workspace.js.map +1 -1
- package/dist/components/field-config-panel/result-type-selector.d.ts.map +1 -1
- package/dist/components/field-config-panel/result-type-selector.js +2 -2
- package/dist/components/field-config-panel/result-type-selector.js.map +1 -1
- package/dist/components/pdf-view/pdf-view-field-container.d.ts +2 -1
- package/dist/components/pdf-view/pdf-view-field-container.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view-field-container.js +6 -2
- package/dist/components/pdf-view/pdf-view-field-container.js.map +1 -1
- package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view.js +1 -1
- package/dist/components/pdf-view/pdf-view.js.map +1 -1
- package/dist/constants/calculated.constants.d.ts +2 -0
- package/dist/constants/calculated.constants.d.ts.map +1 -0
- package/dist/constants/calculated.constants.js +2 -0
- package/dist/constants/calculated.constants.js.map +1 -0
- package/dist/constants/conditions.constants.d.ts +6 -0
- package/dist/constants/conditions.constants.d.ts.map +1 -0
- package/dist/constants/conditions.constants.js +17 -0
- package/dist/constants/conditions.constants.js.map +1 -0
- package/dist/constants/index.d.ts +3 -0
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/constants/index.js +3 -0
- package/dist/constants/index.js.map +1 -1
- package/dist/interface/types.d.ts +74 -0
- package/dist/interface/types.d.ts.map +1 -1
- package/dist/interface/types.js +25 -0
- package/dist/interface/types.js.map +1 -1
- package/dist/utils/conditions/evaluate.utils.d.ts +6 -0
- package/dist/utils/conditions/evaluate.utils.d.ts.map +1 -0
- package/dist/utils/conditions/evaluate.utils.js +95 -0
- package/dist/utils/conditions/evaluate.utils.js.map +1 -0
- package/dist/utils/conditions/index.d.ts +3 -0
- package/dist/utils/conditions/index.d.ts.map +1 -0
- package/dist/utils/conditions/index.js +3 -0
- package/dist/utils/conditions/index.js.map +1 -0
- package/dist/utils/conditions/schema-data-points.utils.d.ts +12 -0
- package/dist/utils/conditions/schema-data-points.utils.d.ts.map +1 -0
- package/dist/utils/conditions/schema-data-points.utils.js +67 -0
- package/dist/utils/conditions/schema-data-points.utils.js.map +1 -0
- package/dist/utils/data-model/extract-fields.utils.d.ts +1 -0
- package/dist/utils/data-model/extract-fields.utils.d.ts.map +1 -1
- package/dist/utils/data-model/extract-fields.utils.js.map +1 -1
- package/dist/utils/formula/render-formula.utils.d.ts +0 -6
- package/dist/utils/formula/render-formula.utils.d.ts.map +1 -1
- package/dist/utils/formula/render-formula.utils.js +0 -17
- package/dist/utils/formula/render-formula.utils.js.map +1 -1
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/shared/index.d.ts +2 -0
- package/dist/utils/shared/index.d.ts.map +1 -0
- package/dist/utils/shared/index.js +2 -0
- package/dist/utils/shared/index.js.map +1 -0
- package/dist/utils/shared/number.utils.d.ts +2 -0
- package/dist/utils/shared/number.utils.d.ts.map +1 -0
- package/dist/utils/shared/number.utils.js +12 -0
- package/dist/utils/shared/number.utils.js.map +1 -0
- package/package.json +1 -1
- package/src/components/display-conditions/condition-group.tsx +141 -0
- package/src/components/display-conditions/condition-groups-section.tsx +63 -0
- package/src/components/display-conditions/condition-row.tsx +182 -0
- package/src/components/display-conditions/display-condition-modal.tsx +180 -0
- package/src/components/display-conditions/display-condition.tsx +41 -0
- package/src/components/field-config-panel/advanced-settings.tsx +42 -46
- package/src/components/field-config-panel/field-config-panel.tsx +13 -3
- package/src/components/field-config-panel/formula-generator.tsx +9 -44
- package/src/components/field-config-panel/formula-modal.tsx +72 -82
- package/src/components/field-config-panel/formula-workspace.tsx +6 -5
- package/src/components/field-config-panel/result-type-selector.tsx +8 -11
- package/src/components/pdf-view/pdf-view-field-container.tsx +11 -2
- package/src/components/pdf-view/pdf-view.tsx +1 -0
- package/src/constants/calculated.constants.ts +1 -0
- package/src/constants/conditions.constants.ts +26 -0
- package/src/constants/index.ts +3 -0
- package/src/interface/types.ts +59 -0
- package/src/styles/formula-modal.css +1 -155
- package/src/utils/conditions/evaluate.utils.ts +134 -0
- package/src/utils/conditions/index.ts +2 -0
- package/src/utils/conditions/schema-data-points.utils.ts +93 -0
- package/src/utils/data-model/extract-fields.utils.ts +1 -0
- package/src/utils/formula/render-formula.utils.ts +0 -19
- package/src/utils/index.ts +2 -0
- package/src/utils/shared/index.ts +1 -0
- package/src/utils/shared/number.utils.ts +13 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Alert, Button, Flex, Text } from '@servicetitan/anvil2';
|
|
1
|
+
import { Alert, Button, Flex, LinkButton, Text } from '@servicetitan/anvil2';
|
|
2
2
|
import {
|
|
3
3
|
Dispatch,
|
|
4
4
|
FC,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
SetStateAction,
|
|
10
10
|
useCallback,
|
|
11
11
|
} from 'react';
|
|
12
|
+
import { CALCULATED_OPERATIONS } from '../../constants';
|
|
12
13
|
import { CalculatedFieldFormat } from '../../interface/types';
|
|
13
14
|
import { AdvancedSettings } from './advanced-settings';
|
|
14
15
|
import { ResultTypeSelector } from './result-type-selector';
|
|
@@ -70,7 +71,7 @@ export const FormulaWorkspace: FC<FormulaWorkspaceProps> = ({
|
|
|
70
71
|
);
|
|
71
72
|
|
|
72
73
|
return (
|
|
73
|
-
<Flex direction="column" flex={1} gap={2}>
|
|
74
|
+
<Flex direction="column" flex={1} gap={2} style={{ padding: '12px' }}>
|
|
74
75
|
<Text variant="body" size="small">
|
|
75
76
|
Click a field on the left to add it. Use +, -, *, /, and parentheses to build
|
|
76
77
|
formulas.
|
|
@@ -100,16 +101,16 @@ export const FormulaWorkspace: FC<FormulaWorkspaceProps> = ({
|
|
|
100
101
|
/>
|
|
101
102
|
{isInvalid && validationError && <Alert status="danger" title={validationError} />}
|
|
102
103
|
<Flex gap={1}>
|
|
103
|
-
{
|
|
104
|
+
{CALCULATED_OPERATIONS.map(op => (
|
|
104
105
|
<Button key={op} onClick={() => onOperatorSelect(op)} size="small">
|
|
105
106
|
{op}
|
|
106
107
|
</Button>
|
|
107
108
|
))}
|
|
108
109
|
</Flex>
|
|
109
110
|
<ResultTypeSelector format={format} onChange={onResultTypeChange} />
|
|
110
|
-
<
|
|
111
|
+
<LinkButton appearance="primary" onClick={onToggleAdvanced}>
|
|
111
112
|
{advancedOpen ? 'Hide advanced settings' : 'Show advanced settings'}
|
|
112
|
-
</
|
|
113
|
+
</LinkButton>
|
|
113
114
|
{advancedOpen && <AdvancedSettings format={format} setFormat={setFormat} />}
|
|
114
115
|
</Flex>
|
|
115
116
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Flex, SegmentedControl, Text } from '@servicetitan/anvil2';
|
|
2
2
|
import { FC, Fragment } from 'react';
|
|
3
3
|
import { CalculatedFieldFormat } from '../../interface/types';
|
|
4
4
|
|
|
@@ -19,16 +19,13 @@ export const ResultTypeSelector: FC<ResultTypeSelectorProps> = ({ format, onChan
|
|
|
19
19
|
Result type
|
|
20
20
|
</Text>
|
|
21
21
|
<Flex gap={2}>
|
|
22
|
-
{
|
|
23
|
-
|
|
24
|
-
key={value}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
{label}
|
|
30
|
-
</Button>
|
|
31
|
-
))}
|
|
22
|
+
<SegmentedControl size="medium" selected={format.resultType} onChange={onChange} fill>
|
|
23
|
+
{RESULT_TYPE_OPTIONS.map(opt => (
|
|
24
|
+
<SegmentedControl.Segment key={opt.value} value={opt.value}>
|
|
25
|
+
{opt.label}
|
|
26
|
+
</SegmentedControl.Segment>
|
|
27
|
+
))}
|
|
28
|
+
</SegmentedControl>
|
|
32
29
|
</Flex>
|
|
33
30
|
</Fragment>
|
|
34
31
|
);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { FC, PropsWithChildren, RefObject } from 'react';
|
|
2
|
-
import { PdfField } from '../../interface/types';
|
|
3
|
-
import { getFieldBackgroundColor, getPagePosition } from '../../utils';
|
|
2
|
+
import { DataModelValues, PdfField } from '../../interface/types';
|
|
3
|
+
import { evaluateDisplayCondition, getFieldBackgroundColor, getPagePosition } from '../../utils';
|
|
4
4
|
|
|
5
5
|
interface PdfViewFieldContainer {
|
|
6
6
|
pdfWrapperRef: RefObject<HTMLDivElement>;
|
|
7
|
+
data?: DataModelValues;
|
|
7
8
|
error?: string;
|
|
8
9
|
field: PdfField;
|
|
9
10
|
recipientsColors: Record<string, string>;
|
|
@@ -11,11 +12,19 @@ interface PdfViewFieldContainer {
|
|
|
11
12
|
|
|
12
13
|
export const PdfViewFieldContainer: FC<PropsWithChildren<PdfViewFieldContainer>> = ({
|
|
13
14
|
children,
|
|
15
|
+
data,
|
|
14
16
|
error,
|
|
15
17
|
field,
|
|
16
18
|
pdfWrapperRef,
|
|
17
19
|
recipientsColors,
|
|
18
20
|
}) => {
|
|
21
|
+
const visible =
|
|
22
|
+
!field.displayCondition || evaluateDisplayCondition(field.displayCondition, data);
|
|
23
|
+
|
|
24
|
+
if (!visible) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
const pagePos = getPagePosition(field.page, pdfWrapperRef);
|
|
20
29
|
const bgColor = getFieldBackgroundColor(field.recipient, recipientsColors);
|
|
21
30
|
return (
|
|
@@ -90,6 +90,7 @@ export const PdfView: FC<PdfViewProps> = ({
|
|
|
90
90
|
<PdfViewFieldContainer
|
|
91
91
|
key={field.id}
|
|
92
92
|
field={field}
|
|
93
|
+
data={previewMode === 'fillable' ? previewData : data}
|
|
93
94
|
error={errors[field.path!] || ''}
|
|
94
95
|
recipientsColors={recipientsColors}
|
|
95
96
|
pdfWrapperRef={pdfWrapperRef}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CALCULATED_OPERATIONS = ['+', '-', '*', '/', '(', ')'];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
DisplayConditionGroup,
|
|
5
|
+
DisplayConditionSingle,
|
|
6
|
+
DisplayConditionState,
|
|
7
|
+
} from '../interface/types';
|
|
8
|
+
|
|
9
|
+
export const defaultCondition = (): DisplayConditionSingle => ({
|
|
10
|
+
dataPointKey: '',
|
|
11
|
+
id: uuidv4(),
|
|
12
|
+
operator: 'is_equal_to',
|
|
13
|
+
value: '',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const defaultGroup = (): DisplayConditionGroup => ({
|
|
17
|
+
conditions: [defaultCondition()],
|
|
18
|
+
id: uuidv4(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const defaultState = (): DisplayConditionState => ({
|
|
22
|
+
behavior: 'show',
|
|
23
|
+
groups: [defaultGroup()],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const MODAL_CONTENT_MAX_HEIGHT = '70vh';
|
package/src/constants/index.ts
CHANGED
package/src/interface/types.ts
CHANGED
|
@@ -16,6 +16,25 @@ export type FillableFieldType = 'text' | 'date' | 'checkbox' | 'radio' | 'number
|
|
|
16
16
|
|
|
17
17
|
export type PdfFieldSubType = FillableFieldType | ESignFieldType;
|
|
18
18
|
|
|
19
|
+
export interface DisplayConditionState {
|
|
20
|
+
behavior: 'show' | 'hide';
|
|
21
|
+
groups: DisplayConditionGroup[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DisplayConditionGroup {
|
|
25
|
+
id: string;
|
|
26
|
+
logicalOperator?: 'and' | 'or';
|
|
27
|
+
conditions: DisplayConditionSingle[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DisplayConditionSingle {
|
|
31
|
+
id: string;
|
|
32
|
+
dataPointKey: string;
|
|
33
|
+
logicalOperator?: 'and' | 'or';
|
|
34
|
+
operator: string;
|
|
35
|
+
value: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
export interface PdfField {
|
|
20
39
|
id: string;
|
|
21
40
|
type: FieldTypeEnum;
|
|
@@ -32,6 +51,7 @@ export interface PdfField {
|
|
|
32
51
|
description?: string;
|
|
33
52
|
formula?: StructuredFormula;
|
|
34
53
|
formulaFormat?: CalculatedFieldFormat;
|
|
54
|
+
displayCondition?: DisplayConditionState | null;
|
|
35
55
|
}
|
|
36
56
|
|
|
37
57
|
export interface FieldTypeOption {
|
|
@@ -144,3 +164,42 @@ export interface DataChangePayload {
|
|
|
144
164
|
* it is displayed.
|
|
145
165
|
*/
|
|
146
166
|
export type PreviewMode = 'fillable' | 'view';
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Display condition types for the Rules and Conditions modal and evaluation.
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
/** Operators for string-type fields. */
|
|
173
|
+
export const STRING_OPERATORS = [
|
|
174
|
+
{ label: 'Contains', value: 'contains' },
|
|
175
|
+
{ label: 'Does not contain', value: 'does_not_contain' },
|
|
176
|
+
{ label: 'Is equal to', value: 'is_equal_to' },
|
|
177
|
+
{ label: 'Is not equal to', value: 'is_not_equal_to' },
|
|
178
|
+
{ label: 'Starts with', value: 'starts_with' },
|
|
179
|
+
{ label: 'Ends with', value: 'ends_with' },
|
|
180
|
+
{ label: 'Is empty', value: 'is_empty' },
|
|
181
|
+
{ label: 'Is not empty', value: 'is_not_empty' },
|
|
182
|
+
] as const;
|
|
183
|
+
|
|
184
|
+
/** Operators for number-type fields. */
|
|
185
|
+
export const NUMBER_OPERATORS = [
|
|
186
|
+
{ label: '== (equal to)', value: 'num_eq' },
|
|
187
|
+
{ label: '!= (not equal to)', value: 'num_neq' },
|
|
188
|
+
{ label: '> (greater than)', value: 'num_gt' },
|
|
189
|
+
{ label: '< (less than)', value: 'num_lt' },
|
|
190
|
+
{ label: '>= (greater than or equal to)', value: 'num_gte' },
|
|
191
|
+
{ label: '<= (less than or equal to)', value: 'num_lte' },
|
|
192
|
+
] as const;
|
|
193
|
+
|
|
194
|
+
export type ConditionOperator =
|
|
195
|
+
| (typeof STRING_OPERATORS)[number]['value']
|
|
196
|
+
| (typeof NUMBER_OPERATORS)[number]['value'];
|
|
197
|
+
|
|
198
|
+
/** Operators that do not require a value (empty/is not empty) */
|
|
199
|
+
export const VALUE_LESS_OPERATORS: ConditionOperator[] = ['is_empty', 'is_not_empty'];
|
|
200
|
+
|
|
201
|
+
export interface DataPointOption {
|
|
202
|
+
fieldType: 'number' | 'string';
|
|
203
|
+
fullKey: string;
|
|
204
|
+
title: string;
|
|
205
|
+
}
|
|
@@ -15,19 +15,9 @@
|
|
|
15
15
|
max-height: 85vh;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
.dte-formula-modal-body {
|
|
19
|
-
padding: var(--spacing-2);
|
|
20
|
-
overflow: auto;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
.dte-formula-modal-columns {
|
|
24
|
-
min-height: 280px;
|
|
25
|
-
max-height: 60vh;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
18
|
/* Field sidebar (left) */
|
|
29
19
|
.dte-formula-sidebar {
|
|
30
|
-
flex: 0 0
|
|
20
|
+
flex: 0 0 250px;
|
|
31
21
|
border-right: 1px solid var(--border-color);
|
|
32
22
|
padding-right: var(--spacing-2);
|
|
33
23
|
gap: var(--spacing-2);
|
|
@@ -115,20 +105,6 @@
|
|
|
115
105
|
opacity: 1;
|
|
116
106
|
}
|
|
117
107
|
|
|
118
|
-
.dte-formula-advanced-toggle {
|
|
119
|
-
background: none;
|
|
120
|
-
border: none;
|
|
121
|
-
padding: var(--spacing-0);
|
|
122
|
-
font-size: var(--typescale-2);
|
|
123
|
-
color: var(--menu-active-color, #0265dc);
|
|
124
|
-
cursor: pointer;
|
|
125
|
-
text-decoration: none;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
.dte-formula-advanced-toggle:hover {
|
|
129
|
-
text-decoration: underline;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
108
|
.dte-formula-advanced {
|
|
133
109
|
margin-top: var(--spacing-2);
|
|
134
110
|
padding-top: var(--spacing-2);
|
|
@@ -138,30 +114,10 @@
|
|
|
138
114
|
gap: var(--spacing-2);
|
|
139
115
|
}
|
|
140
116
|
|
|
141
|
-
.dte-formula-advanced-row {
|
|
142
|
-
margin-bottom: var(--spacing-0);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
117
|
.dte-formula-advanced-value {
|
|
146
118
|
color: var(--color-neutral-300);
|
|
147
119
|
}
|
|
148
120
|
|
|
149
|
-
.dte-formula-advanced-range {
|
|
150
|
-
width: 100%;
|
|
151
|
-
margin: var(--spacing-0);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
.dte-formula-advanced-checkbox {
|
|
155
|
-
display: flex;
|
|
156
|
-
align-items: center;
|
|
157
|
-
gap: var(--spacing-1);
|
|
158
|
-
cursor: pointer;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.dte-formula-advanced-checkbox input {
|
|
162
|
-
margin: var(--spacing-0);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
121
|
.dte-formula-advanced-input {
|
|
166
122
|
flex: 1;
|
|
167
123
|
min-width: 0;
|
|
@@ -182,70 +138,6 @@
|
|
|
182
138
|
border-top: 1px solid var(--border-color);
|
|
183
139
|
}
|
|
184
140
|
|
|
185
|
-
/* Token input */
|
|
186
|
-
.dte-formula-token-input-wrapper {
|
|
187
|
-
display: flex;
|
|
188
|
-
flex-direction: row;
|
|
189
|
-
align-items: stretch;
|
|
190
|
-
gap: var(--spacing-2);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
.dte-formula-token-input {
|
|
194
|
-
flex: 1;
|
|
195
|
-
padding: var(--spacing-1) var(--spacing-2);
|
|
196
|
-
border: 1px solid var(--border-color);
|
|
197
|
-
border-radius: 4px;
|
|
198
|
-
outline: none;
|
|
199
|
-
flex-wrap: wrap;
|
|
200
|
-
gap: 2px;
|
|
201
|
-
cursor: text;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
.dte-formula-token-input:focus {
|
|
205
|
-
border-color: var(--border-color-active);
|
|
206
|
-
box-shadow: 0 0 0 1px var(--border-color-active);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
.dte-formula-placeholder {
|
|
210
|
-
color: var(--color-neutral-200);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
.dte-formula-token {
|
|
214
|
-
margin: var(--spacing-half);
|
|
215
|
-
user-select: none;
|
|
216
|
-
font-size: var(--typescale-2);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
.dte-formula-token-field {
|
|
220
|
-
padding: 2px var(--spacing-half);
|
|
221
|
-
background: var(--menu-active-color, #0265dc);
|
|
222
|
-
border-radius: var(--spacing-half, 4px);
|
|
223
|
-
color: var(--white);
|
|
224
|
-
flex-shrink: 0;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
.dte-formula-token-delete {
|
|
228
|
-
display: inline-flex;
|
|
229
|
-
align-items: center;
|
|
230
|
-
justify-content: center;
|
|
231
|
-
padding: var(--spacing-0);
|
|
232
|
-
margin: var(--spacing-0);
|
|
233
|
-
margin-left: 2px;
|
|
234
|
-
border: none;
|
|
235
|
-
background: transparent;
|
|
236
|
-
color: var(--color-neutral-200);
|
|
237
|
-
cursor: pointer;
|
|
238
|
-
border-radius: 2px;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
.dte-formula-token-delete:hover {
|
|
242
|
-
color: var(--danger, #dc3545);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
.dte-formula-token-number {
|
|
246
|
-
font-variant-numeric: tabular-nums;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
141
|
.dte-formula-caret {
|
|
250
142
|
width: 0;
|
|
251
143
|
height: 1em;
|
|
@@ -259,49 +151,3 @@
|
|
|
259
151
|
opacity: 0;
|
|
260
152
|
}
|
|
261
153
|
}
|
|
262
|
-
|
|
263
|
-
/* Autosuggest - left side, always visible */
|
|
264
|
-
.dte-formula-autosuggest {
|
|
265
|
-
margin: var(--spacing-0);
|
|
266
|
-
padding: var(--spacing-0);
|
|
267
|
-
list-style: none;
|
|
268
|
-
width: 200px;
|
|
269
|
-
min-width: 200px;
|
|
270
|
-
max-height: 200px;
|
|
271
|
-
overflow-y: auto;
|
|
272
|
-
background: var(--white);
|
|
273
|
-
border: 1px solid var(--border-color);
|
|
274
|
-
border-radius: 4px;
|
|
275
|
-
flex-shrink: 0;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
.dte-formula-autosuggest-item {
|
|
279
|
-
padding: var(--spacing-1) var(--spacing-2);
|
|
280
|
-
cursor: pointer;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
.dte-formula-autosuggest-item:hover,
|
|
284
|
-
.dte-formula-autosuggest-item.--highlight {
|
|
285
|
-
color: var(--menu-active-color);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
.dte-formula-autosuggest-item.empty {
|
|
289
|
-
cursor: default;
|
|
290
|
-
color: var(--color-neutral-200);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
.dte-formula-box {
|
|
294
|
-
padding: var(--spacing-1) var(--spacing-2);
|
|
295
|
-
border: 1px solid var(--border-color);
|
|
296
|
-
border-radius: 4px;
|
|
297
|
-
background: var(--color-neutral-0);
|
|
298
|
-
cursor: pointer;
|
|
299
|
-
display: flex;
|
|
300
|
-
overflow-x: auto;
|
|
301
|
-
align-items: center;
|
|
302
|
-
gap: 2px;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
.dte-formula-box:hover {
|
|
306
|
-
border-color: var(--border-color-hover);
|
|
307
|
-
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConditionOperator,
|
|
3
|
+
DataModelValues,
|
|
4
|
+
DisplayConditionGroup,
|
|
5
|
+
DisplayConditionState,
|
|
6
|
+
VALUE_LESS_OPERATORS,
|
|
7
|
+
} from '../../interface/types';
|
|
8
|
+
|
|
9
|
+
function getValueAtPath(
|
|
10
|
+
data: DataModelValues | undefined,
|
|
11
|
+
path: string,
|
|
12
|
+
): string | number | undefined {
|
|
13
|
+
if (!data || !path) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const keys = path.split('.');
|
|
17
|
+
let value: unknown = data;
|
|
18
|
+
for (const key of keys) {
|
|
19
|
+
if (value === null || value === undefined || typeof value !== 'object') {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
value = (value as Record<string, unknown>)[key];
|
|
23
|
+
}
|
|
24
|
+
if (value === null || value === undefined) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
if (typeof value === 'number' || typeof value === 'string') {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
return String(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function evaluateSingleCondition(
|
|
34
|
+
dataPointKey: string,
|
|
35
|
+
operator: string,
|
|
36
|
+
value: string,
|
|
37
|
+
data: DataModelValues | undefined,
|
|
38
|
+
): boolean {
|
|
39
|
+
const raw = getValueAtPath(data, dataPointKey);
|
|
40
|
+
const strVal = raw === undefined ? '' : String(raw);
|
|
41
|
+
const numVal = typeof raw === 'number' ? raw : Number(strVal);
|
|
42
|
+
const trimmedInput = value.trim();
|
|
43
|
+
const parsed = Number(trimmedInput);
|
|
44
|
+
const numInput = trimmedInput !== '' && Number.isFinite(parsed) ? parsed : 0;
|
|
45
|
+
|
|
46
|
+
switch (operator as ConditionOperator) {
|
|
47
|
+
case 'is_equal_to':
|
|
48
|
+
return strVal === trimmedInput;
|
|
49
|
+
case 'is_not_equal_to':
|
|
50
|
+
return strVal !== trimmedInput;
|
|
51
|
+
case 'contains':
|
|
52
|
+
return strVal.includes(trimmedInput);
|
|
53
|
+
case 'does_not_contain':
|
|
54
|
+
return !strVal.includes(trimmedInput);
|
|
55
|
+
case 'starts_with':
|
|
56
|
+
return strVal.startsWith(trimmedInput);
|
|
57
|
+
case 'ends_with':
|
|
58
|
+
return strVal.endsWith(trimmedInput);
|
|
59
|
+
case 'is_empty':
|
|
60
|
+
return strVal === '' || strVal.length === 0;
|
|
61
|
+
case 'is_not_empty':
|
|
62
|
+
return strVal !== '' && strVal.length > 0;
|
|
63
|
+
case 'num_eq':
|
|
64
|
+
return numVal === numInput;
|
|
65
|
+
case 'num_neq':
|
|
66
|
+
return numVal !== numInput;
|
|
67
|
+
case 'num_gt':
|
|
68
|
+
return numVal > numInput;
|
|
69
|
+
case 'num_lt':
|
|
70
|
+
return numVal < numInput;
|
|
71
|
+
case 'num_gte':
|
|
72
|
+
return numVal >= numInput;
|
|
73
|
+
case 'num_lte':
|
|
74
|
+
return numVal <= numInput;
|
|
75
|
+
default:
|
|
76
|
+
return strVal === trimmedInput;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function evaluateGroup(group: DisplayConditionGroup, data: DataModelValues | undefined): boolean {
|
|
81
|
+
const valid = group.conditions.filter(
|
|
82
|
+
c =>
|
|
83
|
+
c.dataPointKey &&
|
|
84
|
+
(VALUE_LESS_OPERATORS.includes(c.operator as ConditionOperator) ||
|
|
85
|
+
c.value.trim() !== ''),
|
|
86
|
+
);
|
|
87
|
+
if (valid.length === 0) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
let result = evaluateSingleCondition(
|
|
91
|
+
valid[0].dataPointKey,
|
|
92
|
+
valid[0].operator,
|
|
93
|
+
valid[0].value,
|
|
94
|
+
data,
|
|
95
|
+
);
|
|
96
|
+
for (let i = 1; i < valid.length; i++) {
|
|
97
|
+
const op = valid[i].logicalOperator === 'or' ? 'or' : 'and';
|
|
98
|
+
const next = evaluateSingleCondition(
|
|
99
|
+
valid[i].dataPointKey,
|
|
100
|
+
valid[i].operator,
|
|
101
|
+
valid[i].value,
|
|
102
|
+
data,
|
|
103
|
+
);
|
|
104
|
+
result = op === 'or' ? result || next : result && next;
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Evaluate display condition state against data. Returns true if the field should be visible.
|
|
111
|
+
*/
|
|
112
|
+
export function evaluateDisplayCondition(
|
|
113
|
+
state: DisplayConditionState,
|
|
114
|
+
data: DataModelValues | undefined,
|
|
115
|
+
): boolean {
|
|
116
|
+
const validGroups = state.groups.filter(g =>
|
|
117
|
+
g.conditions.some(
|
|
118
|
+
c =>
|
|
119
|
+
c.dataPointKey &&
|
|
120
|
+
(VALUE_LESS_OPERATORS.includes(c.operator as ConditionOperator) ||
|
|
121
|
+
c.value.trim() !== ''),
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
if (validGroups.length === 0) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
let result = evaluateGroup(validGroups[0], data);
|
|
128
|
+
for (let i = 1; i < validGroups.length; i++) {
|
|
129
|
+
const op = validGroups[i].logicalOperator === 'or' ? 'or' : 'and';
|
|
130
|
+
const next = evaluateGroup(validGroups[i], data);
|
|
131
|
+
result = op === 'or' ? result || next : result && next;
|
|
132
|
+
}
|
|
133
|
+
return state.behavior === 'hide' ? !result : result;
|
|
134
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DataPointOption,
|
|
3
|
+
FieldTypeEnum,
|
|
4
|
+
type PdfField,
|
|
5
|
+
type SchemaNode,
|
|
6
|
+
type SchemaObject,
|
|
7
|
+
} from '../../interface/types';
|
|
8
|
+
|
|
9
|
+
function isUseInConditionals(
|
|
10
|
+
node:
|
|
11
|
+
| { options?: { useInConditionals?: boolean; useInCalculatedFields?: boolean } }
|
|
12
|
+
| undefined,
|
|
13
|
+
): boolean {
|
|
14
|
+
return (
|
|
15
|
+
node?.options?.useInConditionals === true || node?.options?.useInCalculatedFields === true
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isValueNode(
|
|
20
|
+
node: SchemaNode,
|
|
21
|
+
): node is { type: 'string' | 'number'; title?: string; options?: object } {
|
|
22
|
+
return node?.type === 'string' || node?.type === 'number';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function walkSchema(
|
|
26
|
+
schema: SchemaObject | undefined,
|
|
27
|
+
prefix: string,
|
|
28
|
+
parentTitles: string[],
|
|
29
|
+
): DataPointOption[] {
|
|
30
|
+
if (!schema?.properties) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const result: DataPointOption[] = [];
|
|
34
|
+
for (const [key, node] of Object.entries(schema.properties)) {
|
|
35
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
36
|
+
const title = (node as { title?: string }).title ?? key;
|
|
37
|
+
const fullTitle = [...parentTitles, title].join(' - ');
|
|
38
|
+
|
|
39
|
+
if (node?.type === 'object') {
|
|
40
|
+
result.push(...walkSchema(node as SchemaObject, fullKey, [...parentTitles, title]));
|
|
41
|
+
} else if (isValueNode(node) && isUseInConditionals(node)) {
|
|
42
|
+
result.push({
|
|
43
|
+
fieldType: node.type === 'number' ? 'number' : 'string',
|
|
44
|
+
fullKey,
|
|
45
|
+
title: fullTitle || fullKey,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getSchemaDataPointOptions(schema: SchemaObject | undefined): DataPointOption[] {
|
|
53
|
+
const options = walkSchema(schema, '', []);
|
|
54
|
+
return options.sort((a, b) => a.title.localeCompare(b.title));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Data point options from document fields: fillable only (no calculated fields).
|
|
59
|
+
* Uses field.path as fullKey so condition evaluation can read from the same data object.
|
|
60
|
+
*/
|
|
61
|
+
export function getDocumentFieldsDataPointOptions(
|
|
62
|
+
fields: PdfField[] | undefined,
|
|
63
|
+
): DataPointOption[] {
|
|
64
|
+
if (!fields?.length) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const result: DataPointOption[] = [];
|
|
68
|
+
for (const f of fields) {
|
|
69
|
+
if (!f.path || f.type !== FieldTypeEnum.fillable) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const fieldType = f.subType === 'number' ? 'number' : 'string';
|
|
73
|
+
result.push({
|
|
74
|
+
fieldType,
|
|
75
|
+
fullKey: f.path,
|
|
76
|
+
title: f.label || f.path,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return result.sort((a, b) => a.title.localeCompare(b.title));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Combined data point options: schema (data model) + fillable fields.
|
|
84
|
+
*/
|
|
85
|
+
export function getDataPointOptions(
|
|
86
|
+
schema: SchemaObject | undefined,
|
|
87
|
+
documentFields: PdfField[] | undefined,
|
|
88
|
+
): DataPointOption[] {
|
|
89
|
+
const schemaOptions = getSchemaDataPointOptions(schema);
|
|
90
|
+
const documentOptions = getDocumentFieldsDataPointOptions(documentFields);
|
|
91
|
+
const combined = [...schemaOptions, ...documentOptions];
|
|
92
|
+
return combined.sort((a, b) => a.title.localeCompare(b.title));
|
|
93
|
+
}
|
|
@@ -23,6 +23,7 @@ function useInCalculatedFields(node: SchemaNode): boolean {
|
|
|
23
23
|
export interface ExtractGroupedFieldsOptions {
|
|
24
24
|
/** When true, only include fields with SchemaFieldBaseOptions.useInCalculatedFields === true */
|
|
25
25
|
onlyUseInCalculatedFields?: boolean;
|
|
26
|
+
onlyUseInConditionals?: boolean;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
/**
|
|
@@ -1,25 +1,6 @@
|
|
|
1
|
-
import { FormulaToken } from '../../interface/types';
|
|
2
1
|
import { escapeHtml } from './dom.utils';
|
|
3
2
|
import { tokenizeExpression } from './expression.utils';
|
|
4
3
|
|
|
5
|
-
/**
|
|
6
|
-
* Renders formula tokens to HTML for the read-only preview (e.g. formula box).
|
|
7
|
-
* Uses the same token classes as the modal (dte-formula-token, dte-formula-token-field, etc.).
|
|
8
|
-
*/
|
|
9
|
-
export function renderFormulaPreviewHtml(tokens: FormulaToken[]): string {
|
|
10
|
-
if (!tokens?.length) {
|
|
11
|
-
return '';
|
|
12
|
-
}
|
|
13
|
-
return tokens
|
|
14
|
-
.map(token => {
|
|
15
|
-
const type = token.type;
|
|
16
|
-
const text = type === 'field' ? token.label : (token as { value: string }).value;
|
|
17
|
-
const escaped = escapeHtml(text).replace(/ /g, ' ');
|
|
18
|
-
return `<span class="dte-formula-token dte-formula-token-${type}">${escaped}</span>`;
|
|
19
|
-
})
|
|
20
|
-
.join('');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
4
|
export function renderFormulaHtml(expression: string, labelMap: Map<string, string>): string {
|
|
24
5
|
if (!expression.trim()) {
|
|
25
6
|
return '';
|