feat: Companies page (#12842)
# Pull Request Template ## Description This PR introduces a new Companies section in the Chatwoot dashboard. It lists all companies associated with the account and includes features such as **search**, **sorting**, and **pagination** to enable easier navigation and efficient management. Fixes https://linear.app/chatwoot/issue/CW-5928/add-companies-tab-to-dashboard ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? ### Screenshot <img width="1619" height="1200" alt="image" src="https://github.com/user-attachments/assets/21f0a666-c3d6-4dec-bd02-1e38e0cd9542" /> ## 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 - [ ] 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: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com> Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
|
||||
import CompaniesListLayout from 'dashboard/components-next/Companies/CompaniesListLayout.vue';
|
||||
import CompaniesCard from 'dashboard/components-next/Companies/CompaniesCard/CompaniesCard.vue';
|
||||
|
||||
const DEBOUNCE_DELAY = 300;
|
||||
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const searchQuery = computed(() => route.query?.search || '');
|
||||
const searchValue = ref(searchQuery.value);
|
||||
const pageNumber = computed(() => Number(route.query?.page) || 1);
|
||||
|
||||
const activeSort = computed(() => {
|
||||
const sortParam = route.query?.sort || 'name';
|
||||
return sortParam.startsWith('-') ? sortParam.slice(1) : sortParam;
|
||||
});
|
||||
|
||||
const activeOrdering = computed(() => {
|
||||
const sortParam = route.query?.sort || 'name';
|
||||
return sortParam.startsWith('-') ? '-' : '';
|
||||
});
|
||||
|
||||
const companies = useMapGetter('companies/getCompaniesList');
|
||||
const meta = useMapGetter('companies/getMeta');
|
||||
const uiFlags = useMapGetter('companies/getUIFlags');
|
||||
|
||||
const isFetchingList = computed(() => uiFlags.value.fetchingList);
|
||||
|
||||
const sortParam = computed(() => {
|
||||
return activeOrdering.value === '-'
|
||||
? `-${activeSort.value}`
|
||||
: activeSort.value;
|
||||
});
|
||||
|
||||
const updateURLParams = (page, search = '', sort = '') => {
|
||||
const query = {
|
||||
...route.query,
|
||||
page: page.toString(),
|
||||
};
|
||||
|
||||
if (search) {
|
||||
query.search = search;
|
||||
} else {
|
||||
delete query.search;
|
||||
}
|
||||
|
||||
if (sort) {
|
||||
query.sort = sort;
|
||||
} else {
|
||||
delete query.sort;
|
||||
}
|
||||
|
||||
router.replace({ query });
|
||||
};
|
||||
|
||||
const fetchCompanies = async (page, search, sort) => {
|
||||
const currentPage = page ?? pageNumber.value;
|
||||
const currentSearch = search ?? searchQuery.value;
|
||||
const currentSort = sort ?? sortParam.value;
|
||||
|
||||
// Only update URL if arguments were explicitly provided
|
||||
if (page !== undefined || search !== undefined || sort !== undefined) {
|
||||
updateURLParams(currentPage, currentSearch, currentSort);
|
||||
}
|
||||
|
||||
if (currentSearch) {
|
||||
await store.dispatch('companies/search', {
|
||||
search: currentSearch,
|
||||
page: currentPage,
|
||||
sort: currentSort,
|
||||
});
|
||||
} else {
|
||||
await store.dispatch('companies/get', {
|
||||
page: currentPage,
|
||||
sort: currentSort,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onSearch = debounce(query => {
|
||||
searchValue.value = query;
|
||||
fetchCompanies(1, query, sortParam.value);
|
||||
}, DEBOUNCE_DELAY);
|
||||
|
||||
const onPageChange = page => {
|
||||
fetchCompanies(page, searchValue.value, sortParam.value);
|
||||
};
|
||||
|
||||
const handleSort = ({ sort, order }) => {
|
||||
const newSortParam = order === '-' ? `-${sort}` : sort;
|
||||
fetchCompanies(1, searchValue.value, newSortParam);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
searchValue.value = searchQuery.value;
|
||||
fetchCompanies();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CompaniesListLayout
|
||||
:search-value="searchValue"
|
||||
:header-title="t('COMPANIES.HEADER')"
|
||||
:current-page="pageNumber"
|
||||
:total-items="Number(meta.totalCount || 0)"
|
||||
:active-sort="activeSort"
|
||||
:active-ordering="activeOrdering"
|
||||
:is-fetching-list="isFetchingList"
|
||||
@update:current-page="onPageChange"
|
||||
@update:sort="handleSort"
|
||||
@search="onSearch"
|
||||
>
|
||||
<div v-if="isFetchingList" class="flex items-center justify-center p-8">
|
||||
<span class="text-n-slate-11 text-base">{{
|
||||
t('COMPANIES.LOADING')
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="companies.length === 0"
|
||||
class="flex items-center justify-center p-8"
|
||||
>
|
||||
<span class="text-n-slate-11 text-base">{{
|
||||
t('COMPANIES.EMPTY_STATE.TITLE')
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-4 p-4">
|
||||
<CompaniesCard
|
||||
v-for="company in companies"
|
||||
:id="company.id"
|
||||
:key="company.id"
|
||||
:name="company.name"
|
||||
:domain="company.domain"
|
||||
:contacts-count="company.contactsCount || 0"
|
||||
:description="company.description"
|
||||
:avatar-url="company.avatarUrl"
|
||||
:updated-at="company.updatedAt"
|
||||
/>
|
||||
</div>
|
||||
</CompaniesListLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
import CompaniesIndex from './pages/CompaniesIndex.vue';
|
||||
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
|
||||
|
||||
const commonMeta = {
|
||||
featureFlag: FEATURE_FLAGS.COMPANIES,
|
||||
permissions: ['administrator', 'agent'],
|
||||
installationTypes: [INSTALLATION_TYPES.CLOUD, INSTALLATION_TYPES.ENTERPRISE],
|
||||
};
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/companies'),
|
||||
component: CompaniesIndex,
|
||||
meta: commonMeta,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'companies_dashboard_index',
|
||||
component: CompaniesIndex,
|
||||
meta: commonMeta,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -2,6 +2,7 @@ import settings from './settings/settings.routes';
|
||||
import conversation from './conversation/conversation.routes';
|
||||
import { routes as searchRoutes } from '../../modules/search/search.routes';
|
||||
import { routes as contactRoutes } from './contacts/routes';
|
||||
import { routes as companyRoutes } from './companies/routes';
|
||||
import { routes as notificationRoutes } from './notifications/routes';
|
||||
import { routes as inboxRoutes } from './inbox/routes';
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
@@ -23,6 +24,7 @@ export default {
|
||||
...conversation.routes,
|
||||
...settings.routes,
|
||||
...contactRoutes,
|
||||
...companyRoutes,
|
||||
...searchRoutes,
|
||||
...notificationRoutes,
|
||||
...helpcenterRoutes.routes,
|
||||
|
||||
Reference in New Issue
Block a user