Restructure omni services and add Chatwoot research snapshot

This commit is contained in:
Ruslan Bakiev
2026-02-21 11:11:27 +07:00
parent edea7a0034
commit b73babbbf6
7732 changed files with 978203 additions and 32 deletions

View File

@@ -0,0 +1,328 @@
import {
DuplicateContactException,
ExceptionWithMessage,
} from 'shared/helpers/CustomErrors';
import types from '../../mutation-types';
import ContactAPI from '../../../api/contacts';
import snakecaseKeys from 'snakecase-keys';
import AccountActionsAPI from '../../../api/accountActions';
import AnalyticsHelper from '../../../helper/AnalyticsHelper';
import { CONTACTS_EVENTS } from '../../../helper/AnalyticsHelper/events';
const buildContactFormData = contactParams => {
const formData = new FormData();
const { additional_attributes = {}, ...contactProperties } = contactParams;
Object.keys(contactProperties).forEach(key => {
if (contactProperties[key]) {
formData.append(key, contactProperties[key]);
}
});
const { social_profiles, ...additionalAttributesProperties } =
additional_attributes;
Object.keys(additionalAttributesProperties).forEach(key => {
formData.append(
`additional_attributes[${key}]`,
additionalAttributesProperties[key]
);
});
Object.keys(social_profiles).forEach(key => {
formData.append(
`additional_attributes[social_profiles][${key}]`,
social_profiles[key]
);
});
return formData;
};
export const handleContactOperationErrors = error => {
if (error.response?.status === 422) {
throw new DuplicateContactException(error.response.data.attributes);
} else if (error.response?.data?.message) {
throw new ExceptionWithMessage(error.response.data.message);
} else {
throw new Error(error);
}
};
export const actions = {
search: async (
{ commit },
{ search, page, sortAttr, label, append = false }
) => {
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
try {
const {
data: { payload, meta },
} = await ContactAPI.search(search, page, sortAttr, label);
if (!append) {
commit(types.CLEAR_CONTACTS);
}
commit(append ? types.APPEND_CONTACTS : types.SET_CONTACTS, payload);
commit(types.SET_CONTACT_META, meta);
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
} catch (error) {
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
}
},
get: async ({ commit }, { page = 1, sortAttr, label } = {}) => {
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
try {
const {
data: { payload, meta },
} = await ContactAPI.get(page, sortAttr, label);
commit(types.CLEAR_CONTACTS);
commit(types.SET_CONTACTS, payload);
commit(types.SET_CONTACT_META, meta);
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
} catch (error) {
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
}
},
active: async ({ commit }, { page = 1, sortAttr } = {}) => {
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
try {
const {
data: { payload, meta },
} = await ContactAPI.active(page, sortAttr);
commit(types.CLEAR_CONTACTS);
commit(types.SET_CONTACTS, payload);
commit(types.SET_CONTACT_META, meta);
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
} catch (error) {
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
}
},
show: async ({ commit }, { id }) => {
commit(types.SET_CONTACT_UI_FLAG, { isFetchingItem: true });
try {
const response = await ContactAPI.show(id);
commit(types.SET_CONTACT_ITEM, response.data.payload);
commit(types.SET_CONTACT_UI_FLAG, {
isFetchingItem: false,
});
} catch (error) {
commit(types.SET_CONTACT_UI_FLAG, {
isFetchingItem: false,
});
}
},
update: async ({ commit }, { id, isFormData = false, ...contactParams }) => {
const { avatar, customAttributes, ...paramsToDecamelize } = contactParams;
const decamelizedContactParams = {
...snakecaseKeys(paramsToDecamelize, { deep: true }),
...(customAttributes && { custom_attributes: customAttributes }),
...(avatar && { avatar }),
};
commit(types.SET_CONTACT_UI_FLAG, { isUpdating: true });
try {
const response = await ContactAPI.update(
id,
isFormData
? buildContactFormData(decamelizedContactParams)
: decamelizedContactParams
);
commit(types.EDIT_CONTACT, response.data.payload);
commit(types.SET_CONTACT_UI_FLAG, { isUpdating: false });
} catch (error) {
commit(types.SET_CONTACT_UI_FLAG, { isUpdating: false });
handleContactOperationErrors(error);
}
},
create: async ({ commit }, { isFormData = false, ...contactParams }) => {
const decamelizedContactParams = snakecaseKeys(contactParams, {
deep: true,
});
commit(types.SET_CONTACT_UI_FLAG, { isCreating: true });
try {
const response = await ContactAPI.create(
isFormData
? buildContactFormData(decamelizedContactParams)
: decamelizedContactParams
);
AnalyticsHelper.track(CONTACTS_EVENTS.CREATE_CONTACT);
commit(types.SET_CONTACT_ITEM, response.data.payload.contact);
commit(types.SET_CONTACT_UI_FLAG, { isCreating: false });
return response.data.payload.contact;
} catch (error) {
commit(types.SET_CONTACT_UI_FLAG, { isCreating: false });
return handleContactOperationErrors(error);
}
},
import: async ({ commit }, file) => {
commit(types.SET_CONTACT_UI_FLAG, { isImporting: true });
try {
await ContactAPI.importContacts(file);
commit(types.SET_CONTACT_UI_FLAG, { isImporting: false });
} catch (error) {
commit(types.SET_CONTACT_UI_FLAG, { isImporting: false });
if (error.response?.data?.message) {
throw new ExceptionWithMessage(error.response.data.message);
}
}
},
export: async ({ commit }, { payload, label }) => {
commit(types.SET_CONTACT_UI_FLAG, { isExporting: true });
try {
await ContactAPI.exportContacts({ payload, label });
commit(types.SET_CONTACT_UI_FLAG, { isExporting: false });
} catch (error) {
commit(types.SET_CONTACT_UI_FLAG, { isExporting: false });
if (error.response?.data?.message) {
throw new Error(error.response.data.message);
} else {
throw new Error(error);
}
}
},
delete: async ({ commit }, id) => {
commit(types.SET_CONTACT_UI_FLAG, { isDeleting: true });
try {
await ContactAPI.delete(id);
commit(types.SET_CONTACT_UI_FLAG, { isDeleting: false });
} catch (error) {
commit(types.SET_CONTACT_UI_FLAG, { isDeleting: false });
if (error.response?.data?.message) {
throw new Error(error.response.data.message);
} else {
throw new Error(error);
}
}
},
deleteCustomAttributes: async ({ commit }, { id, customAttributes }) => {
try {
const response = await ContactAPI.destroyCustomAttributes(
id,
customAttributes
);
commit(types.EDIT_CONTACT, response.data.payload);
} catch (error) {
throw new Error(error);
}
},
deleteAvatar: async ({ commit }, id) => {
try {
const response = await ContactAPI.destroyAvatar(id);
commit(types.EDIT_CONTACT, response.data.payload);
} catch (error) {
throw new Error(error);
}
},
fetchContactableInbox: async ({ commit }, id) => {
commit(types.SET_CONTACT_UI_FLAG, { isFetchingInboxes: true });
try {
const response = await ContactAPI.getContactableInboxes(id);
const contact = {
id: Number(id),
contact_inboxes: response.data.payload,
};
commit(types.SET_CONTACT_ITEM, contact);
} catch (error) {
if (error.response?.data?.message) {
throw new ExceptionWithMessage(error.response.data.message);
} else {
throw new Error(error);
}
} finally {
commit(types.SET_CONTACT_UI_FLAG, { isFetchingInboxes: false });
}
},
updatePresence: ({ commit }, data) => {
commit(types.UPDATE_CONTACTS_PRESENCE, data);
},
setContact({ commit }, data) {
commit(types.SET_CONTACT_ITEM, data);
},
merge: async ({ commit }, { childId, parentId }) => {
commit(types.SET_CONTACT_UI_FLAG, { isMerging: true });
try {
const response = await AccountActionsAPI.merge(parentId, childId);
commit(types.SET_CONTACT_ITEM, response.data);
} catch (error) {
throw new Error(error);
} finally {
commit(types.SET_CONTACT_UI_FLAG, { isMerging: false });
}
},
deleteContactThroughConversations: ({ commit }, id) => {
commit(types.DELETE_CONTACT, id);
commit(types.CLEAR_CONTACT_CONVERSATIONS, id, { root: true });
commit(`contactConversations/${types.DELETE_CONTACT_CONVERSATION}`, id, {
root: true,
});
},
updateContact: async ({ commit }, updateObj) => {
commit(types.SET_CONTACT_UI_FLAG, { isUpdating: true });
try {
commit(types.EDIT_CONTACT, updateObj);
commit(types.SET_CONTACT_UI_FLAG, { isUpdating: false });
} catch (error) {
commit(types.SET_CONTACT_UI_FLAG, { isUpdating: false });
}
},
filter: async (
{ commit },
{ page = 1, sortAttr, queryPayload, resetState = true } = {}
) => {
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
try {
const {
data: { payload, meta },
} = await ContactAPI.filter(page, sortAttr, queryPayload);
if (resetState) {
commit(types.CLEAR_CONTACTS);
commit(types.SET_CONTACTS, payload);
commit(types.SET_CONTACT_META, meta);
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
}
return payload;
} catch (error) {
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
}
return [];
},
setContactFilters({ commit }, data) {
commit(types.SET_CONTACT_FILTERS, data);
},
clearContactFilters({ commit }) {
commit(types.CLEAR_CONTACT_FILTERS);
},
initiateCall: async ({ commit }, { contactId, inboxId }) => {
commit(types.SET_CONTACT_UI_FLAG, { isInitiatingCall: true });
try {
const response = await ContactAPI.initiateCall(contactId, inboxId);
commit(types.SET_CONTACT_UI_FLAG, { isInitiatingCall: false });
return response.data;
} catch (error) {
commit(types.SET_CONTACT_UI_FLAG, { isInitiatingCall: false });
if (error.response?.data?.message) {
throw new ExceptionWithMessage(error.response.data.message);
} else if (error.response?.data?.error) {
throw new ExceptionWithMessage(error.response.data.error);
} else {
throw new Error(error);
}
}
},
};

View File

@@ -0,0 +1,36 @@
import camelcaseKeys from 'camelcase-keys';
export const getters = {
getContacts($state) {
return $state.sortOrder.map(contactId => $state.records[contactId]);
},
getContactsList($state) {
const contacts = $state.sortOrder.map(
contactId => $state.records[contactId]
);
return camelcaseKeys(contacts, { deep: true });
},
getUIFlags($state) {
return $state.uiFlags;
},
getContact: $state => id => {
const contact = $state.records[id];
return contact || {};
},
getContactById: $state => id => {
const contact = $state.records[id];
return camelcaseKeys(contact || {}, {
deep: true,
stopPaths: ['custom_attributes'],
});
},
getMeta: $state => {
return $state.meta;
},
getAppliedContactFilters: _state => {
return _state.appliedFilters;
},
getAppliedContactFiltersV4: _state => {
return _state.appliedFilters.map(camelcaseKeys);
},
};

View File

@@ -0,0 +1,33 @@
import { getters } from './getters';
import { actions } from './actions';
import { mutations } from './mutations';
const state = {
meta: {
count: 0,
currentPage: 1,
hasMore: false,
},
records: {},
uiFlags: {
isFetching: false,
isFetchingItem: false,
isFetchingInboxes: false,
isUpdating: false,
isMerging: false,
isDeleting: false,
isExporting: false,
isImporting: false,
isInitiatingCall: false,
},
sortOrder: [],
appliedFilters: [],
};
export default {
namespaced: true,
state,
getters,
actions,
mutations,
};

View File

@@ -0,0 +1,99 @@
import types from '../../mutation-types';
import * as Sentry from '@sentry/vue';
export const mutations = {
[types.SET_CONTACT_UI_FLAG]($state, data) {
$state.uiFlags = {
...$state.uiFlags,
...data,
};
},
[types.CLEAR_CONTACTS]: $state => {
$state.records = {};
$state.sortOrder = [];
},
[types.SET_CONTACT_META]: ($state, data) => {
const { count, current_page: currentPage, has_more: hasMore } = data;
$state.meta.count = count;
$state.meta.currentPage = currentPage;
if (hasMore !== undefined) {
$state.meta.hasMore = hasMore;
}
},
[types.APPEND_CONTACTS]: ($state, data) => {
data.forEach(contact => {
$state.records[contact.id] = {
...($state.records[contact.id] || {}),
...contact,
};
if (!$state.sortOrder.includes(contact.id)) {
$state.sortOrder.push(contact.id);
}
});
},
[types.SET_CONTACTS]: ($state, data) => {
const sortOrder = data.map(contact => {
$state.records[contact.id] = {
...($state.records[contact.id] || {}),
...contact,
};
return contact.id;
});
$state.sortOrder = sortOrder;
},
[types.SET_CONTACT_ITEM]: ($state, data) => {
$state.records[data.id] = {
...($state.records[data.id] || {}),
...data,
};
if (!$state.sortOrder.includes(data.id)) {
$state.sortOrder.push(data.id);
}
},
[types.EDIT_CONTACT]: ($state, data) => {
$state.records[data.id] = data;
},
[types.DELETE_CONTACT]: ($state, id) => {
const index = $state.sortOrder.findIndex(item => item === id);
$state.sortOrder.splice(index, 1);
delete $state.records[id];
},
[types.UPDATE_CONTACTS_PRESENCE]: ($state, data) => {
Object.values($state.records).forEach(element => {
let availabilityStatus;
try {
availabilityStatus = data[element.id];
} catch (error) {
Sentry.setContext('contact is undefined', {
records: $state.records,
data: data,
});
Sentry.captureException(error);
return;
}
if (availabilityStatus) {
$state.records[element.id].availability_status = availabilityStatus;
} else {
$state.records[element.id].availability_status = null;
}
});
},
[types.SET_CONTACT_FILTERS](_state, data) {
_state.appliedFilters = data;
},
[types.CLEAR_CONTACT_FILTERS](_state) {
_state.appliedFilters = [];
},
};