@nyaruka/temba-components 0.129.7 → 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/.devcontainer/Dockerfile +11 -4
- package/.devcontainer/devcontainer.json +3 -2
- package/.github/workflows/build.yml +4 -14
- package/CHANGELOG.md +29 -0
- package/demo/components/flow/example.html +1 -1
- package/demo/components/message-editor/example.html +125 -0
- package/demo/components/textinput/completion.html +1 -0
- package/demo/data/flows/food-order.json +12 -21
- package/demo/data/flows/sample-flow.json +210 -104
- package/dist/temba-components.js +715 -364
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Thumbnail.js +2 -1
- package/out-tsc/src/display/Thumbnail.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 +342 -276
- 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 +26 -17
- 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/send_msg.js +147 -6
- package/out-tsc/src/flow/actions/send_msg.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 +87 -57
- package/out-tsc/src/form/ArrayEditor.js.map +1 -1
- package/out-tsc/src/form/BaseListEditor.js +19 -4
- package/out-tsc/src/form/BaseListEditor.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 +4 -4
- package/out-tsc/src/form/FormField.js.map +1 -1
- package/out-tsc/src/form/KeyValueEditor.js +1 -1
- package/out-tsc/src/form/KeyValueEditor.js.map +1 -1
- package/out-tsc/src/form/MediaPicker.js +13 -1
- package/out-tsc/src/form/MediaPicker.js.map +1 -1
- package/out-tsc/src/form/MessageEditor.js +422 -0
- package/out-tsc/src/form/MessageEditor.js.map +1 -0
- package/out-tsc/src/form/TextInput.js +13 -6
- package/out-tsc/src/form/TextInput.js.map +1 -1
- package/out-tsc/src/form/select/Select.js +52 -24
- package/out-tsc/src/form/select/Select.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +66 -15
- 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/temba-modules.js +2 -0
- package/out-tsc/temba-modules.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-config.test.js +4 -2
- package/out-tsc/test/temba-field-config.test.js.map +1 -1
- 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-message-editor.test.js +194 -0
- package/out-tsc/test/temba-message-editor.test.js.map +1 -0
- package/out-tsc/test/temba-node-editor.test.js +471 -0
- package/out-tsc/test/temba-node-editor.test.js.map +1 -1
- package/out-tsc/test/temba-select.test.js +7 -4
- package/out-tsc/test/temba-select.test.js.map +1 -1
- package/out-tsc/test/temba-textinput.test.js +16 -0
- package/out-tsc/test/temba-textinput.test.js.map +1 -1
- package/out-tsc/test/temba-webchat.test.js +5 -1
- package/out-tsc/test/temba-webchat.test.js.map +1 -1
- package/out-tsc/test/utils.test.js +2 -8
- package/out-tsc/test/utils.test.js.map +1 -1
- package/package.json +7 -4
- 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/add_contact_groups/editor/multiple-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/single-group.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/cleanup-groups.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/multiple-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
- package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
- package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
- package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
- package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
- package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
- package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
- package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
- package/screenshots/truth/actions/send_email/editor/with-expressions.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/formfield/markdown-errors.png +0 -0
- package/screenshots/truth/formfield/no-errors.png +0 -0
- package/screenshots/truth/formfield/plain-text-errors.png +0 -0
- package/screenshots/truth/message-editor/autogrow-initial-content.png +0 -0
- package/screenshots/truth/message-editor/default.png +0 -0
- package/screenshots/truth/message-editor/drag-highlight.png +0 -0
- package/screenshots/truth/message-editor/filtered-attachments.png +0 -0
- package/screenshots/truth/message-editor/with-completion.png +0 -0
- package/screenshots/truth/message-editor/with-properties.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/screenshots/truth/textinput/autogrow-initial.png +0 -0
- package/screenshots/truth/textinput/input-form.png +0 -0
- package/src/display/Thumbnail.ts +2 -1
- package/src/events.ts +13 -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 +412 -354
- 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 +28 -18
- package/src/flow/actions/open_ticket.ts +74 -3
- package/src/flow/actions/send_msg.ts +170 -6
- 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 +46 -128
- package/src/form/ArrayEditor.ts +96 -66
- package/src/form/BaseListEditor.ts +22 -6
- package/src/form/FieldRenderer.ts +465 -0
- package/src/form/FormField.ts +4 -4
- package/src/form/KeyValueEditor.ts +1 -1
- package/src/form/MediaPicker.ts +13 -1
- package/src/form/MessageEditor.ts +449 -0
- package/src/form/TextInput.ts +16 -8
- package/src/form/select/Select.ts +55 -24
- package/src/live/ContactChat.ts +69 -19
- 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 +5 -3
- package/static/mr/docs/en-us/editor.json +2588 -0
- package/stress-test.js +143 -0
- package/temba-modules.ts +2 -0
- 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-config.test.ts +4 -2
- package/test/temba-field-renderer.test.ts +482 -0
- package/test/temba-markdown.test.ts +1 -1
- package/test/temba-message-editor.test.ts +300 -0
- package/test/temba-node-editor.test.ts +590 -0
- package/test/temba-select.test.ts +7 -7
- package/test/temba-textinput.test.ts +26 -0
- package/test/temba-webchat.test.ts +6 -1
- package/test/utils.test.ts +2 -13
- package/test-assets/contacts/history.json +19 -0
- package/test-assets/select/llms.json +18 -0
- package/test-assets/style.css +2 -0
- package/web-dev-mock.mjs +523 -0
- package/web-dev-server.config.mjs +74 -6
- package/web-test-runner.config.mjs +9 -4
- package/test/temba-flow-editor.test.ts.backup +0 -563
- package/test/temba-utils-index.test.ts.backup +0 -1737
package/src/flow/NodeEditor.ts
CHANGED
|
@@ -10,16 +10,11 @@ import {
|
|
|
10
10
|
FieldConfig,
|
|
11
11
|
ActionConfig
|
|
12
12
|
} from './config';
|
|
13
|
-
import {
|
|
14
|
-
SelectFieldConfig,
|
|
15
|
-
CheckboxFieldConfig,
|
|
16
|
-
TextareaFieldConfig,
|
|
17
|
-
LayoutItem,
|
|
18
|
-
RowLayoutConfig,
|
|
19
|
-
GroupLayoutConfig
|
|
20
|
-
} from './types';
|
|
13
|
+
import { LayoutItem, RowLayoutConfig, GroupLayoutConfig } from './types';
|
|
21
14
|
import { CustomEventType } from '../interfaces';
|
|
22
15
|
import { generateUUID } from '../utils';
|
|
16
|
+
import { FieldRenderer } from '../form/FieldRenderer';
|
|
17
|
+
import { renderMarkdownInline } from '../markdown';
|
|
23
18
|
|
|
24
19
|
export class NodeEditor extends RapidElement {
|
|
25
20
|
static get styles() {
|
|
@@ -31,6 +26,10 @@ export class NodeEditor extends RapidElement {
|
|
|
31
26
|
gap: 15px;
|
|
32
27
|
min-width: 400px;
|
|
33
28
|
padding-bottom: 40px;
|
|
29
|
+
|
|
30
|
+
--color-bubble-bg: rgba(var(--primary-rgb), 0.7);
|
|
31
|
+
--color-bubble-border: rgba(0, 0, 0, 0.2);
|
|
32
|
+
--color-bubble-text: #fff;
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
.form-field {
|
|
@@ -39,10 +38,6 @@ export class NodeEditor extends RapidElement {
|
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
.form-field label {
|
|
42
|
-
font-weight: 500;
|
|
43
|
-
margin-bottom: 6px;
|
|
44
|
-
color: #333;
|
|
45
|
-
font-size: 14px;
|
|
46
41
|
}
|
|
47
42
|
|
|
48
43
|
.field-errors {
|
|
@@ -103,10 +98,16 @@ export class NodeEditor extends RapidElement {
|
|
|
103
98
|
border-color: var(--color-error, tomato);
|
|
104
99
|
}
|
|
105
100
|
|
|
101
|
+
.form-group.has-bubble {
|
|
102
|
+
border-width: 2px;
|
|
103
|
+
border-color: rgba(var(--primary-rgb), 0.5);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
106
|
.form-group-header {
|
|
107
107
|
background: #f8f9fa;
|
|
108
|
-
padding:
|
|
108
|
+
padding: 8px 10px;
|
|
109
109
|
border-bottom: 1px solid #e0e0e0;
|
|
110
|
+
|
|
110
111
|
display: flex;
|
|
111
112
|
align-items: center;
|
|
112
113
|
justify-content: space-between;
|
|
@@ -114,6 +115,18 @@ export class NodeEditor extends RapidElement {
|
|
|
114
115
|
user-select: none;
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
.form-group.has-bubble .form-group-header {
|
|
119
|
+
background: rgba(var(--primary-rgb), 0.1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.collapsed .form-group-header {
|
|
123
|
+
border: none;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.form-group-header:hover {
|
|
127
|
+
background: rgba(0, 0, 0, 0.05);
|
|
128
|
+
}
|
|
129
|
+
|
|
117
130
|
.form-group-header.collapsible:hover {
|
|
118
131
|
background: #f1f3f4;
|
|
119
132
|
}
|
|
@@ -124,7 +137,7 @@ export class NodeEditor extends RapidElement {
|
|
|
124
137
|
|
|
125
138
|
.form-group-title {
|
|
126
139
|
font-weight: 500;
|
|
127
|
-
color: #
|
|
140
|
+
color: var(--color-label, #777);
|
|
128
141
|
font-size: 14px;
|
|
129
142
|
display: flex;
|
|
130
143
|
}
|
|
@@ -147,13 +160,13 @@ export class NodeEditor extends RapidElement {
|
|
|
147
160
|
}
|
|
148
161
|
|
|
149
162
|
.form-group-content {
|
|
150
|
-
padding:
|
|
163
|
+
padding: 6px;
|
|
151
164
|
display: flex;
|
|
152
165
|
flex-direction: column;
|
|
153
166
|
gap: 15px;
|
|
154
167
|
overflow: hidden;
|
|
155
|
-
transition: all 0.
|
|
156
|
-
|
|
168
|
+
transition: all 0.2s ease-in-out;
|
|
169
|
+
|
|
157
170
|
opacity: 1;
|
|
158
171
|
}
|
|
159
172
|
|
|
@@ -166,9 +179,14 @@ export class NodeEditor extends RapidElement {
|
|
|
166
179
|
|
|
167
180
|
.group-toggle-icon {
|
|
168
181
|
color: #666;
|
|
169
|
-
transition: transform 0.3s ease;
|
|
182
|
+
transition: transform 0.3s ease, opacity 0.3s ease;
|
|
170
183
|
cursor: pointer;
|
|
171
184
|
transform: rotate(0deg);
|
|
185
|
+
opacity: 1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.group-toggle-icon.faded {
|
|
189
|
+
opacity: 0;
|
|
172
190
|
}
|
|
173
191
|
|
|
174
192
|
.group-toggle-icon.expanded {
|
|
@@ -187,6 +205,58 @@ export class NodeEditor extends RapidElement {
|
|
|
187
205
|
color: var(--color-error, tomato);
|
|
188
206
|
margin-right: 8px;
|
|
189
207
|
}
|
|
208
|
+
|
|
209
|
+
.group-count-bubble {
|
|
210
|
+
border-radius: 50%;
|
|
211
|
+
display: flex;
|
|
212
|
+
align-items: center;
|
|
213
|
+
justify-content: center;
|
|
214
|
+
font-size: 11px;
|
|
215
|
+
font-weight: 600;
|
|
216
|
+
padding: 4px;
|
|
217
|
+
min-width: 12px;
|
|
218
|
+
min-height: 12px;
|
|
219
|
+
position: absolute;
|
|
220
|
+
top: 50%;
|
|
221
|
+
left: 50%;
|
|
222
|
+
transform: translate(-50%, -50%);
|
|
223
|
+
line-height: 0px;
|
|
224
|
+
opacity: 1;
|
|
225
|
+
transition: opacity 0.3s ease;
|
|
226
|
+
background: var(--color-bubble-bg, #fff);
|
|
227
|
+
border: 1px solid var(--color-bubble-border, #777);
|
|
228
|
+
color: var(--color-bubble-text, #000);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.group-count-bubble.hidden {
|
|
232
|
+
opacity: 0;
|
|
233
|
+
pointer-events: none;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.group-checkmark-icon {
|
|
237
|
+
position: absolute;
|
|
238
|
+
top: 50%;
|
|
239
|
+
left: 50%;
|
|
240
|
+
transform: translate(-50%, -50%);
|
|
241
|
+
opacity: 1;
|
|
242
|
+
transition: opacity 0.3s ease;
|
|
243
|
+
border-radius: 50%;
|
|
244
|
+
color: var(--color-bubble-text, #000);
|
|
245
|
+
background: var(--color-bubble-bg, #fff);
|
|
246
|
+
border: 1px solid var(--color-bubble-border, #777);
|
|
247
|
+
padding: 0.2em;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.group-checkmark-icon.hidden {
|
|
251
|
+
opacity: 0;
|
|
252
|
+
pointer-events: none;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.group-toggle-container {
|
|
256
|
+
position: relative;
|
|
257
|
+
display: flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
}
|
|
190
260
|
`;
|
|
191
261
|
}
|
|
192
262
|
|
|
@@ -214,6 +284,9 @@ export class NodeEditor extends RapidElement {
|
|
|
214
284
|
@state()
|
|
215
285
|
private groupCollapseState: { [key: string]: boolean } = {};
|
|
216
286
|
|
|
287
|
+
@state()
|
|
288
|
+
private groupHoverState: { [key: string]: boolean } = {};
|
|
289
|
+
|
|
217
290
|
connectedCallback(): void {
|
|
218
291
|
super.connectedCallback();
|
|
219
292
|
this.initializeFormData();
|
|
@@ -221,10 +294,21 @@ export class NodeEditor extends RapidElement {
|
|
|
221
294
|
|
|
222
295
|
updated(changedProperties: Map<string | number | symbol, unknown>): void {
|
|
223
296
|
super.updated(changedProperties);
|
|
224
|
-
if (
|
|
225
|
-
|
|
297
|
+
if (
|
|
298
|
+
changedProperties.has('node') ||
|
|
299
|
+
changedProperties.has('action') ||
|
|
300
|
+
changedProperties.has('nodeUI')
|
|
301
|
+
) {
|
|
302
|
+
// For action editing, we only need the action
|
|
303
|
+
if (this.action && (!this.node || !this.nodeUI)) {
|
|
226
304
|
this.openDialog();
|
|
227
|
-
}
|
|
305
|
+
}
|
|
306
|
+
// For node editing, we need both node and nodeUI
|
|
307
|
+
else if (this.node && this.nodeUI) {
|
|
308
|
+
this.openDialog();
|
|
309
|
+
}
|
|
310
|
+
// If we don't have the required data, close the dialog
|
|
311
|
+
else if (!this.action && (!this.node || !this.nodeUI)) {
|
|
228
312
|
this.isOpen = false;
|
|
229
313
|
}
|
|
230
314
|
}
|
|
@@ -241,10 +325,13 @@ export class NodeEditor extends RapidElement {
|
|
|
241
325
|
this.formData = {};
|
|
242
326
|
this.errors = {};
|
|
243
327
|
this.groupCollapseState = {};
|
|
328
|
+
this.groupHoverState = {};
|
|
244
329
|
}
|
|
245
330
|
|
|
246
331
|
private initializeFormData(): void {
|
|
247
|
-
|
|
332
|
+
const nodeConfig = this.getNodeConfig();
|
|
333
|
+
|
|
334
|
+
if ((!nodeConfig || nodeConfig.type === 'execute_actions') && this.action) {
|
|
248
335
|
// Action editing mode - use action config
|
|
249
336
|
const actionConfig = ACTION_CONFIG[this.action.type];
|
|
250
337
|
|
|
@@ -252,8 +339,6 @@ export class NodeEditor extends RapidElement {
|
|
|
252
339
|
this.formData = actionConfig.toFormData(this.action);
|
|
253
340
|
} else {
|
|
254
341
|
this.formData = { ...this.action };
|
|
255
|
-
// Apply smart transformations for select fields that expect {name, value} format
|
|
256
|
-
this.applySmartSelectTransformations(actionConfig);
|
|
257
342
|
}
|
|
258
343
|
|
|
259
344
|
// Convert Record objects to array format for key-value editors
|
|
@@ -311,52 +396,35 @@ export class NodeEditor extends RapidElement {
|
|
|
311
396
|
this.formData = processed;
|
|
312
397
|
}
|
|
313
398
|
|
|
314
|
-
private
|
|
315
|
-
if
|
|
399
|
+
private isKeyValueField(fieldName: string): boolean {
|
|
400
|
+
// Check if this field is configured as a key-value type
|
|
401
|
+
const config = this.getConfig();
|
|
402
|
+
const fields = config?.form;
|
|
403
|
+
return fields?.[fieldName]?.type === 'key-value';
|
|
404
|
+
}
|
|
316
405
|
|
|
317
|
-
|
|
318
|
-
if
|
|
406
|
+
private getConfig(): ActionConfig | NodeConfig | null {
|
|
407
|
+
// If we have a node and nodeUI, check if we should use node config
|
|
408
|
+
if (this.node && this.nodeUI) {
|
|
409
|
+
const nodeConfig = this.getNodeConfig();
|
|
319
410
|
|
|
320
|
-
|
|
321
|
-
if (this.
|
|
322
|
-
|
|
323
|
-
if (
|
|
324
|
-
Array.isArray(value) &&
|
|
325
|
-
value.length > 0 &&
|
|
326
|
-
typeof value[0] === 'string'
|
|
327
|
-
) {
|
|
328
|
-
// Transform string array to select options format
|
|
329
|
-
this.formData[fieldName] = value.map((item: string) => ({
|
|
330
|
-
name: item,
|
|
331
|
-
value: item
|
|
332
|
-
}));
|
|
333
|
-
}
|
|
411
|
+
// For execute_actions nodes, defer to action editing if an action is selected
|
|
412
|
+
if (this.nodeUI.type === 'execute_actions' && this.action) {
|
|
413
|
+
return ACTION_CONFIG[this.action.type] || null;
|
|
334
414
|
}
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
415
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
return (
|
|
344
|
-
(fieldConfig.type === 'select' &&
|
|
345
|
-
(selectConfig.multi || selectConfig.tags) &&
|
|
346
|
-
// Don't transform if already has explicit transformations
|
|
347
|
-
!this.action) ||
|
|
348
|
-
!ACTION_CONFIG[this.action.type]?.toFormData
|
|
349
|
-
);
|
|
350
|
-
}
|
|
416
|
+
// For all other nodes with a config, use the node config
|
|
417
|
+
if (nodeConfig) {
|
|
418
|
+
return nodeConfig;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
351
421
|
|
|
352
|
-
|
|
353
|
-
// Check if this field is configured as a key-value type
|
|
422
|
+
// Fall back to action config if no node config or for pure action editing
|
|
354
423
|
if (this.action) {
|
|
355
|
-
|
|
356
|
-
const fields = actionConfig?.form;
|
|
357
|
-
return fields?.[fieldName]?.type === 'key-value';
|
|
424
|
+
return ACTION_CONFIG[this.action.type] || null;
|
|
358
425
|
}
|
|
359
|
-
|
|
426
|
+
|
|
427
|
+
return null;
|
|
360
428
|
}
|
|
361
429
|
|
|
362
430
|
private getNodeConfig(): NodeConfig | null {
|
|
@@ -366,16 +434,8 @@ export class NodeEditor extends RapidElement {
|
|
|
366
434
|
}
|
|
367
435
|
|
|
368
436
|
private getHeaderColor(): string {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const actionConfig = ACTION_CONFIG[this.action.type];
|
|
372
|
-
return actionConfig?.color || '#666666';
|
|
373
|
-
} else if (this.node) {
|
|
374
|
-
// Node editing mode
|
|
375
|
-
const nodeConfig = this.getNodeConfig();
|
|
376
|
-
return nodeConfig?.color || '#666666';
|
|
377
|
-
}
|
|
378
|
-
return '#666666';
|
|
437
|
+
const config = this.getConfig();
|
|
438
|
+
return config?.color || '#666666';
|
|
379
439
|
}
|
|
380
440
|
|
|
381
441
|
private handleDialogButtonClick(event: CustomEvent): void {
|
|
@@ -466,89 +526,70 @@ export class NodeEditor extends RapidElement {
|
|
|
466
526
|
|
|
467
527
|
private validateForm(): ValidationResult {
|
|
468
528
|
const errors: { [key: string]: string } = {};
|
|
529
|
+
const config = this.getConfig();
|
|
469
530
|
|
|
470
|
-
if (
|
|
471
|
-
// Action validation using fields configuration
|
|
472
|
-
const actionConfig = ACTION_CONFIG[this.action.type];
|
|
473
|
-
|
|
531
|
+
if (config) {
|
|
474
532
|
// Check if new field configuration system is available
|
|
475
|
-
if (
|
|
476
|
-
Object.entries(
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
533
|
+
if (config.form) {
|
|
534
|
+
Object.entries(config.form).forEach(([fieldName, fieldConfig]) => {
|
|
535
|
+
const value = this.formData[fieldName];
|
|
536
|
+
|
|
537
|
+
// Check required fields
|
|
538
|
+
if (
|
|
539
|
+
(fieldConfig as any).required &&
|
|
540
|
+
(!value || (Array.isArray(value) && value.length === 0))
|
|
541
|
+
) {
|
|
542
|
+
errors[fieldName] = `${
|
|
543
|
+
(fieldConfig as any).label || fieldName
|
|
544
|
+
} is required.`;
|
|
545
|
+
}
|
|
489
546
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
547
|
+
// Check minLength for text fields
|
|
548
|
+
if (
|
|
549
|
+
typeof value === 'string' &&
|
|
550
|
+
(fieldConfig as any).minLength &&
|
|
551
|
+
value.length < (fieldConfig as any).minLength
|
|
552
|
+
) {
|
|
553
|
+
errors[fieldName] = `${
|
|
554
|
+
(fieldConfig as any).label || fieldName
|
|
555
|
+
} must be at least ${(fieldConfig as any).minLength} characters`;
|
|
556
|
+
}
|
|
500
557
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
558
|
+
// Check maxLength for text fields
|
|
559
|
+
if (
|
|
560
|
+
typeof value === 'string' &&
|
|
561
|
+
(fieldConfig as any).maxLength &&
|
|
562
|
+
value.length > (fieldConfig as any).maxLength
|
|
563
|
+
) {
|
|
564
|
+
errors[fieldName] = `${
|
|
565
|
+
(fieldConfig as any).label || fieldName
|
|
566
|
+
} must be no more than ${
|
|
567
|
+
(fieldConfig as any).maxLength
|
|
568
|
+
} characters`;
|
|
513
569
|
}
|
|
514
|
-
);
|
|
570
|
+
});
|
|
515
571
|
}
|
|
516
572
|
|
|
573
|
+
// Universal validation for category arrays to check for reserved names
|
|
574
|
+
this.validateCategoryNames(errors);
|
|
575
|
+
|
|
517
576
|
// Run custom validation if available
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
if (actionConfig.fromFormData) {
|
|
522
|
-
actionForValidation = actionConfig.fromFormData(this.formData);
|
|
523
|
-
} else {
|
|
524
|
-
actionForValidation = { ...this.action, ...this.formData } as Action;
|
|
577
|
+
if (config.validate) {
|
|
578
|
+
if (config.sanitize) {
|
|
579
|
+
config.sanitize(this.formData);
|
|
525
580
|
}
|
|
526
581
|
|
|
527
|
-
|
|
582
|
+
let customValidation;
|
|
583
|
+
if (this.action) {
|
|
584
|
+
customValidation = config.validate({
|
|
585
|
+
...this.action,
|
|
586
|
+
...this.formData
|
|
587
|
+
});
|
|
588
|
+
} else {
|
|
589
|
+
customValidation = config.validate(this.formData);
|
|
590
|
+
}
|
|
528
591
|
Object.assign(errors, customValidation.errors);
|
|
529
592
|
}
|
|
530
|
-
} else if (this.node) {
|
|
531
|
-
// Node validation
|
|
532
|
-
const nodeConfig = this.getNodeConfig();
|
|
533
|
-
|
|
534
|
-
// Check required fields from node properties
|
|
535
|
-
if (nodeConfig?.properties) {
|
|
536
|
-
Object.entries(nodeConfig.properties).forEach(
|
|
537
|
-
([fieldName, fieldConfig]) => {
|
|
538
|
-
const value = this.formData[fieldName];
|
|
539
|
-
|
|
540
|
-
// Check required fields
|
|
541
|
-
if (
|
|
542
|
-
fieldConfig.required &&
|
|
543
|
-
(!value || (Array.isArray(value) && value.length === 0))
|
|
544
|
-
) {
|
|
545
|
-
errors[fieldName] = `${
|
|
546
|
-
fieldConfig.label || fieldName
|
|
547
|
-
} is required`;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
);
|
|
551
|
-
}
|
|
552
593
|
}
|
|
553
594
|
|
|
554
595
|
// Validate key-value fields for unique keys
|
|
@@ -599,6 +640,44 @@ export class NodeEditor extends RapidElement {
|
|
|
599
640
|
});
|
|
600
641
|
}
|
|
601
642
|
|
|
643
|
+
private validateCategoryNames(errors: { [key: string]: string }): void {
|
|
644
|
+
// Universal validation for category names across all node types
|
|
645
|
+
// Prevents use of reserved category names that have special meaning in the system
|
|
646
|
+
// Define reserved category names (case-insensitive)
|
|
647
|
+
const reservedNames = [
|
|
648
|
+
'other',
|
|
649
|
+
'failure',
|
|
650
|
+
'success',
|
|
651
|
+
'all responses',
|
|
652
|
+
'no response'
|
|
653
|
+
];
|
|
654
|
+
|
|
655
|
+
// Check all form fields for category arrays
|
|
656
|
+
Object.entries(this.formData).forEach(([fieldName, value]) => {
|
|
657
|
+
if (Array.isArray(value) && fieldName === 'categories') {
|
|
658
|
+
const categories = value.filter(
|
|
659
|
+
(item: any) => item?.name && item.name.trim() !== ''
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
// Check for reserved names
|
|
663
|
+
const reservedUsed = categories
|
|
664
|
+
.filter((item: any) => {
|
|
665
|
+
const lowerName = item.name.trim().toLowerCase();
|
|
666
|
+
return reservedNames.includes(lowerName);
|
|
667
|
+
})
|
|
668
|
+
.map((item: any) => item.name.trim()); // Preserve original case
|
|
669
|
+
|
|
670
|
+
if (reservedUsed.length > 0) {
|
|
671
|
+
errors[
|
|
672
|
+
fieldName
|
|
673
|
+
] = `Reserved category names cannot be used: ${reservedUsed.join(
|
|
674
|
+
', '
|
|
675
|
+
)}`;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
602
681
|
private formDataToNode(formData: any = this.formData): Node {
|
|
603
682
|
if (!this.node) throw new Error('No node to update');
|
|
604
683
|
let updatedNode: Node = { ...this.node };
|
|
@@ -765,42 +844,11 @@ export class NodeEditor extends RapidElement {
|
|
|
765
844
|
if (actionConfig?.fromFormData) {
|
|
766
845
|
return actionConfig.fromFormData(formData);
|
|
767
846
|
} else {
|
|
768
|
-
//
|
|
769
|
-
|
|
770
|
-
formData,
|
|
771
|
-
actionConfig
|
|
772
|
-
);
|
|
773
|
-
return { ...this.action, ...processedFormData };
|
|
847
|
+
// Default 1:1 mapping
|
|
848
|
+
return { ...this.action, ...formData };
|
|
774
849
|
}
|
|
775
850
|
}
|
|
776
851
|
|
|
777
|
-
private reverseSmartSelectTransformations(
|
|
778
|
-
formData: any,
|
|
779
|
-
actionConfig: ActionConfig
|
|
780
|
-
): any {
|
|
781
|
-
if (!actionConfig || !actionConfig.form) return formData;
|
|
782
|
-
const processed = { ...formData };
|
|
783
|
-
|
|
784
|
-
Object.entries(actionConfig.form).forEach(([fieldName, fieldConfig]) => {
|
|
785
|
-
if (this.shouldApplySmartSelectTransformation(fieldName, fieldConfig)) {
|
|
786
|
-
const value = processed[fieldName];
|
|
787
|
-
if (
|
|
788
|
-
Array.isArray(value) &&
|
|
789
|
-
value.length > 0 &&
|
|
790
|
-
typeof value[0] === 'object' &&
|
|
791
|
-
'value' in value[0]
|
|
792
|
-
) {
|
|
793
|
-
// Transform select options format back to string array
|
|
794
|
-
processed[fieldName] = value.map(
|
|
795
|
-
(item: any) => item.value || item.name || item
|
|
796
|
-
);
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
});
|
|
800
|
-
|
|
801
|
-
return processed;
|
|
802
|
-
}
|
|
803
|
-
|
|
804
852
|
private handleFormFieldChange(propertyName: string, event: Event): void {
|
|
805
853
|
const target = event.target as any;
|
|
806
854
|
let value: any;
|
|
@@ -834,14 +882,49 @@ export class NodeEditor extends RapidElement {
|
|
|
834
882
|
// Check for computed values in dependent fields
|
|
835
883
|
this.updateComputedFields(propertyName);
|
|
836
884
|
|
|
885
|
+
// Re-evaluate group collapse states that depend on form data
|
|
886
|
+
this.updateGroupCollapseStates();
|
|
887
|
+
|
|
837
888
|
// Trigger re-render to handle conditional field visibility
|
|
838
889
|
this.requestUpdate();
|
|
839
890
|
}
|
|
840
891
|
|
|
841
|
-
private
|
|
842
|
-
|
|
892
|
+
private updateGroupCollapseStates(): void {
|
|
893
|
+
const config = this.getConfig();
|
|
894
|
+
if (!config?.layout) return;
|
|
843
895
|
|
|
844
|
-
|
|
896
|
+
this.updateGroupCollapseStatesRecursive(config.layout);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
private updateGroupCollapseStatesRecursive(items: LayoutItem[]): void {
|
|
900
|
+
items.forEach((item) => {
|
|
901
|
+
if (typeof item === 'object' && item.type === 'group') {
|
|
902
|
+
const { label, collapsed, collapsible } = item;
|
|
903
|
+
|
|
904
|
+
// Only update if the group is collapsible and has a function-based collapsed property
|
|
905
|
+
if (collapsible && typeof collapsed === 'function') {
|
|
906
|
+
const newCollapsedState = collapsed(this.formData);
|
|
907
|
+
|
|
908
|
+
// Only update if the state has changed to avoid unnecessary re-renders
|
|
909
|
+
if (this.groupCollapseState[label] !== newCollapsedState) {
|
|
910
|
+
this.groupCollapseState = {
|
|
911
|
+
...this.groupCollapseState,
|
|
912
|
+
[label]: newCollapsedState
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Recursively check nested items
|
|
918
|
+
this.updateGroupCollapseStatesRecursive(item.items);
|
|
919
|
+
} else if (typeof item === 'object' && item.type === 'row') {
|
|
920
|
+
// Recursively check items in rows
|
|
921
|
+
this.updateGroupCollapseStatesRecursive(item.items);
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
private updateComputedFields(changedFieldName: string): void {
|
|
927
|
+
const config = this.getConfig();
|
|
845
928
|
if (!config?.form) return;
|
|
846
929
|
|
|
847
930
|
// Check all fields to see if any depend on the changed field
|
|
@@ -912,154 +995,33 @@ export class NodeEditor extends RapidElement {
|
|
|
912
995
|
value: any,
|
|
913
996
|
errors: string[]
|
|
914
997
|
): TemplateResult {
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
: '';
|
|
933
|
-
|
|
934
|
-
if (config.evaluated) {
|
|
935
|
-
return html`<temba-completion
|
|
936
|
-
name="${fieldName}"
|
|
937
|
-
label="${config.label}"
|
|
938
|
-
?required="${config.required}"
|
|
939
|
-
.errors="${errors}"
|
|
940
|
-
.value="${value || ''}"
|
|
941
|
-
placeholder="${config.placeholder || ''}"
|
|
942
|
-
textarea
|
|
943
|
-
expressions="session"
|
|
944
|
-
style="${minHeightStyle}"
|
|
945
|
-
.helpText="${config.helpText || ''}"
|
|
946
|
-
@input="${(e: Event) => this.handleFormFieldChange(fieldName, e)}"
|
|
947
|
-
></temba-completion>`;
|
|
998
|
+
// Use FieldRenderer for consistent field rendering
|
|
999
|
+
return FieldRenderer.renderField(fieldName, config, value, {
|
|
1000
|
+
errors,
|
|
1001
|
+
onChange: (e: Event) => {
|
|
1002
|
+
// Handle different change event types
|
|
1003
|
+
if (fieldName && config.type === 'key-value') {
|
|
1004
|
+
// Special handling for key-value editor
|
|
1005
|
+
const customEvent = e as CustomEvent;
|
|
1006
|
+
if (customEvent.detail) {
|
|
1007
|
+
this.handleNewFieldChange(fieldName, customEvent.detail.value);
|
|
1008
|
+
}
|
|
1009
|
+
} else if (fieldName && config.type === 'array') {
|
|
1010
|
+
// Special handling for array editor
|
|
1011
|
+
this.handleNewFieldChange(fieldName, (e.target as any).value);
|
|
1012
|
+
} else if (fieldName && config.type === 'message-editor') {
|
|
1013
|
+
// Special handling for message editor
|
|
1014
|
+
this.handleMessageEditorChange(fieldName, e);
|
|
948
1015
|
} else {
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
label="${config.label}"
|
|
952
|
-
?required="${config.required}"
|
|
953
|
-
.errors="${errors}"
|
|
954
|
-
.value="${value || ''}"
|
|
955
|
-
placeholder="${config.placeholder || ''}"
|
|
956
|
-
textarea
|
|
957
|
-
.rows="${textareaConfig.rows || 3}"
|
|
958
|
-
style="${minHeightStyle}"
|
|
959
|
-
.helpText="${config.helpText || ''}"
|
|
960
|
-
@input="${(e: Event) => this.handleFormFieldChange(fieldName, e)}"
|
|
961
|
-
></temba-textinput>`;
|
|
1016
|
+
// Default handling for most field types
|
|
1017
|
+
this.handleFormFieldChange(fieldName, e);
|
|
962
1018
|
}
|
|
1019
|
+
},
|
|
1020
|
+
showLabel: true,
|
|
1021
|
+
additionalData: {
|
|
1022
|
+
attachments: this.formData.attachments || []
|
|
963
1023
|
}
|
|
964
|
-
|
|
965
|
-
case 'select': {
|
|
966
|
-
const selectConfig = config as SelectFieldConfig;
|
|
967
|
-
return html`<temba-select
|
|
968
|
-
name="${fieldName}"
|
|
969
|
-
label="${config.label}"
|
|
970
|
-
?required="${config.required}"
|
|
971
|
-
.errors="${errors}"
|
|
972
|
-
.values="${value || (selectConfig.multi ? [] : '')}"
|
|
973
|
-
?multi="${selectConfig.multi}"
|
|
974
|
-
?searchable="${selectConfig.searchable}"
|
|
975
|
-
?tags="${selectConfig.tags}"
|
|
976
|
-
?emails="${selectConfig.emails}"
|
|
977
|
-
placeholder="${selectConfig.placeholder || ''}"
|
|
978
|
-
maxItems="${selectConfig.maxItems || 0}"
|
|
979
|
-
valueKey="${selectConfig.valueKey || 'value'}"
|
|
980
|
-
nameKey="${selectConfig.nameKey || 'name'}"
|
|
981
|
-
endpoint="${selectConfig.endpoint || ''}"
|
|
982
|
-
.helpText="${config.helpText || ''}"
|
|
983
|
-
@change="${(e: Event) => this.handleFormFieldChange(fieldName, e)}"
|
|
984
|
-
>
|
|
985
|
-
${selectConfig.options?.map((option: any) => {
|
|
986
|
-
if (typeof option === 'string') {
|
|
987
|
-
return html`<temba-option
|
|
988
|
-
name="${option}"
|
|
989
|
-
value="${option}"
|
|
990
|
-
></temba-option>`;
|
|
991
|
-
} else {
|
|
992
|
-
return html`<temba-option
|
|
993
|
-
name="${option.label || option.name}"
|
|
994
|
-
value="${option.value}"
|
|
995
|
-
></temba-option>`;
|
|
996
|
-
}
|
|
997
|
-
})}
|
|
998
|
-
</temba-select>`;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
case 'key-value':
|
|
1002
|
-
return html`<div class="form-field">
|
|
1003
|
-
<label>${config.label}${config.required ? ' *' : ''}</label>
|
|
1004
|
-
<temba-key-value-editor
|
|
1005
|
-
name="${fieldName}"
|
|
1006
|
-
.value="${value || []}"
|
|
1007
|
-
.sortable="${config.sortable}"
|
|
1008
|
-
.keyPlaceholder="${config.keyPlaceholder || 'Key'}"
|
|
1009
|
-
.valuePlaceholder="${config.valuePlaceholder || 'Value'}"
|
|
1010
|
-
.minRows="${config.minRows || 0}"
|
|
1011
|
-
@change="${(e: CustomEvent) => {
|
|
1012
|
-
if (e.detail) {
|
|
1013
|
-
this.handleNewFieldChange(fieldName, e.detail.value);
|
|
1014
|
-
}
|
|
1015
|
-
}}"
|
|
1016
|
-
></temba-key-value-editor>
|
|
1017
|
-
${errors.length
|
|
1018
|
-
? html`<div class="field-errors">${errors.join(', ')}</div>`
|
|
1019
|
-
: ''}
|
|
1020
|
-
</div>`;
|
|
1021
|
-
|
|
1022
|
-
case 'array':
|
|
1023
|
-
return html`<div class="form-field">
|
|
1024
|
-
<label>${config.label}${config.required ? ' *' : ''}</label>
|
|
1025
|
-
<temba-array-editor
|
|
1026
|
-
.value="${value || []}"
|
|
1027
|
-
.itemConfig="${config.itemConfig}"
|
|
1028
|
-
.sortable="${config.sortable}"
|
|
1029
|
-
.itemLabel="${config.itemLabel || 'Item'}"
|
|
1030
|
-
.minItems="${config.minItems || 0}"
|
|
1031
|
-
.onItemChange="${config.onItemChange}"
|
|
1032
|
-
@change="${(e: CustomEvent) =>
|
|
1033
|
-
this.handleNewFieldChange(fieldName, e.detail.value)}"
|
|
1034
|
-
></temba-array-editor>
|
|
1035
|
-
${errors.length
|
|
1036
|
-
? html`<div class="field-errors">${errors.join(', ')}</div>`
|
|
1037
|
-
: ''}
|
|
1038
|
-
</div>`;
|
|
1039
|
-
|
|
1040
|
-
case 'checkbox': {
|
|
1041
|
-
const checkboxConfig = config as CheckboxFieldConfig;
|
|
1042
|
-
return html`<div class="form-field">
|
|
1043
|
-
<temba-checkbox
|
|
1044
|
-
name="${fieldName}"
|
|
1045
|
-
label="${config.label}"
|
|
1046
|
-
.helpText="${config.helpText || ''}"
|
|
1047
|
-
?required="${config.required}"
|
|
1048
|
-
.errors="${errors}"
|
|
1049
|
-
?checked="${value || false}"
|
|
1050
|
-
size="${checkboxConfig.size || 1.2}"
|
|
1051
|
-
animateChange="${checkboxConfig.animateChange || 'pulse'}"
|
|
1052
|
-
@change="${(e: Event) => this.handleFormFieldChange(fieldName, e)}"
|
|
1053
|
-
></temba-checkbox>
|
|
1054
|
-
${errors.length
|
|
1055
|
-
? html`<div class="field-errors">${errors.join(', ')}</div>`
|
|
1056
|
-
: ''}
|
|
1057
|
-
</div>`;
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
default:
|
|
1061
|
-
return html`<div>Unsupported field type: ${(config as any).type}</div>`;
|
|
1062
|
-
}
|
|
1024
|
+
});
|
|
1063
1025
|
}
|
|
1064
1026
|
|
|
1065
1027
|
private handleGroupToggle(groupLabel: string): void {
|
|
@@ -1069,10 +1031,22 @@ export class NodeEditor extends RapidElement {
|
|
|
1069
1031
|
};
|
|
1070
1032
|
}
|
|
1071
1033
|
|
|
1072
|
-
private
|
|
1073
|
-
|
|
1034
|
+
private handleGroupMouseEnter(groupLabel: string): void {
|
|
1035
|
+
this.groupHoverState = {
|
|
1036
|
+
...this.groupHoverState,
|
|
1037
|
+
[groupLabel]: true
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
private handleGroupMouseLeave(groupLabel: string): void {
|
|
1042
|
+
this.groupHoverState = {
|
|
1043
|
+
...this.groupHoverState,
|
|
1044
|
+
[groupLabel]: false
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1074
1047
|
|
|
1075
|
-
|
|
1048
|
+
private expandGroupsWithErrors(errors: { [key: string]: string }): void {
|
|
1049
|
+
const config = this.getConfig();
|
|
1076
1050
|
if (!config?.layout) return;
|
|
1077
1051
|
|
|
1078
1052
|
const errorFields = new Set(Object.keys(errors));
|
|
@@ -1109,7 +1083,7 @@ export class NodeEditor extends RapidElement {
|
|
|
1109
1083
|
|
|
1110
1084
|
private renderLayoutItem(
|
|
1111
1085
|
item: LayoutItem,
|
|
1112
|
-
config: ActionConfig,
|
|
1086
|
+
config: ActionConfig | NodeConfig,
|
|
1113
1087
|
renderedFields: Set<string>
|
|
1114
1088
|
): TemplateResult {
|
|
1115
1089
|
if (typeof item === 'string') {
|
|
@@ -1146,7 +1120,7 @@ export class NodeEditor extends RapidElement {
|
|
|
1146
1120
|
|
|
1147
1121
|
private renderRow(
|
|
1148
1122
|
rowConfig: RowLayoutConfig,
|
|
1149
|
-
config: ActionConfig,
|
|
1123
|
+
config: ActionConfig | NodeConfig,
|
|
1150
1124
|
renderedFields: Set<string>
|
|
1151
1125
|
): TemplateResult {
|
|
1152
1126
|
const { items, gap = '1rem' } = rowConfig;
|
|
@@ -1183,7 +1157,7 @@ export class NodeEditor extends RapidElement {
|
|
|
1183
1157
|
|
|
1184
1158
|
private renderGroup(
|
|
1185
1159
|
groupConfig: GroupLayoutConfig,
|
|
1186
|
-
config: ActionConfig,
|
|
1160
|
+
config: ActionConfig | NodeConfig,
|
|
1187
1161
|
renderedFields: Set<string>
|
|
1188
1162
|
): TemplateResult {
|
|
1189
1163
|
const {
|
|
@@ -1191,19 +1165,25 @@ export class NodeEditor extends RapidElement {
|
|
|
1191
1165
|
items,
|
|
1192
1166
|
collapsible = false,
|
|
1193
1167
|
collapsed = false,
|
|
1194
|
-
helpText
|
|
1168
|
+
helpText,
|
|
1169
|
+
getGroupValueCount
|
|
1195
1170
|
} = groupConfig;
|
|
1196
1171
|
|
|
1197
1172
|
// Initialize collapse state if not set
|
|
1198
1173
|
if (collapsible && !(label in this.groupCollapseState)) {
|
|
1174
|
+
// Evaluate collapsed property - can be boolean or function
|
|
1175
|
+
const initialCollapsed =
|
|
1176
|
+
typeof collapsed === 'function' ? collapsed(this.formData) : collapsed;
|
|
1177
|
+
|
|
1199
1178
|
this.groupCollapseState = {
|
|
1200
1179
|
...this.groupCollapseState,
|
|
1201
|
-
[label]:
|
|
1180
|
+
[label]: initialCollapsed
|
|
1202
1181
|
};
|
|
1203
1182
|
}
|
|
1204
1183
|
|
|
1205
1184
|
const isCollapsed = collapsible
|
|
1206
|
-
? this.groupCollapseState[label] ??
|
|
1185
|
+
? this.groupCollapseState[label] ??
|
|
1186
|
+
(typeof collapsed === 'function' ? collapsed(this.formData) : collapsed)
|
|
1207
1187
|
: false;
|
|
1208
1188
|
|
|
1209
1189
|
// Check if any field in this group has errors
|
|
@@ -1212,10 +1192,41 @@ export class NodeEditor extends RapidElement {
|
|
|
1212
1192
|
(fieldName) => this.errors[fieldName]
|
|
1213
1193
|
);
|
|
1214
1194
|
|
|
1195
|
+
// Calculate count for bubble display
|
|
1196
|
+
let valueCount = 0;
|
|
1197
|
+
let showBubble = false;
|
|
1198
|
+
let showCheckmark = false;
|
|
1199
|
+
let hasValue = false;
|
|
1200
|
+
const isHovered = this.groupHoverState[label] ?? false;
|
|
1201
|
+
|
|
1202
|
+
if (getGroupValueCount && collapsible) {
|
|
1203
|
+
try {
|
|
1204
|
+
const result = getGroupValueCount(this.formData);
|
|
1205
|
+
|
|
1206
|
+
if (typeof result === 'boolean') {
|
|
1207
|
+
// Boolean result - show checkmark when true
|
|
1208
|
+
showCheckmark = result && isCollapsed && !isHovered;
|
|
1209
|
+
hasValue = result;
|
|
1210
|
+
} else if (typeof result === 'number') {
|
|
1211
|
+
// Numeric result - show count bubble
|
|
1212
|
+
valueCount = result;
|
|
1213
|
+
showBubble = valueCount > 0 && isCollapsed && !isHovered;
|
|
1214
|
+
hasValue = valueCount > 0;
|
|
1215
|
+
}
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
console.error(
|
|
1218
|
+
`Error calculating group value count for ${label}:`,
|
|
1219
|
+
error
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1215
1224
|
return html`
|
|
1216
1225
|
<div
|
|
1217
1226
|
class="form-group ${collapsible ? 'collapsible' : ''} ${groupHasErrors
|
|
1218
1227
|
? 'has-errors'
|
|
1228
|
+
: ''} ${isCollapsed ? 'collapsed' : 'expanded'} ${hasValue
|
|
1229
|
+
? 'has-bubble'
|
|
1219
1230
|
: ''}"
|
|
1220
1231
|
>
|
|
1221
1232
|
<div
|
|
@@ -1223,11 +1234,19 @@ export class NodeEditor extends RapidElement {
|
|
|
1223
1234
|
@click=${collapsible
|
|
1224
1235
|
? () => this.handleGroupToggle(label)
|
|
1225
1236
|
: undefined}
|
|
1237
|
+
@mouseenter=${collapsible
|
|
1238
|
+
? () => this.handleGroupMouseEnter(label)
|
|
1239
|
+
: undefined}
|
|
1240
|
+
@mouseleave=${collapsible
|
|
1241
|
+
? () => this.handleGroupMouseLeave(label)
|
|
1242
|
+
: undefined}
|
|
1226
1243
|
>
|
|
1227
1244
|
<div class="form-group-info">
|
|
1228
1245
|
<div class="form-group-title">${label}</div>
|
|
1229
1246
|
${helpText
|
|
1230
|
-
? html`<div class="form-group-help"
|
|
1247
|
+
? html`<div class="form-group-help">
|
|
1248
|
+
${renderMarkdownInline(helpText)}
|
|
1249
|
+
</div>`
|
|
1231
1250
|
: ''}
|
|
1232
1251
|
</div>
|
|
1233
1252
|
${groupHasErrors
|
|
@@ -1238,13 +1257,28 @@ export class NodeEditor extends RapidElement {
|
|
|
1238
1257
|
></temba-icon>`
|
|
1239
1258
|
: ''}
|
|
1240
1259
|
${collapsible && !groupHasErrors
|
|
1241
|
-
? html`<
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1260
|
+
? html`<div class="group-toggle-container">
|
|
1261
|
+
<temba-icon
|
|
1262
|
+
name="arrow_right"
|
|
1263
|
+
size="1.5"
|
|
1264
|
+
class="group-toggle-icon ${isCollapsed
|
|
1265
|
+
? 'collapsed'
|
|
1266
|
+
: 'expanded'} ${showBubble || showCheckmark ? 'faded' : ''}"
|
|
1267
|
+
></temba-icon>
|
|
1268
|
+
${showCheckmark
|
|
1269
|
+
? html`<temba-icon
|
|
1270
|
+
name="check"
|
|
1271
|
+
size="1"
|
|
1272
|
+
class="group-checkmark-icon"
|
|
1273
|
+
></temba-icon>`
|
|
1274
|
+
: showBubble
|
|
1275
|
+
? html`<div
|
|
1276
|
+
class="group-count-bubble ${!showBubble ? 'hidden' : ''}"
|
|
1277
|
+
>
|
|
1278
|
+
${valueCount}
|
|
1279
|
+
</div>`
|
|
1280
|
+
: ''}
|
|
1281
|
+
</div>`
|
|
1248
1282
|
: ''}
|
|
1249
1283
|
</div>
|
|
1250
1284
|
<div
|
|
@@ -1278,7 +1312,7 @@ export class NodeEditor extends RapidElement {
|
|
|
1278
1312
|
|
|
1279
1313
|
private renderFieldRow(
|
|
1280
1314
|
rowConfig: RowLayoutConfig,
|
|
1281
|
-
config: ActionConfig
|
|
1315
|
+
config: ActionConfig | NodeConfig
|
|
1282
1316
|
): TemplateResult {
|
|
1283
1317
|
// This method is deprecated - use renderRow instead
|
|
1284
1318
|
return this.renderRow(rowConfig, config, new Set());
|
|
@@ -1286,7 +1320,7 @@ export class NodeEditor extends RapidElement {
|
|
|
1286
1320
|
|
|
1287
1321
|
private renderFieldGroup(
|
|
1288
1322
|
groupConfig: GroupLayoutConfig,
|
|
1289
|
-
config: ActionConfig
|
|
1323
|
+
config: ActionConfig | NodeConfig
|
|
1290
1324
|
): TemplateResult {
|
|
1291
1325
|
// This method is deprecated - use renderGroup instead
|
|
1292
1326
|
return this.renderGroup(groupConfig, config, new Set());
|
|
@@ -1305,18 +1339,38 @@ export class NodeEditor extends RapidElement {
|
|
|
1305
1339
|
this.errors = newErrors;
|
|
1306
1340
|
}
|
|
1307
1341
|
|
|
1342
|
+
// Re-evaluate group collapse states that depend on form data
|
|
1343
|
+
this.updateGroupCollapseStates();
|
|
1344
|
+
|
|
1308
1345
|
// Trigger re-render
|
|
1309
1346
|
this.requestUpdate();
|
|
1310
1347
|
}
|
|
1348
|
+
private handleMessageEditorChange(fieldName: string, event: Event): void {
|
|
1349
|
+
const target = event.target as any;
|
|
1311
1350
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1351
|
+
// Update both text and attachments from the message editor
|
|
1352
|
+
this.formData = {
|
|
1353
|
+
...this.formData,
|
|
1354
|
+
[fieldName]: target.value,
|
|
1355
|
+
attachments: target.attachments || []
|
|
1356
|
+
};
|
|
1357
|
+
|
|
1358
|
+
// Clear any existing errors for both fields
|
|
1359
|
+
if (this.errors[fieldName]) {
|
|
1360
|
+
const newErrors = { ...this.errors };
|
|
1361
|
+
delete newErrors[fieldName];
|
|
1362
|
+
delete newErrors.attachments;
|
|
1363
|
+
this.errors = newErrors;
|
|
1315
1364
|
}
|
|
1316
1365
|
|
|
1317
|
-
|
|
1366
|
+
// Trigger re-render
|
|
1367
|
+
this.requestUpdate();
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
private renderFields(): TemplateResult {
|
|
1371
|
+
const config = this.getConfig();
|
|
1318
1372
|
if (!config) {
|
|
1319
|
-
return html` <div>No configuration available
|
|
1373
|
+
return html` <div>No configuration available</div> `;
|
|
1320
1374
|
}
|
|
1321
1375
|
|
|
1322
1376
|
// Use the new fields configuration system
|
|
@@ -1357,7 +1411,12 @@ export class NodeEditor extends RapidElement {
|
|
|
1357
1411
|
}
|
|
1358
1412
|
}
|
|
1359
1413
|
|
|
1360
|
-
|
|
1414
|
+
// Fallback for configs without form configuration
|
|
1415
|
+
if (this.action) {
|
|
1416
|
+
return html` <div>No form configuration available for this action</div> `;
|
|
1417
|
+
} else {
|
|
1418
|
+
return html` <div>No form configuration available for this node</div> `;
|
|
1419
|
+
}
|
|
1361
1420
|
}
|
|
1362
1421
|
|
|
1363
1422
|
private renderActionSection(): TemplateResult {
|
|
@@ -1430,12 +1489,11 @@ export class NodeEditor extends RapidElement {
|
|
|
1430
1489
|
}
|
|
1431
1490
|
|
|
1432
1491
|
const headerColor = this.getHeaderColor();
|
|
1433
|
-
const
|
|
1434
|
-
const actionConfig = ACTION_CONFIG[this.action?.type];
|
|
1492
|
+
const config = this.getConfig();
|
|
1435
1493
|
|
|
1436
1494
|
return html`
|
|
1437
1495
|
<temba-dialog
|
|
1438
|
-
header="${
|
|
1496
|
+
header="${config?.name || 'Edit'}"
|
|
1439
1497
|
.open="${this.isOpen}"
|
|
1440
1498
|
@temba-button-clicked=${this.handleDialogButtonClick}
|
|
1441
1499
|
primaryButtonName="Save"
|
|
@@ -1444,7 +1502,7 @@ export class NodeEditor extends RapidElement {
|
|
|
1444
1502
|
>
|
|
1445
1503
|
<div class="node-editor-form">
|
|
1446
1504
|
${this.renderFields()}
|
|
1447
|
-
${
|
|
1505
|
+
${this.getNodeConfig()?.router?.configurable
|
|
1448
1506
|
? this.renderRouterSection()
|
|
1449
1507
|
: null}
|
|
1450
1508
|
</div>
|