feat: Contact filtering by labels and segments

This commit is contained in:
iamsivin
2024-11-08 15:44:10 +05:30
parent 3d6d6e2afb
commit d53c60363b
6 changed files with 227 additions and 62 deletions
@@ -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"
>
@@ -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] },
}),
});
};