From 138840a23fae24ab67ff1a9ceba800dadb9772e8 Mon Sep 17 00:00:00 2001
From: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com>
Date: Tue, 17 Feb 2026 15:40:50 +0530
Subject: [PATCH 1/6] fix: typo in metadata key in captain v2 (#13558)
# Pull Request Template
## Description
## Type of change
typo fix
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
---
.../services/captain/assistant/agent_runner_service.rb | 4 ++--
.../captain/assistant/agent_runner_service_spec.rb | 8 ++++----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/enterprise/app/services/captain/assistant/agent_runner_service.rb b/enterprise/app/services/captain/assistant/agent_runner_service.rb
index a40e096e6..c832da93e 100644
--- a/enterprise/app/services/captain/assistant/agent_runner_service.rb
+++ b/enterprise/app/services/captain/assistant/agent_runner_service.rb
@@ -189,8 +189,8 @@ class Captain::Assistant::AgentRunnerService
root_span = context_wrapper&.context&.dig(:__otel_tracing, :root_span)
return unless root_span
- credits_used = !context_wrapper.context[:captain_v2_handoff_tool_called]
- root_span.set_attribute(format(ATTR_LANGFUSE_METADATA, 'credits_used'), credits_used)
+ credit_used = !context_wrapper.context[:captain_v2_handoff_tool_called]
+ root_span.set_attribute(format(ATTR_LANGFUSE_METADATA, 'credit_used'), credit_used.to_s)
end
def add_agent_thinking_callback(runner)
diff --git a/spec/enterprise/services/captain/assistant/agent_runner_service_spec.rb b/spec/enterprise/services/captain/assistant/agent_runner_service_spec.rb
index 04fa0f967..bbf712622 100644
--- a/spec/enterprise/services/captain/assistant/agent_runner_service_spec.rb
+++ b/spec/enterprise/services/captain/assistant/agent_runner_service_spec.rb
@@ -308,7 +308,7 @@ RSpec.describe Captain::Assistant::AgentRunnerService do
end
describe '#add_usage_metadata_callback' do
- it 'sets credits_used=false when handoff tool is used' do
+ it 'sets credit_used=false when handoff tool is used' do
service = described_class.new(assistant: assistant, conversation: conversation)
runner = instance_double(Agents::AgentRunner)
tool_complete_callback = nil
@@ -333,11 +333,11 @@ RSpec.describe Captain::Assistant::AgentRunnerService do
tool_complete_callback.call(Captain::Tools::HandoffTool.new(assistant).name, 'ok', context_wrapper)
- expect(root_span).to receive(:set_attribute).with('langfuse.trace.metadata.credits_used', false)
+ expect(root_span).to receive(:set_attribute).with('langfuse.trace.metadata.credit_used', 'false')
run_complete_callback.call('assistant', nil, context_wrapper)
end
- it 'sets credits_used=true when handoff tool is not used' do
+ it 'sets credit_used=true when handoff tool is not used' do
service = described_class.new(assistant: assistant, conversation: conversation)
runner = instance_double(Agents::AgentRunner)
run_complete_callback = nil
@@ -356,7 +356,7 @@ RSpec.describe Captain::Assistant::AgentRunnerService do
service.send(:add_usage_metadata_callback, runner)
- expect(root_span).to receive(:set_attribute).with('langfuse.trace.metadata.credits_used', true)
+ expect(root_span).to receive(:set_attribute).with('langfuse.trace.metadata.credit_used', 'true')
run_complete_callback.call('assistant', nil, context_wrapper)
end
end
From 229f56d6e3422979ef52bcd2d9093df8aeb8303c Mon Sep 17 00:00:00 2001
From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Date: Tue, 17 Feb 2026 16:40:12 +0530
Subject: [PATCH 2/6] chore: Remove `vue-multiselect` and migrate to next
components (#13506)
# Pull Request Template
## Description
This PR includes:
1. Removes multiselect usage from the Merge Contact modal (Conversation
sidebar) and replaces it with the existing component used on the Contact
Details page.
2. Replaces legacy form and multiselect elements in Add and Edit
automations flows with next components.**(Also check Macros)**
3. Replace multiselect with ComboBox in contact form country field.
4. Replace multiselect with TagInput in create/edit attribute form.
5. Replace multiselect with TagInput for agent selection in inbox
creation.
6. Replace multiselect with ComboBox in Facebook channel page selection
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
**Screenshots**
1. **Merge modal**
2. **Automations**
3. **Country field**
4. **Add/Edit attribute form**
5. **Agent selection in inbox creation**
7. **Facebook channel page selection**
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Shivam Mishra
---
.../ContactListHeaderWrapper.vue | 3 +-
.../components/EmailOptions.vue | 2 +
.../components-next/combobox/ComboBox.vue | 9 +-
.../components-next/dialog/Dialog.vue | 22 +-
.../dropdown-menu/base/DropdownSection.vue | 9 +-
.../components-next/filter/ConditionRow.vue | 16 +-
.../filter/inputs/FilterSelect.vue | 27 +-
.../filter/inputs/MultiSelect.vue | 8 +-
.../filter/inputs/SingleSelect.vue | 16 +-
.../components-next/taginput/TagInput.vue | 59 ++-
.../helper/spec/tagInputHelper.spec.js | 65 +++
.../taginput/helper/tagInputHelper.js | 14 +-
.../widgets/AutomationActionInput.vue | 264 ++++-------
.../AutomationActionTeamMessageInput.vue | 58 +--
.../widgets/AutomationFileInput.vue | 2 +-
.../components/widgets/FilterInput/Index.vue | 305 -------------
.../dashboard/composables/useAutomation.js | 16 +-
.../dashboard/helper/automationHelper.js | 8 +-
.../modules/contact/ContactMergeModal.vue | 162 +++----
.../components/ContactDropdownItem.vue | 87 ----
.../contact/components/MergeContact.vue | 268 ++++--------
.../conversation/contact/ContactForm.vue | 53 ++-
.../conversation/contact/ContactInfo.vue | 13 +-
.../settings/attributes/AddAttribute.vue | 86 ++--
.../settings/attributes/EditAttribute.vue | 78 ++--
.../settings/automation/AddAutomationRule.vue | 381 +++-------------
.../automation/AutomationRuleForm.vue | 414 ++++++++++++++++++
.../automation/EditAutomationRule.vue | 401 +++--------------
.../dashboard/settings/automation/Index.vue | 43 +-
.../dashboard/settings/inbox/AddAgents.vue | 71 ++-
.../settings/inbox/channels/Facebook.vue | 44 +-
31 files changed, 1209 insertions(+), 1795 deletions(-)
delete mode 100644 app/javascript/dashboard/components/widgets/FilterInput/Index.vue
delete mode 100644 app/javascript/dashboard/modules/contact/components/ContactDropdownItem.vue
create mode 100644 app/javascript/dashboard/routes/dashboard/settings/automation/AutomationRuleForm.vue
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue
index 0b4880264..3d4b5c8a5 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue
@@ -68,8 +68,7 @@ const hasActiveSegments = computed(
);
const activeSegmentName = computed(() => props.activeSegment?.name);
-const openCreateNewContactDialog = async () => {
- await createNewContactDialogRef.value?.contactsFormRef.resetValidation();
+const openCreateNewContactDialog = () => {
createNewContactDialogRef.value?.dialogRef.open();
};
const openContactImportDialog = () =>
diff --git a/app/javascript/dashboard/components-next/NewConversation/components/EmailOptions.vue b/app/javascript/dashboard/components-next/NewConversation/components/EmailOptions.vue
index ac7e8d659..4e0c71adb 100644
--- a/app/javascript/dashboard/components-next/NewConversation/components/EmailOptions.vue
+++ b/app/javascript/dashboard/components-next/NewConversation/components/EmailOptions.vue
@@ -95,6 +95,7 @@ const inputClass = computed(() => {
:show-dropdown="showCcEmailsDropdown"
:is-loading="isLoading"
type="email"
+ allow-create
class="flex-1 min-h-7"
@focus="emit('updateDropdown', 'cc', true)"
@input="emit('searchCcEmails', $event)"
@@ -127,6 +128,7 @@ const inputClass = computed(() => {
:show-dropdown="showBccEmailsDropdown"
:is-loading="isLoading"
type="email"
+ allow-create
class="flex-1 min-h-7"
focus-on-mount
@focus="emit('updateDropdown', 'bcc', true)"
diff --git a/app/javascript/dashboard/components-next/combobox/ComboBox.vue b/app/javascript/dashboard/components-next/combobox/ComboBox.vue
index d50134e88..58d9e634d 100644
--- a/app/javascript/dashboard/components-next/combobox/ComboBox.vue
+++ b/app/javascript/dashboard/components-next/combobox/ComboBox.vue
@@ -56,8 +56,13 @@ const selectedLabel = computed(() => {
});
const selectOption = option => {
- selectedValue.value = option.value;
- emit('update:modelValue', option.value);
+ if (selectedValue.value === option.value) {
+ selectedValue.value = '';
+ emit('update:modelValue', '');
+ } else {
+ selectedValue.value = option.value;
+ emit('update:modelValue', option.value);
+ }
open.value = false;
search.value = '';
};
diff --git a/app/javascript/dashboard/components-next/dialog/Dialog.vue b/app/javascript/dashboard/components-next/dialog/Dialog.vue
index 78ca5206b..8a5439554 100644
--- a/app/javascript/dashboard/components-next/dialog/Dialog.vue
+++ b/app/javascript/dashboard/components-next/dialog/Dialog.vue
@@ -53,6 +53,11 @@ const props = defineProps({
default: 'lg',
validator: value => ['3xl', '2xl', 'xl', 'lg', 'md', 'sm'].includes(value),
},
+ position: {
+ type: String,
+ default: 'center',
+ validator: value => ['center', 'top'].includes(value),
+ },
});
const emit = defineEmits(['confirm', 'close']);
@@ -61,6 +66,7 @@ const { t } = useI18n();
const dialogRef = ref(null);
const dialogContentRef = ref(null);
+const isOpen = ref(false);
const maxWidthClass = computed(() => {
const classesMap = {
@@ -75,13 +81,19 @@ const maxWidthClass = computed(() => {
return classesMap[props.width] ?? 'max-w-md';
});
+const positionClass = computed(() =>
+ props.position === 'top' ? 'dialog-position-top' : ''
+);
+
const open = () => {
+ isOpen.value = true;
dialogRef.value?.showModal();
};
const close = () => {
emit('close');
dialogRef.value?.close();
+ isOpen.value = false;
};
const confirm = () => {
@@ -98,6 +110,7 @@ defineExpose({ open, close });
class="w-full transition-all duration-300 ease-in-out shadow-xl rounded-xl"
:class="[
maxWidthClass,
+ positionClass,
overflowYAuto ? 'overflow-y-auto' : 'overflow-visible',
]"
@close="close"
@@ -105,7 +118,7 @@ defineExpose({ open, close });
-
+
diff --git a/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownSection.vue b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownSection.vue
index 54872c440..5fc63b85c 100644
--- a/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownSection.vue
+++ b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownSection.vue
@@ -4,6 +4,10 @@ defineProps({
type: String,
default: '',
},
+ height: {
+ type: String,
+ default: 'max-h-96',
+ },
});
@@ -15,7 +19,10 @@ defineProps({
>
{{ title }}
-
+
diff --git a/app/javascript/dashboard/components-next/filter/ConditionRow.vue b/app/javascript/dashboard/components-next/filter/ConditionRow.vue
index c74879a58..510ddbde3 100644
--- a/app/javascript/dashboard/components-next/filter/ConditionRow.vue
+++ b/app/javascript/dashboard/components-next/filter/ConditionRow.vue
@@ -50,12 +50,12 @@ const currentFilter = computed(() =>
);
const getOperator = (filter, selectedOperator) => {
- const operatorFromOptions = filter.filterOperators.find(
+ const operatorFromOptions = filter?.filterOperators?.find(
operator => operator.value === selectedOperator
);
if (!operatorFromOptions) {
- return filter.filterOperators[0];
+ return filter?.filterOperators?.[0];
}
return operatorFromOptions;
@@ -138,7 +138,11 @@ const validate = () => {
return !validationError.value;
};
-defineExpose({ validate });
+const resetValidation = () => {
+ showErrors.value = false;
+};
+
+defineExpose({ validate, resetValidation });
@@ -166,18 +170,20 @@ defineExpose({ validate });
-
+
{
- return props.options.find(o => o.value === selected.value) || {};
+ return props.options?.find(o => o.value === selected.value) || {};
});
const iconToRender = computed(() => {
@@ -87,18 +87,25 @@ const updateSelected = newValue => {
-
-
+
+
+ -
+ {{ option.label }}
+
+
+
diff --git a/app/javascript/dashboard/components-next/filter/inputs/MultiSelect.vue b/app/javascript/dashboard/components-next/filter/inputs/MultiSelect.vue
index 7f6bab135..f64c8eb76 100644
--- a/app/javascript/dashboard/components-next/filter/inputs/MultiSelect.vue
+++ b/app/javascript/dashboard/components-next/filter/inputs/MultiSelect.vue
@@ -8,7 +8,7 @@ import DropdownSection from 'next/dropdown-menu/base/DropdownSection.vue';
import DropdownBody from 'next/dropdown-menu/base/DropdownBody.vue';
import DropdownItem from 'next/dropdown-menu/base/DropdownItem.vue';
-const { options, maxChips } = defineProps({
+const { options, maxChips, dropdownMaxHeight } = defineProps({
options: {
type: Array,
required: true,
@@ -17,6 +17,10 @@ const { options, maxChips } = defineProps({
type: Number,
default: 3,
},
+ dropdownMaxHeight: {
+ type: String,
+ default: 'max-h-80',
+ },
});
const { t } = useI18n();
@@ -123,7 +127,7 @@ const toggleOption = option => {
-
+
{
const optionToSearch = Array.isArray(selected.value)
? selected.value[0]
: selected.value;
+
+ if (!optionToSearch) return null;
// extract the selected item from the options array
// this ensures that options like icon is also included
return options.find(option => option.id === optionToSearch.id);
@@ -77,7 +89,7 @@ const toggleSelected = option => {
};
if (selected.value && selected.value.id === optionToToggle.id) {
- selected.value = null;
+ if (!disableDeselect) selected.value = null;
} else {
selected.value = optionToToggle;
}
@@ -124,7 +136,7 @@ const toggleSelected = option => {
:placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')"
/>
-
+
getValidationRules(props.type));
const v$ = useVuelidate(rules, { newTag });
@@ -74,11 +79,11 @@ const showInput = computed(() =>
const showDropdownMenu = computed(() =>
props.mode === MODE.SINGLE && tags.value.length >= 1
? false
- : props.showDropdown
+ : props.showDropdown && isFocused.value
);
-const filteredMenuItems = computed(() =>
- buildTagMenuItems({
+const filteredMenuItems = computed(() => {
+ const items = buildTagMenuItems({
mode: props.mode,
tags: tags.value,
menuItems: props.menuItems,
@@ -86,8 +91,14 @@ const filteredMenuItems = computed(() =>
isLoading: props.isLoading,
type: props.type,
isNewTagInValidType: isNewTagInValidType.value,
- })
-);
+ allowCreate: props.allowCreate,
+ skipLabelDedup: props.skipLabelDedup,
+ });
+ if (props.type !== INPUT_TYPES.TEXT) return items;
+ const query = newTag.value?.trim()?.toLowerCase();
+ if (!query) return items;
+ return items.filter(item => item.label?.toLowerCase().includes(query));
+});
const emitDataOnAdd = value => {
const matchingMenuItem = findMatchingMenuItem(props.menuItems, value);
@@ -112,10 +123,13 @@ const addTag = async () => {
return;
}
- if (
- [INPUT_TYPES.EMAIL, INPUT_TYPES.TEL].includes(props.type) ||
- props.allowCreate
- ) {
+ const isValidatedType = [INPUT_TYPES.EMAIL, INPUT_TYPES.TEL].includes(
+ props.type
+ );
+
+ if (!isValidatedType && !props.allowCreate && props.showDropdown) return;
+
+ if (isValidatedType || props.allowCreate) {
if (!(await v$.value.$validate())) return;
emitDataOnAdd(trimmedTag);
}
@@ -125,28 +139,31 @@ const addTag = async () => {
const removeTag = index => {
tags.value.splice(index, 1);
modelValue.value = tags.value;
- emit('remove');
+ emit('remove', index);
};
const handleDropdownAction = async ({
email: emailAddress,
phoneNumber,
+ label,
...rest
}) => {
if (props.mode === MODE.SINGLE && tags.value.length >= 1) return;
if (!props.showDropdown) return;
- const isEmail = props.type === 'email';
- newTag.value = isEmail ? emailAddress : phoneNumber;
+ const isEmail = props.type === INPUT_TYPES.EMAIL;
+ const tagValue = isEmail ? emailAddress : phoneNumber || label;
- if (!(await v$.value.$validate())) return;
+ if (isEmail || props.type === INPUT_TYPES.TEL) {
+ newTag.value = tagValue;
+ if (!(await v$.value.$validate())) return;
+ }
- const payload = isEmail
- ? { email: emailAddress, ...rest }
- : { phoneNumber, ...rest };
-
- emit('add', payload);
- updateValueAndFocus(emailAddress);
+ emit(
+ 'add',
+ isEmail ? { email: emailAddress, ...rest } : { phoneNumber, label, ...rest }
+ );
+ updateValueAndFocus(tagValue);
};
const handleFocus = () => {
@@ -163,7 +180,7 @@ const handleKeydown = event => {
};
const handleClickOutside = () => {
- if (tags.value.length) isFocused.value = false;
+ isFocused.value = false;
emit('onClickOutside');
};
diff --git a/app/javascript/dashboard/components-next/taginput/helper/spec/tagInputHelper.spec.js b/app/javascript/dashboard/components-next/taginput/helper/spec/tagInputHelper.spec.js
index f1886de22..24ab3339e 100644
--- a/app/javascript/dashboard/components-next/taginput/helper/spec/tagInputHelper.spec.js
+++ b/app/javascript/dashboard/components-next/taginput/helper/spec/tagInputHelper.spec.js
@@ -261,6 +261,71 @@ describe('tagInputHelper', () => {
});
expect(result).toEqual(menuItems);
});
+
+ it('does not create suggestion when allowCreate is false', () => {
+ const result = buildTagMenuItems({
+ ...baseParams,
+ type: INPUT_TYPES.EMAIL,
+ newTag: 'test@example.com',
+ allowCreate: false,
+ });
+ expect(result).toEqual([]);
+ });
+
+ it('still returns available menu items when allowCreate is false', () => {
+ const menuItems = [
+ { label: 'Agent 1', value: '1' },
+ { label: 'Agent 2', value: '2' },
+ ];
+ const result = buildTagMenuItems({
+ ...baseParams,
+ menuItems,
+ newTag: 'Agent',
+ allowCreate: false,
+ });
+ expect(result).toEqual(menuItems);
+ });
+
+ it('creates new item when allowCreate is true (default)', () => {
+ const result = buildTagMenuItems({
+ ...baseParams,
+ type: INPUT_TYPES.EMAIL,
+ newTag: 'test@example.com',
+ allowCreate: true,
+ });
+ expect(result).toHaveLength(1);
+ expect(result[0]).toMatchObject({
+ label: 'test@example.com',
+ action: 'create',
+ });
+ });
+
+ it('skips label dedup when skipLabelDedup is true', () => {
+ const menuItems = [
+ { label: 'HDMA', value: 1 },
+ { label: 'HDMA', value: 2 },
+ ];
+ const result = buildTagMenuItems({
+ ...baseParams,
+ tags: ['HDMA'],
+ menuItems,
+ skipLabelDedup: true,
+ });
+ expect(result).toEqual(menuItems);
+ });
+
+ it('filters by label when skipLabelDedup is false (default)', () => {
+ const menuItems = [
+ { label: 'HDMA', value: 1 },
+ { label: 'HDMA', value: 2 },
+ ];
+ const result = buildTagMenuItems({
+ ...baseParams,
+ tags: ['HDMA'],
+ menuItems,
+ });
+ expect(result).toEqual([]);
+ });
});
describe('canAddTag', () => {
diff --git a/app/javascript/dashboard/components-next/taginput/helper/tagInputHelper.js b/app/javascript/dashboard/components-next/taginput/helper/tagInputHelper.js
index fb0af711e..21426ca7b 100644
--- a/app/javascript/dashboard/components-next/taginput/helper/tagInputHelper.js
+++ b/app/javascript/dashboard/components-next/taginput/helper/tagInputHelper.js
@@ -84,25 +84,29 @@ export const buildTagMenuItems = ({
isLoading,
type,
isNewTagInValidType,
+ allowCreate = true,
+ skipLabelDedup = false,
}) => {
if (mode === MODE.SINGLE && tags.length >= 1) return [];
- const availableMenuItems = menuItems.filter(
- item => !tags.includes(item.label)
- );
+ const availableMenuItems = skipLabelDedup
+ ? menuItems
+ : menuItems.filter(item => !tags.includes(item.label));
// Show typed value as suggestion only if:
// 1. There's a value being typed
// 2. The value isn't already in the tags
// 3. Validation passes (email/phone) and There are no menu items available
+ // 4. allowCreate is enabled
const trimmedNewTag = newTag?.trim();
- const shouldShowTypedValue =
+ const shouldShowCreateSuggestion =
+ allowCreate &&
trimmedNewTag &&
!tags.includes(trimmedNewTag) &&
!isLoading &&
!availableMenuItems.length;
- if (shouldShowTypedValue) {
+ if (shouldShowCreateSuggestion) {
const { isValid, formattedValue } = validateAndFormatNewTag(
trimmedNewTag,
type,
diff --git a/app/javascript/dashboard/components/widgets/AutomationActionInput.vue b/app/javascript/dashboard/components/widgets/AutomationActionInput.vue
index 5f9aa8f0d..b0b30c2fa 100644
--- a/app/javascript/dashboard/components/widgets/AutomationActionInput.vue
+++ b/app/javascript/dashboard/components/widgets/AutomationActionInput.vue
@@ -3,6 +3,9 @@ import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput
import AutomationActionFileInput from './AutomationFileInput.vue';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
+import SingleSelect from 'dashboard/components-next/filter/inputs/SingleSelect.vue';
+import MultiSelect from 'dashboard/components-next/filter/inputs/MultiSelect.vue';
+import NextInput from 'dashboard/components-next/input/Input.vue';
export default {
components: {
@@ -10,6 +13,9 @@ export default {
AutomationActionFileInput,
WootMessageEditor,
NextButton,
+ SingleSelect,
+ MultiSelect,
+ NextInput,
},
props: {
modelValue: {
@@ -40,6 +46,10 @@ export default {
type: Boolean,
default: false,
},
+ dropdownMaxHeight: {
+ type: String,
+ default: 'max-h-80',
+ },
},
emits: ['update:modelValue', 'input', 'removeAction', 'resetAction'],
computed: {
@@ -69,11 +79,21 @@ export default {
return this.actionTypes.find(action => action.key === this.action_name)
.inputType;
},
- actionInputStyles() {
- return {
- 'has-error': this.errorMessage,
- 'is-a-macro': this.isMacro,
- };
+ actionNameAsSelectModel: {
+ get() {
+ if (!this.action_name) return null;
+ const found = this.actionTypes.find(a => a.key === this.action_name);
+ return found ? { id: found.key, name: found.label } : null;
+ },
+ set(value) {
+ this.action_name = value?.id || value;
+ },
+ },
+ actionTypesAsOptions() {
+ return this.actionTypes.map(a => ({ id: a.key, name: a.label }));
+ },
+ isVerticalLayout() {
+ return ['team_message', 'textarea'].includes(this.inputType);
},
castMessageVmodel: {
get() {
@@ -94,203 +114,89 @@ export default {
resetAction() {
this.$emit('resetAction');
},
+ onActionNameChange(value) {
+ this.actionNameAsSelectModel = value;
+ this.resetAction();
+ },
},
};
-
-
+
+
-
-
diff --git a/app/javascript/dashboard/components/widgets/AutomationActionTeamMessageInput.vue b/app/javascript/dashboard/components/widgets/AutomationActionTeamMessageInput.vue
index 275b292d0..316c9958c 100644
--- a/app/javascript/dashboard/components/widgets/AutomationActionTeamMessageInput.vue
+++ b/app/javascript/dashboard/components/widgets/AutomationActionTeamMessageInput.vue
@@ -1,8 +1,14 @@
-
-
-
-
-
+
+
+
-
-
diff --git a/app/javascript/dashboard/components/widgets/AutomationFileInput.vue b/app/javascript/dashboard/components/widgets/AutomationFileInput.vue
index 1eff406bf..a184c369a 100644
--- a/app/javascript/dashboard/components/widgets/AutomationFileInput.vue
+++ b/app/javascript/dashboard/components/widgets/AutomationFileInput.vue
@@ -79,7 +79,7 @@ input[type='file'] {
@apply hidden;
}
.input-wrapper {
- @apply flex h-9 bg-n-background py-1 px-2 items-center text-xs cursor-pointer rounded-sm border border-dashed border-n-strong;
+ @apply flex h-8 bg-n-background py-1 px-2 items-center text-xs cursor-pointer rounded-lg border border-dashed border-n-strong;
}
.success-icon {
@apply text-n-teal-9 mr-2;
diff --git a/app/javascript/dashboard/components/widgets/FilterInput/Index.vue b/app/javascript/dashboard/components/widgets/FilterInput/Index.vue
deleted file mode 100644
index f38bc814b..000000000
--- a/app/javascript/dashboard/components/widgets/FilterInput/Index.vue
+++ /dev/null
@@ -1,305 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t('FORMS.MULTISELECT.NO_OPTIONS') }}
-
-
-
-
-
-
- {{ $t('FORMS.MULTISELECT.NO_OPTIONS') }}
-
-
-
-
-
-
-
-
-
-
-
- {{ errorMessage }}
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/javascript/dashboard/composables/useAutomation.js b/app/javascript/dashboard/composables/useAutomation.js
index 09828513f..f70a5bf3b 100644
--- a/app/javascript/dashboard/composables/useAutomation.js
+++ b/app/javascript/dashboard/composables/useAutomation.js
@@ -1,4 +1,4 @@
-import { ref, computed } from 'vue';
+import { ref, reactive, computed } from 'vue';
import { useStoreGetters } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
@@ -40,7 +40,7 @@ export function useAutomation(startValue = null) {
} = useAutomationValues();
const automation = ref(startValue);
- const automationTypes = structuredClone(AUTOMATIONS);
+ const automationTypes = reactive(structuredClone(AUTOMATIONS));
const eventName = computed(() => automation.value?.event_name);
/**
@@ -160,14 +160,24 @@ export function useAutomation(startValue = null) {
t('AUTOMATION.CONDITION.CONTACT_CUSTOM_ATTR_LABEL')
);
+ const CUSTOM_ATTR_HEADER_KEYS = new Set([
+ 'conversation_custom_attribute',
+ 'contact_custom_attribute',
+ ]);
+
[
'message_created',
'conversation_created',
'conversation_updated',
'conversation_opened',
].forEach(eventToUpdate => {
+ const standardConditions = automationTypes[
+ eventToUpdate
+ ].conditions.filter(
+ c => !c.customAttributeType && !CUSTOM_ATTR_HEADER_KEYS.has(c.key)
+ );
automationTypes[eventToUpdate].conditions = [
- ...automationTypes[eventToUpdate].conditions,
+ ...standardConditions,
...manifestedCustomAttributes,
];
});
diff --git a/app/javascript/dashboard/helper/automationHelper.js b/app/javascript/dashboard/helper/automationHelper.js
index 3e5f46f90..fa6120c16 100644
--- a/app/javascript/dashboard/helper/automationHelper.js
+++ b/app/javascript/dashboard/helper/automationHelper.js
@@ -169,19 +169,19 @@ export const getFileName = (action, files = []) => {
export const getDefaultConditions = eventName => {
if (eventName === 'message_created') {
- return DEFAULT_MESSAGE_CREATED_CONDITION;
+ return structuredClone(DEFAULT_MESSAGE_CREATED_CONDITION);
}
if (
eventName === 'conversation_opened' ||
eventName === 'conversation_resolved'
) {
- return DEFAULT_CONVERSATION_CONDITION;
+ return structuredClone(DEFAULT_CONVERSATION_CONDITION);
}
- return DEFAULT_OTHER_CONDITION;
+ return structuredClone(DEFAULT_OTHER_CONDITION);
};
export const getDefaultActions = () => {
- return DEFAULT_ACTIONS;
+ return structuredClone(DEFAULT_ACTIONS);
};
export const filterCustomAttributes = customAttributes => {
diff --git a/app/javascript/dashboard/modules/contact/ContactMergeModal.vue b/app/javascript/dashboard/modules/contact/ContactMergeModal.vue
index 6e5105883..8db16d9e5 100644
--- a/app/javascript/dashboard/modules/contact/ContactMergeModal.vue
+++ b/app/javascript/dashboard/modules/contact/ContactMergeModal.vue
@@ -1,91 +1,101 @@
-
-
-
-
+
+
diff --git a/app/javascript/dashboard/modules/contact/components/ContactDropdownItem.vue b/app/javascript/dashboard/modules/contact/components/ContactDropdownItem.vue
deleted file mode 100644
index 29f38f11f..000000000
--- a/app/javascript/dashboard/modules/contact/components/ContactDropdownItem.vue
+++ /dev/null
@@ -1,87 +0,0 @@
-
-
-
-
-
-
-
- {{ name }}
-
- {{ $t('MERGE_CONTACTS.DROPDOWN_ITEM.ID', { identifier }) }}
-
-
-
-
-
- {{ email }}
-
-
-
- {{ phoneNumber }}
-
- {{ '---' }}
-
-
-
-
-
-
diff --git a/app/javascript/dashboard/modules/contact/components/MergeContact.vue b/app/javascript/dashboard/modules/contact/components/MergeContact.vue
index 85fda4464..29dc6b62d 100644
--- a/app/javascript/dashboard/modules/contact/components/MergeContact.vue
+++ b/app/javascript/dashboard/modules/contact/components/MergeContact.vue
@@ -1,174 +1,105 @@
-
-
-
diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue
index 6298a3f2d..77d4b9773 100644
--- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue
+++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue
@@ -11,11 +11,13 @@ import { isPhoneNumberValid } from 'shared/helpers/Validators';
import parsePhoneNumber from 'libphonenumber-js';
import NextButton from 'dashboard/components-next/button/Button.vue';
import Avatar from 'next/avatar/Avatar.vue';
+import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
export default {
components: {
NextButton,
Avatar,
+ ComboBox,
},
props: {
contact: {
@@ -133,6 +135,12 @@ export default {
if (!name && !id) return '';
return `${name} (${id})`;
},
+ onCountryChange(value) {
+ const selected = this.countries.find(c => c.id === value);
+ this.country = selected
+ ? { id: selected.id, name: selected.name }
+ : { id: '', name: '' };
+ },
setDialCode() {
if (
this.phoneNumber !== '' &&
@@ -363,26 +371,23 @@ export default {
:label="$t('CONTACT_FORM.FORM.COMPANY_NAME.LABEL')"
:placeholder="$t('CONTACT_FORM.FORM.COMPANY_NAME.PLACEHOLDER')"
/>
-
-
-
-
-
+
+
+
-
-
diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue
index d1c43b3f2..7392bea78 100644
--- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue
+++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue
@@ -51,7 +51,6 @@ export default {
data() {
return {
showEditModal: false,
- showMergeModal: false,
showDeleteModal: false,
};
},
@@ -167,11 +166,8 @@ export default {
);
}
},
- closeMergeModal() {
- this.showMergeModal = false;
- },
openMergeModal() {
- this.showMergeModal = true;
+ this.$refs.mergeModal?.open();
},
},
};
@@ -324,12 +320,7 @@ export default {
:contact="contact"
@cancel="toggleEditModal"
/>
-
+
item.name);
+ return this.values;
},
isButtonDisabled() {
return (
this.v$.displayName.$invalid ||
this.v$.description.$invalid ||
this.uiFlags.isCreating ||
- this.isTagInputInvalid
+ this.isTagInputEmpty
);
},
keyErrorMessage() {
@@ -119,17 +120,14 @@ export default {
},
},
+ watch: {
+ attributeType() {
+ this.tagInputTouched = false;
+ this.values = [];
+ },
+ },
+
methods: {
- addTagValue(tagValue) {
- const tag = {
- name: tagValue,
- };
- this.values.push(tag);
- this.$refs.tagInput.$el.focus();
- },
- onTouch() {
- this.isTouched = true;
- },
onDisplayNameChange() {
this.attributeKey = convertToAttributeSlug(this.displayName);
},
@@ -237,27 +235,25 @@ export default {
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.ERROR') }}
-
-