feat(companies): add company creation flow (#14402)

This commit is contained in:
Sojan Jose
2026-05-08 11:46:33 +05:30
committed by GitHub
parent 53b2a517d7
commit 10597863d7
8 changed files with 222 additions and 4 deletions
@@ -2,6 +2,7 @@
import Input from 'dashboard/components-next/input/Input.vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import CompanySortMenu from './components/CompanySortMenu.vue';
import CompanyMoreActions from './components/CompanyMoreActions.vue';
defineProps({
showSearch: { type: Boolean, default: true },
@@ -11,7 +12,7 @@ defineProps({
activeOrdering: { type: String, default: '' },
});
const emit = defineEmits(['search', 'update:sort']);
const emit = defineEmits(['search', 'update:sort', 'create']);
</script>
<template>
@@ -48,6 +49,7 @@ const emit = defineEmits(['search', 'update:sort']);
:active-ordering="activeOrdering"
@update:sort="emit('update:sort', $event)"
/>
<CompanyMoreActions @create="emit('create')" />
</div>
</div>
</div>
@@ -0,0 +1,45 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import Button from 'dashboard/components-next/button/Button.vue';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
const emit = defineEmits(['create']);
const { t } = useI18n();
const showActionsDropdown = ref(false);
const menuItems = [
{
label: t('COMPANIES.ACTIONS.CREATE'),
action: 'create',
value: 'create',
icon: 'i-lucide-plus',
},
];
const handleAction = ({ action }) => {
if (action === 'create') emit('create');
showActionsDropdown.value = false;
};
</script>
<template>
<div v-on-clickaway="() => (showActionsDropdown = false)" class="relative">
<Button
icon="i-lucide-ellipsis-vertical"
color="slate"
variant="ghost"
size="sm"
:class="showActionsDropdown ? 'bg-n-alpha-2' : ''"
@click="showActionsDropdown = !showActionsDropdown"
/>
<DropdownMenu
v-if="showActionsDropdown"
:menu-items="menuItems"
class="ltr:right-0 rtl:left-0 mt-1 w-52 top-full"
@action="handleAction($event)"
/>
</div>
</template>
@@ -12,7 +12,12 @@ defineProps({
showPaginationFooter: { type: Boolean, default: true },
});
const emit = defineEmits(['update:currentPage', 'update:sort', 'search']);
const emit = defineEmits([
'update:currentPage',
'update:sort',
'search',
'create',
]);
const updateCurrentPage = page => {
emit('update:currentPage', page);
@@ -31,6 +36,7 @@ const updateCurrentPage = page => {
:active-ordering="activeOrdering"
@search="emit('search', $event)"
@update:sort="emit('update:sort', $event)"
@create="emit('create')"
/>
<main class="flex-1 px-6 overflow-y-auto">
<div class="w-full mx-auto max-w-5xl py-4">
@@ -0,0 +1,110 @@
<script setup>
import { computed, reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import Button from 'dashboard/components-next/button/Button.vue';
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
import Input from 'dashboard/components-next/input/Input.vue';
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
defineProps({
isLoading: { type: Boolean, default: false },
});
const emit = defineEmits(['create']);
const { t } = useI18n();
const dialogRef = ref(null);
const form = reactive({ name: '', domain: '', description: '' });
const isFormInvalid = computed(() => !form.name.trim());
const resetForm = () => {
form.name = '';
form.domain = '';
form.description = '';
};
const handleConfirm = () => {
if (isFormInvalid.value) return;
emit('create', {
name: form.name.trim(),
domain: form.domain.trim() || null,
description: form.description.trim() || null,
});
};
const closeDialog = () => {
dialogRef.value?.close();
};
const onSuccess = () => {
resetForm();
closeDialog();
};
defineExpose({ dialogRef, onSuccess });
</script>
<template>
<Dialog
ref="dialogRef"
width="3xl"
overflow-y-auto
@confirm="handleConfirm"
@close="resetForm"
>
<div class="flex flex-col gap-6">
<div class="flex flex-col items-start gap-2">
<span class="py-1 text-sm font-medium text-n-slate-12">
{{ t('COMPANIES.CREATE.TITLE') }}
</span>
<div class="grid w-full grid-cols-1 gap-4 sm:grid-cols-2">
<Input
v-model="form.name"
:placeholder="t('COMPANIES.DETAIL.PROFILE.FIELDS.NAME')"
:disabled="isLoading"
custom-input-class="h-8 !pt-1 !pb-1 [&:not(.error,.focus)]:!outline-transparent"
autofocus
/>
<Input
v-model="form.domain"
:placeholder="t('COMPANIES.DETAIL.PROFILE.FIELDS.DOMAIN')"
:disabled="isLoading"
custom-input-class="h-8 !pt-1 !pb-1 [&:not(.error,.focus)]:!outline-transparent"
/>
</div>
</div>
<TextArea
v-model="form.description"
:placeholder="t('COMPANIES.DETAIL.PROFILE.DESCRIPTION_PLACEHOLDER')"
:disabled="isLoading"
:max-length="280"
class="w-full"
show-character-count
auto-height
/>
</div>
<template #footer>
<div class="flex items-center justify-between w-full gap-3">
<Button
:label="t('DIALOG.BUTTONS.CANCEL')"
variant="link"
type="reset"
class="h-10 hover:!no-underline hover:text-n-brand"
@click="closeDialog"
/>
<Button
:label="t('COMPANIES.CREATE.ACTIONS.SAVE')"
color="blue"
type="submit"
:disabled="isFormInvalid || isLoading"
:is-loading="isLoading"
/>
</div>
</template>
</Dialog>
</template>
@@ -169,7 +169,7 @@ const handleContactSelect = contactId => {
</script>
<template>
<div class="flex flex-col gap-6 px-6 pb-6 pt-1">
<div class="flex flex-col gap-6 px-6 pb-8 pt-1">
<div v-if="!selectedContact" class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<label class="text-base text-n-slate-12">
@@ -346,7 +346,7 @@ const handleContactSelect = contactId => {
:current-page="currentPage"
:total-items="totalContacts"
:items-per-page="15"
class="px-0 before:hidden"
class="!px-0 before:hidden"
@update:current-page="emit('update:currentPage', $event)"
/>
</div>
@@ -22,6 +22,19 @@
"LOADING": "Loading companies...",
"UNNAMED": "Unnamed Company",
"CONTACTS_COUNT": "{n} contact | {n} contacts",
"ACTIONS": {
"CREATE": "Add company"
},
"CREATE": {
"TITLE": "Add company details",
"ACTIONS": {
"SAVE": "Add company"
},
"MESSAGES": {
"SUCCESS": "Company created.",
"ERROR": "Could not create the company."
}
},
"DETAIL": {
"LOADING": "Loading company details...",
"EMPTY_STATE": {
@@ -3,11 +3,13 @@ import { ref, computed, onMounted, reactive } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useAlert } from 'dashboard/composables';
import { debounce } from '@chatwoot/utils';
import { useCompaniesStore } from 'dashboard/stores/companies';
import CompaniesListLayout from 'dashboard/components-next/Companies/CompaniesListLayout.vue';
import CompaniesCard from 'dashboard/components-next/Companies/CompaniesCard/CompaniesCard.vue';
import CompanyCreateDialog from 'dashboard/components-next/Companies/CompanyCreateDialog.vue';
const DEFAULT_SORT_FIELD = 'name';
const DEBOUNCE_DELAY = 300;
@@ -26,6 +28,7 @@ const uiFlags = computed(() => companiesStore.getUIFlags);
const searchQuery = computed(() => route.query?.search || '');
const searchValue = ref(searchQuery.value);
const createCompanyDialogRef = ref(null);
const pageNumber = computed(() => Number(route.query?.page) || 1);
const parseSortSettings = (sortString = '') => {
@@ -51,6 +54,7 @@ const activeSort = computed(() => sortState.activeSort);
const activeOrdering = computed(() => sortState.activeOrdering);
const isFetchingList = computed(() => uiFlags.value.fetchingList);
const isCreatingCompany = computed(() => uiFlags.value.creatingItem);
const buildSortAttr = () =>
`${sortState.activeOrdering}${sortState.activeSort}`;
@@ -121,6 +125,21 @@ const showCompany = companyId => {
});
};
const openCreateCompanyDialog = () => {
createCompanyDialogRef.value?.dialogRef.open();
};
const createCompany = async company => {
try {
const newCompany = await companiesStore.create(company);
createCompanyDialogRef.value?.onSuccess();
useAlert(t('COMPANIES.CREATE.MESSAGES.SUCCESS'));
showCompany(newCompany.id);
} catch {
useAlert(t('COMPANIES.CREATE.MESSAGES.ERROR'));
}
};
const handleSort = async ({ sort, order }) => {
Object.assign(sortState, { activeSort: sort, activeOrdering: order });
@@ -155,6 +174,7 @@ onMounted(() => {
@update:current-page="onPageChange"
@update:sort="handleSort"
@search="onSearch"
@create="openCreateCompanyDialog"
>
<div v-if="isFetchingList" class="flex items-center justify-center p-8">
<span class="text-n-slate-11 text-base">{{
@@ -182,5 +202,10 @@ onMounted(() => {
@show-company="showCompany"
/>
</div>
<CompanyCreateDialog
ref="createCompanyDialogRef"
:is-loading="isCreatingCompany"
@create="createCompany"
/>
</CompaniesListLayout>
</template>
@@ -8,6 +8,7 @@ const createInitialUIFlags = () => ({
fetchingList: false,
fetchingItem: false,
updatingItem: false,
creatingItem: false,
deletingItem: false,
deletingAvatar: false,
deletingCustomAttributes: false,
@@ -172,6 +173,22 @@ export const useCompaniesStore = createStore({
}
},
async create(companyAttrs) {
this.setUIFlag({ creatingItem: true });
try {
const {
data: { payload },
} = await CompanyAPI.create(buildCompanyRequestPayload(companyAttrs));
const company = camelizeCompany(payload);
this.upsertCompanyRecord(company);
return company;
} catch (error) {
return throwErrorMessage(error);
} finally {
this.setUIFlag({ creatingItem: false });
}
},
async delete(id) {
this.setUIFlag({ deletingItem: true });
try {