Chore: Migrate ee/omnichannel folder to Typescript (#3749)

* Chore: Migrate ee/omnichannel folder to Typescript

* omnichannelstatus and queue list

* boolean searching and react.ref

* test initi

* test and refactor interfaces

* minor tweak

* minor tweaks
This commit is contained in:
Reinaldo Neto 2022-03-07 18:16:20 -03:00 committed by GitHub
parent 091055a255
commit b7e523a267
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 334 additions and 164 deletions

View File

@ -4,7 +4,7 @@ import { MarkdownAST } from '@rocket.chat/message-parser';
import { IAttachment } from './IAttachment';
import { IMessage } from './IMessage';
import { IServedBy } from './IServedBy';
import { SubscriptionType } from './ISubscription';
import { IVisitor, SubscriptionType } from './ISubscription';
import { IUser } from './IUser';
interface IRequestTranscript {
@ -28,11 +28,8 @@ export interface IRoom {
broadcast: boolean;
encrypted: boolean;
ro: boolean;
v?: {
_id?: string;
token?: string;
status: 'online' | 'busy' | 'away' | 'offline';
};
v?: IVisitor;
status?: string;
servedBy?: IServedBy;
departmentId?: string;
livechatData?: any;
@ -55,13 +52,11 @@ export enum OmnichannelSourceType {
API = 'api',
OTHER = 'other' // catch-all source type
}
export interface IOmnichannelRoom extends Omit<IRoom, 'default' | 'featured' | 'broadcast' | ''> {
export interface IOmnichannelRoom extends Partial<Omit<IRoom, 'default' | 'featured' | 'broadcast'>> {
_id: string;
rid: string;
t: SubscriptionType.OMNICHANNEL;
v: {
_id?: string;
token?: string;
status: 'online' | 'busy' | 'away' | 'offline';
};
v: IVisitor;
email?: {
// Data used when the room is created from an email, via email Integration.
inbox: string;
@ -83,6 +78,8 @@ export interface IOmnichannelRoom extends Omit<IRoom, 'default' | 'featured' | '
sidebarIcon?: string;
// The default sidebar icon
defaultIcon?: string;
_updatedAt?: Date;
queuedAt?: Date;
};
transcriptRequest?: IRequestTranscript;
servedBy?: IServedBy;
@ -91,18 +88,22 @@ export interface IOmnichannelRoom extends Omit<IRoom, 'default' | 'featured' | '
lastMessage?: IMessage & { token?: string };
tags: any;
closedAt: any;
metrics: any;
waitingResponse: any;
responseBy: any;
priorityId: any;
livechatData: any;
tags?: string[];
closedAt?: Date;
metrics?: any;
waitingResponse?: any;
responseBy?: any;
priorityId?: any;
livechatData?: any;
queuedAt?: Date;
ts: Date;
label?: string;
crmData?: unknown;
message?: string;
queueOrder?: string;
estimatedWaitingTimeQueue?: string;
estimatedServiceTimeAt?: Date;
}
export type TRoomModel = IRoom & Model;

View File

@ -17,11 +17,11 @@ export enum SubscriptionType {
}
export interface IVisitor {
_id: string;
username: string;
token: string;
status: string;
lastMessageTs: Date;
_id?: string;
token?: string;
status: 'online' | 'busy' | 'away' | 'offline';
username?: string;
lastMessageTs?: Date;
}
export enum ERoomTypes {

View File

@ -1,4 +1,5 @@
// ACTIONS
import { TActionInquiry } from '../../ee/omnichannel/actions/inquiry';
import { TActionActiveUsers } from '../../actions/activeUsers';
import { TActionApp } from '../../actions/app';
import { TActionCreateChannel } from '../../actions/createChannel';
@ -30,6 +31,7 @@ import { ISelectedUsers } from '../../reducers/selectedUsers';
import { IServer } from '../../reducers/server';
import { TSettingsState } from '../../reducers/settings';
import { IShare } from '../../reducers/share';
import { IInquiry } from '../../ee/omnichannel/reducers/inquiry';
import { IPermissionsState } from '../../reducers/permissions';
import { IEnterpriseModules } from '../../reducers/enterpriseModules';
@ -50,7 +52,7 @@ export interface IApplicationState {
usersTyping: any;
inviteLinks: IInviteLinks;
createDiscussion: ICreateDiscussion;
inquiry: any;
inquiry: IInquiry;
enterpriseModules: IEnterpriseModules;
encryption: IEncryption;
permissions: IPermissionsState;
@ -71,5 +73,6 @@ export type TApplicationActions = TActionActiveUsers &
TActionsShare &
TActionServer &
TActionApp &
TActionInquiry &
TActionPermissions &
TActionEnterpriseModules;

View File

@ -1,55 +0,0 @@
import * as types from '../../../actions/actionsTypes';
export function inquirySetEnabled(enabled) {
return {
type: types.INQUIRY.SET_ENABLED,
enabled
};
}
export function inquiryReset() {
return {
type: types.INQUIRY.RESET
};
}
export function inquiryQueueAdd(inquiry) {
return {
type: types.INQUIRY.QUEUE_ADD,
inquiry
};
}
export function inquiryQueueUpdate(inquiry) {
return {
type: types.INQUIRY.QUEUE_UPDATE,
inquiry
};
}
export function inquiryQueueRemove(inquiryId) {
return {
type: types.INQUIRY.QUEUE_REMOVE,
inquiryId
};
}
export function inquiryRequest() {
return {
type: types.INQUIRY.REQUEST
};
}
export function inquirySuccess(inquiries) {
return {
type: types.INQUIRY.SUCCESS,
inquiries
};
}
export function inquiryFailure(error) {
return {
type: types.INQUIRY.FAILURE,
error
};
}

View File

@ -0,0 +1,84 @@
import { Action } from 'redux';
import { IOmnichannelRoom } from '../../../definitions';
import { INQUIRY } from '../../../actions/actionsTypes';
interface IInquirySetEnabled extends Action {
enabled: boolean;
}
interface IInquiryQueueAddAndUpdate extends Action {
inquiry: IOmnichannelRoom;
}
interface IInquirySuccess extends Action {
inquiries: IOmnichannelRoom[];
}
interface IInquiryQueueRemove extends Action {
inquiryId: string;
}
interface IInquiryFailure extends Action {
error: unknown;
}
export type TActionInquiry = IInquirySetEnabled &
IInquiryQueueAddAndUpdate &
IInquirySuccess &
IInquiryQueueRemove &
IInquiryFailure;
export function inquirySetEnabled(enabled: boolean): IInquirySetEnabled {
return {
type: INQUIRY.SET_ENABLED,
enabled
};
}
export function inquiryReset(): Action {
return {
type: INQUIRY.RESET
};
}
export function inquiryQueueAdd(inquiry: IOmnichannelRoom): IInquiryQueueAddAndUpdate {
return {
type: INQUIRY.QUEUE_ADD,
inquiry
};
}
export function inquiryQueueUpdate(inquiry: IOmnichannelRoom): IInquiryQueueAddAndUpdate {
return {
type: INQUIRY.QUEUE_UPDATE,
inquiry
};
}
export function inquiryQueueRemove(inquiryId: string): IInquiryQueueRemove {
return {
type: INQUIRY.QUEUE_REMOVE,
inquiryId
};
}
export function inquiryRequest(): Action {
return {
type: INQUIRY.REQUEST
};
}
export function inquirySuccess(inquiries: IOmnichannelRoom[]): IInquirySuccess {
return {
type: INQUIRY.SUCCESS,
inquiries
};
}
export function inquiryFailure(error: unknown): IInquiryFailure {
return {
type: INQUIRY.FAILURE,
error
};
}

View File

@ -1,19 +1,28 @@
import React, { memo, useEffect, useState } from 'react';
import { Switch, View } from 'react-native';
import PropTypes from 'prop-types';
import * as List from '../../../containers/List';
import styles from '../../../views/RoomsListView/styles';
import { SWITCH_TRACK_COLOR, themes } from '../../../constants/colors';
import { withTheme } from '../../../theme';
import { useTheme } from '../../../theme';
import UnreadBadge from '../../../presentation/UnreadBadge';
import RocketChat from '../../../lib/rocketchat';
import { changeLivechatStatus, isOmnichannelStatusAvailable } from '../lib';
import { IUser } from '../../../definitions/IUser';
const OmnichannelStatus = memo(({ searching, goQueue, theme, queueSize, inquiryEnabled, user }) => {
if (searching > 0 || !(RocketChat.isOmnichannelModuleAvailable() && user?.roles?.includes('livechat-agent'))) {
interface IOmnichannelStatus {
searching: boolean;
goQueue: () => void;
queueSize: number;
inquiryEnabled: boolean;
user: IUser;
}
const OmnichannelStatus = memo(({ searching, goQueue, queueSize, inquiryEnabled, user }: IOmnichannelStatus) => {
if (searching || !(RocketChat.isOmnichannelModuleAvailable() && user?.roles?.includes('livechat-agent'))) {
return null;
}
const { theme } = useTheme();
const [status, setStatus] = useState(isOmnichannelStatusAvailable(user));
useEffect(() => {
@ -48,16 +57,4 @@ const OmnichannelStatus = memo(({ searching, goQueue, theme, queueSize, inquiryE
);
});
OmnichannelStatus.propTypes = {
searching: PropTypes.bool,
goQueue: PropTypes.func,
queueSize: PropTypes.number,
inquiryEnabled: PropTypes.bool,
theme: PropTypes.string,
user: PropTypes.shape({
roles: PropTypes.array,
statusLivechat: PropTypes.string
})
};
export default withTheme(OmnichannelStatus);
export default OmnichannelStatus;

View File

@ -1,21 +1,24 @@
import RocketChat from '../../../lib/rocketchat';
import sdk from '../../../lib/rocketchat/services/sdk';
import { IUser } from '../../../definitions';
import EventEmitter from '../../../utils/events';
import subscribeInquiry from './subscriptions/inquiry';
export const isOmnichannelStatusAvailable = user => user?.statusLivechat === 'available';
export const isOmnichannelStatusAvailable = (user: IUser): boolean => user?.statusLivechat === 'available';
// RC 0.26.0
export const changeLivechatStatus = () => RocketChat.methodCallWrapper('livechat:changeLivechatStatus');
export const changeLivechatStatus = () => sdk.methodCallWrapper('livechat:changeLivechatStatus');
// RC 2.4.0
export const getInquiriesQueued = () => RocketChat.sdk.get('livechat/inquiries.queued');
// @ts-ignore
export const getInquiriesQueued = () => sdk.get('livechat/inquiries.queued');
// this inquiry is added to the db by the subscriptions stream
// and will be removed by the queue stream
// RC 2.4.0
export const takeInquiry = inquiryId => RocketChat.methodCallWrapper('livechat:takeInquiry', inquiryId);
export const takeInquiry = (inquiryId: string) => sdk.methodCallWrapper('livechat:takeInquiry', inquiryId);
class Omnichannel {
private inquirySub: { stop: () => void } | null;
constructor() {
this.inquirySub = null;
EventEmitter.addEventListener('INQUIRY_SUBSCRIBE', this.subscribeInquiry);
@ -36,5 +39,5 @@ class Omnichannel {
};
}
// eslint-disable-next-line no-unused-vars
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const omnichannel = new Omnichannel();

View File

@ -2,11 +2,28 @@ import log from '../../../../utils/log';
import { store } from '../../../../lib/auxStore';
import RocketChat from '../../../../lib/rocketchat';
import { inquiryQueueAdd, inquiryQueueRemove, inquiryQueueUpdate, inquiryRequest } from '../../actions/inquiry';
import sdk from '../../../../lib/rocketchat/services/sdk';
import { ILivechatDepartment } from '../../../../definitions/ILivechatDepartment';
import { IOmnichannelRoom } from '../../../../definitions';
const removeListener = listener => listener.stop();
interface IArgsQueueOmnichannel extends IOmnichannelRoom {
type: string;
}
let connectedListener;
let queueListener;
interface IDdpMessage {
msg: string;
collection: string;
id: string;
fields: {
eventName: string;
args: IArgsQueueOmnichannel[];
};
}
const removeListener = (listener: any) => listener.stop();
let connectedListener: any;
let queueListener: any;
const streamTopic = 'stream-livechat-inquiry-queue-observer';
@ -15,7 +32,7 @@ export default function subscribeInquiry() {
store.dispatch(inquiryRequest());
};
const handleQueueMessageReceived = ddpMessage => {
const handleQueueMessageReceived = (ddpMessage: IDdpMessage) => {
const [{ type, ...sub }] = ddpMessage.fields.args;
// added can be ignored, since it is handled by 'changed' event
@ -26,7 +43,9 @@ export default function subscribeInquiry() {
// if the sub isn't on the queue anymore
if (sub.status !== 'queued') {
// remove it from the queue
store.dispatch(inquiryQueueRemove(sub._id));
if (sub._id) {
store.dispatch(inquiryQueueRemove(sub._id));
}
return;
}
@ -53,23 +72,28 @@ export default function subscribeInquiry() {
}
};
connectedListener = RocketChat.onStreamData('connected', handleConnection);
queueListener = RocketChat.onStreamData(streamTopic, handleQueueMessageReceived);
connectedListener = sdk.onStreamData('connected', handleConnection);
queueListener = sdk.onStreamData(streamTopic, handleQueueMessageReceived);
try {
const { user } = store.getState().login;
RocketChat.getAgentDepartments(user.id).then(result => {
if (!user.id) {
throw new Error('inquiry: @subscribeInquiry user.id not found');
}
RocketChat.getAgentDepartments(user.id).then((result: { success: boolean; departments: ILivechatDepartment[] }) => {
if (result.success) {
const { departments } = result;
if (!departments.length || RocketChat.hasRole('livechat-manager')) {
RocketChat.subscribe(streamTopic, 'public').catch(e => console.log(e));
sdk.subscribe(streamTopic, 'public').catch((e: unknown) => console.log(e));
}
const departmentIds = departments.map(({ departmentId }) => departmentId);
departmentIds.forEach(departmentId => {
// subscribe to all departments of the agent
RocketChat.subscribe(streamTopic, `department/${departmentId}`).catch(e => console.log(e));
sdk.subscribe(streamTopic, `department/${departmentId}`).catch((e: unknown) => console.log(e));
});
}
});

View File

@ -0,0 +1,98 @@
import {
inquiryFailure,
inquiryQueueAdd,
inquiryQueueRemove,
inquiryQueueUpdate,
inquiryReset,
inquirySetEnabled,
inquirySuccess
} from '../actions/inquiry';
import { mockedStore } from '../../../reducers/mockedStore';
import { initialState } from './inquiry';
import { IOmnichannelRoom, OmnichannelSourceType, SubscriptionType } from '../../../definitions';
describe('test inquiry reduce', () => {
const enabledObj = {
enabled: true
};
const queued: IOmnichannelRoom = {
_id: '_id',
rid: 'rid',
name: 'Rocket Chat',
ts: new Date(),
message: 'ola',
status: 'queued',
v: {
_id: 'id-visitor',
username: 'guest-24',
token: '123456789',
status: 'online'
},
t: SubscriptionType.OMNICHANNEL,
queueOrder: '1',
estimatedWaitingTimeQueue: '0',
estimatedServiceTimeAt: new Date(),
source: {
type: OmnichannelSourceType.WIDGET,
_updatedAt: new Date(),
queuedAt: new Date()
}
};
const error = 'Error Test';
it('should return inital state', () => {
const state = mockedStore.getState().inquiry;
expect(state).toEqual(initialState);
});
it('should return correct inquiry state after dispatch inquirySetEnabled action', () => {
mockedStore.dispatch(inquirySetEnabled(enabledObj.enabled));
const { inquiry } = mockedStore.getState();
expect(inquiry).toEqual({ ...initialState, ...enabledObj });
});
it('after inquiry state is modified, should return inquiry state as initial state after dispatch inquiryReset action', () => {
mockedStore.dispatch(inquiryReset());
const { inquiry } = mockedStore.getState();
expect(inquiry).toEqual(initialState);
});
it('should return correct inquiry state after dispatch inquiryQueueAdd action', () => {
mockedStore.dispatch(inquiryQueueAdd(queued));
const { inquiry } = mockedStore.getState();
expect(inquiry).toEqual({ ...initialState, queued: [queued] });
});
it('should update correct inquiry state after dispatch inquiryQueueUpdate action', () => {
const modifiedQueued: IOmnichannelRoom = { ...queued, message: 'inquiryQueueUpdate' };
mockedStore.dispatch(inquiryQueueUpdate(modifiedQueued));
const { inquiry } = mockedStore.getState();
expect(inquiry).toEqual({ ...initialState, queued: [modifiedQueued] });
});
it('should remove correct from queue in inquiry state after dispatch inquiryQueueRemove action', () => {
mockedStore.dispatch(inquiryQueueRemove(queued._id));
const { inquiry } = mockedStore.getState();
expect(inquiry).toEqual(initialState);
});
it('should return correct inquiry state after dispatch inquirySuccess action', () => {
mockedStore.dispatch(inquirySuccess([queued]));
const { inquiry } = mockedStore.getState();
expect(inquiry).toEqual({ ...initialState, queued: [queued] });
});
it('after inquiry state is modified, should return inquiry state as initial state after dispatch inquiryReset action', () => {
mockedStore.dispatch(inquiryReset());
const { inquiry } = mockedStore.getState();
expect(inquiry).toEqual(initialState);
});
it('should return correct inquiry state after dispatch inquiryFailure action', () => {
mockedStore.dispatch(inquiryFailure(error));
const { inquiry } = mockedStore.getState();
expect(inquiry).toEqual({ ...initialState, error });
});
});

View File

@ -1,12 +1,19 @@
import { IOmnichannelRoom, TApplicationActions } from '../../../definitions';
import { INQUIRY } from '../../../actions/actionsTypes';
const initialState = {
export interface IInquiry {
enabled: boolean;
queued: IOmnichannelRoom[];
error: any;
}
export const initialState: IInquiry = {
enabled: false,
queued: [],
error: {}
};
export default function inquiry(state = initialState, action) {
export default function inquiry(state = initialState, action: TApplicationActions): IInquiry {
switch (action.type) {
case INQUIRY.SUCCESS:
return {

View File

@ -1,5 +1,7 @@
import { createSelector } from 'reselect';
const getInquiryQueue = state => state.inquiry.queued;
import { IApplicationState } from '../../../definitions';
const getInquiryQueue = (state: IApplicationState) => state.inquiry.queued;
export const getInquiryQueueSelector = createSelector([getInquiryQueue], queue => queue);

View File

@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FlatList } from 'react-native';
import { CompositeNavigationProp } from '@react-navigation/native';
import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
import { FlatList, ListRenderItem } from 'react-native';
import { connect } from 'react-redux';
import { dequal } from 'dequal';
@ -19,18 +20,50 @@ import * as HeaderButton from '../../../containers/HeaderButton';
import RocketChat from '../../../lib/rocketchat';
import { events, logEvent } from '../../../utils/log';
import { getInquiryQueueSelector } from '../selectors/inquiry';
import { IOmnichannelRoom, IApplicationState } from '../../../definitions';
import { DisplayMode } from '../../../constants/constantDisplayMode';
import { ChatsStackParamList } from '../../../stacks/types';
import { MasterDetailInsideStackParamList } from '../../../stacks/MasterDetailStack/types';
import { TSettingsValues } from '../../../reducers/settings';
interface INavigationOptions {
isMasterDetail: boolean;
navigation: CompositeNavigationProp<
StackNavigationProp<ChatsStackParamList, 'QueueListView'>,
StackNavigationProp<MasterDetailInsideStackParamList>
>;
}
interface IQueueListView extends INavigationOptions {
user: {
id: string;
username: string;
token: string;
};
width: number;
queued: IOmnichannelRoom[];
server: string;
useRealName?: TSettingsValues;
theme: string;
showAvatar: any;
displayMode: DisplayMode;
}
const INITIAL_NUM_TO_RENDER = isTablet ? 20 : 12;
const getItemLayout = (data, index) => ({
const getItemLayout = (data: IOmnichannelRoom[] | null | undefined, index: number) => ({
length: ROW_HEIGHT,
offset: ROW_HEIGHT * index,
index
});
const keyExtractor = item => item.rid;
const keyExtractor = (item: IOmnichannelRoom) => item.rid;
class QueueListView extends React.Component {
static navigationOptions = ({ navigation, isMasterDetail }) => {
const options = {
class QueueListView extends React.Component<IQueueListView, any> {
private getScrollRef?: React.Ref<FlatList<IOmnichannelRoom>>;
private onEndReached: ((info: { distanceFromEnd: number }) => void) | null | undefined;
static navigationOptions = ({ navigation, isMasterDetail }: INavigationOptions) => {
const options: StackNavigationOptions = {
title: I18n.t('Queued_chats')
};
if (isMasterDetail) {
@ -39,24 +72,7 @@ class QueueListView extends React.Component {
return options;
};
static propTypes = {
user: PropTypes.shape({
id: PropTypes.string,
username: PropTypes.string,
token: PropTypes.string
}),
isMasterDetail: PropTypes.bool,
width: PropTypes.number,
queued: PropTypes.array,
server: PropTypes.string,
useRealName: PropTypes.bool,
navigation: PropTypes.object,
theme: PropTypes.string,
showAvatar: PropTypes.bool,
displayMode: PropTypes.string
};
shouldComponentUpdate(nextProps) {
shouldComponentUpdate(nextProps: IQueueListView) {
const { queued } = this.props;
if (!dequal(nextProps.queued, queued)) {
return true;
@ -65,7 +81,7 @@ class QueueListView extends React.Component {
return false;
}
onPressItem = (item = {}) => {
onPressItem = (item = {} as IOmnichannelRoom) => {
logEvent(events.QL_GO_ROOM);
const { navigation, isMasterDetail } = this.props;
if (isMasterDetail) {
@ -84,13 +100,13 @@ class QueueListView extends React.Component {
});
};
getRoomTitle = item => RocketChat.getRoomTitle(item);
getRoomTitle = (item: IOmnichannelRoom) => RocketChat.getRoomTitle(item);
getRoomAvatar = item => RocketChat.getRoomAvatar(item);
getRoomAvatar = (item: IOmnichannelRoom) => RocketChat.getRoomAvatar(item);
getUidDirectMessage = room => RocketChat.getUidDirectMessage(room);
getUidDirectMessage = (room: IOmnichannelRoom) => RocketChat.getUidDirectMessage(room);
renderItem = ({ item }) => {
renderItem: ListRenderItem<IOmnichannelRoom> = ({ item }) => {
const {
user: { id: userId, username, token },
server,
@ -152,7 +168,7 @@ class QueueListView extends React.Component {
}
}
const mapStateToProps = state => ({
const mapStateToProps = (state: IApplicationState) => ({
user: getUserSelector(state),
isMasterDetail: state.app.isMasterDetail,
server: state.server.server,

View File

@ -1,7 +1,7 @@
import { ChatsStackParamList } from '../stacks/types';
import Navigation from '../lib/Navigation';
import RocketChat from '../lib/rocketchat';
import { ISubscription, IVisitor, SubscriptionType, TSubscriptionModel } from '../definitions/ISubscription';
import { IOmnichannelRoom, SubscriptionType, IVisitor, TSubscriptionModel, ISubscription } from '../definitions';
interface IGoRoomItem {
search?: boolean; // comes from spotlight
@ -13,7 +13,7 @@ interface IGoRoomItem {
visitor?: IVisitor;
}
export type TGoRoomItem = IGoRoomItem | TSubscriptionModel | ISubscription;
export type TGoRoomItem = IGoRoomItem | TSubscriptionModel | ISubscription | IOmnichannelRoomVisitor;
const navigate = ({
item,
@ -42,6 +42,11 @@ const navigate = ({
});
};
interface IOmnichannelRoomVisitor extends IOmnichannelRoom {
// this visitor came from ee/omnichannel/views/QueueListView
visitor: IVisitor;
}
export const goRoom = async ({
item,
isMasterDetail = false,

View File

@ -1,15 +0,0 @@
export interface ILivechatDepartment {
_id: string;
name: string;
enabled: boolean;
description: string;
showOnRegistration: boolean;
showOnOfflineForm: boolean;
requestTagBeforeClosingChat: boolean;
email: string;
chatClosingTags: string[];
offlineMessageChannelName: string;
numAgents: number;
_updatedAt?: Date;
businessHourId?: string;
}