feat(v4): Update the design for the contacts list page (#10501)
--------- Co-authored-by: Pranav <pranavrajs@gmail.com> Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
@@ -83,7 +83,7 @@ export default {
|
||||
this.filterTypes = [...this.filterTypes, ...filterTypes];
|
||||
this.filterGroups = filterGroups;
|
||||
|
||||
if (this.getAppliedContactFilters.length) {
|
||||
if (this.getAppliedContactFilters.length && !this.isSegmentsView) {
|
||||
this.appliedFilters = [...this.getAppliedContactFilters];
|
||||
} else if (!this.isSegmentsView) {
|
||||
this.appliedFilters.push({
|
||||
@@ -318,7 +318,7 @@ export default {
|
||||
@reset-filter="resetFilter(i, appliedFilters[i])"
|
||||
@remove-filter="removeFilter(i)"
|
||||
/>
|
||||
<div class="mt-4">
|
||||
<div class="flex items-center gap-2 mt-4">
|
||||
<woot-button
|
||||
icon="add"
|
||||
color-scheme="success"
|
||||
|
||||
@@ -45,6 +45,9 @@ export default {
|
||||
return this.$store.getters['contacts/getContact'](this.contactId);
|
||||
},
|
||||
backUrl() {
|
||||
if (window.history.state?.back || window.history.length > 1) {
|
||||
return '';
|
||||
}
|
||||
return `/app/accounts/${this.$route.params.accountId}/contacts`;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
<script setup>
|
||||
import { onMounted, computed, ref, reactive, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import filterQueryGenerator from 'dashboard/helper/filterQueryGenerator';
|
||||
|
||||
import ContactsListLayout from 'dashboard/components-next/Contacts/ContactsListLayout.vue';
|
||||
import ContactsList from 'dashboard/components-next/Contacts/Pages/ContactsList.vue';
|
||||
import ContactEmptyState from 'dashboard/components-next/Contacts/EmptyState/ContactEmptyState.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
|
||||
const DEFAULT_SORT_FIELD = 'last_activity_at';
|
||||
const DEBOUNCE_DELAY = 300;
|
||||
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const { updateUISettings, uiSettings } = useUISettings();
|
||||
|
||||
const contacts = useMapGetter('contacts/getContactsList');
|
||||
const uiFlags = useMapGetter('contacts/getUIFlags');
|
||||
const customViewsUiFlags = useMapGetter('customViews/getUIFlags');
|
||||
const segments = useMapGetter('customViews/getContactCustomViews');
|
||||
const appliedFilters = useMapGetter('contacts/getAppliedContactFilters');
|
||||
const meta = useMapGetter('contacts/getMeta');
|
||||
|
||||
const searchQuery = computed(() => route.query?.search);
|
||||
const searchValue = ref(searchQuery.value || '');
|
||||
const pageNumber = computed(() => Number(route.query?.page) || 1);
|
||||
|
||||
const parseSortSettings = (sortString = '') => {
|
||||
const hasDescending = sortString.startsWith('-');
|
||||
const sortField = hasDescending ? sortString.slice(1) : sortString;
|
||||
return {
|
||||
sort: sortField || DEFAULT_SORT_FIELD,
|
||||
order: hasDescending ? '-' : '',
|
||||
};
|
||||
};
|
||||
|
||||
const { contacts_sort_by: contactSortBy = '' } = uiSettings.value ?? {};
|
||||
const { sort: initialSort, order: initialOrder } =
|
||||
parseSortSettings(contactSortBy);
|
||||
|
||||
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 || customViewsUiFlags.value.isFetching
|
||||
);
|
||||
const currentPage = computed(() => Number(meta.value?.currentPage));
|
||||
const totalItems = computed(() => meta.value?.count);
|
||||
const activeSegment = computed(() => {
|
||||
if (!activeSegmentId.value) return undefined;
|
||||
return segments.value.find(view => view.id === Number(activeSegmentId.value));
|
||||
});
|
||||
|
||||
const hasContacts = computed(() => contacts.value.length > 0);
|
||||
const isContactIndexView = computed(
|
||||
() => route.name === 'contacts_dashboard_index' && pageNumber.value === 1
|
||||
);
|
||||
const hasAppliedFilters = computed(() => {
|
||||
return appliedFilters.value.length > 0;
|
||||
});
|
||||
const showEmptyStateLayout = computed(() => {
|
||||
return (
|
||||
!searchQuery.value &&
|
||||
!hasContacts.value &&
|
||||
isContactIndexView.value &&
|
||||
!hasAppliedFilters.value
|
||||
);
|
||||
});
|
||||
const showEmptyText = computed(() => {
|
||||
return (
|
||||
(searchQuery.value ||
|
||||
hasAppliedFilters.value ||
|
||||
!isContactIndexView.value) &&
|
||||
!hasContacts.value
|
||||
);
|
||||
});
|
||||
|
||||
const headerTitle = computed(() => {
|
||||
if (searchQuery.value) return t('CONTACTS_LAYOUT.HEADER.SEARCH_TITLE');
|
||||
if (activeSegmentId.value) return activeSegment.value?.name;
|
||||
if (activeLabel.value) return `#${activeLabel.value}`;
|
||||
return t('CONTACTS_LAYOUT.HEADER.TITLE');
|
||||
});
|
||||
|
||||
const updatePageParam = (page, search = '') => {
|
||||
const query = {
|
||||
...route.query,
|
||||
page: page.toString(),
|
||||
...(search ? { search } : {}),
|
||||
};
|
||||
|
||||
if (!search) {
|
||||
delete query.search;
|
||||
}
|
||||
|
||||
router.replace({ query });
|
||||
};
|
||||
|
||||
const buildSortAttr = () =>
|
||||
`${sortState.activeOrdering}${sortState.activeSort}`;
|
||||
|
||||
const getCommonFetchParams = (page = 1) => ({
|
||||
page,
|
||||
sortAttr: buildSortAttr(),
|
||||
label: activeLabel.value,
|
||||
});
|
||||
|
||||
const fetchContacts = async (page = 1) => {
|
||||
await store.dispatch('contacts/get', getCommonFetchParams(page));
|
||||
updatePageParam(page);
|
||||
};
|
||||
|
||||
const fetchSavedOrAppliedFilteredContact = async (payload, page = 1) => {
|
||||
if (!activeSegmentId.value && !hasAppliedFilters.value) return;
|
||||
await store.dispatch('contacts/filter', {
|
||||
...getCommonFetchParams(page),
|
||||
queryPayload: payload,
|
||||
});
|
||||
updatePageParam(page);
|
||||
};
|
||||
|
||||
const searchContacts = debounce(async (value, page = 1) => {
|
||||
searchValue.value = value;
|
||||
|
||||
if (!value) {
|
||||
updatePageParam(page);
|
||||
await fetchContacts(page);
|
||||
return;
|
||||
}
|
||||
|
||||
updatePageParam(page, value);
|
||||
await store.dispatch('contacts/search', {
|
||||
...getCommonFetchParams(page),
|
||||
search: encodeURIComponent(value),
|
||||
});
|
||||
}, DEBOUNCE_DELAY);
|
||||
|
||||
const fetchContactsBasedOnContext = async page => {
|
||||
updatePageParam(page, searchValue.value);
|
||||
if (isFetchingList.value) return;
|
||||
if (searchQuery.value) {
|
||||
await searchContacts(searchQuery.value, page);
|
||||
return;
|
||||
}
|
||||
// Reset the search value when we change the view
|
||||
searchValue.value = '';
|
||||
// If there are applied filters or active segment with query
|
||||
if (
|
||||
(hasAppliedFilters.value || activeSegment.value?.query) &&
|
||||
!activeLabel.value
|
||||
) {
|
||||
const queryPayload =
|
||||
activeSegment.value?.query || filterQueryGenerator(appliedFilters.value);
|
||||
await fetchSavedOrAppliedFilteredContact(queryPayload, page);
|
||||
return;
|
||||
}
|
||||
// Default case: fetch regular contacts + label
|
||||
await fetchContacts(page);
|
||||
};
|
||||
|
||||
const handleSort = async ({ sort, order }) => {
|
||||
Object.assign(sortState, { activeSort: sort, activeOrdering: order });
|
||||
|
||||
await updateUISettings({
|
||||
contacts_sort_by: buildSortAttr(),
|
||||
});
|
||||
|
||||
if (searchQuery.value) {
|
||||
await searchContacts(searchValue.value);
|
||||
return;
|
||||
}
|
||||
|
||||
await (activeSegmentId.value || hasAppliedFilters.value
|
||||
? fetchSavedOrAppliedFilteredContact(
|
||||
activeSegmentId.value
|
||||
? activeSegment.value?.query
|
||||
: filterQueryGenerator(appliedFilters.value)
|
||||
)
|
||||
: fetchContacts());
|
||||
};
|
||||
|
||||
const createContact = async contact => {
|
||||
await store.dispatch('contacts/create', contact);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => uiSettings.value?.contacts_sort_by,
|
||||
newSortBy => {
|
||||
if (newSortBy) {
|
||||
const { sort, order } = parseSortSettings(newSortBy);
|
||||
sortState.activeSort = sort;
|
||||
sortState.activeOrdering = order;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
[activeLabel, activeSegment],
|
||||
() => {
|
||||
fetchContactsBasedOnContext(pageNumber.value);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(searchQuery, value => {
|
||||
if (isFetchingList.value) return;
|
||||
searchValue.value = value || '';
|
||||
// Reset the view if there is search query when we click on the sidebar group
|
||||
if (value === undefined) {
|
||||
fetchContacts();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (!activeSegmentId.value) {
|
||||
if (searchQuery.value) {
|
||||
await searchContacts(searchQuery.value, pageNumber.value);
|
||||
return;
|
||||
}
|
||||
await fetchContacts(pageNumber.value);
|
||||
} else if (activeSegment.value && activeSegmentId.value) {
|
||||
await fetchSavedOrAppliedFilteredContact(
|
||||
activeSegment.value.query,
|
||||
pageNumber.value
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col justify-between flex-1 h-full m-0 overflow-auto bg-n-background"
|
||||
>
|
||||
<ContactsListLayout
|
||||
:search-value="searchValue"
|
||||
:header-title="headerTitle"
|
||||
:current-page="currentPage"
|
||||
:total-items="totalItems"
|
||||
:show-pagination-footer="!isFetchingList && hasContacts"
|
||||
:active-sort="sortState.activeSort"
|
||||
:active-ordering="sortState.activeOrdering"
|
||||
:active-segment="activeSegment"
|
||||
:segments-id="activeSegmentId"
|
||||
:has-applied-filters="hasAppliedFilters"
|
||||
@update:current-page="fetchContactsBasedOnContext"
|
||||
@search="searchContacts"
|
||||
@update:sort="handleSort"
|
||||
@apply-filter="fetchSavedOrAppliedFilteredContact"
|
||||
@clear-filters="fetchContacts"
|
||||
>
|
||||
<div
|
||||
v-if="isFetchingList"
|
||||
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<ContactEmptyState
|
||||
v-if="showEmptyStateLayout"
|
||||
class="pt-14"
|
||||
:title="t('CONTACTS_LAYOUT.EMPTY_STATE.TITLE')"
|
||||
:subtitle="t('CONTACTS_LAYOUT.EMPTY_STATE.SUBTITLE')"
|
||||
:button-label="t('CONTACTS_LAYOUT.EMPTY_STATE.BUTTON_LABEL')"
|
||||
@create="createContact"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-else-if="showEmptyText"
|
||||
class="flex items-center justify-center py-10"
|
||||
>
|
||||
<span class="text-base text-n-slate-11">
|
||||
{{
|
||||
searchQuery || !hasAppliedFilters
|
||||
? t('CONTACTS_LAYOUT.EMPTY_STATE.SEARCH_EMPTY_STATE_TITLE')
|
||||
: t('CONTACTS_LAYOUT.EMPTY_STATE.LIST_EMPTY_STATE_TITLE')
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ContactsList v-else :contacts="contacts" />
|
||||
</template>
|
||||
</ContactsListLayout>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,42 +1,38 @@
|
||||
/* eslint arrow-body-style: 0 */
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
import ContactsView from './components/ContactsView.vue';
|
||||
import ContactsIndex from './pages/ContactsIndex.vue';
|
||||
import ContactManageView from './pages/ContactManageView.vue';
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/contacts'),
|
||||
name: 'contacts_dashboard',
|
||||
component: ContactsIndex,
|
||||
name: 'contacts_dashboard_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'contact_manage'],
|
||||
},
|
||||
component: ContactsView,
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/contacts/custom_view/:id'),
|
||||
name: 'contacts_segments_dashboard',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'contact_manage'],
|
||||
},
|
||||
component: ContactsView,
|
||||
props: route => {
|
||||
return { segmentsId: route.params.id };
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/labels/:label/contacts'),
|
||||
name: 'contacts_labels_dashboard',
|
||||
path: frontendURL('accounts/:accountId/contacts/segments/:segmentId'),
|
||||
component: ContactsIndex,
|
||||
name: 'contacts_dashboard_segments_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'contact_manage'],
|
||||
},
|
||||
component: ContactsView,
|
||||
props: route => {
|
||||
return { label: route.params.label };
|
||||
},
|
||||
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/contacts/labels/:label'),
|
||||
component: ContactsIndex,
|
||||
name: 'contacts_dashboard_labels_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'contact_manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/contacts/:contactId'),
|
||||
name: 'contact_profile_dashboard',
|
||||
name: 'contacts_edit',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'contact_manage'],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user