@nyaruka/temba-components 0.129.3 → 0.129.4
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/.eslintrc.js +1 -0
- package/.github/workflows/build.yml +135 -3
- package/CHANGELOG.md +18 -0
- package/demo/data/flows/sample-flow.json +110 -87
- package/demo/field-config-demo.html +135 -0
- package/dist/temba-components.js +1257 -675
- package/dist/temba-components.js.map +1 -1
- package/docs/ActionEditor-Migration.md +118 -0
- package/out-tsc/src/events.js.map +1 -1
- package/out-tsc/src/flow/{EditorNode.js → CanvasNode.js} +345 -42
- package/out-tsc/src/flow/CanvasNode.js.map +1 -0
- package/out-tsc/src/flow/Editor.js +107 -3
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +1200 -0
- package/out-tsc/src/flow/NodeEditor.js.map +1 -0
- package/out-tsc/src/flow/Plumber.js +0 -6
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/flow/actions/add_contact_groups.js +40 -0
- package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -0
- package/out-tsc/src/flow/actions/add_contact_urn.js +16 -0
- package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -0
- package/out-tsc/src/flow/actions/add_input_labels.js +11 -0
- package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -0
- package/out-tsc/src/flow/actions/call_classifier.js +11 -0
- package/out-tsc/src/flow/actions/call_classifier.js.map +1 -0
- package/out-tsc/src/flow/actions/call_llm.js +11 -0
- package/out-tsc/src/flow/actions/call_llm.js.map +1 -0
- package/out-tsc/src/flow/actions/call_resthook.js +11 -0
- package/out-tsc/src/flow/actions/call_resthook.js.map +1 -0
- package/out-tsc/src/flow/actions/call_webhook.js +122 -0
- package/out-tsc/src/flow/actions/call_webhook.js.map +1 -0
- package/out-tsc/src/flow/actions/enter_flow.js +14 -0
- package/out-tsc/src/flow/actions/enter_flow.js.map +1 -0
- package/out-tsc/src/flow/actions/open_ticket.js +11 -0
- package/out-tsc/src/flow/actions/open_ticket.js.map +1 -0
- package/out-tsc/src/flow/actions/play_audio.js +11 -0
- package/out-tsc/src/flow/actions/play_audio.js.map +1 -0
- package/out-tsc/src/flow/actions/remove_contact_groups.js +62 -0
- package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -0
- package/out-tsc/src/flow/actions/request_optin.js +11 -0
- package/out-tsc/src/flow/actions/request_optin.js.map +1 -0
- package/out-tsc/src/flow/actions/say_msg.js +11 -0
- package/out-tsc/src/flow/actions/say_msg.js.map +1 -0
- package/out-tsc/src/flow/actions/send_broadcast.js +33 -0
- package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -0
- package/out-tsc/src/flow/actions/send_email.js +56 -0
- package/out-tsc/src/flow/actions/send_email.js.map +1 -0
- package/out-tsc/src/flow/actions/send_msg.js +55 -0
- package/out-tsc/src/flow/actions/send_msg.js.map +1 -0
- package/out-tsc/src/flow/actions/set_contact_channel.js +12 -0
- package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -0
- package/out-tsc/src/flow/actions/set_contact_field.js +12 -0
- package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -0
- package/out-tsc/src/flow/actions/set_contact_language.js +10 -0
- package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -0
- package/out-tsc/src/flow/actions/set_contact_name.js +10 -0
- package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -0
- package/out-tsc/src/flow/actions/set_contact_status.js +10 -0
- package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -0
- package/out-tsc/src/flow/actions/set_run_result.js +10 -0
- package/out-tsc/src/flow/actions/set_run_result.js.map +1 -0
- package/out-tsc/src/flow/actions/split_by_expression_example.js +77 -0
- package/out-tsc/src/flow/actions/split_by_expression_example.js.map +1 -0
- package/out-tsc/src/flow/actions/start_session.js +11 -0
- package/out-tsc/src/flow/actions/start_session.js.map +1 -0
- package/out-tsc/src/flow/actions/transfer_airtime.js +11 -0
- package/out-tsc/src/flow/actions/transfer_airtime.js.map +1 -0
- package/out-tsc/src/flow/config.js +88 -193
- package/out-tsc/src/flow/config.js.map +1 -1
- package/out-tsc/src/flow/nodes/execute_actions.js +4 -0
- package/out-tsc/src/flow/nodes/execute_actions.js.map +1 -0
- package/out-tsc/src/flow/nodes/split_by_airtime.js +9 -0
- package/out-tsc/src/flow/nodes/split_by_airtime.js.map +1 -0
- package/out-tsc/src/flow/nodes/split_by_contact_field.js +7 -0
- package/out-tsc/src/flow/nodes/split_by_contact_field.js.map +1 -0
- package/out-tsc/src/flow/nodes/split_by_expression.js +7 -0
- package/out-tsc/src/flow/nodes/split_by_expression.js.map +1 -0
- package/out-tsc/src/flow/nodes/split_by_groups.js +7 -0
- package/out-tsc/src/flow/nodes/split_by_groups.js.map +1 -0
- package/out-tsc/src/flow/nodes/split_by_random.js +10 -0
- package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -0
- package/out-tsc/src/flow/nodes/split_by_run_result.js +7 -0
- package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -0
- package/out-tsc/src/flow/nodes/split_by_scheme.js +7 -0
- package/out-tsc/src/flow/nodes/split_by_scheme.js.map +1 -0
- package/out-tsc/src/flow/nodes/split_by_subflow.js +9 -0
- package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -0
- package/out-tsc/src/flow/nodes/split_by_webhook.js +18 -0
- package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_audio.js +7 -0
- package/out-tsc/src/flow/nodes/wait_for_audio.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_digits.js +7 -0
- package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_image.js +7 -0
- package/out-tsc/src/flow/nodes/wait_for_image.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_location.js +7 -0
- package/out-tsc/src/flow/nodes/wait_for_location.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_menu.js +7 -0
- package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_response.js +7 -0
- package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_video.js +7 -0
- package/out-tsc/src/flow/nodes/wait_for_video.js.map +1 -0
- package/out-tsc/src/flow/types.js +79 -0
- package/out-tsc/src/flow/types.js.map +1 -0
- package/out-tsc/src/flow/utils.js +65 -0
- package/out-tsc/src/flow/utils.js.map +1 -0
- package/out-tsc/src/form/ArrayEditor.js +199 -0
- package/out-tsc/src/form/ArrayEditor.js.map +1 -0
- package/out-tsc/src/form/BaseListEditor.js +128 -0
- package/out-tsc/src/form/BaseListEditor.js.map +1 -0
- package/out-tsc/src/form/Checkbox.js +17 -2
- package/out-tsc/src/form/Checkbox.js.map +1 -1
- package/out-tsc/src/form/Completion.js +6 -0
- package/out-tsc/src/form/Completion.js.map +1 -1
- package/out-tsc/src/form/FormField.js +110 -11
- package/out-tsc/src/form/FormField.js.map +1 -1
- package/out-tsc/src/form/KeyValueEditor.js +223 -0
- package/out-tsc/src/form/KeyValueEditor.js.map +1 -0
- package/out-tsc/src/form/select/Select.js +77 -32
- package/out-tsc/src/form/select/Select.js.map +1 -1
- package/out-tsc/src/interfaces.js +6 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +2 -76
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/temba-modules.js +9 -2
- package/out-tsc/temba-modules.js.map +1 -1
- package/out-tsc/test/ActionHelper.js +116 -0
- package/out-tsc/test/ActionHelper.js.map +1 -0
- package/out-tsc/test/actions/add_contact_groups.test.js +66 -0
- package/out-tsc/test/actions/add_contact_groups.test.js.map +1 -0
- package/out-tsc/test/actions/remove_contact_groups.test.js +226 -0
- package/out-tsc/test/actions/remove_contact_groups.test.js.map +1 -0
- package/out-tsc/test/actions/send_email.test.js +160 -0
- package/out-tsc/test/actions/send_email.test.js.map +1 -0
- package/out-tsc/test/actions/send_msg.test.js +95 -0
- package/out-tsc/test/actions/send_msg.test.js.map +1 -0
- package/out-tsc/test/temba-action-editing-integration.test.js +183 -0
- package/out-tsc/test/temba-action-editing-integration.test.js.map +1 -0
- package/out-tsc/test/temba-checkbox.test.js +1 -1
- package/out-tsc/test/temba-checkbox.test.js.map +1 -1
- package/out-tsc/test/temba-field-config.test.js +133 -0
- package/out-tsc/test/temba-field-config.test.js.map +1 -0
- package/out-tsc/test/temba-flow-editor-node.test.js +14 -14
- package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
- package/out-tsc/test/temba-node-editor.test.js +283 -0
- package/out-tsc/test/temba-node-editor.test.js.map +1 -0
- package/out-tsc/test/temba-select.test.js +85 -0
- package/out-tsc/test/temba-select.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/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/add_contact_groups/render/descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/single-group.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/remove_contact_groups/render/cleanup-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/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_email/render/complex-business-email.png +0 -0
- package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
- package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
- package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
- package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
- package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
- package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
- package/screenshots/truth/actions/send_email/render/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/actions/send_msg/render/long-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/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/formfield/markdown-errors.png +0 -0
- package/screenshots/truth/formfield/plain-text-errors.png +0 -0
- package/screenshots/truth/formfield/widget-only-markdown-errors.png +0 -0
- package/screenshots/truth/integration/checkbox-markdown-errors.png +0 -0
- package/src/events.ts +1 -40
- package/src/flow/{EditorNode.ts → CanvasNode.ts} +424 -48
- package/src/flow/Editor.ts +140 -4
- package/src/flow/NodeEditor.ts +1443 -0
- package/src/flow/Plumber.ts +0 -9
- package/src/flow/actions/add_contact_groups.ts +42 -0
- package/src/flow/actions/add_contact_urn.ts +17 -0
- package/src/flow/actions/add_input_labels.ts +12 -0
- package/src/flow/actions/call_classifier.ts +12 -0
- package/src/flow/actions/call_llm.ts +12 -0
- package/src/flow/actions/call_resthook.ts +12 -0
- package/src/flow/actions/call_webhook.ts +133 -0
- package/src/flow/actions/enter_flow.ts +15 -0
- package/src/flow/actions/open_ticket.ts +12 -0
- package/src/flow/actions/play_audio.ts +12 -0
- package/src/flow/actions/remove_contact_groups.ts +66 -0
- package/src/flow/actions/request_optin.ts +12 -0
- package/src/flow/actions/say_msg.ts +12 -0
- package/src/flow/actions/send_broadcast.ts +35 -0
- package/src/flow/actions/send_email.ts +60 -0
- package/src/flow/actions/send_msg.ts +58 -0
- package/src/flow/actions/set_contact_channel.ts +13 -0
- package/src/flow/actions/set_contact_field.ts +13 -0
- package/src/flow/actions/set_contact_language.ts +11 -0
- package/src/flow/actions/set_contact_name.ts +11 -0
- package/src/flow/actions/set_contact_status.ts +11 -0
- package/src/flow/actions/set_run_result.ts +11 -0
- package/src/flow/actions/split_by_expression_example.ts +88 -0
- package/src/flow/actions/start_session.ts +12 -0
- package/src/flow/actions/transfer_airtime.ts +12 -0
- package/src/flow/config.ts +93 -232
- package/src/flow/nodes/execute_actions.ts +5 -0
- package/src/flow/nodes/split_by_airtime.ts +9 -0
- package/src/flow/nodes/split_by_contact_field.ts +7 -0
- package/src/flow/nodes/split_by_expression.ts +7 -0
- package/src/flow/nodes/split_by_groups.ts +7 -0
- package/src/flow/nodes/split_by_random.ts +10 -0
- package/src/flow/nodes/split_by_run_result.ts +7 -0
- package/src/flow/nodes/split_by_scheme.ts +7 -0
- package/src/flow/nodes/split_by_subflow.ts +9 -0
- package/src/flow/nodes/split_by_webhook.ts +19 -0
- package/src/flow/nodes/wait_for_audio.ts +7 -0
- package/src/flow/nodes/wait_for_digits.ts +7 -0
- package/src/flow/nodes/wait_for_image.ts +7 -0
- package/src/flow/nodes/wait_for_location.ts +7 -0
- package/src/flow/nodes/wait_for_menu.ts +7 -0
- package/src/flow/nodes/wait_for_response.ts +7 -0
- package/src/flow/nodes/wait_for_video.ts +7 -0
- package/src/flow/types.ts +352 -0
- package/src/flow/utils.ts +76 -0
- package/src/form/ArrayEditor.ts +240 -0
- package/src/form/BaseListEditor.ts +177 -0
- package/src/form/Checkbox.ts +22 -3
- package/src/form/Completion.ts +6 -0
- package/src/form/FormField.ts +115 -11
- package/src/form/KeyValueEditor.ts +251 -0
- package/src/form/select/Select.ts +89 -32
- package/src/interfaces.ts +7 -2
- package/src/live/ContactChat.ts +3 -97
- package/src/store/flow-definition.d.ts +6 -1
- package/static/api/contacts.json +30 -0
- package/static/api/groups.json +4 -426
- package/static/api/locations.json +24 -0
- package/static/api/media.json +5 -0
- package/static/api/optins.json +16 -0
- package/static/api/orgs.json +13 -0
- package/static/api/topics.json +21 -0
- package/static/api/users.json +26 -0
- package/static/css/temba-components.css +3 -6
- package/temba-modules.ts +9 -2
- package/test/ActionHelper.ts +142 -0
- package/test/actions/add_contact_groups.test.ts +89 -0
- package/test/actions/remove_contact_groups.test.ts +265 -0
- package/test/actions/send_email.test.ts +214 -0
- package/test/actions/send_msg.test.ts +130 -0
- package/test/temba-action-editing-integration.test.ts +240 -0
- package/test/temba-checkbox.test.ts +1 -1
- package/test/temba-field-config.test.ts +152 -0
- package/test/temba-flow-editor-node.test.ts +18 -18
- package/test/temba-node-editor.test.ts +353 -0
- package/test/temba-select.test.ts +127 -0
- package/test-assets/contacts/history.json +11 -33
- package/web-dev-server.config.mjs +34 -0
- package/.github/workflows/coverage.yml +0 -80
- package/demo/sticky-note-demo.html +0 -155
- package/out-tsc/src/flow/EditorNode.js.map +0 -1
- package/out-tsc/src/flow/render.js +0 -358
- package/out-tsc/src/flow/render.js.map +0 -1
- package/out-tsc/test/temba-flow-render.test.js +0 -794
- package/out-tsc/test/temba-flow-render.test.js.map +0 -1
- package/src/flow/render.ts +0 -443
- package/test/temba-flow-render.test.ts +0 -1003
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { LitElement, TemplateResult, html } from 'lit';
|
|
2
|
+
import { property } from 'lit/decorators.js';
|
|
3
|
+
|
|
4
|
+
export interface ListItem {
|
|
5
|
+
[key: string]: any;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ListEditorConfig {
|
|
9
|
+
// Determines if empty items should be automatically maintained
|
|
10
|
+
maintainEmptyItem?: boolean;
|
|
11
|
+
// Function to check if an item is considered empty
|
|
12
|
+
isEmptyItem?: (item: ListItem) => boolean;
|
|
13
|
+
// Function to create a new empty item
|
|
14
|
+
createEmptyItem?: () => ListItem;
|
|
15
|
+
// Function to clean items before emitting (e.g., filter out empty items)
|
|
16
|
+
cleanItems?: (items: ListItem[]) => ListItem[];
|
|
17
|
+
// Minimum number of items to maintain
|
|
18
|
+
minItems?: number;
|
|
19
|
+
// Maximum number of items allowed
|
|
20
|
+
maxItems?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export abstract class BaseListEditor<
|
|
24
|
+
T extends ListItem = ListItem
|
|
25
|
+
> extends LitElement {
|
|
26
|
+
@property({ attribute: false })
|
|
27
|
+
protected _items: T[] = [];
|
|
28
|
+
|
|
29
|
+
@property({ type: Number })
|
|
30
|
+
minItems = 0;
|
|
31
|
+
|
|
32
|
+
@property({ type: Number })
|
|
33
|
+
maxItems?: number;
|
|
34
|
+
|
|
35
|
+
@property({ type: Boolean })
|
|
36
|
+
maintainEmptyItem = false;
|
|
37
|
+
|
|
38
|
+
// Abstract methods that must be implemented by subclasses
|
|
39
|
+
abstract isEmptyItem(item: T): boolean;
|
|
40
|
+
abstract createEmptyItem(): T;
|
|
41
|
+
abstract renderItem(item: T, index: number): TemplateResult;
|
|
42
|
+
|
|
43
|
+
// Optional methods that subclasses can override
|
|
44
|
+
protected getContainerClass(): string {
|
|
45
|
+
return 'base-list-editor';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
protected renderAddButton(): TemplateResult {
|
|
49
|
+
return html`
|
|
50
|
+
<button class="add-btn" @click=${() => this.addItem()}>Add Item</button>
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
protected shouldShowAddButton(): boolean {
|
|
55
|
+
return (
|
|
56
|
+
!this.maintainEmptyItem &&
|
|
57
|
+
(!this.maxItems || this._items.length < this.maxItems)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
render(): TemplateResult {
|
|
62
|
+
const items = this.displayItems;
|
|
63
|
+
|
|
64
|
+
return html`
|
|
65
|
+
<div class=${this.getContainerClass()}>
|
|
66
|
+
<div
|
|
67
|
+
class="list-items"
|
|
68
|
+
style="gap: 8px; display: grid; grid-template-columns: 1fr;"
|
|
69
|
+
>
|
|
70
|
+
${items.map((item, index) => this.renderItem(item, index))}
|
|
71
|
+
</div>
|
|
72
|
+
${this.shouldShowAddButton() ? this.renderAddButton() : ''}
|
|
73
|
+
</div>
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Optional method for cleaning items before emission (can return any type)
|
|
78
|
+
protected cleanItems(items: T[]): any {
|
|
79
|
+
if (!this.maintainEmptyItem) {
|
|
80
|
+
return items;
|
|
81
|
+
}
|
|
82
|
+
// Filter out empty items for the emitted value
|
|
83
|
+
return items.filter((item) => !this.isEmptyItem(item));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Get the items to display (may include empty items for UI)
|
|
87
|
+
protected get displayItems(): T[] {
|
|
88
|
+
const items = [...this._items];
|
|
89
|
+
|
|
90
|
+
if (this.maintainEmptyItem) {
|
|
91
|
+
const hasEmptyItem = items.some((item) => this.isEmptyItem(item));
|
|
92
|
+
if (!hasEmptyItem) {
|
|
93
|
+
items.push(this.createEmptyItem());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return items;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Handle changes to an item
|
|
101
|
+
protected handleItemChange(index: number, newItem: T) {
|
|
102
|
+
const updatedItems = [...this._items];
|
|
103
|
+
updatedItems[index] = newItem;
|
|
104
|
+
this.updateValue(updatedItems);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Handle field changes within an item (for complex items)
|
|
108
|
+
protected handleFieldChange(
|
|
109
|
+
index: number,
|
|
110
|
+
fieldName: string,
|
|
111
|
+
fieldValue: any
|
|
112
|
+
) {
|
|
113
|
+
const updatedItems = [...this._items];
|
|
114
|
+
const currentItem = updatedItems[index] || this.createEmptyItem();
|
|
115
|
+
|
|
116
|
+
updatedItems[index] = {
|
|
117
|
+
...currentItem,
|
|
118
|
+
[fieldName]: fieldValue
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
this.updateValue(updatedItems);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Add a new item
|
|
125
|
+
protected addItem(item?: T) {
|
|
126
|
+
if (this.maxItems && this._items.length >= this.maxItems) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const newItem = item || this.createEmptyItem();
|
|
131
|
+
const updatedItems = [...this._items, newItem];
|
|
132
|
+
this.updateValue(updatedItems);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Remove an item
|
|
136
|
+
protected removeItem(index: number) {
|
|
137
|
+
if (this._items.length <= this.minItems) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const updatedItems = this._items.filter((_, i) => i !== index);
|
|
142
|
+
this.updateValue(updatedItems);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check if an item can be removed
|
|
146
|
+
protected canRemoveItem(index: number): boolean {
|
|
147
|
+
const item = this.displayItems[index];
|
|
148
|
+
|
|
149
|
+
// Can't remove if it would go below minimum
|
|
150
|
+
if (this._items.length <= this.minItems) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Can't remove empty items if we're maintaining them
|
|
155
|
+
if (this.maintainEmptyItem && this.isEmptyItem(item)) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Update the value and emit change event
|
|
163
|
+
protected updateValue(newValue: T[]) {
|
|
164
|
+
this._items = newValue;
|
|
165
|
+
this.dispatchEvent(
|
|
166
|
+
new CustomEvent('change', {
|
|
167
|
+
detail: { value: this.cleanItems(newValue) },
|
|
168
|
+
bubbles: true
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Utility method for subclasses to check if two items are equal
|
|
174
|
+
protected itemsEqual(item1: T, item2: T): boolean {
|
|
175
|
+
return JSON.stringify(item1) === JSON.stringify(item2);
|
|
176
|
+
}
|
|
177
|
+
}
|
package/src/form/Checkbox.ts
CHANGED
|
@@ -75,8 +75,29 @@ export class Checkbox extends FormElement {
|
|
|
75
75
|
@property({ type: String })
|
|
76
76
|
animateChange = 'pulse';
|
|
77
77
|
|
|
78
|
+
public connectedCallback() {
|
|
79
|
+
super.connectedCallback();
|
|
80
|
+
}
|
|
81
|
+
|
|
78
82
|
public updated(changes: Map<string, any>) {
|
|
79
83
|
super.updated(changes);
|
|
84
|
+
|
|
85
|
+
// Normalize label property changes
|
|
86
|
+
if (changes.has('label')) {
|
|
87
|
+
// Normalize whitespace labels to empty string for proper behavior
|
|
88
|
+
if (
|
|
89
|
+
this.label &&
|
|
90
|
+
typeof this.label === 'string' &&
|
|
91
|
+
this.label.trim() === ''
|
|
92
|
+
) {
|
|
93
|
+
this.label = '';
|
|
94
|
+
}
|
|
95
|
+
// Ensure undefined labels remain as null to match test expectations
|
|
96
|
+
if (this.label === undefined) {
|
|
97
|
+
this.label = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
80
101
|
if (changes.has('checked') || changes.has('value')) {
|
|
81
102
|
if (this.checked || this.partial) {
|
|
82
103
|
this.internals.setFormValue(this.value || '1');
|
|
@@ -113,8 +134,6 @@ export class Checkbox extends FormElement {
|
|
|
113
134
|
animatechange="${this.animateChange}"
|
|
114
135
|
/>`;
|
|
115
136
|
|
|
116
|
-
this.label = this.label ? this.label.trim() : null;
|
|
117
|
-
|
|
118
137
|
return html`
|
|
119
138
|
<div class="wrapper ${this.label ? 'label' : ''}">
|
|
120
139
|
<temba-field
|
|
@@ -128,7 +147,7 @@ export class Checkbox extends FormElement {
|
|
|
128
147
|
>
|
|
129
148
|
<div class="checkbox-container ${this.disabled ? 'disabled' : ''}">
|
|
130
149
|
${icon}
|
|
131
|
-
${this.label
|
|
150
|
+
${this.label && String(this.label).trim()
|
|
132
151
|
? html`<div class="checkbox-label">${this.label}</div>`
|
|
133
152
|
: null}
|
|
134
153
|
</div>
|
package/src/form/Completion.ts
CHANGED
|
@@ -124,6 +124,9 @@ export class Completion extends FormElement {
|
|
|
124
124
|
@property({ type: Boolean })
|
|
125
125
|
autogrow = false;
|
|
126
126
|
|
|
127
|
+
@property({ type: Number })
|
|
128
|
+
minHeight: number;
|
|
129
|
+
|
|
127
130
|
private hiddenElement: HTMLInputElement;
|
|
128
131
|
private query: string;
|
|
129
132
|
|
|
@@ -290,6 +293,9 @@ export class Completion extends FormElement {
|
|
|
290
293
|
?autogrow=${this.autogrow}
|
|
291
294
|
?textarea=${this.textarea}
|
|
292
295
|
?submitOnEnter=${this.submitOnEnter}
|
|
296
|
+
style=${this.minHeight
|
|
297
|
+
? `--textarea-min-height: ${this.minHeight}px`
|
|
298
|
+
: ''}
|
|
293
299
|
>
|
|
294
300
|
</temba-textinput>
|
|
295
301
|
<temba-options
|
package/src/form/FormField.ts
CHANGED
|
@@ -47,15 +47,101 @@ export class FormField extends LitElement {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
.alert-error {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
position: absolute;
|
|
51
|
+
top: 100%;
|
|
52
|
+
left: 0;
|
|
53
|
+
right: 0;
|
|
54
|
+
z-index: 1000;
|
|
55
|
+
background: white;
|
|
56
|
+
border: 1px solid var(--color-error);
|
|
53
57
|
color: var(--color-error);
|
|
54
|
-
padding:
|
|
55
|
-
margin:
|
|
58
|
+
padding: 8px 12px;
|
|
59
|
+
margin: 2px 0 0 0;
|
|
56
60
|
border-radius: var(--curvature);
|
|
57
|
-
box-shadow: 0
|
|
58
|
-
0
|
|
61
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
62
|
+
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
63
|
+
font-size: 0.85em;
|
|
64
|
+
line-height: 1.2;
|
|
65
|
+
opacity: 0;
|
|
66
|
+
visibility: hidden;
|
|
67
|
+
transform: translateY(-12px);
|
|
68
|
+
transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out,
|
|
69
|
+
transform 0.2s ease-in-out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.field:hover .alert-error {
|
|
73
|
+
opacity: 1;
|
|
74
|
+
visibility: visible;
|
|
75
|
+
transform: translateY(2px);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Hide error popup when widget is focused */
|
|
79
|
+
.field:focus-within .alert-error {
|
|
80
|
+
opacity: 0;
|
|
81
|
+
visibility: hidden;
|
|
82
|
+
transform: translateY(-4px);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.field.has-error {
|
|
86
|
+
position: relative;
|
|
87
|
+
/* Set CSS custom properties that form components can use */
|
|
88
|
+
--color-widget-border: var(--color-error);
|
|
89
|
+
--widget-box-shadow-focused: var(
|
|
90
|
+
--widget-box-shadow-focused-error,
|
|
91
|
+
0 0 0 3px rgba(255, 99, 71, 0.3)
|
|
92
|
+
);
|
|
93
|
+
--color-focus: var(--color-error);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.field.has-error .widget {
|
|
97
|
+
border-radius: var(--curvature-widget);
|
|
98
|
+
position: relative;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Force error styling with higher specificity */
|
|
102
|
+
:host(.has-error) .field.has-error .widget .input-container,
|
|
103
|
+
:host(.has-error) .field.has-error .widget .select-container,
|
|
104
|
+
:host(.has-error) .field.has-error .widget .comp-container,
|
|
105
|
+
:host(.has-error) .field.has-error .widget .checkbox-container,
|
|
106
|
+
:host(.has-error) .field.has-error .widget .container,
|
|
107
|
+
:host(.has-error) .field.has-error .widget .range-container,
|
|
108
|
+
.field.has-error .widget .input-container,
|
|
109
|
+
.field.has-error .widget .select-container,
|
|
110
|
+
.field.has-error .widget .comp-container,
|
|
111
|
+
.field.has-error .widget .checkbox-container,
|
|
112
|
+
.field.has-error .widget .container,
|
|
113
|
+
.field.has-error .widget .range-container {
|
|
114
|
+
border-color: var(--color-error) !important;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* When error field is focused, use error-colored focus ring */
|
|
118
|
+
:host(.has-error) .field.has-error .widget .input-container:focus-within,
|
|
119
|
+
:host(.has-error) .field.has-error .widget .select-container:focus-within,
|
|
120
|
+
:host(.has-error) .field.has-error .widget .select-container.focused,
|
|
121
|
+
:host(.has-error) .field.has-error .widget .comp-container:focus-within,
|
|
122
|
+
:host(.has-error)
|
|
123
|
+
.field.has-error
|
|
124
|
+
.widget
|
|
125
|
+
.checkbox-container:focus-within,
|
|
126
|
+
:host(.has-error) .field.has-error .widget .container:focus-within,
|
|
127
|
+
:host(.has-error) .field.has-error .widget .range-container:focus-within,
|
|
128
|
+
.field.has-error .widget .input-container:focus-within,
|
|
129
|
+
.field.has-error .widget .select-container:focus-within,
|
|
130
|
+
.field.has-error .widget .select-container.focused,
|
|
131
|
+
.field.has-error .widget .comp-container:focus-within,
|
|
132
|
+
.field.has-error .widget .checkbox-container:focus-within,
|
|
133
|
+
.field.has-error .widget .container:focus-within,
|
|
134
|
+
.field.has-error .widget .range-container:focus-within {
|
|
135
|
+
border-color: var(--color-error) !important;
|
|
136
|
+
box-shadow: var(
|
|
137
|
+
--widget-box-shadow-focused-error,
|
|
138
|
+
0 0 0 3px rgba(255, 99, 71, 0.3)
|
|
139
|
+
) !important;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.alert-error p {
|
|
143
|
+
margin: 0;
|
|
144
|
+
padding: 0;
|
|
59
145
|
}
|
|
60
146
|
|
|
61
147
|
.disabled {
|
|
@@ -92,9 +178,23 @@ export class FormField extends LitElement {
|
|
|
92
178
|
@property({ type: Boolean })
|
|
93
179
|
disabled = false;
|
|
94
180
|
|
|
181
|
+
updated(changedProperties: Map<string | number | symbol, unknown>): void {
|
|
182
|
+
super.updated(changedProperties);
|
|
183
|
+
|
|
184
|
+
if (
|
|
185
|
+
changedProperties.has('errors') ||
|
|
186
|
+
changedProperties.has('hideErrors')
|
|
187
|
+
) {
|
|
188
|
+
const hasErrors =
|
|
189
|
+
!this.hideErrors && this.errors && this.errors.length > 0;
|
|
190
|
+
this.classList.toggle('has-error', hasErrors);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
95
194
|
public render(): TemplateResult {
|
|
96
|
-
const
|
|
97
|
-
|
|
195
|
+
const hasErrors = !this.hideErrors && this.errors && this.errors.length > 0;
|
|
196
|
+
const errors = hasErrors
|
|
197
|
+
? this.errors.map((error: string) => {
|
|
98
198
|
return html`
|
|
99
199
|
<div class="alert-error">${renderMarkdown(error)}</div>
|
|
100
200
|
`;
|
|
@@ -109,7 +209,11 @@ export class FormField extends LitElement {
|
|
|
109
209
|
}
|
|
110
210
|
|
|
111
211
|
return html`
|
|
112
|
-
<div
|
|
212
|
+
<div
|
|
213
|
+
class="field ${this.disabled ? 'disabled' : ''} ${hasErrors
|
|
214
|
+
? 'has-error'
|
|
215
|
+
: ''}"
|
|
216
|
+
>
|
|
113
217
|
${!!this.name && !this.hideLabel && !!this.label
|
|
114
218
|
? html`
|
|
115
219
|
<label class="control-label" for="${this.name}"
|
|
@@ -119,6 +223,7 @@ export class FormField extends LitElement {
|
|
|
119
223
|
: null}
|
|
120
224
|
<div class="widget">
|
|
121
225
|
<slot></slot>
|
|
226
|
+
${errors}
|
|
122
227
|
</div>
|
|
123
228
|
${this.helpText && this.helpText !== 'None'
|
|
124
229
|
? html`
|
|
@@ -127,7 +232,6 @@ export class FormField extends LitElement {
|
|
|
127
232
|
</div>
|
|
128
233
|
`
|
|
129
234
|
: null}
|
|
130
|
-
${errors}
|
|
131
235
|
</div>
|
|
132
236
|
`;
|
|
133
237
|
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { html, css, TemplateResult } from 'lit';
|
|
2
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
3
|
+
import { BaseListEditor, ListItem } from './BaseListEditor';
|
|
4
|
+
|
|
5
|
+
interface KeyValueItem extends ListItem {
|
|
6
|
+
key: string;
|
|
7
|
+
value: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@customElement('temba-key-value-editor')
|
|
11
|
+
export class KeyValueEditor extends BaseListEditor<KeyValueItem> {
|
|
12
|
+
@property({ type: String })
|
|
13
|
+
keyPlaceholder = 'Key';
|
|
14
|
+
|
|
15
|
+
@property({ type: String })
|
|
16
|
+
valuePlaceholder = 'Value';
|
|
17
|
+
|
|
18
|
+
@property({ type: Boolean })
|
|
19
|
+
showValidation = true;
|
|
20
|
+
|
|
21
|
+
@state()
|
|
22
|
+
private keyErrors: { [index: number]: string } = {};
|
|
23
|
+
|
|
24
|
+
// Configure to maintain empty items
|
|
25
|
+
maintainEmptyItem = true;
|
|
26
|
+
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
this._items = [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// External API uses array format to preserve duplicate keys
|
|
33
|
+
@property({ type: Array })
|
|
34
|
+
get value(): KeyValueItem[] {
|
|
35
|
+
return this._items.filter(
|
|
36
|
+
({ key, value }) => key.trim() !== '' || value.trim() !== ''
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
set value(newValue: KeyValueItem[] | Record<string, string>) {
|
|
41
|
+
if (Array.isArray(newValue)) {
|
|
42
|
+
this._items = [...newValue];
|
|
43
|
+
} else {
|
|
44
|
+
// Convert Record to array format
|
|
45
|
+
this._items = Object.entries(newValue || {}).map(([key, value]) => ({
|
|
46
|
+
key,
|
|
47
|
+
value: typeof value === 'string' ? value : String(value)
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
this.requestUpdate();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Implement abstract methods
|
|
54
|
+
isEmptyItem(item: KeyValueItem): boolean {
|
|
55
|
+
return item.key.trim() === '' && item.value.trim() === '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
createEmptyItem(): KeyValueItem {
|
|
59
|
+
return { key: '', value: '' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Override cleanItems to return array format to preserve duplicate keys
|
|
63
|
+
protected cleanItems(items: KeyValueItem[]): KeyValueItem[] {
|
|
64
|
+
return items.filter(
|
|
65
|
+
({ key, value }) => key.trim() !== '' || value.trim() !== ''
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Method to convert to Record format for final form submission
|
|
70
|
+
toRecord(): Record<string, string> {
|
|
71
|
+
const result: Record<string, string> = {};
|
|
72
|
+
this._items.forEach(({ key, value }) => {
|
|
73
|
+
if (key.trim() !== '' || value.trim() !== '') {
|
|
74
|
+
result[key] = value;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Method to validate and set key errors for duplicates and empty keys with values
|
|
81
|
+
validateKeys(): boolean {
|
|
82
|
+
const newKeyErrors: { [index: number]: string } = {};
|
|
83
|
+
|
|
84
|
+
// Check for empty keys with values
|
|
85
|
+
this._items.forEach(({ key, value }, index) => {
|
|
86
|
+
if (key.trim() === '' && value.trim() !== '') {
|
|
87
|
+
newKeyErrors[index] = 'Key is required when value is provided';
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Check for duplicate keys (only non-empty ones)
|
|
92
|
+
const nonEmptyKeys = this._items
|
|
93
|
+
.map(({ key }, index) => ({ key: key.trim(), index }))
|
|
94
|
+
.filter(({ key }) => key !== '');
|
|
95
|
+
|
|
96
|
+
const keyCount = new Map<string, number[]>();
|
|
97
|
+
nonEmptyKeys.forEach(({ key, index }) => {
|
|
98
|
+
if (!keyCount.has(key)) {
|
|
99
|
+
keyCount.set(key, []);
|
|
100
|
+
}
|
|
101
|
+
keyCount.get(key)!.push(index);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Mark duplicate keys with errors
|
|
105
|
+
keyCount.forEach((indices, key) => {
|
|
106
|
+
if (indices.length > 1) {
|
|
107
|
+
indices.forEach((index) => {
|
|
108
|
+
// Only show duplicate error if there's no empty key error already
|
|
109
|
+
if (!newKeyErrors[index]) {
|
|
110
|
+
newKeyErrors[index] = `Duplicate key "${key}"`;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
this.keyErrors = newKeyErrors;
|
|
117
|
+
return Object.keys(newKeyErrors).length === 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Clear key errors
|
|
121
|
+
clearKeyErrors(): void {
|
|
122
|
+
this.keyErrors = {};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Override updateValue to emit array format and validate keys
|
|
126
|
+
protected updateValue(newValue: KeyValueItem[]) {
|
|
127
|
+
this._items = newValue;
|
|
128
|
+
|
|
129
|
+
// Clear errors and re-validate when items change
|
|
130
|
+
this.clearKeyErrors();
|
|
131
|
+
this.validateKeys();
|
|
132
|
+
|
|
133
|
+
this.dispatchEvent(
|
|
134
|
+
new CustomEvent('change', {
|
|
135
|
+
detail: { value: this.cleanItems(newValue) },
|
|
136
|
+
bubbles: true
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
this.requestUpdate();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private handleKeyChange(index: number, newKey: string) {
|
|
143
|
+
const items = this.displayItems;
|
|
144
|
+
const currentItem = items[index];
|
|
145
|
+
|
|
146
|
+
// Clear any existing error for this key when it's modified
|
|
147
|
+
if (this.keyErrors[index]) {
|
|
148
|
+
const newKeyErrors = { ...this.keyErrors };
|
|
149
|
+
delete newKeyErrors[index];
|
|
150
|
+
this.keyErrors = newKeyErrors;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.handleItemChange(index, {
|
|
154
|
+
key: newKey,
|
|
155
|
+
value: currentItem.value
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private handleValueChange(index: number, newValue: string) {
|
|
160
|
+
const items = this.displayItems;
|
|
161
|
+
const currentItem = items[index];
|
|
162
|
+
|
|
163
|
+
// Clear any existing error for this key when value is modified
|
|
164
|
+
if (this.keyErrors[index]) {
|
|
165
|
+
const newKeyErrors = { ...this.keyErrors };
|
|
166
|
+
delete newKeyErrors[index];
|
|
167
|
+
this.keyErrors = newKeyErrors;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.handleItemChange(index, {
|
|
171
|
+
key: currentItem.key,
|
|
172
|
+
value: newValue
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
renderItem(item: KeyValueItem, index: number): TemplateResult {
|
|
177
|
+
const canRemove = this.canRemoveItem(index);
|
|
178
|
+
const keyError =
|
|
179
|
+
this.showValidation && this.keyErrors[index] ? this.keyErrors[index] : '';
|
|
180
|
+
|
|
181
|
+
return html`
|
|
182
|
+
<div class="row">
|
|
183
|
+
<temba-textinput
|
|
184
|
+
.value=${item.key}
|
|
185
|
+
.placeholder=${this.keyPlaceholder}
|
|
186
|
+
.errors=${keyError ? [keyError] : []}
|
|
187
|
+
@change=${(e: any) => this.handleKeyChange(index, e.target.value)}
|
|
188
|
+
></temba-textinput>
|
|
189
|
+
<temba-textinput
|
|
190
|
+
.value=${item.value}
|
|
191
|
+
.placeholder=${this.valuePlaceholder}
|
|
192
|
+
@change=${(e: any) => this.handleValueChange(index, e.target.value)}
|
|
193
|
+
></temba-textinput>
|
|
194
|
+
${canRemove
|
|
195
|
+
? html`
|
|
196
|
+
<button class="remove-btn" @click=${() => this.removeItem(index)}>
|
|
197
|
+
×
|
|
198
|
+
</button>
|
|
199
|
+
`
|
|
200
|
+
: html`<div class="remove-btn-spacer"></div>`}
|
|
201
|
+
</div>
|
|
202
|
+
`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
protected getContainerClass(): string {
|
|
206
|
+
return 'key-value-editor';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
static styles = css`
|
|
210
|
+
.key-value-editor {
|
|
211
|
+
display: flex;
|
|
212
|
+
flex-direction: column;
|
|
213
|
+
gap: 8px;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.row {
|
|
217
|
+
display: grid;
|
|
218
|
+
grid-template-columns: 1fr 1fr auto;
|
|
219
|
+
gap: 8px;
|
|
220
|
+
align-items: center;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.remove-btn {
|
|
224
|
+
width: 32px;
|
|
225
|
+
height: 32px;
|
|
226
|
+
border: 1px solid #ccc;
|
|
227
|
+
border-radius: 4px;
|
|
228
|
+
background: #f8f8f8;
|
|
229
|
+
color: #666;
|
|
230
|
+
cursor: pointer;
|
|
231
|
+
display: flex;
|
|
232
|
+
align-items: center;
|
|
233
|
+
justify-content: center;
|
|
234
|
+
font-size: 18px;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.remove-btn:hover:not(:disabled) {
|
|
238
|
+
background: #f0f0f0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.remove-btn:disabled {
|
|
242
|
+
opacity: 0.5;
|
|
243
|
+
cursor: not-allowed;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.remove-btn-spacer {
|
|
247
|
+
width: 32px;
|
|
248
|
+
height: 32px;
|
|
249
|
+
}
|
|
250
|
+
`;
|
|
251
|
+
}
|