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**
<img width="741" height="449" alt="image"
src="https://github.com/user-attachments/assets/a05a96ec-0692-4d94-9e27-d3e85fd143e4"
/>
<img width="741" height="449" alt="image"
src="https://github.com/user-attachments/assets/fc1dc977-689d-4440-869d-2124e4ca9083"
/>

2. **Automations**
<img width="849" height="1089" alt="image"
src="https://github.com/user-attachments/assets/b0155f06-ab21-4f90-a2c8-5bfbd97b08f7"
/>
<img width="813" height="879" alt="image"
src="https://github.com/user-attachments/assets/0921ac4a-88f5-49ac-a776-cc02941b479c"
/>
<img width="849" height="826" alt="image"
src="https://github.com/user-attachments/assets/44358dae-a076-4e10-b7ba-a4e40ccd817f"
/>

3. **Country field**
<img width="462" height="483" alt="image"
src="https://github.com/user-attachments/assets/d5db9aa1-b859-4327-9960-957d7091678f"
/>

4. **Add/Edit attribute form**
<img width="619" height="646" alt="image"
src="https://github.com/user-attachments/assets/6ab2ea94-73e5-40b8-ac29-399c0543fa7b"
/>
<img width="619" height="646" alt="image"
src="https://github.com/user-attachments/assets/b4c5bb0e-baa0-4ef7-a6a2-adb0f0203243"
/>
<img width="635" height="731" alt="image"
src="https://github.com/user-attachments/assets/74890c80-b213-4567-bf5f-4789dda39d2d"
/>

5. **Agent selection in inbox creation**
<img width="635" height="534" alt="image"
src="https://github.com/user-attachments/assets/0003bad1-1a75-4f20-b014-587e1c19a620"
/>
<img width="809" height="602" alt="image"
src="https://github.com/user-attachments/assets/5e7ab635-7340-420a-a191-e6cd49c02704"
/>

7. **Facebook channel page selection**
<img width="597" height="444" alt="image"
src="https://github.com/user-attachments/assets/f7ec8d84-0a7d-4bc6-92a1-a1365178e319"
/>
<img width="597" height="444" alt="image"
src="https://github.com/user-attachments/assets/d0596c4d-94c1-4544-8b50-e7103ff207a6"
/>
<img width="597" height="444" alt="image"
src="https://github.com/user-attachments/assets/be097921-011b-4dbe-b5f1-5d1306e25349"
/>



## 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 <scm.mymail@gmail.com>
This commit is contained in:
Sivin Varghese
2026-02-17 16:40:12 +05:30
committed by GitHub
parent 138840a23f
commit 229f56d6e3
31 changed files with 1209 additions and 1795 deletions

View File

@@ -1,91 +1,101 @@
<script>
<script setup>
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'vuex';
import { useAlert, useTrack } from 'dashboard/composables';
import { useMapGetter } from 'dashboard/composables/store';
import MergeContact from 'dashboard/modules/contact/components/MergeContact.vue';
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
import ContactAPI from 'dashboard/api/contacts';
import { mapGetters } from 'vuex';
import { CONTACTS_EVENTS } from '../../helper/AnalyticsHelper/events';
export default {
components: { MergeContact },
props: {
show: {
type: Boolean,
default: false,
},
primaryContact: {
type: Object,
required: true,
},
},
emits: ['close', 'update:show'],
data() {
return {
isSearching: false,
searchResults: [],
};
},
computed: {
...mapGetters({
uiFlags: 'contacts/getUIFlags',
}),
localShow: {
get() {
return this.show;
},
set(value) {
this.$emit('update:show', value);
},
},
const props = defineProps({
primaryContact: {
type: Object,
required: true,
},
});
methods: {
onClose() {
this.$emit('close');
},
async onContactSearch(query) {
this.isSearching = true;
this.searchResults = [];
const emit = defineEmits(['close']);
try {
const {
data: { payload },
} = await ContactAPI.search(query);
this.searchResults = payload.filter(
contact => contact.id !== this.primaryContact.id
);
} catch (error) {
useAlert(this.$t('MERGE_CONTACTS.SEARCH.ERROR_MESSAGE'));
} finally {
this.isSearching = false;
}
},
async onMergeContacts(parentContactId) {
useTrack(CONTACTS_EVENTS.MERGED_CONTACTS);
try {
await this.$store.dispatch('contacts/merge', {
childId: this.primaryContact.id,
parentId: parentContactId,
});
useAlert(this.$t('MERGE_CONTACTS.FORM.SUCCESS_MESSAGE'));
this.onClose();
} catch (error) {
useAlert(this.$t('MERGE_CONTACTS.FORM.ERROR_MESSAGE'));
}
},
},
const { t } = useI18n();
const store = useStore();
const uiFlags = useMapGetter('contacts/getUIFlags');
const dialogRef = ref(null);
const isSearching = ref(false);
const searchResults = ref([]);
watch(
() => props.primaryContact.id,
() => {
isSearching.value = false;
searchResults.value = [];
}
);
const open = () => {
dialogRef.value?.open();
};
const close = () => {
dialogRef.value?.close();
};
defineExpose({ open, close });
const onClose = () => {
close();
emit('close');
};
const onContactSearch = async query => {
isSearching.value = true;
searchResults.value = [];
try {
const {
data: { payload },
} = await ContactAPI.search(query);
searchResults.value = payload.filter(
contact => contact.id !== props.primaryContact.id
);
} catch (error) {
useAlert(t('MERGE_CONTACTS.SEARCH.ERROR_MESSAGE'));
} finally {
isSearching.value = false;
}
};
const onMergeContacts = async parentContactId => {
useTrack(CONTACTS_EVENTS.MERGED_CONTACTS);
try {
await store.dispatch('contacts/merge', {
childId: props.primaryContact.id,
parentId: parentContactId,
});
useAlert(t('MERGE_CONTACTS.FORM.SUCCESS_MESSAGE'));
close();
emit('close');
} catch (error) {
useAlert(t('MERGE_CONTACTS.FORM.ERROR_MESSAGE'));
}
};
</script>
<template>
<woot-modal v-model:show="localShow" :on-close="onClose">
<woot-modal-header
:header-title="$t('MERGE_CONTACTS.TITLE')"
:header-content="$t('MERGE_CONTACTS.DESCRIPTION')"
/>
<Dialog
ref="dialogRef"
type="edit"
width="2xl"
:title="$t('MERGE_CONTACTS.TITLE')"
:description="$t('MERGE_CONTACTS.DESCRIPTION')"
:show-cancel-button="false"
:show-confirm-button="false"
>
<MergeContact
:key="primaryContact.id"
:primary-contact="primaryContact"
:is-searching="isSearching"
:is-merging="uiFlags.isMerging"
@@ -94,5 +104,5 @@ export default {
@cancel="onClose"
@submit="onMergeContacts"
/>
</woot-modal>
</Dialog>
</template>

View File

@@ -1,87 +0,0 @@
<script setup>
import Avatar from 'next/avatar/Avatar.vue';
defineProps({
name: {
type: String,
default: '',
},
thumbnail: {
type: String,
default: '',
},
email: {
type: String,
default: '',
},
phoneNumber: {
type: String,
default: '',
},
identifier: {
type: [String, Number],
required: true,
},
});
</script>
<template>
<div class="option-item--user">
<Avatar :src="thumbnail" :size="28" :name="name" rounded-full />
<div class="option__user-data">
<h5 class="option__title">
{{ name }}
<span v-if="identifier" class="user-identifier">
{{ $t('MERGE_CONTACTS.DROPDOWN_ITEM.ID', { identifier }) }}
</span>
</h5>
<p class="option__body">
<span v-if="email" class="email-icon-wrap">
<fluent-icon class="merge-contact--icon" icon="mail" size="12" />
{{ email }}
</span>
<span v-if="phoneNumber" class="phone-icon-wrap">
<fluent-icon class="merge-contact--icon" icon="call" size="12" />
{{ phoneNumber }}
</span>
<span v-if="!phoneNumber && !email">{{ '---' }}</span>
</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.option-item--user {
@apply flex items-center;
}
.user-identifier {
@apply text-xs ml-0.5 text-n-slate-12;
}
.option__user-data {
@apply flex flex-col flex-grow ml-2 mr-2;
}
.option__body,
.option__title {
@apply flex items-center justify-start leading-[1.2] text-sm;
}
.option__body .icon {
@apply relative top-px mr-0.5 rtl:mr-0 rtl:ml-0.5;
}
.option__title {
@apply text-n-slate-12 font-medium mb-0.5;
}
.option__body {
@apply text-xs text-n-slate-12 mt-1;
}
.option__user-data .option__body {
> .phone-icon-wrap,
> .email-icon-wrap {
@apply w-auto flex items-center;
}
}
.merge-contact--icon {
@apply -mb-0.5 mr-0.5;
}
</style>

View File

@@ -1,174 +1,105 @@
<script>
<script setup>
import { ref, computed } from 'vue';
import { required } from '@vuelidate/validators';
import { useVuelidate } from '@vuelidate/core';
import { useI18n } from 'vue-i18n';
import MergeContactSummary from 'dashboard/modules/contact/components/MergeContactSummary.vue';
import ContactDropdownItem from './ContactDropdownItem.vue';
import ContactMergeForm from 'dashboard/components-next/Contacts/ContactsForm/ContactMergeForm.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: { MergeContactSummary, ContactDropdownItem, NextButton },
props: {
primaryContact: {
type: Object,
required: true,
},
isSearching: {
type: Boolean,
default: false,
},
isMerging: {
type: Boolean,
default: false,
},
searchResults: {
type: Array,
default: () => [],
},
const props = defineProps({
primaryContact: {
type: Object,
required: true,
},
emits: ['search', 'submit', 'cancel'],
setup() {
return { v$: useVuelidate() };
isSearching: {
type: Boolean,
default: false,
},
validations: {
primaryContact: {
required,
},
parentContact: {
required,
},
isMerging: {
type: Boolean,
default: false,
},
data() {
return {
parentContact: undefined,
};
searchResults: {
type: Array,
default: () => [],
},
});
computed: {
parentContactName() {
return this.parentContact ? this.parentContact.name : '';
const emit = defineEmits(['search', 'submit', 'cancel']);
const { t } = useI18n();
const parentContactId = ref(null);
const validationRules = {
parentContactId: { required },
};
const v$ = useVuelidate(validationRules, { parentContactId });
const parentContact = computed(() => {
if (!parentContactId.value) return null;
return props.searchResults.find(
contact => contact.id === parentContactId.value
);
});
const parentContactName = computed(() => {
return parentContact.value ? parentContact.value.name : '';
});
const primaryContactList = computed(() => {
return props.searchResults.map(contact => ({
id: contact.id,
label: contact.name,
value: contact.id,
meta: {
thumbnail: contact.thumbnail,
email: contact.email,
phoneNumber: contact.phone_number,
},
},
methods: {
searchChange(query) {
this.$emit('search', query);
},
onSubmit() {
this.v$.$touch();
if (this.v$.$invalid) {
return;
}
this.$emit('submit', this.parentContact.id);
},
onCancel() {
this.$emit('cancel');
},
},
}));
});
const hasValidationError = computed(() => v$.value.parentContactId.$error);
const validationErrorMessage = computed(() => {
if (v$.value.parentContactId.$error) {
return t('MERGE_CONTACTS.FORM.CHILD_CONTACT.ERROR');
}
return '';
});
const onSearch = query => {
emit('search', query);
};
const onSubmit = () => {
v$.value.$touch();
if (v$.value.$invalid) {
return;
}
emit('submit', parentContactId.value);
};
const onCancel = () => {
emit('cancel');
};
</script>
<template>
<form @submit.prevent="onSubmit">
<div>
<div>
<div
class="mt-1 multiselect-wrap--medium"
:class="{ error: v$.parentContact.$error }"
>
<label class="multiselect__label">
{{ $t('MERGE_CONTACTS.PARENT.TITLE') }}
<woot-label
:title="$t('MERGE_CONTACTS.PARENT.HELP_LABEL')"
color-scheme="success"
small
class="ml-2"
/>
</label>
<multiselect
v-model="parentContact"
:options="searchResults"
label="name"
track-by="id"
:internal-search="false"
:clear-on-select="false"
:show-labels="false"
:placeholder="$t('MERGE_CONTACTS.PARENT.PLACEHOLDER')"
allow-empty
:loading="isSearching"
:max-height="150"
open-direction="top"
@search-change="searchChange"
>
<template #singleLabel="props">
<ContactDropdownItem
:thumbnail="props.option.thumbnail"
:identifier="props.option.id"
:name="props.option.name"
:email="props.option.email"
:phone-number="props.option.phone_number"
/>
</template>
<template #option="props">
<ContactDropdownItem
:thumbnail="props.option.thumbnail"
:identifier="props.option.id"
:name="props.option.name"
:email="props.option.email"
:phone-number="props.option.phone_number"
/>
</template>
<template #noResult>
<span>
{{ $t('AGENT_MGMT.SEARCH.NO_RESULTS') }}
</span>
</template>
</multiselect>
<span v-if="v$.parentContact.$error" class="message">
{{ $t('MERGE_CONTACTS.FORM.CHILD_CONTACT.ERROR') }}
</span>
</div>
</div>
<div class="flex multiselect-wrap--medium">
<div
class="w-8 relative text-base text-n-strong after:content-[''] after:h-12 after:w-0 ltr:after:left-4 rtl:after:right-4 after:absolute after:border-l after:border-solid after:border-n-strong before:content-[''] before:h-0 before:w-4 ltr:before:left-4 rtl:before:right-4 before:top-12 before:absolute before:border-b before:border-solid before:border-n-strong"
>
<fluent-icon
icon="arrow-up"
class="absolute -top-1 ltr:left-2 rtl:right-2"
size="17"
/>
</div>
<div class="flex flex-col w-full ltr:pl-8 rtl:pr-8">
<label class="multiselect__label">
{{ $t('MERGE_CONTACTS.PRIMARY.TITLE') }}
<woot-label
:title="$t('MERGE_CONTACTS.PRIMARY.HELP_LABEL')"
color-scheme="alert"
small
class="ml-2"
/>
</label>
<multiselect
:model-value="primaryContact"
disabled
:options="[]"
:show-labels="false"
label="name"
track-by="id"
>
<template #singleLabel="props">
<ContactDropdownItem
:thumbnail="props.option.thumbnail"
:name="props.option.name"
:identifier="props.option.id"
:email="props.option.email"
:phone-number="props.option.phoneNumber"
/>
</template>
</multiselect>
</div>
</div>
</div>
<ContactMergeForm
:selected-contact="primaryContact"
:primary-contact-id="parentContactId"
:primary-contact-list="primaryContactList"
:is-searching="isSearching"
:has-error="hasValidationError"
:error-message="validationErrorMessage"
@update:primary-contact-id="parentContactId = $event"
@search="onSearch"
/>
<MergeContactSummary
:primary-contact-name="primaryContact.name"
:parent-contact-name="parentContactName"
@@ -189,32 +120,3 @@ export default {
</div>
</form>
</template>
<style lang="scss" scoped>
/* TDOD: Clean errors in forms style */
.error .message {
@apply mt-0;
}
::v-deep {
.multiselect {
@apply rounded-md;
}
.multiselect--disabled {
@apply border-0;
.multiselect__tags {
@apply border;
}
}
.multiselect__tags {
@apply h-auto;
}
.multiselect__select {
@apply mt-px mr-1;
}
}
</style>