From 995c55831c9ce01f983517c847193bb292409b20 Mon Sep 17 00:00:00 2001 From: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com> Date: Fri, 3 Sep 2021 22:49:07 +0530 Subject: [PATCH] [IMPROVE] Use PaginatedSelectFiltered in department edition (#23054) * Fix only first 10 channels getting listed in channels autocomplete * Use paginated select component for displaying channel select data * Fix lint issues Co-authored-by: Tiago Evangelista Pinto Co-authored-by: Kevin Aleman --- app/api/server/lib/rooms.js | 31 ++++++++- app/api/server/v1/rooms.js | 24 ++++++- .../RoomAutoComplete/hooks/useRoomsList.ts | 63 +++++++++++++++++++ client/contexts/ServerContext/endpoints.ts | 2 + ...completeChannelAndPrivateWithPagination.ts | 10 +++ .../omnichannel/departments/EditDepartment.js | 42 ++++++------- 6 files changed, 148 insertions(+), 24 deletions(-) create mode 100644 client/components/RoomAutoComplete/hooks/useRoomsList.ts create mode 100644 client/contexts/ServerContext/endpoints/v1/rooms/autocompleteChannelAndPrivateWithPagination.ts diff --git a/app/api/server/lib/rooms.js b/app/api/server/lib/rooms.js index 0b908620f18a9..ea184b1d2fa20 100644 --- a/app/api/server/lib/rooms.js +++ b/app/api/server/lib/rooms.js @@ -1,6 +1,6 @@ import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { Rooms } from '../../../models/server/raw'; -import { Subscriptions } from '../../../models'; +import { Subscriptions } from '../../../models/server'; export async function findAdminRooms({ uid, filter, types = [], pagination: { offset, count, sort } }) { if (!await hasPermissionAsync(uid, 'view-room-administration')) { @@ -119,6 +119,35 @@ export async function findChannelAndPrivateAutocomplete({ uid, selector }) { }; } +export async function findChannelAndPrivateAutocompleteWithPagination({ uid, selector, pagination: { offset, count, sort } }) { + const userRoomsIds = Subscriptions.cachedFindByUserId(uid, { fields: { rid: 1 } }) + .fetch() + .map((item) => item.rid); + + const options = { + fields: { + _id: 1, + fname: 1, + name: 1, + t: 1, + avatarETag: 1, + }, + sort: sort || { name: 1 }, + skip: offset, + limit: count, + }; + + const cursor = await Rooms.findRoomsWithoutDiscussionsByRoomIds(selector.name, userRoomsIds, options); + + const total = await cursor.count(); + const rooms = await cursor.toArray(); + + return { + items: rooms, + total, + }; +} + export async function findRoomsAvailableForTeams({ uid, name }) { const options = { fields: { diff --git a/app/api/server/v1/rooms.js b/app/api/server/v1/rooms.js index bbc23d165e3fc..eee7230a0bbfe 100644 --- a/app/api/server/v1/rooms.js +++ b/app/api/server/v1/rooms.js @@ -3,7 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { FileUpload } from '../../../file-upload'; import { Rooms, Messages } from '../../../models'; import { API } from '../api'; -import { findAdminRooms, findChannelAndPrivateAutocomplete, findAdminRoom, findRoomsAvailableForTeams } from '../lib/rooms'; +import { findAdminRooms, findChannelAndPrivateAutocomplete, findAdminRoom, findRoomsAvailableForTeams, findChannelAndPrivateAutocompleteWithPagination } from '../lib/rooms'; import { sendFile, sendViaEmail } from '../../../../server/lib/channelExport'; import { canAccessRoom, hasPermission } from '../../../authorization/server'; import { Media } from '../../../../server/sdk'; @@ -314,6 +314,28 @@ API.v1.addRoute('rooms.autocomplete.channelAndPrivate', { authRequired: true }, }, }); +API.v1.addRoute('rooms.autocomplete.channelAndPrivate.withPagination', { authRequired: true }, { + get() { + const { selector } = this.queryParams; + const { offset, count } = this.getPaginationItems(); + const { sort } = this.parseJsonQuery(); + + if (!selector) { + return API.v1.failure('The \'selector\' param is required'); + } + + return API.v1.success(Promise.await(findChannelAndPrivateAutocompleteWithPagination({ + uid: this.userId, + selector: JSON.parse(selector), + pagination: { + offset, + count, + sort, + }, + }))); + }, +}); + API.v1.addRoute('rooms.autocomplete.availableForTeams', { authRequired: true }, { get() { const { name } = this.queryParams; diff --git a/client/components/RoomAutoComplete/hooks/useRoomsList.ts b/client/components/RoomAutoComplete/hooks/useRoomsList.ts new file mode 100644 index 0000000000000..b35a660fb2422 --- /dev/null +++ b/client/components/RoomAutoComplete/hooks/useRoomsList.ts @@ -0,0 +1,63 @@ +import { useCallback, useState } from 'react'; + +import { IRoom } from '../../../../definition/IRoom'; +import { useEndpoint } from '../../../contexts/ServerContext'; +import { useScrollableRecordList } from '../../../hooks/lists/useScrollableRecordList'; +import { useComponentDidUpdate } from '../../../hooks/useComponentDidUpdate'; +import { RecordList } from '../../../lib/lists/RecordList'; + +type RoomListOptions = { + text: string; +}; + +export const useRoomsList = ( + options: RoomListOptions, +): { + itemsList: RecordList; + initialItemCount: number; + reload: () => void; + loadMoreItems: (start: number, end: number) => void; +} => { + const [itemsList, setItemsList] = useState(() => new RecordList()); + const reload = useCallback(() => setItemsList(new RecordList()), []); + const endpoint = 'rooms.autocomplete.channelAndPrivate.withPagination'; + + const getRooms = useEndpoint('GET', endpoint); + + useComponentDidUpdate(() => { + options && reload(); + }, [options, reload]); + + const fetchData = useCallback( + async (start, end) => { + const { items: rooms, total } = await getRooms({ + selector: JSON.stringify({ name: options.text || '' }), + offset: start, + count: start + end, + sort: JSON.stringify({ name: 1 }), + }); + + const items = rooms.map((room: any) => { + room._updatedAt = new Date(room._updatedAt); + room.label = room.name; + room.value = room.name; + return room; + }); + + return { + items, + itemCount: total, + }; + }, + [getRooms, options.text], + ); + + const { loadMoreItems, initialItemCount } = useScrollableRecordList(itemsList, fetchData, 25); + + return { + reload, + itemsList, + loadMoreItems, + initialItemCount, + }; +}; diff --git a/client/contexts/ServerContext/endpoints.ts b/client/contexts/ServerContext/endpoints.ts index f2a4c07a37744..774c37f4d1213 100644 --- a/client/contexts/ServerContext/endpoints.ts +++ b/client/contexts/ServerContext/endpoints.ts @@ -31,6 +31,7 @@ import { CannedResponseEndpoint } from './endpoints/v1/omnichannel/cannedRespons import { CannedResponsesEndpoint } from './endpoints/v1/omnichannel/cannedResponses'; import { AutocompleteAvailableForTeamsEndpoint as RoomsAutocompleteTeamsEndpoint } from './endpoints/v1/rooms/autocompleteAvailableForTeams'; import { AutocompleteChannelAndPrivateEndpoint as RoomsAutocompleteEndpoint } from './endpoints/v1/rooms/autocompleteChannelAndPrivate'; +import { AutocompleteChannelAndPrivateEndpointWithPagination as RoomsAutocompleteEndpointWithPagination } from './endpoints/v1/rooms/autocompleteChannelAndPrivateWithPagination'; import { RoomsInfo as RoomsInfoEndpoint } from './endpoints/v1/rooms/roomsInfo'; import { AddRoomsEndpoint as TeamsAddRoomsEndpoint } from './endpoints/v1/teams/addRooms'; import { ListRoomsEndpoint } from './endpoints/v1/teams/listRooms'; @@ -59,6 +60,7 @@ export type ServerEndpoints = { 'custom-user-status.list': CustomUserStatusListEndpoint; '/apps/externalComponents': AppsExternalComponentsEndpoint; 'rooms.autocomplete.channelAndPrivate': RoomsAutocompleteEndpoint; + 'rooms.autocomplete.channelAndPrivate.withPagination': RoomsAutocompleteEndpointWithPagination; 'rooms.autocomplete.availableForTeams': RoomsAutocompleteTeamsEndpoint; 'teams.listRooms': ListRoomsEndpoint; 'teams.listRoomsOfUser': ListRoomsOfUserEndpoint; diff --git a/client/contexts/ServerContext/endpoints/v1/rooms/autocompleteChannelAndPrivateWithPagination.ts b/client/contexts/ServerContext/endpoints/v1/rooms/autocompleteChannelAndPrivateWithPagination.ts new file mode 100644 index 0000000000000..ee0ce416b4946 --- /dev/null +++ b/client/contexts/ServerContext/endpoints/v1/rooms/autocompleteChannelAndPrivateWithPagination.ts @@ -0,0 +1,10 @@ +import { IRoom } from '../../../../../../definition/IRoom'; + +export type AutocompleteChannelAndPrivateEndpointWithPagination = { + GET: (params: { selector: string; offset?: number; count?: number; sort?: string }) => { + items: IRoom[]; + count: number; + offset: number; + total: number; + }; +}; diff --git a/client/views/omnichannel/departments/EditDepartment.js b/client/views/omnichannel/departments/EditDepartment.js index bfe364e4c770e..d5f1267414d8c 100644 --- a/client/views/omnichannel/departments/EditDepartment.js +++ b/client/views/omnichannel/departments/EditDepartment.js @@ -4,7 +4,6 @@ import { Field, TextInput, Chip, - SelectFiltered, Box, Icon, Divider, @@ -12,6 +11,7 @@ import { TextAreaInput, ButtonGroup, Button, + PaginatedSelectFiltered, } from '@rocket.chat/fuselage'; import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; import React, { useMemo, useState, useRef } from 'react'; @@ -19,19 +19,19 @@ import { useSubscription } from 'use-subscription'; import { isEmail } from '../../../../app/utils/client'; import Page from '../../../components/Page'; +import { useRoomsList } from '../../../components/RoomAutoComplete/hooks/useRoomsList'; import { useRoute } from '../../../contexts/RouterContext'; import { useMethod } from '../../../contexts/ServerContext'; import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext'; import { useTranslation } from '../../../contexts/TranslationContext'; +import { useRecordList } from '../../../hooks/lists/useRecordList'; import { useComponentDidUpdate } from '../../../hooks/useComponentDidUpdate'; import { useEndpointAction } from '../../../hooks/useEndpointAction'; -import { useEndpointData } from '../../../hooks/useEndpointData'; import { useForm } from '../../../hooks/useForm'; +import { AsyncStatePhase } from '../../../lib/asyncState'; import { formsSubscription } from '../additionalForms'; import DepartmentsAgentsTable from './DepartmentsAgentsTable'; -const useQuery = ({ name }) => useMemo(() => ({ selector: JSON.stringify({ name }) }), [name]); - function EditDepartment({ data, id, title, reload, allowedToForwardData }) { const t = useTranslation(); const agentsRoute = useRoute('omnichannel-departments'); @@ -113,6 +113,12 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { departmentsAllowedToForward, } = values; + const { itemsList: RoomsList, loadMoreItems: loadMoreRooms } = useRoomsList( + useMemo(() => ({ text: offlineMessageChannelName }), [offlineMessageChannelName]), + ); + + const { phase: roomsPhase, items: roomsItems, itemCount: roomsTotal } = useRecordList(RoomsList); + const handleTagChipClick = (tag) => () => { setTags((tags) => tags.filter((_tag) => _tag !== tag)); }; @@ -128,21 +134,6 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { setTagsText(e.target.value); }); - const query = useQuery({ offlineMessageChannelName }); - - const { value: autoCompleteChannels = {} } = useEndpointData( - 'rooms.autocomplete.channelAndPrivate', - query, - ); - - const channelOpts = useMemo( - () => - autoCompleteChannels && autoCompleteChannels.items - ? autoCompleteChannels.items.map(({ name }) => [name, name]) - : [], - [autoCompleteChannels], - ); - const saveDepartmentInfo = useMethod('livechat:saveDepartment'); const saveDepartmentAgentsInfoOnEdit = useEndpointAction( 'POST', @@ -356,12 +347,19 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { {t('Livechat_DepartmentOfflineMessageToChannel')} - {} + : (start) => loadMoreRooms(start, Math.min(50, roomsTotal)) + } />