# 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>
318 lines
10 KiB
Vue
318 lines
10 KiB
Vue
<script setup>
|
|
import { ref, computed, unref } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
|
import { useRouter } from 'vue-router';
|
|
import { useAlert, useTrack } from 'dashboard/composables';
|
|
import { CONTACTS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
|
import filterQueryGenerator from 'dashboard/helper/filterQueryGenerator';
|
|
import contactFilterItems from 'dashboard/routes/dashboard/contacts/contactFilterItems';
|
|
import {
|
|
DuplicateContactException,
|
|
ExceptionWithMessage,
|
|
} from 'shared/helpers/CustomErrors';
|
|
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
|
import countries from 'shared/constants/countries';
|
|
import {
|
|
useCamelCase,
|
|
useSnakeCase,
|
|
} from 'dashboard/composables/useTransformKeys';
|
|
|
|
import ContactsHeader from 'dashboard/components-next/Contacts/ContactsHeader/ContactHeader.vue';
|
|
import CreateNewContactDialog from 'dashboard/components-next/Contacts/ContactsForm/CreateNewContactDialog.vue';
|
|
import ContactExportDialog from 'dashboard/components-next/Contacts/ContactsForm/ContactExportDialog.vue';
|
|
import ContactImportDialog from 'dashboard/components-next/Contacts/ContactsForm/ContactImportDialog.vue';
|
|
import CreateSegmentDialog from 'dashboard/components-next/Contacts/ContactsForm/CreateSegmentDialog.vue';
|
|
import DeleteSegmentDialog from 'dashboard/components-next/Contacts/ContactsForm/DeleteSegmentDialog.vue';
|
|
import ContactsFilter from 'dashboard/components-next/filter/ContactsFilter.vue';
|
|
|
|
const props = defineProps({
|
|
showSearch: { type: Boolean, default: true },
|
|
searchValue: { type: String, default: '' },
|
|
activeSort: { type: String, default: 'last_activity_at' },
|
|
activeOrdering: { type: String, default: '' },
|
|
headerTitle: { type: String, default: '' },
|
|
segmentsId: { type: [String, Number], default: 0 },
|
|
activeSegment: { type: Object, default: null },
|
|
hasAppliedFilters: { type: Boolean, default: false },
|
|
isLabelView: { type: Boolean, default: false },
|
|
isActiveView: { type: Boolean, default: false },
|
|
});
|
|
|
|
const emit = defineEmits([
|
|
'update:sort',
|
|
'search',
|
|
'applyFilter',
|
|
'clearFilters',
|
|
]);
|
|
|
|
const { t } = useI18n();
|
|
const store = useStore();
|
|
const router = useRouter();
|
|
|
|
const createNewContactDialogRef = ref(null);
|
|
const contactExportDialogRef = ref(null);
|
|
const contactImportDialogRef = ref(null);
|
|
const createSegmentDialogRef = ref(null);
|
|
const deleteSegmentDialogRef = ref(null);
|
|
|
|
const showFiltersModal = ref(false);
|
|
const appliedFilter = ref([]);
|
|
const segmentsQuery = ref({});
|
|
|
|
const appliedFilters = useMapGetter('contacts/getAppliedContactFiltersV4');
|
|
const contactAttributes = useMapGetter('attributes/getContactAttributes');
|
|
const labels = useMapGetter('labels/getLabels');
|
|
const hasActiveSegments = computed(
|
|
() => props.activeSegment && props.segmentsId !== 0
|
|
);
|
|
const activeSegmentName = computed(() => props.activeSegment?.name);
|
|
|
|
const openCreateNewContactDialog = () => {
|
|
createNewContactDialogRef.value?.dialogRef.open();
|
|
};
|
|
const openContactImportDialog = () =>
|
|
contactImportDialogRef.value?.dialogRef.open();
|
|
const openContactExportDialog = () =>
|
|
contactExportDialogRef.value?.dialogRef.open();
|
|
const openCreateSegmentDialog = () =>
|
|
createSegmentDialogRef.value?.dialogRef.open();
|
|
const openDeleteSegmentDialog = () =>
|
|
deleteSegmentDialogRef.value?.dialogRef.open();
|
|
|
|
const onCreate = async contact => {
|
|
try {
|
|
await store.dispatch('contacts/create', contact);
|
|
createNewContactDialogRef.value?.onSuccess();
|
|
useAlert(
|
|
t('CONTACTS_LAYOUT.HEADER.ACTIONS.CONTACT_CREATION.SUCCESS_MESSAGE')
|
|
);
|
|
} catch (error) {
|
|
const i18nPrefix = 'CONTACTS_LAYOUT.HEADER.ACTIONS.CONTACT_CREATION';
|
|
if (error instanceof DuplicateContactException) {
|
|
if (error.data.includes('email')) {
|
|
useAlert(t(`${i18nPrefix}.EMAIL_ADDRESS_DUPLICATE`));
|
|
} else if (error.data.includes('phone_number')) {
|
|
useAlert(t(`${i18nPrefix}.PHONE_NUMBER_DUPLICATE`));
|
|
}
|
|
} else if (error instanceof ExceptionWithMessage) {
|
|
useAlert(error.data);
|
|
} else {
|
|
useAlert(t(`${i18nPrefix}.ERROR_MESSAGE`));
|
|
}
|
|
}
|
|
};
|
|
|
|
const onImport = async file => {
|
|
try {
|
|
await store.dispatch('contacts/import', file);
|
|
contactImportDialogRef.value?.dialogRef.close();
|
|
useAlert(
|
|
t('CONTACTS_LAYOUT.HEADER.ACTIONS.IMPORT_CONTACT.SUCCESS_MESSAGE')
|
|
);
|
|
useTrack(CONTACTS_EVENTS.IMPORT_SUCCESS);
|
|
} catch (error) {
|
|
useAlert(
|
|
error.message ??
|
|
t('CONTACTS_LAYOUT.HEADER.ACTIONS.IMPORT_CONTACT.ERROR_MESSAGE')
|
|
);
|
|
useTrack(CONTACTS_EVENTS.IMPORT_FAILURE);
|
|
}
|
|
};
|
|
|
|
const onExport = async query => {
|
|
try {
|
|
await store.dispatch('contacts/export', query);
|
|
useAlert(
|
|
t('CONTACTS_LAYOUT.HEADER.ACTIONS.EXPORT_CONTACT.SUCCESS_MESSAGE')
|
|
);
|
|
} catch (error) {
|
|
useAlert(
|
|
error.message ||
|
|
t('CONTACTS_LAYOUT.HEADER.ACTIONS.EXPORT_CONTACT.ERROR_MESSAGE')
|
|
);
|
|
}
|
|
};
|
|
|
|
const onCreateSegment = async payload => {
|
|
try {
|
|
const payloadData = {
|
|
...payload,
|
|
query: segmentsQuery.value,
|
|
};
|
|
const response = await store.dispatch('customViews/create', payloadData);
|
|
createSegmentDialogRef.value?.dialogRef.close();
|
|
useAlert(
|
|
t('CONTACTS_LAYOUT.HEADER.ACTIONS.FILTERS.CREATE_SEGMENT.SUCCESS_MESSAGE')
|
|
);
|
|
const segmentId = response?.data?.id;
|
|
if (!segmentId) return;
|
|
// Navigate to the created segment
|
|
router.push({
|
|
name: 'contacts_dashboard_segments_index',
|
|
params: { segmentId },
|
|
query: { page: 1 },
|
|
});
|
|
} catch {
|
|
useAlert(
|
|
t('CONTACTS_LAYOUT.HEADER.ACTIONS.FILTERS.CREATE_SEGMENT.ERROR_MESSAGE')
|
|
);
|
|
}
|
|
};
|
|
|
|
const onDeleteSegment = async payload => {
|
|
try {
|
|
await store.dispatch('customViews/delete', {
|
|
id: Number(props.segmentsId),
|
|
...payload,
|
|
});
|
|
router.push({
|
|
name: 'contacts_dashboard_index',
|
|
query: {
|
|
page: 1,
|
|
},
|
|
});
|
|
deleteSegmentDialogRef.value?.dialogRef.close();
|
|
useAlert(
|
|
t('CONTACTS_LAYOUT.HEADER.ACTIONS.FILTERS.DELETE_SEGMENT.SUCCESS_MESSAGE')
|
|
);
|
|
} catch (error) {
|
|
useAlert(
|
|
t('CONTACTS_LAYOUT.HEADER.ACTIONS.FILTERS.DELETE_SEGMENT.ERROR_MESSAGE')
|
|
);
|
|
}
|
|
};
|
|
|
|
const closeAdvanceFiltersModal = () => {
|
|
showFiltersModal.value = false;
|
|
appliedFilter.value = [];
|
|
};
|
|
|
|
const clearFilters = async () => {
|
|
emit('clearFilters');
|
|
};
|
|
|
|
const onApplyFilter = async payload => {
|
|
payload = useSnakeCase(payload);
|
|
segmentsQuery.value = filterQueryGenerator(payload);
|
|
emit('applyFilter', filterQueryGenerator(payload));
|
|
showFiltersModal.value = false;
|
|
};
|
|
|
|
const onUpdateSegment = async (payload, segmentName) => {
|
|
payload = useSnakeCase(payload);
|
|
const payloadData = {
|
|
...props.activeSegment,
|
|
name: segmentName,
|
|
query: filterQueryGenerator(payload),
|
|
};
|
|
await store.dispatch('customViews/update', payloadData);
|
|
closeAdvanceFiltersModal();
|
|
};
|
|
|
|
const setParamsForEditSegmentModal = () => {
|
|
return {
|
|
countries,
|
|
filterTypes: contactFilterItems,
|
|
allCustomAttributes: useSnakeCase(contactAttributes.value),
|
|
labels: labels.value || [],
|
|
};
|
|
};
|
|
|
|
const initializeSegmentToFilterModal = segment => {
|
|
const query = unref(segment)?.query?.payload;
|
|
if (!Array.isArray(query)) return;
|
|
|
|
const newFilters = query.map(filter => {
|
|
const transformed = useCamelCase(filter);
|
|
const values = Array.isArray(transformed.values)
|
|
? generateValuesForEditCustomViews(
|
|
useSnakeCase(filter),
|
|
setParamsForEditSegmentModal()
|
|
)
|
|
: [];
|
|
|
|
return {
|
|
attributeKey: transformed.attributeKey,
|
|
attributeModel: transformed.attributeModel,
|
|
customAttributeType: transformed.customAttributeType,
|
|
filterOperator: transformed.filterOperator,
|
|
queryOperator: transformed.queryOperator ?? 'and',
|
|
values,
|
|
};
|
|
});
|
|
|
|
appliedFilter.value = [...appliedFilter.value, ...newFilters];
|
|
};
|
|
|
|
const onToggleFilters = () => {
|
|
appliedFilter.value = [];
|
|
if (hasActiveSegments.value) {
|
|
initializeSegmentToFilterModal(props.activeSegment);
|
|
} else {
|
|
appliedFilter.value = props.hasAppliedFilters
|
|
? [...appliedFilters.value]
|
|
: [
|
|
{
|
|
attributeKey: 'name',
|
|
filterOperator: 'equal_to',
|
|
values: '',
|
|
queryOperator: 'and',
|
|
attributeModel: 'standard',
|
|
},
|
|
];
|
|
}
|
|
showFiltersModal.value = true;
|
|
};
|
|
|
|
defineExpose({
|
|
onToggleFilters,
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<ContactsHeader
|
|
:show-search="showSearch"
|
|
:search-value="searchValue"
|
|
:active-sort="activeSort"
|
|
:active-ordering="activeOrdering"
|
|
:header-title="headerTitle"
|
|
:is-segments-view="hasActiveSegments"
|
|
:is-label-view="isLabelView"
|
|
:is-active-view="isActiveView"
|
|
:has-active-filters="hasAppliedFilters"
|
|
:button-label="t('CONTACTS_LAYOUT.HEADER.MESSAGE_BUTTON')"
|
|
@search="emit('search', $event)"
|
|
@update:sort="emit('update:sort', $event)"
|
|
@add="openCreateNewContactDialog"
|
|
@import="openContactImportDialog"
|
|
@export="openContactExportDialog"
|
|
@filter="onToggleFilters"
|
|
@create-segment="openCreateSegmentDialog"
|
|
@delete-segment="openDeleteSegmentDialog"
|
|
>
|
|
<template #filter>
|
|
<div
|
|
class="absolute mt-1 ltr:-right-52 rtl:-left-52 sm:ltr:right-0 sm:rtl:left-0 top-full"
|
|
>
|
|
<ContactsFilter
|
|
v-if="showFiltersModal"
|
|
v-model="appliedFilter"
|
|
:segment-name="activeSegmentName"
|
|
:is-segment-view="hasActiveSegments"
|
|
@apply-filter="onApplyFilter"
|
|
@update-segment="onUpdateSegment"
|
|
@close="closeAdvanceFiltersModal"
|
|
@clear-filters="clearFilters"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</ContactsHeader>
|
|
|
|
<CreateNewContactDialog ref="createNewContactDialogRef" @create="onCreate" />
|
|
<ContactExportDialog ref="contactExportDialogRef" @export="onExport" />
|
|
<ContactImportDialog ref="contactImportDialogRef" @import="onImport" />
|
|
<CreateSegmentDialog ref="createSegmentDialogRef" @create="onCreateSegment" />
|
|
<DeleteSegmentDialog ref="deleteSegmentDialogRef" @delete="onDeleteSegment" />
|
|
</template>
|