feat: add sorting to filtered conversations (#15753)
## Description Agents can now use the existing conversation sort menu while viewing conversations narrowed by advanced filters or saved folders. Changing the order refetches the filtered result set before pagination, so oldest/newest, activity, unread, priority, and waiting-time ordering remain consistent across loaded pages. ## Closes - [CW-7862](https://linear.app/chatwoot/issue/CW-7862/lack-of-sorting-options-when-using-filters-in-conversations-tab) ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [x] This change requires a documentation update ## What changed - Keep the existing order menu available for ad-hoc filtered and saved-folder conversation lists while hiding the conflicting status selector. - Share the server-side sort whitelist between standard and filtered conversation queries, preserving existing defaults and legacy aliases. - Send the selected sort through filtered fetches, pagination, and reconnect refreshes, with client-side ordering aligned to the server. - Document the optional `sort_by` parameter for the conversation filter API. ## How to test 1. Open Conversations and apply an advanced filter. 2. Open the sort menu and confirm the existing order options are available while the status selector is hidden. 3. Change between oldest/newest, unread, priority, and waiting-time options and confirm the filtered list refetches in the selected order. 4. Open a saved conversation folder and confirm the same sorting behavior, including after loading another page. ## 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 - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] 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 - [x] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
@@ -2,24 +2,6 @@ class ConversationFinder
|
||||
attr_reader :current_user, :current_account, :params
|
||||
|
||||
DEFAULT_STATUS = 'open'.freeze
|
||||
SORT_OPTIONS = {
|
||||
'last_activity_at_asc' => %w[sort_on_last_activity_at asc],
|
||||
'last_activity_at_desc' => %w[sort_on_last_activity_at desc],
|
||||
'created_at_asc' => %w[sort_on_created_at asc],
|
||||
'created_at_desc' => %w[sort_on_created_at desc],
|
||||
'priority_asc' => %w[sort_on_priority asc],
|
||||
'priority_desc' => %w[sort_on_priority desc],
|
||||
'waiting_since_asc' => %w[sort_on_waiting_since asc],
|
||||
'waiting_since_desc' => %w[sort_on_waiting_since desc],
|
||||
'priority_desc_created_at_asc' => %w[sort_on_priority_created_at desc],
|
||||
'unread' => %w[sort_on_unread desc],
|
||||
|
||||
# To be removed in v3.5.0
|
||||
'latest' => %w[sort_on_last_activity_at desc],
|
||||
'sort_on_created_at' => %w[sort_on_created_at asc],
|
||||
'sort_on_priority' => %w[sort_on_priority desc],
|
||||
'sort_on_waiting_since' => %w[sort_on_waiting_since asc]
|
||||
}.with_indifferent_access
|
||||
# assumptions
|
||||
# inbox_id if not given, take from all conversations, else specific to inbox
|
||||
# assignee_type if not given, take 'all'
|
||||
@@ -216,9 +198,7 @@ class ConversationFinder
|
||||
|
||||
def conversations
|
||||
@conversations = conversations_base_query
|
||||
|
||||
sort_by, sort_order = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['last_activity_at_desc']
|
||||
@conversations = @conversations.send(sort_by, sort_order)
|
||||
@conversations = Conversations::SortService.apply(@conversations, params[:sort_by])
|
||||
|
||||
if params[:updated_within].present?
|
||||
@conversations.where('conversations.updated_at > ?', Time.zone.now - params[:updated_within].to_i.seconds)
|
||||
|
||||
@@ -41,6 +41,7 @@ class ConversationApi extends ApiClient {
|
||||
signal: options.signal,
|
||||
params: {
|
||||
page: payload.page,
|
||||
sort_by: payload.sortBy,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -196,6 +196,7 @@ describe('#ConversationAPI', () => {
|
||||
it('#filter', () => {
|
||||
const payload = {
|
||||
page: 1,
|
||||
sortBy: 'last_activity_at_asc',
|
||||
queryData: {
|
||||
payload: [
|
||||
{
|
||||
@@ -223,7 +224,12 @@ describe('#ConversationAPI', () => {
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/filter',
|
||||
payload.queryData,
|
||||
{ params: { page: payload.page } }
|
||||
{
|
||||
params: {
|
||||
page: payload.page,
|
||||
sort_by: payload.sortBy,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
filterItemsByPermission,
|
||||
} from 'dashboard/helper/permissionsHelper.js';
|
||||
import { matchesFilters } from '../store/modules/conversations/helpers/filterHelpers';
|
||||
import { sortComparator } from '../store/modules/conversations/helpers';
|
||||
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
|
||||
import { ASSIGNEE_TYPE_TAB_PERMISSIONS } from 'dashboard/constants/permissions.js';
|
||||
|
||||
@@ -309,12 +310,9 @@ function filterByAssigneeTab(conversations) {
|
||||
}
|
||||
|
||||
function sortByUnreadStatus(conversations) {
|
||||
return [...conversations].sort((a, b) => {
|
||||
const unreadCountDiff = (b.unread_count || 0) - (a.unread_count || 0);
|
||||
if (unreadCountDiff !== 0) return unreadCountDiff;
|
||||
|
||||
return (b.last_activity_at || 0) - (a.last_activity_at || 0);
|
||||
});
|
||||
return [...conversations].sort((a, b) =>
|
||||
sortComparator(a, b, wootConstants.SORT_BY_TYPE.UNREAD)
|
||||
);
|
||||
}
|
||||
|
||||
const conversationList = computed(() => {
|
||||
@@ -400,6 +398,7 @@ function fetchFilteredConversations(payload) {
|
||||
.dispatch('fetchFilteredConversations', {
|
||||
queryData: filterQueryGenerator(payload),
|
||||
page,
|
||||
sortBy: activeSortBy.value,
|
||||
})
|
||||
.catch(() => useAlert(t('CHAT_LIST.FETCH_ERROR')))
|
||||
// emit even on failure so a deep-linked conversation still loads via
|
||||
@@ -416,6 +415,7 @@ function fetchSavedFilteredConversations(payload) {
|
||||
.dispatch('fetchFilteredConversations', {
|
||||
queryData: payload,
|
||||
page,
|
||||
sortBy: activeSortBy.value,
|
||||
})
|
||||
.catch(() => useAlert(t('CHAT_LIST.FETCH_ERROR')))
|
||||
.finally(emitConversationLoaded);
|
||||
@@ -627,6 +627,20 @@ function onBasicFilterChange(value, type) {
|
||||
} else {
|
||||
activeSortBy.value = value;
|
||||
}
|
||||
|
||||
if (type === 'sort' && hasAppliedFiltersOrActiveFolders.value) {
|
||||
resetBulkActions();
|
||||
store.dispatch('conversationPage/reset');
|
||||
store.dispatch('emptyAllConversations');
|
||||
|
||||
if (hasActiveFolders.value) {
|
||||
fetchSavedFilteredConversations(activeFolder.value.query);
|
||||
} else {
|
||||
fetchFilteredConversations(appliedFilters.value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
resetAndFetchData();
|
||||
}
|
||||
|
||||
|
||||
@@ -155,8 +155,8 @@ const toggleConversationLayout = () => {
|
||||
/>
|
||||
</div>
|
||||
<ConversationBasicFilter
|
||||
v-if="!hasAppliedFiltersOrActiveFolders"
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
:show-status-filter="!hasAppliedFiltersOrActiveFolders"
|
||||
@change-filter="onBasicFilterChange"
|
||||
/>
|
||||
<SwitchLayout
|
||||
|
||||
@@ -14,6 +14,10 @@ defineProps({
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
showStatusFilter: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['changeFilter']);
|
||||
@@ -157,7 +161,10 @@ const handleSortChange = value => {
|
||||
'ltr:right-0 rtl:left-0': isOnExpandedLayout,
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center justify-between last:mt-4 gap-2">
|
||||
<div
|
||||
v-if="showStatusFilter"
|
||||
class="flex items-center justify-between gap-2"
|
||||
>
|
||||
<span class="text-sm truncate text-n-slate-12">
|
||||
{{ $t('CHAT_LIST.CHAT_SORT.STATUS') }}
|
||||
</span>
|
||||
@@ -169,7 +176,10 @@ const handleSortChange = value => {
|
||||
@update:model-value="handleStatusChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between last:mt-4 gap-2">
|
||||
<div
|
||||
class="flex items-center justify-between gap-2"
|
||||
:class="{ 'mt-4': showStatusFilter }"
|
||||
>
|
||||
<span class="text-sm truncate text-n-slate-12">
|
||||
{{ $t('CHAT_LIST.CHAT_SORT.ORDER_BY') }}
|
||||
</span>
|
||||
|
||||
@@ -114,17 +114,22 @@ const actions = {
|
||||
});
|
||||
},
|
||||
|
||||
fetchFilteredConversations: async ({ commit, dispatch }, params) => {
|
||||
fetchFilteredConversations: async ({ commit, dispatch, state }, params) => {
|
||||
return conversationListRequest.run(async signal => {
|
||||
const { replaceExisting = false, ...requestParams } = params;
|
||||
const {
|
||||
replaceExisting = false,
|
||||
sortBy = state.chatSortFilter,
|
||||
...requestParams
|
||||
} = params;
|
||||
const filterRequestParams = { ...requestParams, sortBy };
|
||||
const countRequest = await dispatch(
|
||||
'conversationStats/onListRequestStarted',
|
||||
requestParams
|
||||
filterRequestParams
|
||||
);
|
||||
if (signal.aborted) return;
|
||||
commit(types.SET_LIST_LOADING_STATUS);
|
||||
try {
|
||||
const { data } = await ConversationApi.filter(requestParams, {
|
||||
const { data } = await ConversationApi.filter(filterRequestParams, {
|
||||
signal,
|
||||
});
|
||||
|
||||
@@ -132,7 +137,7 @@ const actions = {
|
||||
|
||||
buildConversationList(
|
||||
{ commit, dispatch },
|
||||
requestParams,
|
||||
filterRequestParams,
|
||||
data,
|
||||
'appliedFilters',
|
||||
{ replaceExisting, countRequest }
|
||||
|
||||
@@ -117,6 +117,7 @@ const SORT_OPTIONS = {
|
||||
waiting_since_asc: ['sortOnWaitingSince', 'asc'],
|
||||
waiting_since_desc: ['sortOnWaitingSince', 'desc'],
|
||||
priority_desc_created_at_asc: ['sortOnPriorityCreatedAt', 'desc'],
|
||||
unread: ['sortOnUnread', 'desc'],
|
||||
};
|
||||
const sortAscending = (valueA, valueB) => valueA - valueB;
|
||||
const sortDescending = (valueA, valueB) => valueB - valueA;
|
||||
@@ -136,8 +137,10 @@ const sortConfig = {
|
||||
|
||||
const p1 = CONVERSATION_PRIORITY_ORDER[a.priority] || DEFAULT_FOR_NULL;
|
||||
const p2 = CONVERSATION_PRIORITY_ORDER[b.priority] || DEFAULT_FOR_NULL;
|
||||
const priorityDiff = getSortOrderFunction(sortDirection)(p1, p2);
|
||||
if (priorityDiff !== 0) return priorityDiff;
|
||||
|
||||
return getSortOrderFunction(sortDirection)(p1, p2);
|
||||
return sortDescending(a.last_activity_at, b.last_activity_at);
|
||||
},
|
||||
|
||||
sortOnPriorityCreatedAt: (a, b) => {
|
||||
@@ -152,13 +155,20 @@ const sortConfig = {
|
||||
const sortFunc = getSortOrderFunction(sortDirection);
|
||||
if (!a.waiting_since || !b.waiting_since) {
|
||||
if (!a.waiting_since && !b.waiting_since) {
|
||||
return sortFunc(a.created_at, b.created_at);
|
||||
return sortAscending(a.created_at, b.created_at);
|
||||
}
|
||||
return sortFunc(a.waiting_since ? 0 : 1, b.waiting_since ? 0 : 1);
|
||||
return a.waiting_since ? -1 : 1;
|
||||
}
|
||||
|
||||
return sortFunc(a.waiting_since, b.waiting_since);
|
||||
},
|
||||
|
||||
sortOnUnread: (a, b) => {
|
||||
const unreadCountDiff = (b.unread_count || 0) - (a.unread_count || 0);
|
||||
if (unreadCountDiff !== 0) return unreadCountDiff;
|
||||
|
||||
return (b.last_activity_at || 0) - (a.last_activity_at || 0);
|
||||
},
|
||||
};
|
||||
|
||||
export const sortComparator = (a, b, sortKey) => {
|
||||
|
||||
@@ -7,14 +7,17 @@ import actions, {
|
||||
} from '../../conversations/actions';
|
||||
import types from '../../../mutation-types';
|
||||
const dataToSend = {
|
||||
payload: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['open'],
|
||||
query_operator: null,
|
||||
},
|
||||
],
|
||||
page: 1,
|
||||
queryData: {
|
||||
payload: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['open'],
|
||||
query_operator: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
import { dataReceived } from './testConversationResponse';
|
||||
|
||||
@@ -634,7 +637,11 @@ describe('#actions', () => {
|
||||
axios.post.mockResolvedValue({ data: filteredResponse });
|
||||
|
||||
await actions.fetchFilteredConversations(
|
||||
{ commit, dispatch },
|
||||
{
|
||||
commit,
|
||||
dispatch,
|
||||
state: { chatSortFilter: 'last_activity_at_desc' },
|
||||
},
|
||||
{
|
||||
queryData: {
|
||||
payload: [
|
||||
@@ -670,7 +677,11 @@ describe('#actions', () => {
|
||||
data: dataReceived,
|
||||
});
|
||||
await actions.fetchFilteredConversations(
|
||||
{ commit, dispatch },
|
||||
{
|
||||
commit,
|
||||
dispatch,
|
||||
state: { chatSortFilter: 'last_activity_at_desc' },
|
||||
},
|
||||
dataToSend
|
||||
);
|
||||
expect(commit).toHaveBeenCalledTimes(4);
|
||||
@@ -683,12 +694,29 @@ describe('#actions', () => {
|
||||
dataReceived.payload.map(chat => chat.meta.sender),
|
||||
],
|
||||
]);
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/filter',
|
||||
dataToSend.queryData,
|
||||
expect.objectContaining({
|
||||
params: {
|
||||
page: dataToSend.page,
|
||||
sort_by: 'last_activity_at_desc',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('clears the loading state and rethrows if the request fails', async () => {
|
||||
axios.post.mockRejectedValue(new Error('Request failed'));
|
||||
await expect(
|
||||
actions.fetchFilteredConversations({ commit, dispatch }, dataToSend)
|
||||
actions.fetchFilteredConversations(
|
||||
{
|
||||
commit,
|
||||
dispatch,
|
||||
state: { chatSortFilter: 'last_activity_at_desc' },
|
||||
},
|
||||
dataToSend
|
||||
)
|
||||
).rejects.toThrow('Request failed');
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['SET_LIST_LOADING_STATUS'],
|
||||
|
||||
@@ -102,6 +102,18 @@ describe('#getters', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses latest activity to order conversations with the same priority', () => {
|
||||
const state = {
|
||||
allConversations: [conversations[2], conversations[1]],
|
||||
chatSortFilter: 'priority_desc',
|
||||
};
|
||||
|
||||
expect(getters.getAllConversations(state)).toEqual([
|
||||
conversations[1],
|
||||
conversations[2],
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns conversations ordered by priority in ascending order if chatStatusFilter = priority_asc', () => {
|
||||
const state = {
|
||||
allConversations: [...conversations],
|
||||
@@ -127,6 +139,19 @@ describe('#getters', () => {
|
||||
conversations[0],
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns waiting conversations before non-waiting conversations when sorting by shortest wait', () => {
|
||||
const state = {
|
||||
allConversations: [...conversations],
|
||||
chatSortFilter: 'waiting_since_desc',
|
||||
};
|
||||
expect(getters.getAllConversations(state)).toEqual([
|
||||
conversations[3],
|
||||
conversations[1],
|
||||
conversations[2],
|
||||
conversations[0],
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('#getUnAssignedChats', () => {
|
||||
it('order returns only chats assigned to user', () => {
|
||||
@@ -720,5 +745,26 @@ describe('#getters', () => {
|
||||
mockConversations[2],
|
||||
]);
|
||||
});
|
||||
|
||||
it('sorts filtered conversations by unread count and then latest activity', () => {
|
||||
const state = {
|
||||
allConversations: [
|
||||
{ ...mockConversations[0], unread_count: 1 },
|
||||
{ ...mockConversations[1], unread_count: 2 },
|
||||
{ ...mockConversations[2], unread_count: 2 },
|
||||
],
|
||||
chatSortFilter: 'unread',
|
||||
appliedFilters: [],
|
||||
};
|
||||
|
||||
const result = getters.getFilteredConversations(
|
||||
state,
|
||||
{},
|
||||
{},
|
||||
mockRootGetters
|
||||
);
|
||||
|
||||
expect(result.map(conversation => conversation.id)).toEqual([3, 2, 1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,6 +48,7 @@ class WorkingHour < ApplicationRecord
|
||||
|
||||
def open_at?(time)
|
||||
return false if closed_all_day?
|
||||
return true if open_all_day?
|
||||
|
||||
open_time = Time.zone.now.in_time_zone(inbox.timezone).change({ hour: open_hour, min: open_minutes })
|
||||
close_time = Time.zone.now.in_time_zone(inbox.timezone).change({ hour: close_hour, min: close_minutes })
|
||||
|
||||
@@ -54,7 +54,7 @@ class Conversations::FilterService < FilterService
|
||||
end
|
||||
|
||||
def conversations
|
||||
@conversations.sort_on_last_activity_at.page(current_page)
|
||||
Conversations::SortService.apply(@conversations, @params[:sort_by]).page(current_page)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
26
app/services/conversations/sort_service.rb
Normal file
26
app/services/conversations/sort_service.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
class Conversations::SortService
|
||||
DEFAULT_SORT = 'last_activity_at_desc'.freeze
|
||||
SORT_OPTIONS = {
|
||||
'last_activity_at_asc' => %w[sort_on_last_activity_at asc],
|
||||
'last_activity_at_desc' => %w[sort_on_last_activity_at desc],
|
||||
'created_at_asc' => %w[sort_on_created_at asc],
|
||||
'created_at_desc' => %w[sort_on_created_at desc],
|
||||
'priority_asc' => %w[sort_on_priority asc],
|
||||
'priority_desc' => %w[sort_on_priority desc],
|
||||
'waiting_since_asc' => %w[sort_on_waiting_since asc],
|
||||
'waiting_since_desc' => %w[sort_on_waiting_since desc],
|
||||
'priority_desc_created_at_asc' => %w[sort_on_priority_created_at desc],
|
||||
'unread' => %w[sort_on_unread desc],
|
||||
|
||||
# To be removed in v3.5.0
|
||||
'latest' => %w[sort_on_last_activity_at desc],
|
||||
'sort_on_created_at' => %w[sort_on_created_at asc],
|
||||
'sort_on_priority' => %w[sort_on_priority desc],
|
||||
'sort_on_waiting_since' => %w[sort_on_waiting_since asc]
|
||||
}.with_indifferent_access.freeze
|
||||
|
||||
def self.apply(scope, sort_by)
|
||||
sort_method, sort_order = SORT_OPTIONS[sort_by] || SORT_OPTIONS[DEFAULT_SORT]
|
||||
scope.public_send(sort_method, sort_order)
|
||||
end
|
||||
end
|
||||
@@ -67,6 +67,12 @@ RSpec.describe WorkingHour do
|
||||
expect(described_class.today.close_hour).to be 23
|
||||
expect(described_class.today.close_minutes).to be 59
|
||||
end
|
||||
|
||||
it 'remains open through the end of the day' do
|
||||
travel_to '18.02.2022 23:59:59'.to_datetime do
|
||||
expect(described_class.today.open_now?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when open_all_day and closed_all_day true at the same time' do
|
||||
|
||||
@@ -213,6 +213,26 @@ describe Conversations::FilterService do
|
||||
expect(result[:count][:all_count]).to eq conversations.count
|
||||
end
|
||||
|
||||
it 'sorts filtered conversations using the requested sort order' do
|
||||
older_conversation = create(:conversation, account: account, inbox: inbox, last_activity_at: 2.days.ago)
|
||||
newer_conversation = create(:conversation, account: account, inbox: inbox, last_activity_at: 1.day.ago)
|
||||
params[:sort_by] = 'last_activity_at_asc'
|
||||
|
||||
conversation_ids = filter_service.new(params, user_1, account).perform[:conversations].pluck(:id)
|
||||
|
||||
expect(conversation_ids.index(older_conversation.id)).to be < conversation_ids.index(newer_conversation.id)
|
||||
end
|
||||
|
||||
it 'defaults to newest activity first when the requested sort order is invalid' do
|
||||
older_conversation = create(:conversation, account: account, inbox: inbox, last_activity_at: 2.days.ago)
|
||||
newer_conversation = create(:conversation, account: account, inbox: inbox, last_activity_at: 1.day.ago)
|
||||
params[:sort_by] = 'invalid_sort'
|
||||
|
||||
conversation_ids = filter_service.new(params, user_1, account).perform[:conversations].pluck(:id)
|
||||
|
||||
expect(conversation_ids.index(newer_conversation.id)).to be < conversation_ids.index(older_conversation.id)
|
||||
end
|
||||
|
||||
it 'filters items with contains filter_operator with values being an array' do
|
||||
params[:payload] = [{
|
||||
attribute_key: 'browser_language',
|
||||
|
||||
@@ -10,6 +10,23 @@ parameters:
|
||||
in: query
|
||||
schema:
|
||||
type: number
|
||||
- name: sort_by
|
||||
in: query
|
||||
description: Sort filtered conversations using the same options as the conversation list.
|
||||
schema:
|
||||
type: string
|
||||
default: last_activity_at_desc
|
||||
enum:
|
||||
- last_activity_at_asc
|
||||
- last_activity_at_desc
|
||||
- created_at_asc
|
||||
- created_at_desc
|
||||
- priority_asc
|
||||
- priority_desc
|
||||
- waiting_since_asc
|
||||
- waiting_since_desc
|
||||
- priority_desc_created_at_asc
|
||||
- unread
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
|
||||
@@ -4584,6 +4584,27 @@
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sort_by",
|
||||
"in": "query",
|
||||
"description": "Sort filtered conversations using the same options as the conversation list.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "last_activity_at_desc",
|
||||
"enum": [
|
||||
"last_activity_at_asc",
|
||||
"last_activity_at_desc",
|
||||
"created_at_asc",
|
||||
"created_at_desc",
|
||||
"priority_asc",
|
||||
"priority_desc",
|
||||
"waiting_since_asc",
|
||||
"waiting_since_desc",
|
||||
"priority_desc_created_at_asc",
|
||||
"unread"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
|
||||
@@ -3127,6 +3127,27 @@
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sort_by",
|
||||
"in": "query",
|
||||
"description": "Sort filtered conversations using the same options as the conversation list.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "last_activity_at_desc",
|
||||
"enum": [
|
||||
"last_activity_at_asc",
|
||||
"last_activity_at_desc",
|
||||
"created_at_asc",
|
||||
"created_at_desc",
|
||||
"priority_asc",
|
||||
"priority_desc",
|
||||
"waiting_since_asc",
|
||||
"waiting_since_desc",
|
||||
"priority_desc_created_at_asc",
|
||||
"unread"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
|
||||
Reference in New Issue
Block a user