@nyaruka/temba-components 0.129.8 → 0.129.9
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/CHANGELOG.md +27 -3
- package/demo/data/flows/sample-flow.json +186 -96
- package/dist/temba-components.js +414 -351
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/events.js.map +1 -1
- package/out-tsc/src/excellent/helpers.js +2 -2
- package/out-tsc/src/excellent/helpers.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +25 -7
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +11 -1
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +133 -290
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/actions/add_input_labels.js +40 -0
- package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
- package/out-tsc/src/flow/actions/call_llm.js +56 -3
- package/out-tsc/src/flow/actions/call_llm.js.map +1 -1
- package/out-tsc/src/flow/actions/call_webhook.js +1 -1
- package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
- package/out-tsc/src/flow/actions/open_ticket.js +65 -3
- package/out-tsc/src/flow/actions/open_ticket.js.map +1 -1
- package/out-tsc/src/flow/actions/set_run_result.js +75 -0
- package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
- package/out-tsc/src/flow/config.js +4 -0
- package/out-tsc/src/flow/config.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +227 -0
- package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -0
- package/out-tsc/src/flow/nodes/split_by_ticket.js +18 -0
- package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_response.js +27 -1
- package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
- package/out-tsc/src/flow/types.js +0 -65
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/form/ArrayEditor.js +18 -61
- package/out-tsc/src/form/ArrayEditor.js.map +1 -1
- package/out-tsc/src/form/FieldRenderer.js +305 -0
- package/out-tsc/src/form/FieldRenderer.js.map +1 -0
- package/out-tsc/src/form/FormField.js +3 -3
- package/out-tsc/src/form/FormField.js.map +1 -1
- package/out-tsc/src/form/TextInput.js +1 -1
- package/out-tsc/src/form/TextInput.js.map +1 -1
- package/out-tsc/src/form/select/Select.js +48 -20
- package/out-tsc/src/form/select/Select.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +39 -13
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/src/markdown.js +13 -11
- package/out-tsc/src/markdown.js.map +1 -1
- package/out-tsc/test/ActionHelper.js +2 -0
- package/out-tsc/test/ActionHelper.js.map +1 -1
- package/out-tsc/test/NodeHelper.js +148 -0
- package/out-tsc/test/NodeHelper.js.map +1 -0
- package/out-tsc/test/actions/call_llm.test.js +103 -0
- package/out-tsc/test/actions/call_llm.test.js.map +1 -0
- package/out-tsc/test/nodes/split_by_llm_categorize.test.js +532 -0
- package/out-tsc/test/nodes/split_by_llm_categorize.test.js.map +1 -0
- package/out-tsc/test/nodes/split_by_random.test.js +150 -0
- package/out-tsc/test/nodes/split_by_random.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_digits.test.js +150 -0
- package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_response.test.js +171 -0
- package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -0
- package/out-tsc/test/temba-add-input-labels.test.js +70 -0
- package/out-tsc/test/temba-add-input-labels.test.js.map +1 -0
- package/out-tsc/test/temba-field-renderer.test.js +296 -0
- package/out-tsc/test/temba-field-renderer.test.js.map +1 -0
- package/out-tsc/test/temba-markdown.test.js +1 -1
- package/out-tsc/test/temba-markdown.test.js.map +1 -1
- package/out-tsc/test/temba-node-editor.test.js +400 -0
- package/out-tsc/test/temba-node-editor.test.js.map +1 -1
- package/out-tsc/test/temba-select.test.js +6 -3
- package/out-tsc/test/temba-select.test.js.map +1 -1
- package/out-tsc/test/temba-webchat.test.js +1 -1
- package/out-tsc/test/temba-webchat.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
- package/screenshots/truth/actions/call_llm/editor/information-extraction.png +0 -0
- package/screenshots/truth/actions/call_llm/editor/sentiment-analysis.png +0 -0
- package/screenshots/truth/actions/call_llm/editor/summarization.png +0 -0
- package/screenshots/truth/actions/call_llm/editor/translation-task.png +0 -0
- package/screenshots/truth/actions/call_llm/render/information-extraction.png +0 -0
- package/screenshots/truth/actions/call_llm/render/sentiment-analysis.png +0 -0
- package/screenshots/truth/actions/call_llm/render/summarization.png +0 -0
- package/screenshots/truth/actions/call_llm/render/translation-task.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
- package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
- package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
- package/screenshots/truth/editor/router.png +0 -0
- package/screenshots/truth/editor/send_msg.png +0 -0
- package/screenshots/truth/editor/set_contact_language.png +0 -0
- package/screenshots/truth/editor/set_contact_name.png +0 -0
- package/screenshots/truth/editor/set_run_result.png +0 -0
- package/screenshots/truth/editor/wait.png +0 -0
- package/screenshots/truth/field-renderer/checkbox-checked.png +0 -0
- package/screenshots/truth/field-renderer/checkbox-unchecked.png +0 -0
- package/screenshots/truth/field-renderer/checkbox-with-errors.png +0 -0
- package/screenshots/truth/field-renderer/context-comparison.png +0 -0
- package/screenshots/truth/field-renderer/key-value-with-label.png +0 -0
- package/screenshots/truth/field-renderer/message-editor-with-label.png +0 -0
- package/screenshots/truth/field-renderer/select-multi.png +0 -0
- package/screenshots/truth/field-renderer/select-no-label.png +0 -0
- package/screenshots/truth/field-renderer/select-with-label.png +0 -0
- package/screenshots/truth/field-renderer/text-evaluated.png +0 -0
- package/screenshots/truth/field-renderer/text-no-label.png +0 -0
- package/screenshots/truth/field-renderer/text-with-errors.png +0 -0
- package/screenshots/truth/field-renderer/text-with-label.png +0 -0
- package/screenshots/truth/field-renderer/textarea-evaluated.png +0 -0
- package/screenshots/truth/field-renderer/textarea-with-label.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
- package/screenshots/truth/omnibox/selected.png +0 -0
- package/screenshots/truth/select/functions.png +0 -0
- package/screenshots/truth/select/multi-with-endpoint.png +0 -0
- package/screenshots/truth/select/search-enabled.png +0 -0
- package/src/events.ts +8 -1
- package/src/excellent/helpers.ts +2 -2
- package/src/flow/CanvasNode.ts +22 -1
- package/src/flow/Editor.ts +12 -1
- package/src/flow/NodeEditor.ts +186 -374
- package/src/flow/actions/add_input_labels.ts +45 -0
- package/src/flow/actions/call_llm.ts +57 -3
- package/src/flow/actions/call_webhook.ts +1 -1
- package/src/flow/actions/open_ticket.ts +74 -3
- package/src/flow/actions/set_run_result.ts +83 -0
- package/src/flow/config.ts +4 -0
- package/src/flow/nodes/split_by_llm_categorize.ts +277 -0
- package/src/flow/nodes/split_by_ticket.ts +19 -0
- package/src/flow/nodes/wait_for_response.ts +28 -1
- package/src/flow/types.ts +26 -127
- package/src/form/ArrayEditor.ts +34 -82
- package/src/form/FieldRenderer.ts +465 -0
- package/src/form/FormField.ts +3 -3
- package/src/form/TextInput.ts +1 -1
- package/src/form/select/Select.ts +51 -20
- package/src/live/ContactChat.ts +39 -15
- package/src/markdown.ts +19 -11
- package/src/store/flow-definition.d.ts +5 -2
- package/static/api/labels.json +31 -0
- package/static/api/topics.json +24 -9
- package/static/api/users.json +35 -16
- package/static/css/temba-components.css +3 -3
- package/stress-test.js +18 -13
- package/test/ActionHelper.ts +2 -0
- package/test/NodeHelper.ts +184 -0
- package/test/actions/call_llm.test.ts +137 -0
- package/test/nodes/README.md +78 -0
- package/test/nodes/split_by_llm_categorize.test.ts +698 -0
- package/test/nodes/split_by_random.test.ts +177 -0
- package/test/nodes/wait_for_digits.test.ts +176 -0
- package/test/nodes/wait_for_response.test.ts +206 -0
- package/test/temba-add-input-labels.test.ts +87 -0
- package/test/temba-field-renderer.test.ts +482 -0
- package/test/temba-markdown.test.ts +1 -1
- package/test/temba-node-editor.test.ts +496 -0
- package/test/temba-select.test.ts +6 -6
- package/test/temba-webchat.test.ts +1 -1
- package/test-assets/select/llms.json +18 -0
- package/web-dev-mock.mjs +96 -6
- package/web-dev-server.config.mjs +29 -7
- package/test/temba-flow-editor.test.ts.backup +0 -563
- package/test/temba-utils-index.test.ts.backup +0 -1737
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { html, TemplateResult } from 'lit';
|
|
2
|
+
import {
|
|
3
|
+
FieldConfig,
|
|
4
|
+
TextFieldConfig,
|
|
5
|
+
TextareaFieldConfig,
|
|
6
|
+
SelectFieldConfig,
|
|
7
|
+
CheckboxFieldConfig,
|
|
8
|
+
MessageEditorFieldConfig,
|
|
9
|
+
KeyValueFieldConfig,
|
|
10
|
+
ArrayFieldConfig
|
|
11
|
+
} from '../flow/types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* FieldRenderer provides a consistent way to render field configurations
|
|
15
|
+
* into web components across different contexts (NodeEditor, ArrayEditor, etc.)
|
|
16
|
+
*/
|
|
17
|
+
export class FieldRenderer {
|
|
18
|
+
/**
|
|
19
|
+
* Renders a field based on its configuration
|
|
20
|
+
* @param fieldName - The name of the field
|
|
21
|
+
* @param config - The field configuration
|
|
22
|
+
* @param value - The current value of the field
|
|
23
|
+
* @param context - Additional context for rendering
|
|
24
|
+
* @returns A TemplateResult for the rendered field
|
|
25
|
+
*/
|
|
26
|
+
static renderField(
|
|
27
|
+
fieldName: string,
|
|
28
|
+
config: FieldConfig,
|
|
29
|
+
value: any,
|
|
30
|
+
context: FieldRenderContext = {}
|
|
31
|
+
): TemplateResult {
|
|
32
|
+
/*const {
|
|
33
|
+
errors = [],
|
|
34
|
+
onChange,
|
|
35
|
+
showLabel = true,
|
|
36
|
+
flavor,
|
|
37
|
+
extraClasses = '',
|
|
38
|
+
style = ''
|
|
39
|
+
} = context;*/
|
|
40
|
+
switch (config.type) {
|
|
41
|
+
case 'text':
|
|
42
|
+
return FieldRenderer.renderTextInput(fieldName, config, value, context);
|
|
43
|
+
|
|
44
|
+
case 'textarea':
|
|
45
|
+
return FieldRenderer.renderTextarea(
|
|
46
|
+
fieldName,
|
|
47
|
+
config as TextareaFieldConfig,
|
|
48
|
+
value,
|
|
49
|
+
context
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
case 'select':
|
|
53
|
+
return FieldRenderer.renderSelect(
|
|
54
|
+
fieldName,
|
|
55
|
+
config as SelectFieldConfig,
|
|
56
|
+
value,
|
|
57
|
+
context
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
case 'checkbox':
|
|
61
|
+
return FieldRenderer.renderCheckbox(
|
|
62
|
+
fieldName,
|
|
63
|
+
config as CheckboxFieldConfig,
|
|
64
|
+
value,
|
|
65
|
+
context
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
case 'key-value':
|
|
69
|
+
return FieldRenderer.renderKeyValue(
|
|
70
|
+
fieldName,
|
|
71
|
+
config as KeyValueFieldConfig,
|
|
72
|
+
value,
|
|
73
|
+
context
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
case 'array':
|
|
77
|
+
return FieldRenderer.renderArray(
|
|
78
|
+
fieldName,
|
|
79
|
+
config as ArrayFieldConfig,
|
|
80
|
+
value,
|
|
81
|
+
context
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
case 'message-editor':
|
|
85
|
+
return FieldRenderer.renderMessageEditor(
|
|
86
|
+
fieldName,
|
|
87
|
+
config as MessageEditorFieldConfig,
|
|
88
|
+
value,
|
|
89
|
+
context
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
default:
|
|
93
|
+
return html`<div>Unsupported field type: ${(config as any).type}</div>`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private static renderTextInput(
|
|
98
|
+
fieldName: string,
|
|
99
|
+
config: TextFieldConfig,
|
|
100
|
+
value: any,
|
|
101
|
+
context: FieldRenderContext
|
|
102
|
+
): TemplateResult {
|
|
103
|
+
const {
|
|
104
|
+
errors = [],
|
|
105
|
+
onChange,
|
|
106
|
+
showLabel = true,
|
|
107
|
+
extraClasses,
|
|
108
|
+
style
|
|
109
|
+
} = context;
|
|
110
|
+
|
|
111
|
+
// If field supports expression evaluation, use temba-completion
|
|
112
|
+
if (config.evaluated) {
|
|
113
|
+
return html`<temba-completion
|
|
114
|
+
name="${fieldName}"
|
|
115
|
+
label="${showLabel ? config.label : ''}"
|
|
116
|
+
?required="${config.required}"
|
|
117
|
+
.errors="${errors}"
|
|
118
|
+
.value="${value || ''}"
|
|
119
|
+
placeholder="${config.placeholder || ''}"
|
|
120
|
+
expressions="session"
|
|
121
|
+
.helpText="${config.helpText || ''}"
|
|
122
|
+
class="${extraClasses}"
|
|
123
|
+
style="${style}"
|
|
124
|
+
@input="${onChange || (() => {})}"
|
|
125
|
+
></temba-completion>`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return html`<temba-textinput
|
|
129
|
+
name="${fieldName}"
|
|
130
|
+
label="${showLabel ? config.label : ''}"
|
|
131
|
+
?required="${config.required}"
|
|
132
|
+
.errors="${errors}"
|
|
133
|
+
.value="${value || ''}"
|
|
134
|
+
placeholder="${config.placeholder || ''}"
|
|
135
|
+
.helpText="${config.helpText || ''}"
|
|
136
|
+
class="${extraClasses}"
|
|
137
|
+
style="${style}"
|
|
138
|
+
@input="${onChange || (() => {})}"
|
|
139
|
+
></temba-textinput>`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private static renderTextarea(
|
|
143
|
+
fieldName: string,
|
|
144
|
+
config: TextareaFieldConfig,
|
|
145
|
+
value: any,
|
|
146
|
+
context: FieldRenderContext
|
|
147
|
+
): TemplateResult {
|
|
148
|
+
const {
|
|
149
|
+
errors = [],
|
|
150
|
+
onChange,
|
|
151
|
+
showLabel = true,
|
|
152
|
+
extraClasses,
|
|
153
|
+
style
|
|
154
|
+
} = context;
|
|
155
|
+
|
|
156
|
+
const minHeightStyle = config.minHeight
|
|
157
|
+
? `--textarea-min-height: ${config.minHeight}px;`
|
|
158
|
+
: '';
|
|
159
|
+
const combinedStyle = `${minHeightStyle}${style}`;
|
|
160
|
+
|
|
161
|
+
// If field supports expression evaluation, use temba-completion
|
|
162
|
+
if (config.evaluated) {
|
|
163
|
+
return html`<temba-completion
|
|
164
|
+
name="${fieldName}"
|
|
165
|
+
label="${showLabel ? config.label : ''}"
|
|
166
|
+
?required="${config.required}"
|
|
167
|
+
.errors="${errors}"
|
|
168
|
+
.value="${value || ''}"
|
|
169
|
+
placeholder="${config.placeholder || ''}"
|
|
170
|
+
textarea
|
|
171
|
+
expressions="session"
|
|
172
|
+
.helpText="${config.helpText || ''}"
|
|
173
|
+
class="${extraClasses}"
|
|
174
|
+
style="${combinedStyle}"
|
|
175
|
+
@input="${onChange || (() => {})}"
|
|
176
|
+
></temba-completion>`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return html`<temba-textinput
|
|
180
|
+
name="${fieldName}"
|
|
181
|
+
label="${showLabel ? config.label : ''}"
|
|
182
|
+
?required="${config.required}"
|
|
183
|
+
.errors="${errors}"
|
|
184
|
+
.value="${value || ''}"
|
|
185
|
+
placeholder="${config.placeholder || ''}"
|
|
186
|
+
textarea
|
|
187
|
+
.rows="${config.rows || 3}"
|
|
188
|
+
.helpText="${config.helpText || ''}"
|
|
189
|
+
class="${extraClasses}"
|
|
190
|
+
style="${combinedStyle}"
|
|
191
|
+
@input="${onChange || (() => {})}"
|
|
192
|
+
></temba-textinput>`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private static renderSelect(
|
|
196
|
+
fieldName: string,
|
|
197
|
+
config: SelectFieldConfig,
|
|
198
|
+
value: any,
|
|
199
|
+
context: FieldRenderContext
|
|
200
|
+
): TemplateResult {
|
|
201
|
+
const {
|
|
202
|
+
errors = [],
|
|
203
|
+
onChange,
|
|
204
|
+
showLabel = true,
|
|
205
|
+
flavor,
|
|
206
|
+
extraClasses,
|
|
207
|
+
style
|
|
208
|
+
} = context;
|
|
209
|
+
|
|
210
|
+
// Ensure proper value handling for multi vs single select
|
|
211
|
+
const normalizedValue = (() => {
|
|
212
|
+
if (config.multi) {
|
|
213
|
+
// Multi-select: ensure we have an array and convert strings to option objects
|
|
214
|
+
const valueArray = Array.isArray(value) ? value : value ? [value] : [];
|
|
215
|
+
return valueArray.map((val) => {
|
|
216
|
+
if (typeof val === 'string') {
|
|
217
|
+
// Convert string values to option objects
|
|
218
|
+
return { name: val, value: val };
|
|
219
|
+
}
|
|
220
|
+
return val;
|
|
221
|
+
});
|
|
222
|
+
} else {
|
|
223
|
+
// Single select: use the value as-is
|
|
224
|
+
return value || '';
|
|
225
|
+
}
|
|
226
|
+
})();
|
|
227
|
+
|
|
228
|
+
if (typeof normalizedValue === 'string') {
|
|
229
|
+
return html`<temba-select
|
|
230
|
+
name="${fieldName}"
|
|
231
|
+
?required="${config.required}"
|
|
232
|
+
.errors="${errors}"
|
|
233
|
+
value="${config.multi ? '' : normalizedValue}"
|
|
234
|
+
.values="${config.multi ? normalizedValue : undefined}"
|
|
235
|
+
?multi="${config.multi}"
|
|
236
|
+
?searchable="${config.searchable}"
|
|
237
|
+
?tags="${config.tags}"
|
|
238
|
+
?emails="${config.emails}"
|
|
239
|
+
?clearable="${config.clearable || false}"
|
|
240
|
+
label="${showLabel ? config.label : ''}"
|
|
241
|
+
placeholder="${config.placeholder || ''}"
|
|
242
|
+
maxItems="${config.maxItems || 0}"
|
|
243
|
+
valueKey="${config.valueKey || 'value'}"
|
|
244
|
+
nameKey="${config.nameKey || 'name'}"
|
|
245
|
+
endpoint="${config.endpoint || ''}"
|
|
246
|
+
.helpText="${config.helpText || ''}"
|
|
247
|
+
flavor="${flavor || config.flavor || 'small'}"
|
|
248
|
+
class="${extraClasses}"
|
|
249
|
+
style="${style}"
|
|
250
|
+
.getName=${config.getName}
|
|
251
|
+
.createArbitraryOption=${config.createArbitraryOption}
|
|
252
|
+
?allowCreate="${config.allowCreate || false}"
|
|
253
|
+
@change="${onChange || (() => {})}"
|
|
254
|
+
>
|
|
255
|
+
${config.options?.map((option: any) => {
|
|
256
|
+
if (typeof option === 'string') {
|
|
257
|
+
return html`<temba-option
|
|
258
|
+
name="${option}"
|
|
259
|
+
value="${option}"
|
|
260
|
+
></temba-option>`;
|
|
261
|
+
} else {
|
|
262
|
+
return html`<temba-option
|
|
263
|
+
name="${option.label || option.name}"
|
|
264
|
+
value="${option.value}"
|
|
265
|
+
></temba-option>`;
|
|
266
|
+
}
|
|
267
|
+
})}
|
|
268
|
+
</temba-select>`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return html`<temba-select
|
|
272
|
+
name="${fieldName}"
|
|
273
|
+
label="${showLabel ? config.label : ''}"
|
|
274
|
+
?required="${config.required}"
|
|
275
|
+
.errors="${errors}"
|
|
276
|
+
.values="${normalizedValue}"
|
|
277
|
+
?multi="${config.multi}"
|
|
278
|
+
?searchable="${config.searchable}"
|
|
279
|
+
?tags="${config.tags}"
|
|
280
|
+
?emails="${config.emails}"
|
|
281
|
+
?clearable="${config.clearable || false}"
|
|
282
|
+
placeholder="${config.placeholder || ''}"
|
|
283
|
+
maxItems="${config.maxItems || 0}"
|
|
284
|
+
valueKey="${config.valueKey || 'value'}"
|
|
285
|
+
nameKey="${config.nameKey || 'name'}"
|
|
286
|
+
endpoint="${config.endpoint || ''}"
|
|
287
|
+
.helpText="${config.helpText || ''}"
|
|
288
|
+
flavor="${flavor || config.flavor || 'small'}"
|
|
289
|
+
class="${extraClasses}"
|
|
290
|
+
style="${style}"
|
|
291
|
+
.getName=${config.getName}
|
|
292
|
+
.createArbitraryOption=${config.createArbitraryOption}
|
|
293
|
+
?allowCreate="${config.allowCreate || false}"
|
|
294
|
+
@change="${onChange || (() => {})}"
|
|
295
|
+
>
|
|
296
|
+
${config.options?.map((option: any) => {
|
|
297
|
+
if (typeof option === 'string') {
|
|
298
|
+
return html`<temba-option
|
|
299
|
+
name="${option}"
|
|
300
|
+
value="${option}"
|
|
301
|
+
></temba-option>`;
|
|
302
|
+
} else {
|
|
303
|
+
return html`<temba-option
|
|
304
|
+
name="${option.label || option.name}"
|
|
305
|
+
value="${option.value}"
|
|
306
|
+
></temba-option>`;
|
|
307
|
+
}
|
|
308
|
+
})}
|
|
309
|
+
</temba-select>`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private static renderCheckbox(
|
|
313
|
+
fieldName: string,
|
|
314
|
+
config: CheckboxFieldConfig,
|
|
315
|
+
value: any,
|
|
316
|
+
context: FieldRenderContext
|
|
317
|
+
): TemplateResult {
|
|
318
|
+
const { errors = [], onChange, extraClasses, style } = context;
|
|
319
|
+
|
|
320
|
+
return html`<div class="form-field">
|
|
321
|
+
<temba-checkbox
|
|
322
|
+
name="${fieldName}"
|
|
323
|
+
label="${config.label}"
|
|
324
|
+
.helpText="${config.helpText || ''}"
|
|
325
|
+
?required="${config.required}"
|
|
326
|
+
.errors="${errors}"
|
|
327
|
+
?checked="${value || false}"
|
|
328
|
+
size="${config.size || 1.2}"
|
|
329
|
+
animateChange="${config.animateChange || 'pulse'}"
|
|
330
|
+
class="${extraClasses}"
|
|
331
|
+
style="${style}"
|
|
332
|
+
@change="${onChange || (() => {})}"
|
|
333
|
+
></temba-checkbox>
|
|
334
|
+
${errors.length
|
|
335
|
+
? html`<div class="field-errors">${errors.join(', ')}</div>`
|
|
336
|
+
: ''}
|
|
337
|
+
</div>`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private static renderKeyValue(
|
|
341
|
+
fieldName: string,
|
|
342
|
+
config: KeyValueFieldConfig,
|
|
343
|
+
value: any,
|
|
344
|
+
context: FieldRenderContext
|
|
345
|
+
): TemplateResult {
|
|
346
|
+
const {
|
|
347
|
+
errors = [],
|
|
348
|
+
onChange,
|
|
349
|
+
showLabel = true,
|
|
350
|
+
extraClasses,
|
|
351
|
+
style
|
|
352
|
+
} = context;
|
|
353
|
+
|
|
354
|
+
return html`<div class="form-field">
|
|
355
|
+
${showLabel ? html`<label>${config.label}</label>` : ''}
|
|
356
|
+
<temba-key-value-editor
|
|
357
|
+
name="${fieldName}"
|
|
358
|
+
.value="${value || []}"
|
|
359
|
+
.sortable="${config.sortable}"
|
|
360
|
+
.keyPlaceholder="${config.keyPlaceholder || 'Key'}"
|
|
361
|
+
.valuePlaceholder="${config.valuePlaceholder || 'Value'}"
|
|
362
|
+
.minRows="${config.minRows || 0}"
|
|
363
|
+
class="${extraClasses}"
|
|
364
|
+
style="${style}"
|
|
365
|
+
@change="${onChange || (() => {})}"
|
|
366
|
+
></temba-key-value-editor>
|
|
367
|
+
${errors.length
|
|
368
|
+
? html`<div class="field-errors">${errors.join(', ')}</div>`
|
|
369
|
+
: ''}
|
|
370
|
+
</div>`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private static renderArray(
|
|
374
|
+
fieldName: string,
|
|
375
|
+
config: ArrayFieldConfig,
|
|
376
|
+
value: any,
|
|
377
|
+
context: FieldRenderContext
|
|
378
|
+
): TemplateResult {
|
|
379
|
+
const {
|
|
380
|
+
errors = [],
|
|
381
|
+
onChange,
|
|
382
|
+
showLabel = true,
|
|
383
|
+
extraClasses,
|
|
384
|
+
style
|
|
385
|
+
} = context;
|
|
386
|
+
|
|
387
|
+
return html`<div class="form-field">
|
|
388
|
+
${showLabel ? html`<label>${config.label}</label>` : ''}
|
|
389
|
+
<temba-array-editor
|
|
390
|
+
.value="${value || []}"
|
|
391
|
+
.itemConfig="${config.itemConfig}"
|
|
392
|
+
.sortable="${config.sortable}"
|
|
393
|
+
.itemLabel="${config.itemLabel || 'Item'}"
|
|
394
|
+
.minItems="${config.minItems || 0}"
|
|
395
|
+
.maxItems="${config.maxItems || 0}"
|
|
396
|
+
.onItemChange="${config.onItemChange}"
|
|
397
|
+
.isEmptyItemFn="${config.isEmptyItem}"
|
|
398
|
+
class="${extraClasses}"
|
|
399
|
+
style="${style}"
|
|
400
|
+
@change="${onChange || (() => {})}"
|
|
401
|
+
></temba-array-editor>
|
|
402
|
+
${errors.length
|
|
403
|
+
? html`<div class="field-errors">${errors.join(', ')}</div>`
|
|
404
|
+
: ''}
|
|
405
|
+
</div>`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private static renderMessageEditor(
|
|
409
|
+
fieldName: string,
|
|
410
|
+
config: MessageEditorFieldConfig,
|
|
411
|
+
value: any,
|
|
412
|
+
context: FieldRenderContext
|
|
413
|
+
): TemplateResult {
|
|
414
|
+
const {
|
|
415
|
+
errors = [],
|
|
416
|
+
onChange,
|
|
417
|
+
showLabel = true,
|
|
418
|
+
extraClasses,
|
|
419
|
+
style,
|
|
420
|
+
additionalData = {}
|
|
421
|
+
} = context;
|
|
422
|
+
|
|
423
|
+
return html`<temba-message-editor
|
|
424
|
+
name="${fieldName}"
|
|
425
|
+
label="${showLabel ? config.label : ''}"
|
|
426
|
+
?required="${config.required}"
|
|
427
|
+
.errors="${errors}"
|
|
428
|
+
.value="${value || ''}"
|
|
429
|
+
.attachments="${additionalData.attachments || []}"
|
|
430
|
+
placeholder="${config.placeholder || ''}"
|
|
431
|
+
.helpText="${config.helpText || ''}"
|
|
432
|
+
?autogrow="${config.autogrow}"
|
|
433
|
+
?gsm="${config.gsm}"
|
|
434
|
+
?disableCompletion="${config.disableCompletion}"
|
|
435
|
+
counter="${config.counter || ''}"
|
|
436
|
+
accept="${config.accept || ''}"
|
|
437
|
+
endpoint="${config.endpoint || ''}"
|
|
438
|
+
max-attachments="${config.maxAttachments || 3}"
|
|
439
|
+
minHeight="${config.minHeight || 60}"
|
|
440
|
+
class="${extraClasses}"
|
|
441
|
+
style="${style}"
|
|
442
|
+
@change="${onChange || (() => {})}"
|
|
443
|
+
></temba-message-editor>`;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Context object for field rendering that provides additional options
|
|
449
|
+
*/
|
|
450
|
+
export interface FieldRenderContext {
|
|
451
|
+
/** Array of error messages for the field */
|
|
452
|
+
errors?: string[];
|
|
453
|
+
/** Change event handler */
|
|
454
|
+
onChange?: (event: Event) => void;
|
|
455
|
+
/** Whether to show the field label */
|
|
456
|
+
showLabel?: boolean;
|
|
457
|
+
/** Flavor for components that support it (like temba-select) */
|
|
458
|
+
flavor?: string;
|
|
459
|
+
/** Additional CSS classes to apply */
|
|
460
|
+
extraClasses?: string;
|
|
461
|
+
/** Additional CSS styles to apply */
|
|
462
|
+
style?: string;
|
|
463
|
+
/** Additional data needed for specific field types */
|
|
464
|
+
additionalData?: Record<string, any>;
|
|
465
|
+
}
|
package/src/form/FormField.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TemplateResult, html, css, LitElement } from 'lit';
|
|
2
2
|
import { property } from 'lit/decorators.js';
|
|
3
|
-
import {
|
|
3
|
+
import { renderMarkdownInline } from '../markdown';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* A small wrapper to display labels and help text in a smartmin style.
|
|
@@ -196,7 +196,7 @@ export class FormField extends LitElement {
|
|
|
196
196
|
const errors = hasErrors
|
|
197
197
|
? this.errors.map((error: string) => {
|
|
198
198
|
return html`
|
|
199
|
-
<div class="alert-error">${
|
|
199
|
+
<div class="alert-error">${renderMarkdownInline(error)}</div>
|
|
200
200
|
`;
|
|
201
201
|
})
|
|
202
202
|
: [];
|
|
@@ -228,7 +228,7 @@ export class FormField extends LitElement {
|
|
|
228
228
|
${this.helpText && this.helpText !== 'None'
|
|
229
229
|
? html`
|
|
230
230
|
<div class="help-text ${this.helpAlways ? 'help-always' : null}">
|
|
231
|
-
${this.helpText}
|
|
231
|
+
${renderMarkdownInline(this.helpText)}
|
|
232
232
|
</div>
|
|
233
233
|
`
|
|
234
234
|
: null}
|
package/src/form/TextInput.ts
CHANGED
|
@@ -193,7 +193,7 @@ export class TextInput extends FormElement {
|
|
|
193
193
|
|
|
194
194
|
this.inputElement = this.shadowRoot.querySelector('.textinput');
|
|
195
195
|
|
|
196
|
-
if (changes.has('counter')) {
|
|
196
|
+
if (changes.has('counter') && this.counter && this.counter.trim()) {
|
|
197
197
|
let root = this.getParentModax() as any;
|
|
198
198
|
if (root) {
|
|
199
199
|
root = root.shadowRoot;
|
|
@@ -222,6 +222,7 @@ export class Select<T extends SelectOption> extends FormElement {
|
|
|
222
222
|
|
|
223
223
|
.multi temba-sortable-list {
|
|
224
224
|
margin: 0 !important;
|
|
225
|
+
flex-grow: 1;
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
input {
|
|
@@ -255,6 +256,12 @@ export class Select<T extends SelectOption> extends FormElement {
|
|
|
255
256
|
|
|
256
257
|
.multi .input-wrapper {
|
|
257
258
|
margin-left: 2px !important;
|
|
259
|
+
margin-right: 2px !important;
|
|
260
|
+
margin-top: 2px;
|
|
261
|
+
margin-bottom: 2px;
|
|
262
|
+
flex-shrink: 0;
|
|
263
|
+
min-width: 100px;
|
|
264
|
+
align-self: center;
|
|
258
265
|
}
|
|
259
266
|
|
|
260
267
|
.input-wrapper:focus-within .placeholder {
|
|
@@ -290,11 +297,6 @@ export class Select<T extends SelectOption> extends FormElement {
|
|
|
290
297
|
box-shadow: none !important;
|
|
291
298
|
}
|
|
292
299
|
|
|
293
|
-
.multi .input-wrapper {
|
|
294
|
-
flex-shrink: 0;
|
|
295
|
-
min-width: 100px;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
300
|
.input-wrapper .searchbox {
|
|
299
301
|
}
|
|
300
302
|
|
|
@@ -310,6 +312,17 @@ export class Select<T extends SelectOption> extends FormElement {
|
|
|
310
312
|
margin-left: 6px;
|
|
311
313
|
}
|
|
312
314
|
|
|
315
|
+
.empty .placeholder {
|
|
316
|
+
display: block;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.multi .placeholder {
|
|
320
|
+
display: block;
|
|
321
|
+
margin: 2px 2px;
|
|
322
|
+
padding: 2px 8px;
|
|
323
|
+
align-self: center;
|
|
324
|
+
}
|
|
325
|
+
|
|
313
326
|
.footer {
|
|
314
327
|
padding: 5px 10px;
|
|
315
328
|
background: var(--color-primary-light);
|
|
@@ -322,7 +335,7 @@ export class Select<T extends SelectOption> extends FormElement {
|
|
|
322
335
|
.small {
|
|
323
336
|
--temba-select-selected-padding: 6px;
|
|
324
337
|
--temba-select-selected-line-height: 12px;
|
|
325
|
-
--temba-select-selected-font-size:
|
|
338
|
+
--temba-select-selected-font-size: 14px;
|
|
326
339
|
--temba-select-min-height: 2.28em;
|
|
327
340
|
}
|
|
328
341
|
|
|
@@ -476,15 +489,26 @@ export class Select<T extends SelectOption> extends FormElement {
|
|
|
476
489
|
@property({ type: String, attribute: 'info_text' })
|
|
477
490
|
infoText = '';
|
|
478
491
|
|
|
492
|
+
// Override the setter to ensure values is always an array
|
|
479
493
|
@property({ type: Array })
|
|
480
|
-
values:
|
|
494
|
+
set values(newValues: any) {
|
|
495
|
+
this._values = Array.isArray(newValues) ? newValues : [];
|
|
496
|
+
this.requestUpdate('values');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
get values(): T[] {
|
|
500
|
+
return this._values || [];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private _values: T[] = [];
|
|
481
504
|
|
|
482
505
|
@property({ type: Object })
|
|
483
506
|
selection: any;
|
|
484
507
|
|
|
485
508
|
@property({ attribute: false })
|
|
486
|
-
getName: (option: any) => string = (option: any) =>
|
|
487
|
-
option[this.nameKey || 'name'];
|
|
509
|
+
getName: (option: any) => string = (option: any) => {
|
|
510
|
+
return option[this.nameKey || 'name'];
|
|
511
|
+
};
|
|
488
512
|
|
|
489
513
|
@property({ attribute: false })
|
|
490
514
|
isMatch: (option: any, q: string) => boolean = this.isMatchDefault;
|
|
@@ -545,7 +569,7 @@ export class Select<T extends SelectOption> extends FormElement {
|
|
|
545
569
|
private alphaSort = (a: any, b: any) => {
|
|
546
570
|
// by default, all endpoint values are sorted by name
|
|
547
571
|
if (this.endpoint) {
|
|
548
|
-
return this.
|
|
572
|
+
return this.getNameInternal(a).localeCompare(this.getNameInternal(b));
|
|
549
573
|
}
|
|
550
574
|
return 0;
|
|
551
575
|
};
|
|
@@ -573,7 +597,7 @@ export class Select<T extends SelectOption> extends FormElement {
|
|
|
573
597
|
}
|
|
574
598
|
|
|
575
599
|
public isMatchDefault(option: T, q: string) {
|
|
576
|
-
const name = this.
|
|
600
|
+
const name = this.getNameInternal(option) || '';
|
|
577
601
|
return name.toLowerCase().indexOf(q) > -1;
|
|
578
602
|
}
|
|
579
603
|
|
|
@@ -963,7 +987,9 @@ export class Select<T extends SelectOption> extends FormElement {
|
|
|
963
987
|
}
|
|
964
988
|
|
|
965
989
|
protected getNameInternal: (option: T) => string = (option: T) => {
|
|
966
|
-
return this.getName
|
|
990
|
+
return this.getName
|
|
991
|
+
? this.getName(option)
|
|
992
|
+
: option[this.nameKey || 'name'] || '';
|
|
967
993
|
};
|
|
968
994
|
|
|
969
995
|
private getOptionsDefault(response: WebResponse): any[] {
|
|
@@ -1493,7 +1519,7 @@ export class Select<T extends SelectOption> extends FormElement {
|
|
|
1493
1519
|
name="${icon}"
|
|
1494
1520
|
style="margin-right:0.5em;"
|
|
1495
1521
|
></temba-icon>`
|
|
1496
|
-
: null}<span>${this.
|
|
1522
|
+
: null}<span>${this.getNameInternal(option)}</span>
|
|
1497
1523
|
</div>
|
|
1498
1524
|
`;
|
|
1499
1525
|
}
|
|
@@ -1636,9 +1662,13 @@ export class Select<T extends SelectOption> extends FormElement {
|
|
|
1636
1662
|
|
|
1637
1663
|
public render(): TemplateResult {
|
|
1638
1664
|
const placeholder = this.values.length === 0 ? this.placeholder : '';
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1665
|
+
|
|
1666
|
+
// Single unified placeholder - shows when empty and (not focused OR not searchable)
|
|
1667
|
+
const shouldShowPlaceholder =
|
|
1668
|
+
this.values.length === 0 && (!this.focused || !this.searchable);
|
|
1669
|
+
const placeholderElement = shouldShowPlaceholder
|
|
1670
|
+
? html`<div class="placeholder">${placeholder}</div>`
|
|
1671
|
+
: null;
|
|
1642
1672
|
|
|
1643
1673
|
const clear =
|
|
1644
1674
|
this.clearable && this.values.length > 0 && !this.isMultiMode
|
|
@@ -1683,10 +1713,9 @@ export class Select<T extends SelectOption> extends FormElement {
|
|
|
1683
1713
|
.value=${this.input}
|
|
1684
1714
|
/>
|
|
1685
1715
|
<div id="anchor" style=${styleMap(anchorStyles)}></div>
|
|
1686
|
-
${placeholderDiv}
|
|
1687
1716
|
</div>
|
|
1688
1717
|
`
|
|
1689
|
-
:
|
|
1718
|
+
: null;
|
|
1690
1719
|
|
|
1691
1720
|
const items = html`${!this.isMultiMode && !this.resolving ? input : null}
|
|
1692
1721
|
${this.isMultiMode && this.values.length > 1
|
|
@@ -1760,9 +1789,10 @@ export class Select<T extends SelectOption> extends FormElement {
|
|
|
1760
1789
|
</div>
|
|
1761
1790
|
`
|
|
1762
1791
|
)}
|
|
1792
|
+
${this.searchable && this.focused ? input : null}
|
|
1763
1793
|
</temba-sortable-list>
|
|
1764
1794
|
`
|
|
1765
|
-
: this.values.map(
|
|
1795
|
+
: html`${this.values.map(
|
|
1766
1796
|
(selected: any, index: number) => html`
|
|
1767
1797
|
<div
|
|
1768
1798
|
class="selected-item ${index === this.selectedIndex
|
|
@@ -1811,7 +1841,8 @@ export class Select<T extends SelectOption> extends FormElement {
|
|
|
1811
1841
|
</div>
|
|
1812
1842
|
`
|
|
1813
1843
|
)}
|
|
1814
|
-
|
|
1844
|
+
${this.isMultiMode && this.searchable && this.focused ? input : null}
|
|
1845
|
+
${placeholderElement}`}`;
|
|
1815
1846
|
|
|
1816
1847
|
return html`
|
|
1817
1848
|
|