[FIX] Contact Bar not reactive (#22016)

* WIP

* Bump

* ContactContetualBar -> TS

* Opss

* méeeee

* done

* fix

* ??

* TS fix

* remove tabId from quickactions component

* Fix onhold missing

* prevent error with no contact

* prepare directory chatinfo to use the the endpoint instead the hook useOmnichannelRoom

* Canelada

* fix identation

* fix stream issues

* fix resume chat onhold

* remove unnecessary import

* missed fields chat info panel

* ChatsContextualBar reactive

* Allow agents to edit rooms on contact center.

Co-authored-by: Tiago Evangelista Pinto <tiago.evangelista@rocket.chat>
Co-authored-by: Rafael <rafaelblink@gmail.com>
Co-authored-by: Renato Becker <renato.augusto.becker@gmail.com>
pull/22051/head
Guilherme Gazzo 4 years ago committed by GitHub
parent 4713f3a0b3
commit a157520f10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      app/threads/client/components/ThreadComponent.tsx
  2. 161
      app/ui-sidenav/client/roomList.js
  3. 17
      client/components/ErrorBoundary.js
  4. 6
      client/components/Omnichannel/modals/TranscriptModal.tsx
  5. 8
      client/lib/RoomManager.ts
  6. 25
      client/views/omnichannel/directory/ChatsContextualBar.js
  7. 4
      client/views/omnichannel/directory/ContactContextualBar.js
  8. 57
      client/views/omnichannel/directory/chats/contextualBar/ChatInfo.js
  9. 191
      client/views/omnichannel/directory/chats/contextualBar/ChatInfoDirectory.js
  10. 3
      client/views/omnichannel/directory/chats/contextualBar/RoomEdit.js
  11. 4
      client/views/omnichannel/directory/chats/contextualBar/RoomEditWithData.js
  12. 12
      client/views/omnichannel/directory/chats/contextualBar/VisitorData.js
  13. 6
      client/views/omnichannel/directory/contacts/contextualBar/ContactEditWithData.js
  14. 4
      client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.js
  15. 11
      client/views/omnichannel/directory/contacts/contextualBar/ContactsContextualBar.tsx
  16. 11
      client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx
  17. 4
      client/views/room/Header/RoomHeader.tsx
  18. 2
      client/views/room/MemberListRouter.js
  19. 45
      client/views/room/Room/Room.js
  20. 23
      client/views/room/contexts/RoomContext.ts
  21. 2
      client/views/room/contextualBar/Call/BBB/D.tsx
  22. 4
      client/views/room/providers/RoomProvider.tsx
  23. 35
      definition/IRoom.ts
  24. 4
      ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js
  25. 6
      ee/app/livechat-enterprise/server/methods/resumeOnHold.ts
  26. 14
      server/modules/watchers/publishFields.ts

@ -100,7 +100,7 @@ const ThreadComponent: FC<{
}, [dispatchToastMessage, followMessage, unfollowMessage, mid]);
const handleClose = useCallback(() => {
channelRoute.push(room.t === 'd' ? { rid: room._id } : { name: room.name });
channelRoute.push(room.t === 'd' ? { rid: room._id } : { name: room.name || room._id });
}, [channelRoute, room._id, room.t, room.name]);
const [viewData, setViewData] = useState(() => ({

@ -161,6 +161,11 @@ const mergeSubRoom = (subscription) => {
retention: 1,
teamId: 1,
teamMain: 1,
onHold: 1,
metrics: 1,
servedBy: 1,
ts: 1,
waitingResponse: 1,
},
};
@ -168,32 +173,67 @@ const mergeSubRoom = (subscription) => {
const lastRoomUpdate = room.lm || subscription.ts || subscription._updatedAt;
if (room.uids) {
subscription.uids = room.uids;
}
if (room.v) {
subscription.v = room.v;
}
subscription.usernames = room.usernames;
const {
encrypted,
description,
cl,
topic,
announcement,
broadcast,
archived,
retention,
lastMessage,
streamingOptions,
teamId,
teamMain,
uids,
usernames,
v,
transcriptRequest,
servedBy,
onHold,
tags,
closedAt,
metrics,
waitingResponse,
responseBy,
priorityId,
livechatData,
ts,
} = room;
subscription.lastMessage = room.lastMessage;
subscription.lm = subscription.lr ? new Date(Math.max(subscription.lr, lastRoomUpdate)) : lastRoomUpdate;
subscription.streamingOptions = room.streamingOptions;
subscription.encrypted = room.encrypted;
subscription.description = room.description;
subscription.cl = room.cl;
subscription.topic = room.topic;
subscription.announcement = room.announcement;
subscription.broadcast = room.broadcast;
subscription.archived = room.archived;
subscription.retention = room.retention;
subscription.teamId = room.teamId;
subscription.teamMain = room.teamMain;
return Object.assign(subscription, getLowerCaseNames(subscription));
return Object.assign(subscription, getLowerCaseNames(subscription), {
encrypted,
description,
cl,
topic,
announcement,
broadcast,
archived,
retention,
lastMessage,
streamingOptions,
teamId,
teamMain,
uids,
usernames,
v,
transcriptRequest,
servedBy,
onHold,
tags,
closedAt,
metrics,
waitingResponse,
responseBy,
priorityId,
livechatData,
ts,
});
};
const mergeRoomSub = (room) => {
@ -201,25 +241,68 @@ const mergeRoomSub = (room) => {
if (!sub) {
return room;
}
const {
encrypted,
description,
cl,
topic,
announcement,
broadcast,
archived,
retention,
lastMessage,
streamingOptions,
teamId,
teamMain,
uids,
usernames,
v,
transcriptRequest,
servedBy,
onHold,
tags,
closedAt,
metrics,
waitingResponse,
responseBy,
priorityId,
livechatData,
ts,
} = room;
Subscriptions.update({
rid: room._id,
}, {
$set: {
encrypted: room.encrypted,
description: room.description,
cl: room.cl,
topic: room.topic,
announcement: room.announcement,
broadcast: room.broadcast,
archived: room.archived,
retention: room.retention,
...Array.isArray(room.uids) && { uids: room.uids },
...Array.isArray(room.uids) && { usernames: room.usernames },
...room.v && { v: room.v },
lastMessage: room.lastMessage,
streamingOptions: room.streamingOptions,
teamId: room.teamId,
teamMain: room.teamMain,
encrypted,
description,
cl,
topic,
announcement,
broadcast,
archived,
retention,
uids,
usernames,
lastMessage,
streamingOptions,
teamId,
teamMain,
v,
transcriptRequest,
servedBy,
onHold,
tags,
closedAt,
metrics,
waitingResponse,
responseBy,
priorityId,
livechatData,
ts,
...getLowerCaseNames(room, sub.name, sub.fname),
},
});

@ -0,0 +1,17 @@
import { Component } from 'react';
export class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return null;
}
return this.props.children;
}
}

@ -2,14 +2,14 @@ import { Field, Button, TextInput, Icon, ButtonGroup, Modal } from '@rocket.chat
import { useAutoFocus } from '@rocket.chat/fuselage-hooks';
import React, { FC, useCallback, useEffect, useState, useMemo } from 'react';
import { IRoom } from '../../../../definition/IRoom';
import { IOmnichannelRoom } from '../../../../definition/IRoom';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useComponentDidUpdate } from '../../../hooks/useComponentDidUpdate';
import { useForm } from '../../../hooks/useForm';
type TranscriptModalProps = {
email: string;
room?: IRoom;
room: IOmnichannelRoom;
onRequest: (email: string, subject: string) => void;
onSend?: (email: string, subject: string, token: string) => void;
onCancel: () => void;
@ -38,7 +38,7 @@ const TranscriptModal: FC<TranscriptModalProps> = ({
const { handleEmail, handleSubject } = handlers;
const [emailError, setEmailError] = useState('');
const [subjectError, setSubjectError] = useState('');
const { transcriptRequest } = (room as unknown) as IRoom;
const { transcriptRequest } = room;
const roomOpen = room && room.open;
const token = room?.v?.token;

@ -148,11 +148,11 @@ const subscribeOpenedRoom: Subscription<IRoom['_id'] | undefined> = {
const fields = {};
export const useHandleRoom = (rid: IRoom['_id']): AsyncState<IRoom> => {
const { resolve, update, ...state } = useAsyncState<IRoom>();
export const useHandleRoom = <T extends IRoom>(rid: IRoom['_id']): AsyncState<T> => {
const { resolve, update, ...state } = useAsyncState<T>();
const uid = useUserId();
const subscription = (useUserSubscription(rid, fields) as unknown) as IRoom;
const _room = (useUserRoom(rid, fields) as unknown) as IRoom;
const subscription = (useUserSubscription(rid, fields) as unknown) as T;
const _room = (useUserRoom(rid, fields) as unknown) as T;
const room = uid ? subscription || _room : _room;

@ -1,10 +1,14 @@
import { Box } from '@rocket.chat/fuselage';
import React from 'react';
import VerticalBar from '../../../components/VerticalBar';
import { useRoute, useRouteParameter } from '../../../contexts/RouterContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import { AsyncStatePhase } from '../../../hooks/useAsyncState';
import { useEndpointData } from '../../../hooks/useEndpointData';
import { FormSkeleton } from './Skeleton';
import Chat from './chats/Chat';
import ChatInfo from './chats/contextualBar/ChatInfo';
import ChatInfoDirectory from './chats/contextualBar/ChatInfoDirectory';
import RoomEditWithData from './chats/contextualBar/RoomEditWithData';
const ChatsContextualBar = ({ chatReload }) => {
@ -27,10 +31,26 @@ const ChatsContextualBar = ({ chatReload }) => {
directoryRoute.push({ page: 'chats', id, bar: 'info' });
};
const { value: data, phase: state, error, reload: reloadInfo } = useEndpointData(
`rooms.info?roomId=${id}`,
);
if (bar === 'view') {
return <Chat rid={id} />;
}
if (state === AsyncStatePhase.LOADING) {
return (
<Box pi='x24'>
<FormSkeleton />
</Box>
);
}
if (error || !data || !data.room) {
return <Box mbs='x16'>{t('Room_not_found')}</Box>;
}
return (
<VerticalBar className={'contextual-bar'}>
<VerticalBar.Header>
@ -53,12 +73,13 @@ const ChatsContextualBar = ({ chatReload }) => {
)}
<VerticalBar.Close onClick={handleChatsVerticalBarCloseButtonClick} />
</VerticalBar.Header>
{bar === 'info' && <ChatInfo id={id} />}
{bar === 'info' && <ChatInfoDirectory id={id} room={data.room} />}
{bar === 'edit' && (
<RoomEditWithData
id={id}
close={handleChatsVerticalBarBackButtonClick}
reload={chatReload}
reloadInfo={reloadInfo}
/>
)}
</VerticalBar>

@ -10,12 +10,8 @@ import ContactNewEdit from './contacts/contextualBar/ContactNewEdit';
const ContactContextualBar = ({ contactReload }) => {
const directoryRoute = useRoute('omnichannel-directory');
const bar = useRouteParameter('bar');
const page = useRouteParameter('page');
const tab = useRouteParameter('tab');
const id = useRouteParameter('id');
console.log(bar, tab, page, id);
const t = useTranslation();
const handleContactsVerticalBarCloseButtonClick = () => {

@ -1,5 +1,6 @@
import { Box, Margins, Tag, Button, Icon, ButtonGroup } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { Meteor } from 'meteor/meteor';
import moment from 'moment';
import React, { useEffect, useState } from 'react';
@ -9,15 +10,14 @@ import { useRoute } from '../../../../../contexts/RouterContext';
import { useToastMessageDispatch } from '../../../../../contexts/ToastMessagesContext';
import { useTranslation } from '../../../../../contexts/TranslationContext';
import { useUserSubscription } from '../../../../../contexts/UserContext';
import { AsyncStatePhase } from '../../../../../hooks/useAsyncState';
import { useEndpointData } from '../../../../../hooks/useEndpointData';
import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime';
import { useFormatDuration } from '../../../../../hooks/useFormatDuration';
import { useOmnichannelRoom } from '../../../../room/contexts/RoomContext';
import CustomField from '../../../components/CustomField';
import Field from '../../../components/Field';
import Info from '../../../components/Info';
import Label from '../../../components/Label';
import { FormSkeleton } from '../../Skeleton';
import AgentField from './AgentField';
import ContactField from './ContactField';
import DepartmentField from './DepartmentField';
@ -33,27 +33,29 @@ function ChatInfo({ id, route }) {
);
const [customFields, setCustomFields] = useState([]);
const formatDuration = useFormatDuration();
const { value: data, phase: state, error } = useEndpointData(`rooms.info?roomId=${id}`);
const room = useOmnichannelRoom();
const {
room: {
ts,
tags,
closedAt,
departmentId,
v,
servedBy,
metrics,
topic,
waitingResponse,
responseBy,
priorityId,
livechatData,
},
} = data || { room: { v: {} } };
ts,
tags,
closedAt,
department,
v,
servedBy,
metrics,
topic,
waitingResponse,
responseBy,
priorityId,
livechatData,
} = room || { room: { v: {} } };
const routePath = useRoute(route || 'omnichannel-directory');
const canViewCustomFields = () => hasPermission('view-livechat-room-customfields');
const subscription = useUserSubscription(id);
const hasGlobalEditRoomPermission = hasPermission('save-others-livechat-room-info');
const hasLocalEditRoomPermission = servedBy?._id === Meteor.userId();
const visitorId = v?._id;
const dispatchToastMessage = useToastMessageDispatch();
@ -73,7 +75,8 @@ function ChatInfo({ id, route }) {
};
const onEditClick = useMutableCallback(() => {
const hasEditAccess = !!subscription || hasGlobalEditRoomPermission;
const hasEditAccess =
!!subscription || hasLocalEditRoomPermission || hasGlobalEditRoomPermission;
if (!hasEditAccess) {
return dispatchToastMessage({ type: 'error', message: t('Not_authorized') });
}
@ -93,26 +96,14 @@ function ChatInfo({ id, route }) {
);
});
if (state === AsyncStatePhase.LOADING) {
return (
<Box pi='x24'>
<FormSkeleton />
</Box>
);
}
if (error || !data || !data.room) {
return <Box mbs='x16'>{t('Room_not_found')}</Box>;
}
return (
<>
<VerticalBar.ScrollableContent p='x24'>
<Margins block='x4'>
<ContactField contact={v} room={data.room} />
{room && v && <ContactField contact={v} room={room} />}
{visitorId && <VisitorClientInfo uid={visitorId} />}
{servedBy && <AgentField agent={servedBy} />}
{departmentId && <DepartmentField departmentId={departmentId} />}
{department && <DepartmentField departmentId={department} />}
{tags && tags.length > 0 && (
<Field>
<Label>{t('Tags')}</Label>

@ -0,0 +1,191 @@
import { Box, Margins, Tag, Button, Icon, ButtonGroup } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { Meteor } from 'meteor/meteor';
import moment from 'moment';
import React, { useEffect, useState } from 'react';
import { hasPermission } from '../../../../../../app/authorization/client';
import VerticalBar from '../../../../../components/VerticalBar';
import { useRoute } from '../../../../../contexts/RouterContext';
import { useToastMessageDispatch } from '../../../../../contexts/ToastMessagesContext';
import { useTranslation } from '../../../../../contexts/TranslationContext';
import { useUserSubscription } from '../../../../../contexts/UserContext';
import { useEndpointData } from '../../../../../hooks/useEndpointData';
import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime';
import { useFormatDuration } from '../../../../../hooks/useFormatDuration';
import CustomField from '../../../components/CustomField';
import Field from '../../../components/Field';
import Info from '../../../components/Info';
import Label from '../../../components/Label';
import AgentField from './AgentField';
import ContactField from './ContactField';
import DepartmentField from './DepartmentField';
import PriorityField from './PriorityField';
import VisitorClientInfo from './VisitorClientInfo';
function ChatInfoDirectory({ id, route, room }) {
const t = useTranslation();
const formatDateAndTime = useFormatDateAndTime();
const { value: allCustomFields, phase: stateCustomFields } = useEndpointData(
'livechat/custom-fields',
);
const [customFields, setCustomFields] = useState([]);
const formatDuration = useFormatDuration();
const {
ts,
tags,
closedAt,
departmentId,
v,
servedBy,
metrics,
topic,
waitingResponse,
responseBy,
priorityId,
livechatData,
} = room || { room: { v: {} } };
const routePath = useRoute(route || 'omnichannel-directory');
const canViewCustomFields = () => hasPermission('view-livechat-room-customfields');
const subscription = useUserSubscription(id);
const hasGlobalEditRoomPermission = hasPermission('save-others-livechat-room-info');
const hasLocalEditRoomPermission = servedBy?._id === Meteor.userId();
const visitorId = v?._id;
const dispatchToastMessage = useToastMessageDispatch();
useEffect(() => {
if (allCustomFields) {
const { customFields: customFieldsAPI } = allCustomFields;
setCustomFields(customFieldsAPI);
}
}, [allCustomFields, stateCustomFields]);
const checkIsVisibleAndScopeRoom = (key) => {
const field = customFields.find(({ _id }) => _id === key);
if (field && field.visibility === 'visible' && field.scope === 'room') {
return true;
}
return false;
};
const onEditClick = useMutableCallback(() => {
const hasEditAccess =
!!subscription || hasLocalEditRoomPermission || hasGlobalEditRoomPermission;
if (!hasEditAccess) {
return dispatchToastMessage({ type: 'error', message: t('Not_authorized') });
}
routePath.push(
route
? {
tab: 'room-info',
context: 'edit',
id,
}
: {
page: 'chats',
id,
bar: 'edit',
},
);
});
return (
<>
<VerticalBar.ScrollableContent p='x24'>
<Margins block='x4'>
{room && v && <ContactField contact={v} room={room} />}
{visitorId && <VisitorClientInfo uid={visitorId} />}
{servedBy && <AgentField agent={servedBy} />}
{departmentId && <DepartmentField departmentId={departmentId} />}
{tags && tags.length > 0 && (
<Field>
<Label>{t('Tags')}</Label>
<Info>
{tags.map((tag) => (
<Box key={tag} mie='x4' display='inline'>
<Tag style={{ display: 'inline' }} disabled>
{tag}
</Tag>
</Box>
))}
</Info>
</Field>
)}
{topic && (
<Field>
<Label>{t('Topic')}</Label>
<Info>{topic}</Info>
</Field>
)}
{ts && (
<Field>
<Label>{t('Queue_Time')}</Label>
{servedBy ? (
<Info>{moment(servedBy.ts).from(moment(ts), true)}</Info>
) : (
<Info>{moment(ts).fromNow(true)}</Info>
)}
</Field>
)}
{closedAt && (
<Field>
<Label>{t('Chat_Duration')}</Label>
<Info>{moment(closedAt).from(moment(ts), true)}</Info>
</Field>
)}
{ts && (
<Field>
<Label>{t('Created_at')}</Label>
<Info>{formatDateAndTime(ts)}</Info>
</Field>
)}
{closedAt && (
<Field>
<Label>{t('Closed_At')}</Label>
<Info>{formatDateAndTime(closedAt)}</Info>
</Field>
)}
{servedBy?.ts && (
<Field>
<Label>{t('Taken_at')}</Label>
<Info>{formatDateAndTime(servedBy.ts)}</Info>
</Field>
)}
{metrics?.response?.avg && formatDuration(metrics.response.avg) && (
<Field>
<Label>{t('Avg_response_time')}</Label>
<Info>{formatDuration(metrics.response.avg)}</Info>
</Field>
)}
{!waitingResponse && responseBy?.lastMessageTs && (
<Field>
<Label>{t('Inactivity_Time')}</Label>
<Info>{moment(responseBy.lastMessageTs).fromNow(true)}</Info>
</Field>
)}
{canViewCustomFields() &&
livechatData &&
Object.keys(livechatData).map(
(key) =>
checkIsVisibleAndScopeRoom(key) &&
livechatData[key] && <CustomField key={key} id={key} value={livechatData[key]} />,
)}
{priorityId && <PriorityField id={priorityId} />}
</Margins>
</VerticalBar.ScrollableContent>
<VerticalBar.Footer>
<ButtonGroup stretch>
<Button onClick={onEditClick}>
<Icon name='pencil' size='x20' /> {t('Edit')}
</Button>
</ButtonGroup>
</VerticalBar.Footer>
</>
);
}
export default ChatInfoDirectory;

@ -38,7 +38,7 @@ const getInitialValuesRoom = (room) => {
};
};
function RoomEdit({ room, visitor, reload, close }) {
function RoomEdit({ room, visitor, reload, reloadInfo, close }) {
const t = useTranslation();
const {
@ -123,6 +123,7 @@ function RoomEdit({ room, visitor, reload, close }) {
saveRoom(userData, roomData);
dispatchToastMessage({ type: 'success', message: t('Saved') });
reload && reload();
reloadInfo && reloadInfo();
close();
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });

@ -7,7 +7,7 @@ import { useEndpointData } from '../../../../../hooks/useEndpointData';
import { FormSkeleton } from '../../Skeleton';
import VisitorData from './VisitorData';
function RoomEditWithData({ id, reload, close }) {
function RoomEditWithData({ id, reload, reloadInfo, close }) {
const t = useTranslation();
const { value: roomData, phase: state, error } = useEndpointData(`rooms.info?roomId=${id}`);
@ -20,7 +20,7 @@ function RoomEditWithData({ id, reload, close }) {
return <Box mbs='x16'>{t('Room_not_found')}</Box>;
}
return <VisitorData room={roomData} reload={reload} close={close} />;
return <VisitorData room={roomData} reload={reload} reloadInfo={reloadInfo} close={close} />;
}
export default RoomEditWithData;

@ -7,7 +7,7 @@ import { useEndpointData } from '../../../../../hooks/useEndpointData';
import { FormSkeleton } from '../../Skeleton';
import RoomEdit from './RoomEdit';
function VisitorData({ room, reload, close }) {
function VisitorData({ room, reload, reloadInfo, close }) {
const t = useTranslation();
const {
@ -31,7 +31,15 @@ function VisitorData({ room, reload, close }) {
const { visitor: visitorData } = visitor;
const { room: roomData } = room;
return <RoomEdit room={roomData} visitor={visitorData} reload={reload} close={close} />;
return (
<RoomEdit
room={roomData}
visitor={visitorData}
reload={reload}
reloadInfo={reloadInfo}
close={close}
/>
);
}
export default VisitorData;

@ -7,11 +7,11 @@ import { useEndpointData } from '../../../../../hooks/useEndpointData';
import { FormSkeleton } from '../../Skeleton';
import ContactNewEdit from './ContactNewEdit';
function ContactEditWithData({ id, reload, close }) {
function ContactEditWithData({ id, close }) {
const t = useTranslation();
const { value: data, phase: state, error } = useEndpointData(
`omnichannel/contact?contactId=${id}`,
);
); // TODO OMNICHANNEL
if ([state].includes(AsyncStatePhase.LOADING)) {
return <FormSkeleton />;
@ -21,7 +21,7 @@ function ContactEditWithData({ id, reload, close }) {
return <Box mbs='x16'>{t('Contact_not_found')}</Box>;
}
return <ContactNewEdit id={id} data={data} reload={reload} close={close} />;
return <ContactNewEdit id={id} data={data} close={close} />;
}
export default ContactEditWithData;

@ -45,7 +45,7 @@ const getInitialValues = (data) => {
};
};
function ContactNewEdit({ id, data, reload, close }) {
function ContactNewEdit({ id, data, close }) {
const t = useTranslation();
const canViewCustomFields = () =>
@ -54,6 +54,7 @@ function ContactNewEdit({ id, data, reload, close }) {
const { values, handlers, hasUnsavedChanges: hasUnsavedChangesContact } = useForm(
getInitialValues(data),
);
const eeForms = useSubscription(formsSubscription);
const { useContactManager = () => {} } = eeForms;
@ -182,7 +183,6 @@ function ContactNewEdit({ id, data, reload, close }) {
try {
await saveContact(payload);
dispatchToastMessage({ type: 'success', message: t('Saved') });
reload && reload();
close();
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });

@ -1,16 +1,17 @@
import React from 'react';
import React, { FC } from 'react';
import { IOmnichannelRoom } from '../../../../../../definition/IRoom';
import VerticalBar from '../../../../../components/VerticalBar';
import { useRoute, useRouteParameter } from '../../../../../contexts/RouterContext';
import { useTranslation } from '../../../../../contexts/TranslationContext';
import { useRoom } from '../../../../room/providers/RoomProvider';
import { useOmnichannelRoom } from '../../../../room/contexts/RoomContext';
import { useTabBarClose } from '../../../../room/providers/ToolboxProvider';
import ContactEditWithData from './ContactEditWithData';
import ContactInfo from './ContactInfo';
const PATH = 'live';
const ContactsContextualBar = ({ rid }) => {
const ContactsContextualBar: FC<{ rid: IOmnichannelRoom['_id'] }> = ({ rid }) => {
const t = useTranslation();
const closeContextualBar = useTabBarClose();
@ -19,11 +20,11 @@ const ContactsContextualBar = ({ rid }) => {
const context = useRouteParameter('context');
const handleContactEditBarCloseButtonClick = () => {
const handleContactEditBarCloseButtonClick = (): void => {
directoryRoute.push({ id: rid, tab: 'contact-profile' });
};
const room = useRoom();
const room = useOmnichannelRoom();
const {
v: { _id },

@ -15,7 +15,7 @@ import toastr from 'toastr';
import { RoomManager } from '../../../../../../app/ui-utils/client';
import { handleError } from '../../../../../../app/utils/client';
import { IRoom } from '../../../../../../definition/IRoom';
import { IOmnichannelRoom } from '../../../../../../definition/IRoom';
import PlaceChatOnHoldModal from '../../../../../../ee/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal';
import Header from '../../../../../components/Header';
import CloseChatModal from '../../../../../components/Omnichannel/modals/CloseChatModal';
@ -35,7 +35,7 @@ import { QuickActionsActionConfig, QuickActionsEnum } from '../../../lib/QuickAc
import { QuickActionsContext } from '../../../lib/QuickActions/QuickActionsContext';
type QuickActionsProps = {
room: IRoom;
room: IOmnichannelRoom;
className?: ComponentProps<typeof Box>['className'];
};
@ -49,7 +49,7 @@ const QuickActions: FC<QuickActionsProps> = ({ room, className }) => {
);
const visibleActions = isMobile ? [] : actions.slice(0, 6);
const [email, setEmail] = useState('');
const visitorRoomId = room.v?._id;
const visitorRoomId = room.v._id;
const rid = room._id;
const uid = useUserId();
@ -215,9 +215,9 @@ const QuickActions: FC<QuickActionsProps> = ({ room, className }) => {
break;
case QuickActionsEnum.CloseChat:
setModal(
room?.departmentId ? (
room.departmentId ? (
<CloseChatModalData
departmentId={room?.departmentId}
departmentId={room.departmentId}
onConfirm={handleClose}
onCancel={closeModal}
/>
@ -293,7 +293,6 @@ const QuickActions: FC<QuickActionsProps> = ({ room, className }) => {
color,
'title': t(title as any),
className,
'tabId': id,
index,
'primary': false,
'data-quick-actions': index,

@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { IRoom } from '../../../../definition/IRoom';
import { IOmnichannelRoom } from '../../../../definition/IRoom';
import Header from '../../../components/Header';
import MarkdownText from '../../../components/MarkdownText';
import RoomAvatar from '../../../components/avatar/RoomAvatar';
@ -13,7 +13,7 @@ import Favorite from './icons/Favorite';
import Translate from './icons/Translate';
export type RoomHeaderProps = {
room: IRoom;
room: IOmnichannelRoom;
topic?: string;
slots: {
start?: unknown;

@ -1,9 +1,9 @@
import React from 'react';
import { useUserId } from '../../contexts/UserContext';
import { useRoom } from './contexts/RoomContext';
import RoomMembers from './contextualBar/RoomMembers';
import UserInfo from './contextualBar/UserInfo';
import { useRoom } from './providers/RoomProvider';
import { useTab, useTabBarClose, useTabContext } from './providers/ToolboxProvider';
const getUid = (room, ownUserId) => {

@ -1,13 +1,14 @@
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React, { useMemo } from 'react';
import React, { useDebugValue, useMemo } from 'react';
import { ErrorBoundary } from '../../../components/ErrorBoundary';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useUserPreference } from '../../../contexts/UserContext';
import Header from '../Header';
import BlazeTemplate from '../components/BlazeTemplate';
import { RoomTemplate } from '../components/RoomTemplate/RoomTemplate';
import VerticalBarOldActions from '../components/VerticalBarOldActions';
import { useRoom } from '../providers/RoomProvider';
import { useRoom } from '../contexts/RoomContext';
import {
useTab,
useTabBarOpen,
@ -37,6 +38,8 @@ const Room = () => {
openUserInfo,
]);
useDebugValue(room);
useDebugValue(tab);
return (
<RoomTemplate aria-label={t('Channel')} data-qa-rc-room={room._id}>
<RoomTemplate.Header>
@ -53,24 +56,26 @@ const Room = () => {
</RoomTemplate.Body>
{tab && (
<RoomTemplate.Aside data-qa-tabbar-name={tab.id}>
{typeof tab.template === 'string' && (
<VerticalBarOldActions
{...tab}
name={tab.template}
tabBar={tabBar}
rid={room._id}
_id={room._id}
/>
)}
{typeof tab.template !== 'string' && (
<LazyComponent
template={tab.template}
tabBar={tabBar}
rid={room._id}
teamId={room.teamId}
_id={room._id}
/>
)}
<ErrorBoundary>
{typeof tab.template === 'string' && (
<VerticalBarOldActions
{...tab}
name={tab.template}
tabBar={tabBar}
rid={room._id}
_id={room._id}
/>
)}
{typeof tab.template !== 'string' && (
<LazyComponent
template={tab.template}
tabBar={tabBar}
rid={room._id}
teamId={room.teamId}
_id={room._id}
/>
)}
</ErrorBoundary>
</RoomTemplate.Aside>
)}
</RoomTemplate>

@ -1,6 +1,6 @@
import { createContext } from 'react';
import { createContext, useContext } from 'react';
import { IRoom } from '../../../../definition/IRoom';
import { IRoom, IOmnichannelRoom, isOmnichannelRoom } from '../../../../definition/IRoom';
export type RoomContextValue = {
rid: IRoom['_id'];
@ -9,3 +9,22 @@ export type RoomContextValue = {
};
export const RoomContext = createContext<RoomContextValue | null>(null);
export const useRoom = (): IRoom => {
const { room } = useContext(RoomContext) || {};
if (!room) {
throw new Error('use useRoom only inside opened rooms');
}
return room;
};
export const useOmnichannelRoom = (): IOmnichannelRoom => {
const { room } = useContext(RoomContext) || {};
if (!room) {
throw new Error('use useRoom only inside opened rooms');
}
if (!isOmnichannelRoom(room)) {
throw new Error('invalid room type');
}
return room;
};

@ -6,7 +6,7 @@ import { IRoom } from '../../../../../../definition/IRoom';
import { usePermission } from '../../../../../contexts/AuthorizationContext';
import { useMethod } from '../../../../../contexts/ServerContext';
import { useSetting } from '../../../../../contexts/SettingsContext';
import { useRoom } from '../../../providers/RoomProvider';
import { useRoom } from '../../../contexts/RoomContext';
import { useTabBarClose } from '../../../providers/ToolboxProvider';
import CallBBB from './CallBBB';

@ -1,4 +1,4 @@
import React, { ReactNode, useContext, useMemo, memo, useEffect } from 'react';
import React, { ReactNode, useMemo, memo, useEffect } from 'react';
import { roomTypes } from '../../../../app/utils/client';
import { IRoom } from '../../../../definition/IRoom';
@ -43,6 +43,4 @@ const RoomProvider = ({ rid, children }: Props): JSX.Element => {
</RoomContext.Provider>
);
};
export const useRoom = (): undefined | IRoom => useContext(RoomContext)?.room;
export default memo(RoomProvider);

@ -16,13 +16,14 @@ interface IRequestTranscript {
export interface IRoom extends IRocketChatRecord {
_id: RoomID;
t: RoomType;
name: string;
name?: string;
fname: string;
msgs: number;
default?: true;
broadcast?: true;
featured?: true;
encrypted?: boolean;
topic: any;
u: Pick<IUser, '_id' | 'username' | 'name'>;
@ -49,23 +50,13 @@ export interface IRoom extends IRocketChatRecord {
teamMain?: boolean;
teamId?: string;
teamDefault?: boolean;
v?: {
_id?: string;
token?: string;
status?: string;
};
transcriptRequest?: IRequestTranscript;
open?: boolean;
servedBy?: {
_id: string;
};
onHold?: boolean;
autoTranslateLanguage: string;
autoTranslate?: boolean;
unread?: number;
alert?: boolean;
hideUnreadStatus?: boolean;
departmentId?: string;
}
export interface IDirectMessageRoom extends Omit<IRoom, 'default' | 'featured' | 'u' | 'name'> {
@ -75,9 +66,27 @@ export interface IDirectMessageRoom extends Omit<IRoom, 'default' | 'featured' |
}
export interface IOmnichannelRoom extends Omit<IRoom, 'default' | 'featured' | 'u' | 'name'> {
export interface IOmnichannelRoom extends Omit<IRoom, 'default' | 'featured' | 'broadcast' | 'featured' | ''> {
t: 'l';
v: {
_id?: string;
token?: string;
status: 'online' | 'busy' | 'away' | 'offline';
};
transcriptRequest?: IRequestTranscript;
servedBy?: {
_id: string;
};
onHold?: boolean;
departmentId?: string;
tags: any;
closedAt: any;
metrics: any;
waitingResponse: any;
responseBy: any;
priorityId: any;
livechatData: any;
}
export const isOmnichannelRoom = (room: IRoom): room is IOmnichannelRoom & IRoom => room.t === 'l';

@ -5,7 +5,7 @@ import { Users } from '../../../../../app/models';
import { LivechatInquiry, OmnichannelQueue } from '../../../../../app/models/server/raw';
import LivechatUnit from '../../../models/server/models/LivechatUnit';
import LivechatTag from '../../../models/server/models/LivechatTag';
import { LivechatRooms, Subscriptions, Messages } from '../../../../../app/models/server';
import { LivechatRooms, Messages } from '../../../../../app/models/server';
import LivechatPriority from '../../../models/server/models/LivechatPriority';
import { addUserRoles, removeUserFromRoles } from '../../../../../app/authorization/server';
import { processWaitingQueue, removePriorityFromRooms, updateInquiryQueuePriority, updatePriorityInquiries, updateRoomPriorityHistory } from './Helper';
@ -173,7 +173,6 @@ export const LivechatEnterprise = {
return false;
}
LivechatRooms.setOnHold(roomId);
Subscriptions.setOnHold(roomId);
Messages.createOnHoldHistoryWithRoomIdMessageAndUser(roomId, comment, onHoldBy);
Meteor.defer(() => {
@ -191,7 +190,6 @@ export const LivechatEnterprise = {
await AutoCloseOnHoldScheduler.unscheduleRoom(roomId);
LivechatRooms.unsetAllOnHoldFieldsByRoomId(roomId);
Subscriptions.unsetOnHold(roomId);
},
};

@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { LivechatRooms, LivechatInquiry, Messages, Users, LivechatVisitors } from '../../../../../app/models/server';
import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager';
import { LivechatEnterprise } from '../lib/LivechatEnterprise';
import { callbacks } from '../../../../../app/callbacks/server';
const resolveOnHoldCommentInfo = (options: { clientAction: boolean }, room: any, onHoldChatResumedBy: any): string => {
@ -33,14 +33,12 @@ Meteor.methods({
throw new Meteor.Error('room-closed', 'Room is not OnHold', { method: 'livechat:resumeOnHold' });
}
const { servedBy: { _id: agentId, username } } = room;
const inquiry = LivechatInquiry.findOneByRoomId(roomId, {});
if (!inquiry) {
throw new Meteor.Error('inquiry-not-found', 'Error! No inquiry found for this room', { method: 'livechat:resumeOnHold' });
}
await RoutingManager.takeInquiry(inquiry, { agentId, username }, options);
LivechatEnterprise.releaseOnHoldChat(room);
const onHoldChatResumedBy = options.clientAction ? Meteor.user() : Users.findOneById('rocket.cat');

@ -36,6 +36,8 @@ export const subscriptionFields = {
tunread: 1,
tunreadGroup: 1,
tunreadUser: 1,
// Omnichannel fields
v: 1,
onHold: 1,
};
@ -69,14 +71,12 @@ export const roomFields = {
usersCount: 1,
// @TODO create an API to register this fields based on room type
livechatData: 1,
tags: 1,
sms: 1,
facebook: 1,
code: 1,
joinCodeRequired: 1,
open: 1,
v: 1,
label: 1,
ro: 1,
reactWhenReadOnly: 1,
@ -87,10 +87,18 @@ export const roomFields = {
broadcast: 1,
encrypted: 1,
e2eKeyId: 1,
// Omnichannel fields
livechatData: 1,
priorityId: 1,
v: 1,
departmentId: 1,
servedBy: 1,
priorityId: 1,
transcriptRequest: 1,
onHold: 1,
metrics: 1,
ts: 1,
waitingResponse: 1,
// fields used by DMs
usernames: 1,

Loading…
Cancel
Save