Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1670567bb2 | ||
|
|
bdcb1934c0 | ||
|
|
682a931707 | ||
|
|
7aa3adc4a3 | ||
|
|
89576a6475 | ||
|
|
307dbc4202 | ||
|
|
f34a20e12a | ||
|
|
5e1680e840 | ||
|
|
7bbedf2b6a | ||
|
|
a7d70f418b | ||
|
|
38873fcfc2 | ||
|
|
6cf8a6b7f8 | ||
|
|
26ec0ebd09 | ||
|
|
d248f0bea2 | ||
|
|
1caec05517 |
@@ -23,7 +23,7 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
|
||||
private
|
||||
|
||||
def webhook_params
|
||||
params.require(:webhook).permit(:inbox_id, :url, subscriptions: [])
|
||||
params.require(:webhook).permit(:inbox_id, :name, :url, subscriptions: [])
|
||||
end
|
||||
|
||||
def fetch_webhook
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup>
|
||||
import { defineProps, computed, reactive } from 'vue';
|
||||
import { defineProps, computed, ref } from 'vue';
|
||||
import Message from './Message.vue';
|
||||
import { MESSAGE_TYPES } from './constants.js';
|
||||
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
|
||||
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||
import MessageApi from 'dashboard/api/inbox/message.js';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
||||
|
||||
/**
|
||||
* Props definition for the component
|
||||
@@ -40,53 +39,15 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const emit = defineEmits(['retry']);
|
||||
const store = useStore();
|
||||
|
||||
const fetchingConversations = ref(new Set());
|
||||
const allMessages = computed(() => {
|
||||
return useCamelCase(props.messages, { deep: true });
|
||||
});
|
||||
|
||||
const currentChat = useMapGetter('getSelectedChat');
|
||||
|
||||
// Cache for fetched reply messages to avoid duplicate API calls
|
||||
const fetchedReplyMessages = reactive(new Map());
|
||||
|
||||
/**
|
||||
* Fetches a specific message from the API by trying to get messages around it
|
||||
* @param {number} messageId - The ID of the message to fetch
|
||||
* @param {number} conversationId - The ID of the conversation
|
||||
* @returns {Promise<Object|null>} - The fetched message or null if not found/error
|
||||
*/
|
||||
const fetchReplyMessage = async (messageId, conversationId) => {
|
||||
// Return cached result if already fetched
|
||||
if (fetchedReplyMessages.has(messageId)) {
|
||||
return fetchedReplyMessages.get(messageId);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await MessageApi.getPreviousMessages({
|
||||
conversationId,
|
||||
before: messageId + 100,
|
||||
after: messageId - 100,
|
||||
});
|
||||
|
||||
const messages = response.data?.payload || [];
|
||||
const targetMessage = messages.find(msg => msg.id === messageId);
|
||||
|
||||
if (targetMessage) {
|
||||
const camelCaseMessage = useCamelCase(targetMessage);
|
||||
fetchedReplyMessages.set(messageId, camelCaseMessage);
|
||||
return camelCaseMessage;
|
||||
}
|
||||
|
||||
// Cache null result to avoid repeated API calls
|
||||
fetchedReplyMessages.set(messageId, null);
|
||||
return null;
|
||||
} catch (error) {
|
||||
fetchedReplyMessages.set(messageId, null);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a message should be grouped with the next message
|
||||
* @param {Number} index - Index of the current message
|
||||
@@ -126,36 +87,43 @@ const shouldGroupWithNext = (index, searchList) => {
|
||||
* @returns {Object|null} - The message being replied to, or null if not found
|
||||
*/
|
||||
const getInReplyToMessage = parentMessage => {
|
||||
if (!parentMessage) return null;
|
||||
|
||||
const inReplyToMessageId =
|
||||
parentMessage.contentAttributes?.inReplyTo ??
|
||||
parentMessage.content_attributes?.in_reply_to;
|
||||
parentMessage?.contentAttributes?.inReplyTo ??
|
||||
parentMessage?.content_attributes?.in_reply_to;
|
||||
|
||||
if (!inReplyToMessageId) return null;
|
||||
|
||||
// Try to find in current messages first
|
||||
let replyMessage = props.messages?.find(msg => msg.id === inReplyToMessageId);
|
||||
// 1. Check props messages (already camelCased via allMessages)
|
||||
const foundInProps = allMessages.value?.find(
|
||||
m => m.id === inReplyToMessageId
|
||||
);
|
||||
if (foundInProps) return foundInProps;
|
||||
|
||||
// Then try store messages
|
||||
if (!replyMessage && currentChat.value?.messages) {
|
||||
replyMessage = currentChat.value.messages.find(
|
||||
msg => msg.id === inReplyToMessageId
|
||||
);
|
||||
// 2. Check store messages
|
||||
const foundInStore = currentChat.value?.messages?.find(
|
||||
m => m.id === inReplyToMessageId
|
||||
);
|
||||
if (foundInStore) return useCamelCase(foundInStore);
|
||||
|
||||
// 3. Fetch if not currently fetching for this conversation
|
||||
const conversationId = currentChat.value?.id;
|
||||
if (
|
||||
conversationId &&
|
||||
!currentChat.value.allMessagesLoaded &&
|
||||
!fetchingConversations.value.has(conversationId)
|
||||
) {
|
||||
fetchingConversations.value.add(conversationId);
|
||||
store
|
||||
.dispatch('fetchPreviousMessages', {
|
||||
conversationId,
|
||||
before: currentChat.value.messages?.[0]?.id ?? null,
|
||||
})
|
||||
.finally(() => {
|
||||
fetchingConversations.value.delete(conversationId);
|
||||
});
|
||||
}
|
||||
|
||||
// Then check fetch cache
|
||||
if (!replyMessage && fetchedReplyMessages.has(inReplyToMessageId)) {
|
||||
replyMessage = fetchedReplyMessages.get(inReplyToMessageId);
|
||||
}
|
||||
|
||||
// If still not found and we have conversation context, fetch it
|
||||
if (!replyMessage && currentChat.value?.id) {
|
||||
fetchReplyMessage(inReplyToMessageId, currentChat.value.id);
|
||||
return null; // Let UI handle loading state
|
||||
}
|
||||
|
||||
return replyMessage ? useCamelCase(replyMessage) : null;
|
||||
return null;
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -46,6 +46,10 @@
|
||||
"CONVERSATION_TYPING_OFF": "Conversation Typing Off"
|
||||
}
|
||||
},
|
||||
"NAME": {
|
||||
"LABEL": "Webhook Name",
|
||||
"PLACEHOLDER": "Enter the name of the webhook"
|
||||
},
|
||||
"END_POINT": {
|
||||
"LABEL": "Webhook URL",
|
||||
"PLACEHOLDER": "Example: {webhookExampleURL}",
|
||||
|
||||
+14
@@ -55,6 +55,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
url: this.value.url || '',
|
||||
name: this.value.name || '',
|
||||
subscriptions: this.value.subscriptions || [],
|
||||
supportedWebhookEvents: SUPPORTED_WEBHOOK_EVENTS,
|
||||
};
|
||||
@@ -68,11 +69,15 @@ export default {
|
||||
}
|
||||
);
|
||||
},
|
||||
webhookNameInputPlaceholder() {
|
||||
return this.$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.NAME.PLACEHOLDER');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
this.$emit('submit', {
|
||||
url: this.url,
|
||||
name: this.name,
|
||||
subscriptions: this.subscriptions,
|
||||
});
|
||||
},
|
||||
@@ -97,6 +102,15 @@ export default {
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model="name"
|
||||
type="text"
|
||||
name="name"
|
||||
:placeholder="webhookNameInputPlaceholder"
|
||||
/>
|
||||
</label>
|
||||
<label :class="{ error: v$.url.$error }" class="mb-2">
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.LABEL') }}
|
||||
</label>
|
||||
|
||||
+10
-2
@@ -37,8 +37,16 @@ const subscribedEvents = computed(() => {
|
||||
<template>
|
||||
<tr>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<div class="font-medium break-words text-n-slate-12">
|
||||
{{ webhook.url }}
|
||||
<div class="flex gap-2 font-medium break-words text-n-slate-12">
|
||||
<template v-if="webhook.name">
|
||||
{{ webhook.name }}
|
||||
<span class="text-n-slate-11">
|
||||
{{ webhook.url }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ webhook.url }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="block mt-1 text-sm text-n-slate-11">
|
||||
<span class="font-medium">
|
||||
|
||||
@@ -12,62 +12,6 @@ import {
|
||||
import messageReadActions from './actions/messageReadActions';
|
||||
import messageTranslateActions from './actions/messageTranslateActions';
|
||||
import * as Sentry from '@sentry/vue';
|
||||
import { applyPageFilters } from './helpers';
|
||||
import { matchesFilters } from './helpers/filterHelpers';
|
||||
|
||||
const DEFAULT_TAB = 'all';
|
||||
|
||||
const deriveTabKeys = (conversation, state, rootGetters) => {
|
||||
const tabKeys = new Set([DEFAULT_TAB]);
|
||||
const filtersByTab = state.filtersByTab || {};
|
||||
const currentUserId = rootGetters.getCurrentUser?.id;
|
||||
|
||||
Object.entries(filtersByTab).forEach(([rawTabKey, filters = {}]) => {
|
||||
const tabKey = rawTabKey || DEFAULT_TAB;
|
||||
|
||||
if (tabKey === 'appliedFilters') {
|
||||
if (
|
||||
state.appliedFilters?.length &&
|
||||
matchesFilters(conversation, state.appliedFilters)
|
||||
) {
|
||||
tabKeys.add(tabKey);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { inboxId, status, labels = [], teamId, conversationType } = filters;
|
||||
|
||||
const matchesFilter = applyPageFilters(conversation, {
|
||||
inboxId,
|
||||
status,
|
||||
labels,
|
||||
teamId,
|
||||
conversationType,
|
||||
});
|
||||
|
||||
if (!matchesFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tabKey === 'me') {
|
||||
if (conversation.meta?.assignee?.id === currentUserId) {
|
||||
tabKeys.add(tabKey);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (tabKey === 'unassigned') {
|
||||
if (!conversation.meta?.assignee) {
|
||||
tabKeys.add(tabKey);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
tabKeys.add(tabKey);
|
||||
});
|
||||
|
||||
return Array.from(tabKeys);
|
||||
};
|
||||
|
||||
export const hasMessageFailedWithExternalError = pendingMessage => {
|
||||
// This helper is used to check if the message has failed with an external error.
|
||||
@@ -83,14 +27,10 @@ export const hasMessageFailedWithExternalError = pendingMessage => {
|
||||
|
||||
// actions
|
||||
const actions = {
|
||||
getConversation: async ({ commit, state, rootGetters }, conversationId) => {
|
||||
getConversation: async ({ commit }, conversationId) => {
|
||||
try {
|
||||
const response = await ConversationApi.show(conversationId);
|
||||
const tabKeys = deriveTabKeys(response.data, state, rootGetters);
|
||||
commit(types.UPDATE_CONVERSATION, {
|
||||
conversation: response.data,
|
||||
tabKeys,
|
||||
});
|
||||
commit(types.UPDATE_CONVERSATION, response.data);
|
||||
commit(`contacts/${types.SET_CONTACT_ITEM}`, response.data.meta.sender);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
@@ -185,9 +125,11 @@ const actions = {
|
||||
{ commit, state, dispatch },
|
||||
{ conversationId }
|
||||
) => {
|
||||
const { conversationsById, syncConversationsMessages } = state;
|
||||
const { allConversations, syncConversationsMessages } = state;
|
||||
const lastMessageId = syncConversationsMessages[conversationId];
|
||||
const selectedChat = conversationsById[conversationId];
|
||||
const selectedChat = allConversations.find(
|
||||
conversation => conversation.id === conversationId
|
||||
);
|
||||
if (!selectedChat) return;
|
||||
try {
|
||||
const { messages } = selectedChat;
|
||||
@@ -229,8 +171,10 @@ const actions = {
|
||||
{ commit, state },
|
||||
{ conversationId }
|
||||
) => {
|
||||
const { conversationsById } = state;
|
||||
const selectedChat = conversationsById[conversationId];
|
||||
const { allConversations } = state;
|
||||
const selectedChat = allConversations.find(
|
||||
conversation => conversation.id === conversationId
|
||||
);
|
||||
if (!selectedChat) return;
|
||||
const { messages } = selectedChat;
|
||||
const lastMessage = messages.last();
|
||||
@@ -393,10 +337,7 @@ const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
addConversation(
|
||||
{ commit, state, dispatch, rootState, rootGetters },
|
||||
conversation
|
||||
) {
|
||||
addConversation({ commit, state, dispatch, rootState }, conversation) {
|
||||
const { currentInbox, appliedFilters } = state;
|
||||
const {
|
||||
inbox_id: inboxId,
|
||||
@@ -412,8 +353,7 @@ const actions = {
|
||||
!isOnUnattendedView(rootState) &&
|
||||
isMatchingInboxFilter
|
||||
) {
|
||||
const tabKeys = deriveTabKeys(conversation, state, rootGetters);
|
||||
commit(types.ADD_CONVERSATION, { conversation, tabKeys });
|
||||
commit(types.ADD_CONVERSATION, conversation);
|
||||
dispatch('contacts/setContact', sender);
|
||||
}
|
||||
},
|
||||
@@ -430,12 +370,11 @@ const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
updateConversation({ commit, dispatch, state, rootGetters }, conversation) {
|
||||
updateConversation({ commit, dispatch }, conversation) {
|
||||
const {
|
||||
meta: { sender },
|
||||
} = conversation;
|
||||
const tabKeys = deriveTabKeys(conversation, state, rootGetters);
|
||||
commit(types.UPDATE_CONVERSATION, { conversation, tabKeys });
|
||||
commit(types.UPDATE_CONVERSATION, conversation);
|
||||
|
||||
dispatch('conversationLabels/setConversationLabel', {
|
||||
id: conversation.id,
|
||||
|
||||
@@ -8,80 +8,22 @@ import {
|
||||
} from '../../../helper/permissionsHelper';
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
|
||||
const DEFAULT_TAB = 'all';
|
||||
|
||||
const normalizeTabKey = tabKey => tabKey || DEFAULT_TAB;
|
||||
|
||||
const buildOrderedIdListForTab = (state, tabKey) => {
|
||||
const tabPages = state.conversationPagesByTab[normalizeTabKey(tabKey)];
|
||||
if (!tabPages) {
|
||||
return [];
|
||||
}
|
||||
const seen = new Set();
|
||||
const orderedIds = [];
|
||||
Object.keys(tabPages)
|
||||
.map(page => Number(page))
|
||||
.sort((a, b) => a - b)
|
||||
.forEach(pageNumber => {
|
||||
const ids = tabPages[String(pageNumber)] || [];
|
||||
ids.forEach(id => {
|
||||
if (!seen.has(id)) {
|
||||
seen.add(id);
|
||||
orderedIds.push(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
return orderedIds;
|
||||
};
|
||||
|
||||
const conversationsFromIds = (state, ids) =>
|
||||
ids.map(id => state.conversationsById[id]).filter(Boolean);
|
||||
|
||||
const buildTabList = (state, tabKey) => {
|
||||
const orderedIds = buildOrderedIdListForTab(state, tabKey);
|
||||
return conversationsFromIds(state, orderedIds);
|
||||
};
|
||||
|
||||
const buildAllConversationList = state => {
|
||||
const flattenIds = Object.values(state.conversationPagesByTab).reduce(
|
||||
(acc, tabPages) => {
|
||||
Object.values(tabPages).forEach(pageIds => {
|
||||
pageIds.forEach(id => {
|
||||
if (!acc.set.has(id)) {
|
||||
acc.set.add(id);
|
||||
acc.ids.push(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
{ set: new Set(), ids: [] }
|
||||
);
|
||||
|
||||
if (!flattenIds.ids.length) {
|
||||
return Object.values(state.conversationsById);
|
||||
}
|
||||
|
||||
return conversationsFromIds(state, flattenIds.ids);
|
||||
};
|
||||
|
||||
export const getSelectedChatConversation = ({
|
||||
conversationsById,
|
||||
allConversations,
|
||||
selectedChatId,
|
||||
}) => {
|
||||
const chat = conversationsById[selectedChatId];
|
||||
return chat ? [chat] : [];
|
||||
};
|
||||
}) =>
|
||||
allConversations.filter(conversation => conversation.id === selectedChatId);
|
||||
|
||||
const getters = {
|
||||
getAllConversations: state => {
|
||||
const list = buildTabList(state, DEFAULT_TAB);
|
||||
if (!list.length) {
|
||||
return buildAllConversationList(state);
|
||||
}
|
||||
return [...list].sort((a, b) => sortComparator(a, b, state.chatSortFilter));
|
||||
getAllConversations: ({ allConversations, chatSortFilter: sortKey }) => {
|
||||
return allConversations.sort((a, b) => sortComparator(a, b, sortKey));
|
||||
},
|
||||
getFilteredConversations: (state, _, __, rootGetters) => {
|
||||
getFilteredConversations: (
|
||||
{ allConversations, chatSortFilter, appliedFilters },
|
||||
_,
|
||||
__,
|
||||
rootGetters
|
||||
) => {
|
||||
const currentUser = rootGetters.getCurrentUser;
|
||||
const currentUserId = rootGetters.getCurrentUser.id;
|
||||
const currentAccountId = rootGetters.getCurrentAccountId;
|
||||
@@ -89,15 +31,11 @@ const getters = {
|
||||
const permissions = getUserPermissions(currentUser, currentAccountId);
|
||||
const userRole = getUserRole(currentUser, currentAccountId);
|
||||
|
||||
const baseList = buildTabList(state, 'appliedFilters').length
|
||||
? buildTabList(state, 'appliedFilters')
|
||||
: buildAllConversationList(state);
|
||||
|
||||
return baseList
|
||||
return allConversations
|
||||
.filter(conversation => {
|
||||
const matchesFilterResult = matchesFilters(
|
||||
conversation,
|
||||
state.appliedFilters
|
||||
appliedFilters
|
||||
);
|
||||
const allowedForRole = applyRoleFilter(
|
||||
conversation,
|
||||
@@ -108,10 +46,13 @@ const getters = {
|
||||
|
||||
return matchesFilterResult && allowedForRole;
|
||||
})
|
||||
.sort((a, b) => sortComparator(a, b, state.chatSortFilter));
|
||||
.sort((a, b) => sortComparator(a, b, chatSortFilter));
|
||||
},
|
||||
getSelectedChat: ({ selectedChatId, conversationsById }) => {
|
||||
return conversationsById[selectedChatId] || {};
|
||||
getSelectedChat: ({ selectedChatId, allConversations }) => {
|
||||
const selectedChat = allConversations.find(
|
||||
conversation => conversation.id === selectedChatId
|
||||
);
|
||||
return selectedChat || {};
|
||||
},
|
||||
getSelectedChatAttachments: ({ selectedChatId, attachments }) => {
|
||||
return attachments[selectedChatId] || [];
|
||||
@@ -131,13 +72,10 @@ const getters = {
|
||||
|
||||
return lastEmail;
|
||||
},
|
||||
getMineChats: (state, _, __, rootGetters) => activeFilters => {
|
||||
getMineChats: (_state, _, __, rootGetters) => activeFilters => {
|
||||
const currentUserID = rootGetters.getCurrentUser?.id;
|
||||
const sourceList = buildTabList(state, 'me').length
|
||||
? buildTabList(state, 'me')
|
||||
: buildAllConversationList(state);
|
||||
|
||||
return sourceList.filter(conversation => {
|
||||
return _state.allConversations.filter(conversation => {
|
||||
const { assignee } = conversation.meta;
|
||||
const isAssignedToMe = assignee && assignee.id === currentUserID;
|
||||
const shouldFilter = applyPageFilters(conversation, activeFilters);
|
||||
@@ -157,29 +95,22 @@ const getters = {
|
||||
const hasAppliedFilters = _state.appliedFilters.length !== 0;
|
||||
return hasAppliedFilters ? filterQueryGenerator(_state.appliedFilters) : [];
|
||||
},
|
||||
getUnAssignedChats: state => activeFilters => {
|
||||
const sourceList = buildTabList(state, 'unassigned').length
|
||||
? buildTabList(state, 'unassigned')
|
||||
: buildAllConversationList(state);
|
||||
|
||||
return sourceList.filter(conversation => {
|
||||
getUnAssignedChats: _state => activeFilters => {
|
||||
return _state.allConversations.filter(conversation => {
|
||||
const isUnAssigned = !conversation.meta.assignee;
|
||||
const shouldFilter = applyPageFilters(conversation, activeFilters);
|
||||
return isUnAssigned && shouldFilter;
|
||||
});
|
||||
},
|
||||
getAllStatusChats: (state, _, __, rootGetters) => activeFilters => {
|
||||
getAllStatusChats: (_state, _, __, rootGetters) => activeFilters => {
|
||||
const currentUser = rootGetters.getCurrentUser;
|
||||
const currentUserId = rootGetters.getCurrentUser.id;
|
||||
const currentAccountId = rootGetters.getCurrentAccountId;
|
||||
|
||||
const permissions = getUserPermissions(currentUser, currentAccountId);
|
||||
const userRole = getUserRole(currentUser, currentAccountId);
|
||||
const sourceList = buildTabList(state, DEFAULT_TAB).length
|
||||
? buildTabList(state, DEFAULT_TAB)
|
||||
: buildAllConversationList(state);
|
||||
|
||||
return sourceList.filter(conversation => {
|
||||
return _state.allConversations.filter(conversation => {
|
||||
const shouldFilter = applyPageFilters(conversation, activeFilters);
|
||||
const allowedForRole = applyRoleFilter(
|
||||
conversation,
|
||||
@@ -211,8 +142,10 @@ const getters = {
|
||||
getChatStatusFilter: ({ chatStatusFilter }) => chatStatusFilter,
|
||||
getChatSortFilter: ({ chatSortFilter }) => chatSortFilter,
|
||||
getSelectedInbox: ({ currentInbox }) => currentInbox,
|
||||
getConversationById: state => conversationId => {
|
||||
return state.conversationsById[Number(conversationId)] || null;
|
||||
getConversationById: _state => conversationId => {
|
||||
return _state.allConversations.find(
|
||||
value => value.id === Number(conversationId)
|
||||
);
|
||||
},
|
||||
getConversationParticipants: _state => {
|
||||
return _state.conversationParticipants;
|
||||
|
||||
@@ -45,11 +45,7 @@ export const buildConversationList = (
|
||||
filterType
|
||||
) => {
|
||||
const { payload: conversationList, meta: metaData } = responseData;
|
||||
context.commit(types.SET_ALL_CONVERSATION, {
|
||||
tab: filterType,
|
||||
page: requestPayload.page || 1,
|
||||
conversations: conversationList,
|
||||
});
|
||||
context.commit(types.SET_ALL_CONVERSATION, conversationList);
|
||||
context.dispatch('conversationStats/set', metaData);
|
||||
context.dispatch(
|
||||
'conversationLabels/setBulkConversationLabels',
|
||||
@@ -57,14 +53,10 @@ export const buildConversationList = (
|
||||
);
|
||||
context.commit(types.CLEAR_LIST_LOADING_STATUS);
|
||||
setContacts(context.commit, conversationList);
|
||||
context.commit(types.SET_CONVERSATION_TAB_FILTERS, {
|
||||
tab: filterType,
|
||||
filters: requestPayload || {},
|
||||
});
|
||||
setPageFilter({
|
||||
dispatch: context.dispatch,
|
||||
filter: filterType,
|
||||
page: requestPayload.page || 1,
|
||||
page: requestPayload.page,
|
||||
markEndReached: !conversationList.length,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,169 +1,14 @@
|
||||
import types from '../../mutation-types';
|
||||
import getters, { getSelectedChatConversation } from './getters';
|
||||
import actions from './actions';
|
||||
import { findPendingMessageIndex, sortComparator } from './helpers';
|
||||
import { findPendingMessageIndex } from './helpers';
|
||||
import { MESSAGE_STATUS } from 'shared/constants/messages';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { BUS_EVENTS } from '../../../../shared/constants/busEvents';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
const DEFAULT_TAB = 'all';
|
||||
const DEFAULT_PAGE = 1;
|
||||
const DEFAULT_PAGE_SIZE = 25;
|
||||
|
||||
const normalizeTabKey = tab => tab || DEFAULT_TAB;
|
||||
|
||||
const clonePagesWithUpdate = (pagesByTab, tabKey, updater) => {
|
||||
const normalizedTab = normalizeTabKey(tabKey);
|
||||
const existingTabPages = pagesByTab[normalizedTab] || {};
|
||||
const nextTabPages = updater(existingTabPages);
|
||||
if (nextTabPages === existingTabPages) {
|
||||
return pagesByTab;
|
||||
}
|
||||
return {
|
||||
...pagesByTab,
|
||||
[normalizedTab]: nextTabPages,
|
||||
};
|
||||
};
|
||||
|
||||
const removeIdFromTabPages = (tabPages, conversationId) => {
|
||||
let mutated = false;
|
||||
const nextPages = Object.entries(tabPages).reduce((acc, [pageKey, ids]) => {
|
||||
const filteredIds = ids.filter(id => id !== conversationId);
|
||||
if (filteredIds.length !== ids.length) {
|
||||
mutated = true;
|
||||
acc[pageKey] = filteredIds;
|
||||
} else {
|
||||
acc[pageKey] = ids;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
return mutated ? nextPages : tabPages;
|
||||
};
|
||||
|
||||
const removeIdFromAllTabs = (pagesByTab, conversationId) => {
|
||||
let mutated = false;
|
||||
const nextPagesByTab = Object.entries(pagesByTab).reduce(
|
||||
(acc, [tabKey, tabPages]) => {
|
||||
const updatedTabPages = removeIdFromTabPages(tabPages, conversationId);
|
||||
if (updatedTabPages !== tabPages) {
|
||||
mutated = true;
|
||||
}
|
||||
acc[tabKey] = updatedTabPages;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
return mutated ? nextPagesByTab : pagesByTab;
|
||||
};
|
||||
|
||||
const rebuildTabPagesWithConversation = (_state, tabKey, conversationId) => {
|
||||
_state.conversationPagesByTab = clonePagesWithUpdate(
|
||||
_state.conversationPagesByTab,
|
||||
tabKey,
|
||||
tabPages => {
|
||||
const existingTabPages = Object.keys(tabPages).length
|
||||
? tabPages
|
||||
: { [String(DEFAULT_PAGE)]: [] };
|
||||
|
||||
const pageNumbers = Object.keys(existingTabPages)
|
||||
.map(key => Number(key))
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
const collectedIds = [];
|
||||
const seen = new Set();
|
||||
|
||||
pageNumbers.forEach(pageNumber => {
|
||||
const ids = existingTabPages[String(pageNumber)] || [];
|
||||
ids.forEach(id => {
|
||||
if (id === conversationId) {
|
||||
return;
|
||||
}
|
||||
if (!seen.has(id)) {
|
||||
seen.add(id);
|
||||
collectedIds.push(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
collectedIds.push(conversationId);
|
||||
|
||||
const conversations = collectedIds
|
||||
.map(id => _state.conversationsById[id])
|
||||
.filter(Boolean);
|
||||
|
||||
const totalPagesNeeded = Math.max(
|
||||
Math.ceil(conversations.length / DEFAULT_PAGE_SIZE),
|
||||
1
|
||||
);
|
||||
|
||||
const basePageNumbers = pageNumbers.length
|
||||
? [...pageNumbers]
|
||||
: [DEFAULT_PAGE];
|
||||
|
||||
while (basePageNumbers.length < totalPagesNeeded) {
|
||||
const lastPage = basePageNumbers[basePageNumbers.length - 1];
|
||||
basePageNumbers.push(lastPage + 1);
|
||||
}
|
||||
|
||||
if (basePageNumbers.length > totalPagesNeeded) {
|
||||
basePageNumbers.length = totalPagesNeeded;
|
||||
}
|
||||
|
||||
if (!conversations.length) {
|
||||
return basePageNumbers.reduce((acc, pageNumber) => {
|
||||
acc[String(pageNumber)] = [];
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
const sorted = conversations
|
||||
.slice()
|
||||
.sort((a, b) => sortComparator(a, b, _state.chatSortFilter));
|
||||
|
||||
const sortedIds = sorted.map(item => item.id);
|
||||
|
||||
const nextPages = {};
|
||||
basePageNumbers.forEach((pageNumber, index) => {
|
||||
const startIndex = index * DEFAULT_PAGE_SIZE;
|
||||
nextPages[String(pageNumber)] = sortedIds.slice(
|
||||
startIndex,
|
||||
startIndex + DEFAULT_PAGE_SIZE
|
||||
);
|
||||
});
|
||||
|
||||
return nextPages;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const upsertConversationEntity = (_state, conversation) => {
|
||||
const existing = _state.conversationsById[conversation.id];
|
||||
let updatedConversation = conversation;
|
||||
|
||||
if (existing) {
|
||||
if (conversation.id === _state.selectedChatId) {
|
||||
updatedConversation = {
|
||||
...conversation,
|
||||
allMessagesLoaded: existing.allMessagesLoaded,
|
||||
messages: existing.messages,
|
||||
dataFetched: existing.dataFetched,
|
||||
};
|
||||
} else {
|
||||
updatedConversation = { ...existing, ...conversation };
|
||||
}
|
||||
}
|
||||
|
||||
_state.conversationsById = {
|
||||
..._state.conversationsById,
|
||||
[conversation.id]: updatedConversation,
|
||||
};
|
||||
};
|
||||
|
||||
const state = {
|
||||
conversationsById: {},
|
||||
conversationPagesByTab: {},
|
||||
filtersByTab: {},
|
||||
allConversations: [],
|
||||
attachments: {},
|
||||
listLoadingStatus: true,
|
||||
chatStatusFilter: wootConstants.STATUS_TYPE.OPEN,
|
||||
@@ -181,27 +26,36 @@ const state = {
|
||||
|
||||
// mutations
|
||||
export const mutations = {
|
||||
[types.SET_ALL_CONVERSATION](_state, { tab, page, conversations }) {
|
||||
const pageNumber = page || DEFAULT_PAGE;
|
||||
const tabKey = normalizeTabKey(tab);
|
||||
const conversationIds = conversations.map(conversation => {
|
||||
upsertConversationEntity(_state, conversation);
|
||||
return conversation.id;
|
||||
[types.SET_ALL_CONVERSATION](_state, conversationList) {
|
||||
const newAllConversations = [..._state.allConversations];
|
||||
conversationList.forEach(conversation => {
|
||||
const indexInCurrentList = newAllConversations.findIndex(
|
||||
c => c.id === conversation.id
|
||||
);
|
||||
if (indexInCurrentList < 0) {
|
||||
newAllConversations.push(conversation);
|
||||
} else if (conversation.id !== _state.selectedChatId) {
|
||||
// If the conversation is already in the list, replace it
|
||||
// Added this to fix the issue of the conversation not being updated
|
||||
// When reconnecting to the websocket. If the selectedChatId is not the same as
|
||||
// the conversation.id in the store, replace the existing conversation with the new one
|
||||
newAllConversations[indexInCurrentList] = conversation;
|
||||
} else {
|
||||
// If the conversation is already in the list and selectedChatId is the same,
|
||||
// replace all data except the messages array, attachments, dataFetched, allMessagesLoaded
|
||||
const existingConversation = newAllConversations[indexInCurrentList];
|
||||
newAllConversations[indexInCurrentList] = {
|
||||
...conversation,
|
||||
allMessagesLoaded: existingConversation.allMessagesLoaded,
|
||||
messages: existingConversation.messages,
|
||||
dataFetched: existingConversation.dataFetched,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
_state.conversationPagesByTab = clonePagesWithUpdate(
|
||||
_state.conversationPagesByTab,
|
||||
tabKey,
|
||||
tabPages => ({
|
||||
...tabPages,
|
||||
[String(pageNumber)]: conversationIds,
|
||||
})
|
||||
);
|
||||
_state.allConversations = newAllConversations;
|
||||
},
|
||||
[types.EMPTY_ALL_CONVERSATION](_state) {
|
||||
_state.conversationsById = {};
|
||||
_state.conversationPagesByTab = {};
|
||||
_state.filtersByTab = {};
|
||||
_state.allConversations = [];
|
||||
_state.selectedChatId = null;
|
||||
},
|
||||
[types.SET_ALL_MESSAGES_LOADED](_state) {
|
||||
@@ -218,25 +72,25 @@ export const mutations = {
|
||||
},
|
||||
|
||||
[types.SET_PREVIOUS_CONVERSATIONS](_state, { id, data }) {
|
||||
if (!data.length) {
|
||||
return;
|
||||
if (data.length) {
|
||||
const [chat] = _state.allConversations.filter(c => c.id === id);
|
||||
if (chat) {
|
||||
// Duplicate check: only add messages that don't already exist
|
||||
const existingIds = new Set(chat.messages.map(m => m.id));
|
||||
const newMessages = data.filter(m => !existingIds.has(m.id));
|
||||
if (newMessages.length) {
|
||||
chat.messages.unshift(...newMessages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const chat = _state.conversationsById[id];
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
|
||||
chat.messages = [...data, ...(chat.messages || [])];
|
||||
},
|
||||
|
||||
[types.SET_ALL_ATTACHMENTS](_state, { id, data }) {
|
||||
_state.attachments[id] = [...data];
|
||||
},
|
||||
[types.SET_MISSING_MESSAGES](_state, { id, data }) {
|
||||
const chat = _state.conversationsById[id];
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
const [chat] = _state.allConversations.filter(c => c.id === id);
|
||||
if (!chat) return;
|
||||
chat.messages = data;
|
||||
},
|
||||
|
||||
@@ -252,27 +106,22 @@ export const mutations = {
|
||||
},
|
||||
|
||||
[types.ASSIGN_TEAM](_state, { team, conversationId }) {
|
||||
const chat = _state.conversationsById[conversationId];
|
||||
if (chat) {
|
||||
chat.meta.team = team;
|
||||
}
|
||||
const [chat] = _state.allConversations.filter(c => c.id === conversationId);
|
||||
chat.meta.team = team;
|
||||
},
|
||||
|
||||
[types.UPDATE_CONVERSATION_LAST_ACTIVITY](
|
||||
_state,
|
||||
{ lastActivityAt, conversationId }
|
||||
) {
|
||||
const chat = _state.conversationsById[conversationId];
|
||||
if (!chat) {
|
||||
return;
|
||||
const [chat] = _state.allConversations.filter(c => c.id === conversationId);
|
||||
if (chat) {
|
||||
chat.last_activity_at = lastActivityAt;
|
||||
}
|
||||
chat.last_activity_at = lastActivityAt;
|
||||
},
|
||||
[types.ASSIGN_PRIORITY](_state, { priority, conversationId }) {
|
||||
const chat = _state.conversationsById[conversationId];
|
||||
if (chat) {
|
||||
chat.priority = priority;
|
||||
}
|
||||
const [chat] = _state.allConversations.filter(c => c.id === conversationId);
|
||||
chat.priority = priority;
|
||||
},
|
||||
|
||||
[types.UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES](_state, custom_attributes) {
|
||||
@@ -336,89 +185,59 @@ export const mutations = {
|
||||
});
|
||||
},
|
||||
|
||||
[types.ADD_MESSAGE](_state, message) {
|
||||
[types.ADD_MESSAGE]({ allConversations, selectedChatId }, message) {
|
||||
const { conversation_id: conversationId } = message;
|
||||
const chat = _state.conversationsById[conversationId];
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
const [chat] = getSelectedChatConversation({
|
||||
allConversations,
|
||||
selectedChatId: conversationId,
|
||||
});
|
||||
if (!chat) return;
|
||||
|
||||
const pendingMessageIndex = findPendingMessageIndex(chat, message);
|
||||
if (pendingMessageIndex !== -1) {
|
||||
chat.messages[pendingMessageIndex] = message;
|
||||
return;
|
||||
}
|
||||
|
||||
chat.messages.push(message);
|
||||
chat.timestamp = message.created_at;
|
||||
const { conversation: { unread_count: unreadCount = 0 } = {} } = message;
|
||||
chat.unread_count = unreadCount;
|
||||
if (_state.selectedChatId === conversationId) {
|
||||
emitter.emit(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS);
|
||||
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
} else {
|
||||
chat.messages.push(message);
|
||||
chat.timestamp = message.created_at;
|
||||
const { conversation: { unread_count: unreadCount = 0 } = {} } = message;
|
||||
chat.unread_count = unreadCount;
|
||||
if (selectedChatId === conversationId) {
|
||||
emitter.emit(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS);
|
||||
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
[types.ADD_CONVERSATION](_state, { conversation, tabKeys = [DEFAULT_TAB] }) {
|
||||
upsertConversationEntity(_state, conversation);
|
||||
|
||||
tabKeys.forEach(tabKey => {
|
||||
rebuildTabPagesWithConversation(_state, tabKey, conversation.id);
|
||||
});
|
||||
[types.ADD_CONVERSATION](_state, conversation) {
|
||||
_state.allConversations.push(conversation);
|
||||
},
|
||||
|
||||
[types.DELETE_CONVERSATION](_state, conversationId) {
|
||||
const { [conversationId]: removed, ...rest } = _state.conversationsById;
|
||||
if (!removed) {
|
||||
return;
|
||||
}
|
||||
_state.conversationsById = rest;
|
||||
_state.conversationPagesByTab = removeIdFromAllTabs(
|
||||
_state.conversationPagesByTab,
|
||||
conversationId
|
||||
_state.allConversations = _state.allConversations.filter(
|
||||
c => c.id !== conversationId
|
||||
);
|
||||
},
|
||||
|
||||
[types.UPDATE_CONVERSATION](
|
||||
_state,
|
||||
{ conversation, tabKeys = [DEFAULT_TAB] }
|
||||
) {
|
||||
const existingConversation = _state.conversationsById[conversation.id];
|
||||
[types.UPDATE_CONVERSATION](_state, conversation) {
|
||||
const { allConversations } = _state;
|
||||
const index = allConversations.findIndex(c => c.id === conversation.id);
|
||||
|
||||
if (existingConversation) {
|
||||
if (
|
||||
conversation.updated_at &&
|
||||
existingConversation.updated_at &&
|
||||
conversation.updated_at < existingConversation.updated_at
|
||||
) {
|
||||
if (index > -1) {
|
||||
const selectedConversation = allConversations[index];
|
||||
|
||||
// ignore out of order events
|
||||
if (conversation.updated_at < selectedConversation.updated_at) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { messages, ...updates } = conversation;
|
||||
const mergedConversation = {
|
||||
...existingConversation,
|
||||
...updates,
|
||||
};
|
||||
_state.conversationsById = {
|
||||
..._state.conversationsById,
|
||||
[conversation.id]: mergedConversation,
|
||||
};
|
||||
allConversations[index] = { ...selectedConversation, ...updates };
|
||||
if (_state.selectedChatId === conversation.id) {
|
||||
emitter.emit(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS);
|
||||
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
}
|
||||
} else {
|
||||
upsertConversationEntity(_state, conversation);
|
||||
}
|
||||
|
||||
_state.conversationPagesByTab = removeIdFromAllTabs(
|
||||
_state.conversationPagesByTab,
|
||||
conversation.id
|
||||
);
|
||||
|
||||
tabKeys.forEach(tabKey => {
|
||||
rebuildTabPagesWithConversation(_state, tabKey, conversation.id);
|
||||
});
|
||||
|
||||
if (_state.selectedChatId === conversation.id) {
|
||||
emitter.emit(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS);
|
||||
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
_state.allConversations.push(conversation);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -434,7 +253,7 @@ export const mutations = {
|
||||
_state,
|
||||
{ id, lastSeen, unreadCount = 0 }
|
||||
) {
|
||||
const chat = _state.conversationsById[id];
|
||||
const [chat] = _state.allConversations.filter(c => c.id === id);
|
||||
if (chat) {
|
||||
chat.agent_last_seen_at = lastSeen;
|
||||
chat.unread_count = unreadCount;
|
||||
@@ -450,14 +269,12 @@ export const mutations = {
|
||||
|
||||
// Update assignee on action cable message
|
||||
[types.UPDATE_ASSIGNEE](_state, payload) {
|
||||
const chat = _state.conversationsById[payload.id];
|
||||
if (chat) {
|
||||
chat.meta.assignee = payload.assignee;
|
||||
}
|
||||
const [chat] = _state.allConversations.filter(c => c.id === payload.id);
|
||||
chat.meta.assignee = payload.assignee;
|
||||
},
|
||||
|
||||
[types.UPDATE_CONVERSATION_CONTACT](_state, { conversationId, ...payload }) {
|
||||
const chat = _state.conversationsById[conversationId];
|
||||
const [chat] = _state.allConversations.filter(c => c.id === conversationId);
|
||||
if (chat) {
|
||||
chat.meta.sender = payload;
|
||||
}
|
||||
@@ -468,49 +285,23 @@ export const mutations = {
|
||||
},
|
||||
|
||||
[types.SET_CONVERSATION_CAN_REPLY](_state, { conversationId, canReply }) {
|
||||
const chat = _state.conversationsById[conversationId];
|
||||
const [chat] = _state.allConversations.filter(c => c.id === conversationId);
|
||||
if (chat) {
|
||||
chat.can_reply = canReply;
|
||||
}
|
||||
},
|
||||
|
||||
[types.CLEAR_CONTACT_CONVERSATIONS](_state, contactId) {
|
||||
const remaining = {};
|
||||
const removedIds = [];
|
||||
Object.values(_state.conversationsById).forEach(conversation => {
|
||||
if (conversation.meta?.sender?.id === contactId) {
|
||||
removedIds.push(conversation.id);
|
||||
} else {
|
||||
remaining[conversation.id] = conversation;
|
||||
}
|
||||
});
|
||||
|
||||
if (!removedIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
_state.conversationsById = remaining;
|
||||
removedIds.forEach(conversationId => {
|
||||
_state.conversationPagesByTab = removeIdFromAllTabs(
|
||||
_state.conversationPagesByTab,
|
||||
conversationId
|
||||
);
|
||||
});
|
||||
const chats = _state.allConversations.filter(
|
||||
c => c.meta.sender.id !== contactId
|
||||
);
|
||||
_state.allConversations = chats;
|
||||
},
|
||||
|
||||
[types.SET_CONVERSATION_FILTERS](_state, data) {
|
||||
_state.appliedFilters = data;
|
||||
},
|
||||
|
||||
[types.SET_CONVERSATION_TAB_FILTERS](_state, { tab, filters }) {
|
||||
const normalizedTab = normalizeTabKey(tab);
|
||||
const { page, ...rest } = filters || {};
|
||||
_state.filtersByTab = {
|
||||
..._state.filtersByTab,
|
||||
[normalizedTab]: rest,
|
||||
};
|
||||
},
|
||||
|
||||
[types.CLEAR_CONVERSATION_FILTERS](_state) {
|
||||
_state.appliedFilters = [];
|
||||
},
|
||||
|
||||
@@ -615,6 +615,29 @@ describe('#mutations', () => {
|
||||
mutations[types.SET_PREVIOUS_CONVERSATIONS](state, payload);
|
||||
expect(state.allConversations[0].messages).toEqual([{ id: 'msg2' }]);
|
||||
});
|
||||
|
||||
it('should filter out duplicate messages', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, messages: [{ id: 'msg2' }] }],
|
||||
};
|
||||
const payload = { id: 1, data: [{ id: 'msg1' }, { id: 'msg2' }] };
|
||||
|
||||
mutations[types.SET_PREVIOUS_CONVERSATIONS](state, payload);
|
||||
expect(state.allConversations[0].messages).toEqual([
|
||||
{ id: 'msg1' },
|
||||
{ id: 'msg2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should do nothing if conversation is not found', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 2, messages: [{ id: 'msg2' }] }],
|
||||
};
|
||||
const payload = { id: 1, data: [{ id: 'msg1' }] };
|
||||
|
||||
mutations[types.SET_PREVIOUS_CONVERSATIONS](state, payload);
|
||||
expect(state.allConversations[0].messages).toEqual([{ id: 'msg2' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_MISSING_MESSAGES', () => {
|
||||
|
||||
@@ -22,7 +22,6 @@ export default {
|
||||
UPDATE_CONVERSATION_CONTACT: 'UPDATE_CONVERSATION_CONTACT',
|
||||
CLEAR_CONTACT_CONVERSATIONS: 'CLEAR_CONTACT_CONVERSATIONS',
|
||||
SET_CONVERSATION_FILTERS: 'SET_CONVERSATION_FILTERS',
|
||||
SET_CONVERSATION_TAB_FILTERS: 'SET_CONVERSATION_TAB_FILTERS',
|
||||
CLEAR_CONVERSATION_FILTERS: 'CLEAR_CONVERSATION_FILTERS',
|
||||
SET_CONVERSATION_LAST_SEEN: 'SET_CONVERSATION_LAST_SEEN',
|
||||
SET_LAST_MESSAGE_ID_IN_SYNC_CONVERSATION:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# Table name: webhooks
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# name :string
|
||||
# subscriptions :jsonb
|
||||
# url :string
|
||||
# webhook_type :integer default("account_type")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
json.id webhook.id
|
||||
json.name webhook.name
|
||||
json.url webhook.url
|
||||
json.account_id webhook.account_id
|
||||
json.subscriptions webhook.subscriptions
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddNameToWebhooks < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
add_column :webhooks, :name, :string, null: true
|
||||
end
|
||||
end
|
||||
@@ -1230,6 +1230,7 @@ ActiveRecord::Schema[7.1].define(version: 2025_10_22_152158) do
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "webhook_type", default: 0
|
||||
t.jsonb "subscriptions", default: ["conversation_status_changed", "conversation_updated", "conversation_created", "contact_created", "contact_updated", "message_created", "message_updated", "webwidget_triggered"]
|
||||
t.string "name"
|
||||
t.index ["account_id", "url"], name: "index_webhooks_on_account_id_and_url", unique: true
|
||||
end
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ require 'rails_helper'
|
||||
RSpec.describe 'Webhooks API', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:webhook) { create(:webhook, account: account, inbox: inbox, url: 'https://hello.com') }
|
||||
let(:webhook) { create(:webhook, account: account, inbox: inbox, url: 'https://hello.com', name: 'My Webhook') }
|
||||
let(:administrator) { create(:user, account: account, role: :administrator) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
|
||||
@@ -49,6 +49,16 @@ RSpec.describe 'Webhooks API', type: :request do
|
||||
expect(response.parsed_body['payload']['webhook']['url']).to eql 'https://hello.com'
|
||||
end
|
||||
|
||||
it 'creates webhook with name' do
|
||||
post "/api/v1/accounts/#{account.id}/webhooks",
|
||||
params: { account_id: account.id, inbox_id: inbox.id, url: 'https://hello.com', name: 'My Webhook' },
|
||||
headers: administrator.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
expect(response.parsed_body['payload']['webhook']['name']).to eql 'My Webhook'
|
||||
end
|
||||
|
||||
it 'throws error when invalid url provided' do
|
||||
post "/api/v1/accounts/#{account.id}/webhooks",
|
||||
params: { account_id: account.id, inbox_id: inbox.id, url: 'javascript:alert(1)' },
|
||||
@@ -103,11 +113,12 @@ RSpec.describe 'Webhooks API', type: :request do
|
||||
context 'when it is an authenticated admin user' do
|
||||
it 'updates webhook' do
|
||||
put "/api/v1/accounts/#{account.id}/webhooks/#{webhook.id}",
|
||||
params: { url: 'https://hello.com' },
|
||||
params: { url: 'https://hello.com', name: 'Another Webhook' },
|
||||
headers: administrator.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['payload']['webhook']['url']).to eql 'https://hello.com'
|
||||
expect(response.parsed_body['payload']['webhook']['name']).to eql 'Another Webhook'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,6 +3,7 @@ FactoryBot.define do
|
||||
account_id { 1 }
|
||||
inbox_id { 1 }
|
||||
url { 'https://api.chatwoot.com' }
|
||||
name { 'My Webhook' }
|
||||
subscriptions do
|
||||
%w[
|
||||
conversation_status_changed
|
||||
|
||||
@@ -4,6 +4,9 @@ properties:
|
||||
type: string
|
||||
description: The url where the events should be sent
|
||||
example: https://example.com/webhook
|
||||
name:
|
||||
type: string
|
||||
description: The name of the webhook
|
||||
subscriptions:
|
||||
type: array
|
||||
items:
|
||||
|
||||
@@ -6,6 +6,9 @@ properties:
|
||||
url:
|
||||
type: string
|
||||
description: The url to which the events will be send
|
||||
name:
|
||||
type: string
|
||||
description: The name of the webhook
|
||||
subscriptions:
|
||||
type: array
|
||||
items:
|
||||
|
||||
@@ -9127,6 +9127,10 @@
|
||||
"type": "string",
|
||||
"description": "The url to which the events will be send"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the webhook"
|
||||
},
|
||||
"subscriptions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -10706,6 +10710,10 @@
|
||||
"description": "The url where the events should be sent",
|
||||
"example": "https://example.com/webhook"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the webhook"
|
||||
},
|
||||
"subscriptions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
@@ -7634,6 +7634,10 @@
|
||||
"type": "string",
|
||||
"description": "The url to which the events will be send"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the webhook"
|
||||
},
|
||||
"subscriptions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -9213,6 +9217,10 @@
|
||||
"description": "The url where the events should be sent",
|
||||
"example": "https://example.com/webhook"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the webhook"
|
||||
},
|
||||
"subscriptions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
@@ -1934,6 +1934,10 @@
|
||||
"type": "string",
|
||||
"description": "The url to which the events will be send"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the webhook"
|
||||
},
|
||||
"subscriptions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -3513,6 +3517,10 @@
|
||||
"description": "The url where the events should be sent",
|
||||
"example": "https://example.com/webhook"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the webhook"
|
||||
},
|
||||
"subscriptions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
@@ -1349,6 +1349,10 @@
|
||||
"type": "string",
|
||||
"description": "The url to which the events will be send"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the webhook"
|
||||
},
|
||||
"subscriptions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -2928,6 +2932,10 @@
|
||||
"description": "The url where the events should be sent",
|
||||
"example": "https://example.com/webhook"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the webhook"
|
||||
},
|
||||
"subscriptions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
@@ -2110,6 +2110,10 @@
|
||||
"type": "string",
|
||||
"description": "The url to which the events will be send"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the webhook"
|
||||
},
|
||||
"subscriptions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -3689,6 +3693,10 @@
|
||||
"description": "The url where the events should be sent",
|
||||
"example": "https://example.com/webhook"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the webhook"
|
||||
},
|
||||
"subscriptions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
Reference in New Issue
Block a user