Adds a company selector to the contact details form so agents can associate a contact with an existing company directly from the contact page. Closes - None Why Contacts already expose company information through the CRM fields, but the form only accepted free-text company names. As we split company CRM work into smaller PRs, this keeps the contact page aligned with the structured company model while preserving the existing company-name behavior used by automations. What changed - Shows a company dropdown in the contact details form when the Companies feature is enabled. - Keeps legacy free-text company names editable when a contact has no structured `company_id`. - Allows Enterprise contact create/update APIs to accept account-scoped `company_id`. - Syncs `additional_attributes.company_name` when a contact is associated with a company, including the existing email-domain auto-association path. - Serializes `company_id` in the contact model payload so the form can show the current association. How to test 1. Enable Companies for an account and open a contact details page. 2. In Edit contact details, use the Company field to select an existing company. 3. Save the contact and refresh the page. 4. Confirm the selected company remains visible and the contact is associated with that company. 5. Confirm contacts with only a legacy free-text company name still show the text input instead of an empty selector. --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
354 lines
11 KiB
JavaScript
354 lines
11 KiB
JavaScript
import {
|
|
DuplicateContactException,
|
|
ExceptionWithMessage,
|
|
} from 'shared/helpers/CustomErrors';
|
|
import snakecaseKeys from 'snakecase-keys';
|
|
import AccountActionsAPI from '../../../api/accountActions';
|
|
import ContactAPI from '../../../api/contacts';
|
|
import AnalyticsHelper from '../../../helper/AnalyticsHelper';
|
|
import { CONTACTS_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
|
import types from '../../mutation-types';
|
|
|
|
const buildContactFormData = contactParams => {
|
|
const formData = new FormData();
|
|
const { additional_attributes = {}, ...contactProperties } = contactParams;
|
|
Object.keys(contactProperties).forEach(key => {
|
|
const value = contactProperties[key];
|
|
const shouldAppendBlankCompanyId =
|
|
key === 'company_id' && value !== undefined;
|
|
|
|
if (value || shouldAppendBlankCompanyId) {
|
|
formData.append(key, value ?? '');
|
|
}
|
|
});
|
|
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) {
|
|
const exception = new DuplicateContactException(
|
|
error.response.data.attributes
|
|
);
|
|
exception.message = error.response.data.message || exception.message;
|
|
throw exception;
|
|
} 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,
|
|
});
|
|
}
|
|
},
|
|
|
|
fetchAttachments: async ({ commit }, id) => {
|
|
commit(types.SET_CONTACT_UI_FLAG, { isFetchingAttachments: true });
|
|
try {
|
|
const response = await ContactAPI.getAttachments(id);
|
|
commit(types.SET_CONTACT_ATTACHMENTS, {
|
|
id,
|
|
data: response.data.payload,
|
|
});
|
|
} finally {
|
|
commit(types.SET_CONTACT_UI_FLAG, { isFetchingAttachments: 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, conversationId }) => {
|
|
commit(types.SET_CONTACT_UI_FLAG, { isInitiatingCall: true });
|
|
try {
|
|
const response = await ContactAPI.initiateCall(
|
|
contactId,
|
|
inboxId,
|
|
conversationId
|
|
);
|
|
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);
|
|
}
|
|
}
|
|
},
|
|
};
|