feat: Contact filtering by labels and segments
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
|
||||
@@ -22,6 +22,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(['toggle']);
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
@@ -37,14 +38,26 @@ const handleFormUpdate = debounce(
|
||||
false
|
||||
);
|
||||
|
||||
const onClickViewDetails = () => {
|
||||
store.dispatch('contacts/show', { id: props.id });
|
||||
router.push({
|
||||
name: 'contacts_dashboard_edit_index',
|
||||
params: {
|
||||
contactId: props.id,
|
||||
},
|
||||
});
|
||||
const ROUTE_MAPPINGS = {
|
||||
contacts_dashboard_labels_index: 'contacts_dashboard_labels_edit_index',
|
||||
contacts_dashboard_segments_index: 'contacts_dashboard_segments_edit_index',
|
||||
};
|
||||
|
||||
const onClickViewDetails = async () => {
|
||||
await store.dispatch('contacts/show', { id: props.id });
|
||||
|
||||
const dynamicRouteName =
|
||||
ROUTE_MAPPINGS[route.name] || 'contacts_dashboard_edit_index';
|
||||
|
||||
const params = { contactId: props.id };
|
||||
|
||||
if (route.name.includes('segments')) {
|
||||
params.segmentId = route.params.segmentId;
|
||||
} else if (route.name.includes('labels')) {
|
||||
params.label = route.params.label;
|
||||
}
|
||||
|
||||
await router.push({ name: dynamicRouteName, params });
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Array,
|
||||
required: true,
|
||||
@@ -21,6 +21,8 @@ const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const isOpen = ref(false);
|
||||
|
||||
const labelValue = computed(() => props.label);
|
||||
|
||||
const toggleMenu = () => {
|
||||
isOpen.value = !isOpen.value;
|
||||
};
|
||||
@@ -44,7 +46,7 @@ const handleSelect = value => {
|
||||
variant="faded"
|
||||
class="!w-fit"
|
||||
:class="{ 'dark:!bg-n-alpha-2 !bg-n-slate-9/20': isOpen }"
|
||||
:label="label"
|
||||
:label="labelValue"
|
||||
@click="toggleMenu"
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -172,7 +172,59 @@ const menuItems = computed(() => {
|
||||
name: 'Contacts',
|
||||
label: t('SIDEBAR.CONTACTS'),
|
||||
icon: 'i-lucide-contact',
|
||||
to: accountScopedRoute('contacts_dashboard_index'),
|
||||
activeOn: ['contacts_dashboard_index', 'contacts_dashboard_edit_index'],
|
||||
children: [
|
||||
{
|
||||
name: 'All Contacts',
|
||||
label: t('SIDEBAR.ALL_CONTACTS'),
|
||||
to: accountScopedRoute('contacts_dashboard_index'),
|
||||
activeOn: [
|
||||
'contacts_dashboard_index',
|
||||
'contacts_dashboard_edit_index',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Segments',
|
||||
icon: 'i-lucide-group',
|
||||
label: t('SIDEBAR.CUSTOM_VIEWS_SEGMENTS'),
|
||||
|
||||
children: contactCustomViews.value.map(view => ({
|
||||
name: `${view.name}-${view.id}`,
|
||||
label: view.name,
|
||||
to: accountScopedRoute('contacts_dashboard_segments_index', {
|
||||
segmentId: view.id,
|
||||
}),
|
||||
activeOn: [
|
||||
'contacts_dashboard_segments_index',
|
||||
'contacts_dashboard_segments_edit_index',
|
||||
],
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: 'Tagged With',
|
||||
icon: 'i-lucide-tag',
|
||||
label: t('SIDEBAR.TAGGED_WITH'),
|
||||
activeOn: [
|
||||
'contacts_dashboard_labels_index',
|
||||
'contacts_dashboard_labels_edit_index',
|
||||
],
|
||||
children: labels.value.map(label => ({
|
||||
name: `${label.title}-${label.id}`,
|
||||
label: label.title,
|
||||
icon: h('span', {
|
||||
class: `size-[12px] ring-1 ring-n-alpha-1 dark:ring-white/20 ring-inset rounded-sm`,
|
||||
style: { backgroundColor: label.color },
|
||||
}),
|
||||
to: accountScopedRoute('contacts_dashboard_labels_index', {
|
||||
label: label.title,
|
||||
}),
|
||||
activeOn: [
|
||||
'contacts_dashboard_labels_index',
|
||||
'contacts_dashboard_labels_edit_index',
|
||||
],
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Contacts',
|
||||
|
||||
@@ -21,6 +21,42 @@ const contactsRoutes = {
|
||||
permissions: ['administrator', 'agent', 'contact_manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/contacts-new/segments/:segmentId'),
|
||||
component: ContactsPageRouteView,
|
||||
name: 'contacts_dashboard_segments_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'contact_manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: frontendURL(
|
||||
'accounts/:accountId/contacts-new/:contactId/segments/:segmentId'
|
||||
),
|
||||
component: EditContactsPageRouteView,
|
||||
name: 'contacts_dashboard_segments_edit_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'contact_manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/contacts-new/labels/:label'),
|
||||
component: ContactsPageRouteView,
|
||||
name: 'contacts_dashboard_labels_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'contact_manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: frontendURL(
|
||||
'accounts/:accountId/contacts-new/:contactId/labels/:label'
|
||||
),
|
||||
component: EditContactsPageRouteView,
|
||||
name: 'contacts_dashboard_labels_edit_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'contact_manage'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { onMounted, computed, ref, watch } from 'vue';
|
||||
import { onMounted, computed, ref, reactive, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
@@ -12,10 +13,13 @@ const DEFAULT_SORT_FIELD = 'last_activity_at';
|
||||
const DEBOUNCE_DELAY = 300;
|
||||
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
|
||||
const { updateUISettings, uiSettings } = useUISettings();
|
||||
|
||||
const contacts = useMapGetter('contacts/getContactsList');
|
||||
const uiFlags = useMapGetter('contacts/getUIFlags');
|
||||
const segments = useMapGetter('customViews/getContactCustomViews');
|
||||
const meta = useMapGetter('contacts/getMeta');
|
||||
|
||||
const searchValue = ref('');
|
||||
@@ -33,56 +37,74 @@ const { contacts_sort_by: contactSortBy = '' } = uiSettings.value ?? {};
|
||||
const { sort: initialSort, order: initialOrder } =
|
||||
parseSortSettings(contactSortBy);
|
||||
|
||||
const activeSort = ref(initialSort);
|
||||
const activeOrdering = ref(initialOrder);
|
||||
const sortState = reactive({
|
||||
activeSort: initialSort,
|
||||
activeOrdering: initialOrder,
|
||||
});
|
||||
|
||||
const activeLabel = computed(() => route.params.label);
|
||||
const activeSegmentId = computed(() => route.params.segmentId);
|
||||
const isFetchingList = computed(() => uiFlags.value.isFetching);
|
||||
|
||||
const currentPage = computed(() => Number(meta.value?.currentPage));
|
||||
const totalItems = computed(() => meta.value?.count);
|
||||
|
||||
const buildSortAttr = (order, sort) => `${order}${sort}`;
|
||||
|
||||
const fetchContacts = page => {
|
||||
const sortAttr = buildSortAttr(activeOrdering.value, activeSort.value);
|
||||
store.dispatch('contacts/get', {
|
||||
page,
|
||||
sortAttr,
|
||||
});
|
||||
};
|
||||
|
||||
const debouncedSearch = debounce(
|
||||
value => {
|
||||
searchValue.value = value;
|
||||
const sortAttr = buildSortAttr(activeOrdering.value, activeSort.value);
|
||||
if (!value) {
|
||||
fetchContacts(1);
|
||||
} else {
|
||||
store.dispatch('contacts/search', {
|
||||
search: encodeURIComponent(value),
|
||||
page: 1,
|
||||
sortAttr,
|
||||
});
|
||||
}
|
||||
},
|
||||
DEBOUNCE_DELAY,
|
||||
false
|
||||
const activeSegment = computed(() =>
|
||||
activeSegmentId.value
|
||||
? segments.value.find(view => view.id === Number(activeSegmentId.value))
|
||||
: undefined
|
||||
);
|
||||
|
||||
const searchContacts = value => {
|
||||
debouncedSearch(value);
|
||||
const buildSortAttr = () =>
|
||||
`${sortState.activeOrdering}${sortState.activeSort}`;
|
||||
|
||||
const fetchContacts = async (page = 1) => {
|
||||
await store.dispatch('contacts/get', {
|
||||
page,
|
||||
sortAttr: buildSortAttr(),
|
||||
label: activeLabel.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSort = async ({ sort, order }) => {
|
||||
activeSort.value = sort;
|
||||
activeOrdering.value = order;
|
||||
const fetchSavedFilteredContact = async (payload, page = 1) => {
|
||||
if (activeSegmentId.value) {
|
||||
await store.dispatch('contacts/filter', {
|
||||
queryPayload: payload,
|
||||
page,
|
||||
sortAttr: buildSortAttr(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const sortString = buildSortAttr(order, sort);
|
||||
await updateUISettings({
|
||||
contacts_sort_by: sortString,
|
||||
const searchContacts = debounce(async value => {
|
||||
searchValue.value = value;
|
||||
if (!value) {
|
||||
if (activeSegmentId.value) {
|
||||
await fetchSavedFilteredContact(activeSegment.value.query);
|
||||
} else {
|
||||
await fetchContacts();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await store.dispatch('contacts/search', {
|
||||
search: encodeURIComponent(value),
|
||||
page: 1,
|
||||
sortAttr: buildSortAttr(),
|
||||
label: activeLabel.value,
|
||||
});
|
||||
}, DEBOUNCE_DELAY);
|
||||
|
||||
await fetchContacts(1);
|
||||
const handleSort = async ({ sort, order }) => {
|
||||
sortState.activeSort = sort;
|
||||
sortState.activeOrdering = order;
|
||||
|
||||
await updateUISettings({
|
||||
contacts_sort_by: buildSortAttr(),
|
||||
});
|
||||
if (activeSegmentId.value) {
|
||||
await fetchSavedFilteredContact(activeSegment.value.query);
|
||||
} else {
|
||||
await fetchContacts();
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
@@ -90,18 +112,42 @@ watch(
|
||||
newSortBy => {
|
||||
if (newSortBy) {
|
||||
const { sort, order } = parseSortSettings(newSortBy);
|
||||
activeSort.value = sort;
|
||||
activeOrdering.value = order;
|
||||
sortState.activeSort = sort;
|
||||
sortState.activeOrdering = order;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchContacts(1);
|
||||
const handlePageChange = async page => {
|
||||
if (activeSegmentId.value) {
|
||||
await fetchSavedFilteredContact(activeSegment.value.query, page);
|
||||
} else {
|
||||
await fetchContacts(page);
|
||||
}
|
||||
};
|
||||
|
||||
watch(activeLabel, () => {
|
||||
if (!activeSegmentId.value) {
|
||||
fetchContacts();
|
||||
}
|
||||
});
|
||||
|
||||
const updateCurrentPage = page => fetchContacts(page);
|
||||
watch(activeSegment, () => {
|
||||
if (activeSegment.value && activeSegmentId.value) {
|
||||
fetchSavedFilteredContact(activeSegment.value.query);
|
||||
} else if (!activeLabel.value) {
|
||||
fetchContacts();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (activeSegmentId.value) {
|
||||
await fetchSavedFilteredContact(activeSegment.value.query);
|
||||
} else {
|
||||
await fetchContacts();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -114,9 +160,9 @@ const updateCurrentPage = page => fetchContacts(page);
|
||||
:current-page="currentPage"
|
||||
:total-items="totalItems"
|
||||
:show-pagination-footer="!isFetchingList && searchValue === ''"
|
||||
:active-sort="activeSort"
|
||||
:active-ordering="activeOrdering"
|
||||
@update:current-page="updateCurrentPage"
|
||||
:active-sort="sortState.activeSort"
|
||||
:active-ordering="sortState.activeOrdering"
|
||||
@update:current-page="handlePageChange"
|
||||
@search="searchContacts"
|
||||
@sort="handleSort"
|
||||
>
|
||||
|
||||
+17
-1
@@ -52,9 +52,25 @@ const activeTabIndex = computed(() => {
|
||||
return CONTACT_TABS_OPTIONS.findIndex(v => v.value === activeTab.value);
|
||||
});
|
||||
|
||||
const ROUTE_CONFIG = {
|
||||
segments: ['segmentId', 'contacts_dashboard_segments_index'],
|
||||
labels: ['label', 'contacts_dashboard_labels_index'],
|
||||
default: [null, 'contacts_dashboard_index'],
|
||||
};
|
||||
|
||||
const goToContactsList = () => {
|
||||
const routeType =
|
||||
Object.keys(ROUTE_CONFIG).find(
|
||||
type => route.params[ROUTE_CONFIG[type][0]]
|
||||
) || 'default';
|
||||
|
||||
const [paramKey, routeName] = ROUTE_CONFIG[routeType];
|
||||
|
||||
router.push({
|
||||
name: 'contacts_dashboard_index',
|
||||
name: routeName,
|
||||
...(paramKey && {
|
||||
params: { [paramKey]: route.params[paramKey] },
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user