refactor: Omnichannel Department re-write (#28948)
parent
6e2f78feea
commit
6a474ff952
@ -0,0 +1,6 @@ |
||||
--- |
||||
'@rocket.chat/meteor': minor |
||||
'@rocket.chat/rest-typings': patch |
||||
--- |
||||
|
||||
Refactored Omnichannel department pages to use best practices, also fixed existing bugs |
||||
@ -1,41 +0,0 @@ |
||||
import { Box, Table } from '@rocket.chat/fuselage'; |
||||
import React, { memo } from 'react'; |
||||
|
||||
import UserAvatar from '../../../components/avatar/UserAvatar'; |
||||
import Count from './Count'; |
||||
import Order from './Order'; |
||||
import RemoveAgentButton from './RemoveAgentButton'; |
||||
|
||||
const AgentRow = ({ agentId, username, name, avatarETag, mediaQuery, agentList, setAgentList, setAgentsRemoved }) => ( |
||||
<Table.Row key={agentId} tabIndex={0} role='link' action qa-user-id={agentId}> |
||||
<Table.Cell withTruncatedText> |
||||
<Box display='flex' alignItems='center'> |
||||
<UserAvatar size={mediaQuery ? 'x28' : 'x40'} title={username} username={username} etag={avatarETag} /> |
||||
<Box display='flex' withTruncatedText mi='x8'> |
||||
<Box display='flex' flexDirection='column' alignSelf='center' withTruncatedText> |
||||
<Box fontScale='p2m' withTruncatedText color='default'> |
||||
{name || username} |
||||
</Box> |
||||
{!mediaQuery && name && ( |
||||
<Box fontScale='p2' color='hint' withTruncatedText> |
||||
{' '} |
||||
{`@${username}`}{' '} |
||||
</Box> |
||||
)} |
||||
</Box> |
||||
</Box> |
||||
</Box> |
||||
</Table.Cell> |
||||
<Table.Cell fontScale='p2' color='hint' withTruncatedText> |
||||
<Count agentId={agentId} agentList={agentList} setAgentList={setAgentList} /> |
||||
</Table.Cell> |
||||
<Table.Cell fontScale='p2' color='hint' withTruncatedText> |
||||
<Order agentId={agentId} agentList={agentList} setAgentList={setAgentList} /> |
||||
</Table.Cell> |
||||
<Table.Cell fontScale='p2' color='hint'> |
||||
<RemoveAgentButton agentId={agentId} agentList={agentList} setAgentList={setAgentList} setAgentsRemoved={setAgentsRemoved} /> |
||||
</Table.Cell> |
||||
</Table.Row> |
||||
); |
||||
|
||||
export default memo(AgentRow); |
||||
@ -1,30 +0,0 @@ |
||||
import { Box, NumberInput } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React, { useState } from 'react'; |
||||
|
||||
function Count({ agentId, setAgentList, agentList }) { |
||||
const t = useTranslation(); |
||||
const [agentCount, setAgentCount] = useState(agentList.find((agent) => agent.agentId === agentId).count || 0); |
||||
|
||||
const handleCount = useMutableCallback(async (e) => { |
||||
const countValue = Number(e.currentTarget.value); |
||||
setAgentCount(countValue); |
||||
setAgentList( |
||||
agentList.map((agent) => { |
||||
if (agent.agentId === agentId) { |
||||
agent.count = countValue; |
||||
} |
||||
return agent; |
||||
}), |
||||
); |
||||
}); |
||||
|
||||
return ( |
||||
<Box display='flex'> |
||||
<NumberInput flexShrink={1} key={`${agentId}-count`} title={t('Count')} value={agentCount} onChange={handleCount} /> |
||||
</Box> |
||||
); |
||||
} |
||||
|
||||
export default Count; |
||||
@ -0,0 +1,30 @@ |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { memo } from 'react'; |
||||
|
||||
import UserAvatar from '../../../../components/avatar/UserAvatar'; |
||||
|
||||
const AgentAvatar = ({ name, username, eTag }: { name: string; username: string; eTag?: string }) => { |
||||
const mediaQuery = useMediaQuery('(min-width: 1024px)'); |
||||
|
||||
return ( |
||||
<Box display='flex' alignItems='center'> |
||||
<UserAvatar size={mediaQuery ? 'x28' : 'x40'} title={username} username={username} etag={eTag} /> |
||||
<Box display='flex' withTruncatedText mi='x8'> |
||||
<Box display='flex' flexDirection='column' alignSelf='center' withTruncatedText> |
||||
<Box fontScale='p2m' withTruncatedText color='default'> |
||||
{name || username} |
||||
</Box> |
||||
{!mediaQuery && name && ( |
||||
<Box fontScale='p2' color='hint' withTruncatedText> |
||||
{' '} |
||||
{`@${username}`}{' '} |
||||
</Box> |
||||
)} |
||||
</Box> |
||||
</Box> |
||||
</Box> |
||||
); |
||||
}; |
||||
|
||||
export default memo(AgentAvatar); |
||||
@ -0,0 +1,37 @@ |
||||
import { NumberInput, TableCell, TableRow } from '@rocket.chat/fuselage'; |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React, { memo } from 'react'; |
||||
import type { UseFormRegister } from 'react-hook-form'; |
||||
|
||||
import type { FormValues, IDepartmentAgent } from '../EditDepartment'; |
||||
import AgentAvatar from './AgentAvatar'; |
||||
import RemoveAgentButton from './RemoveAgentButton'; |
||||
|
||||
type AgentRowProps = { |
||||
agent: IDepartmentAgent; |
||||
index: number; |
||||
register: UseFormRegister<FormValues>; |
||||
onRemove: (agentId: string) => void; |
||||
}; |
||||
|
||||
const AgentRow = ({ index, agent, register, onRemove }: AgentRowProps) => { |
||||
const t = useTranslation(); |
||||
|
||||
return ( |
||||
<TableRow key={agent.agentId} tabIndex={0} role='link' action qa-user-id={agent.agentId}> |
||||
<TableCell withTruncatedText> |
||||
<AgentAvatar name={agent.name || ''} username={agent.username || ''} /> |
||||
</TableCell> |
||||
<TableCell fontScale='p2' color='hint' withTruncatedText> |
||||
<NumberInput title={t('Count')} maxWidth='100%' {...register(`agentList.${index}.count`, { valueAsNumber: true })} /> |
||||
</TableCell> |
||||
<TableCell fontScale='p2' color='hint' withTruncatedText> |
||||
<NumberInput title={t('Order')} maxWidth='100%' {...register(`agentList.${index}.order`, { valueAsNumber: true })} /> |
||||
</TableCell> |
||||
<TableCell fontScale='p2' color='hint'> |
||||
<RemoveAgentButton agentId={agent.agentId} onRemove={onRemove} /> |
||||
</TableCell> |
||||
</TableRow> |
||||
); |
||||
}; |
||||
export default memo(AgentRow); |
||||
@ -0,0 +1,43 @@ |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React from 'react'; |
||||
import type { Control, UseFormRegister } from 'react-hook-form'; |
||||
import { useWatch, useFieldArray } from 'react-hook-form'; |
||||
|
||||
import { GenericTable, GenericTableBody, GenericTableHeader, GenericTableHeaderCell } from '../../../../components/GenericTable'; |
||||
import type { FormValues } from '../EditDepartment'; |
||||
import AddAgent from './AddAgent'; |
||||
import AgentRow from './AgentRow'; |
||||
|
||||
type DepartmentAgentsTableProps = { |
||||
control: Control<FormValues>; |
||||
register: UseFormRegister<FormValues>; |
||||
}; |
||||
|
||||
function DepartmentAgentsTable({ control, register }: DepartmentAgentsTableProps) { |
||||
const t = useTranslation(); |
||||
const { fields, append, remove } = useFieldArray({ control, name: 'agentList' }); |
||||
const agentList = useWatch({ control, name: 'agentList' }); |
||||
|
||||
return ( |
||||
<> |
||||
<AddAgent agentList={agentList} data-qa='DepartmentSelect-AgentsTable' onAdd={append} /> |
||||
|
||||
<GenericTable> |
||||
<GenericTableHeader> |
||||
<GenericTableHeaderCell w='x200'>{t('Name')}</GenericTableHeaderCell> |
||||
<GenericTableHeaderCell w='x140'>{t('Count')}</GenericTableHeaderCell> |
||||
<GenericTableHeaderCell w='x120'>{t('Order')}</GenericTableHeaderCell> |
||||
<GenericTableHeaderCell w='x40'>{t('Remove')}</GenericTableHeaderCell> |
||||
</GenericTableHeader> |
||||
|
||||
<GenericTableBody> |
||||
{fields.map((agent, index) => ( |
||||
<AgentRow key={agent.id} index={index} agent={agent} register={register} onRemove={() => remove(index)} /> |
||||
))} |
||||
</GenericTableBody> |
||||
</GenericTable> |
||||
</> |
||||
); |
||||
} |
||||
|
||||
export default DepartmentAgentsTable; |
||||
@ -0,0 +1,63 @@ |
||||
import { Button, Chip, Field, TextInput } from '@rocket.chat/fuselage'; |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import type { FormEvent } from 'react'; |
||||
import React, { useCallback, useState } from 'react'; |
||||
|
||||
type DepartmentTagsProps = { |
||||
error: string; |
||||
value: string[]; |
||||
onChange: (tags: string[]) => void; |
||||
}; |
||||
|
||||
export const DepartmentTags = ({ error, value: tags, onChange }: DepartmentTagsProps) => { |
||||
const t = useTranslation(); |
||||
const [tagText, setTagText] = useState(''); |
||||
|
||||
const handleAddTag = useCallback(() => { |
||||
if (tags.includes(tagText)) { |
||||
return; |
||||
} |
||||
|
||||
setTagText(''); |
||||
onChange([...tags, tagText]); |
||||
}, [onChange, tagText, tags]); |
||||
|
||||
const handleTagChipClick = (tag: string) => () => { |
||||
onChange(tags.filter((_tag) => _tag !== tag)); |
||||
}; |
||||
|
||||
return ( |
||||
<> |
||||
<Field.Row> |
||||
<TextInput |
||||
data-qa='DepartmentEditTextInput-ConversationClosingTags' |
||||
error={error} |
||||
placeholder={t('Enter_a_tag')} |
||||
value={tagText} |
||||
onChange={(e: FormEvent<HTMLInputElement>) => setTagText(e.currentTarget.value)} |
||||
/> |
||||
<Button |
||||
disabled={Boolean(!tagText.trim()) || tags.includes(tagText)} |
||||
data-qa='DepartmentEditAddButton-ConversationClosingTags' |
||||
mis='x8' |
||||
title={t('Add')} |
||||
onClick={handleAddTag} |
||||
> |
||||
{t('Add')} |
||||
</Button> |
||||
</Field.Row> |
||||
|
||||
<Field.Hint>{t('Conversation_closing_tags_description')}</Field.Hint> |
||||
|
||||
{tags?.length > 0 && ( |
||||
<Field.Row justifyContent='flex-start'> |
||||
{tags.map((tag, i) => ( |
||||
<Chip key={i} onClick={handleTagChipClick(tag)} mie='x8'> |
||||
{tag} |
||||
</Chip> |
||||
))} |
||||
</Field.Row> |
||||
)} |
||||
</> |
||||
); |
||||
}; |
||||
@ -1,56 +0,0 @@ |
||||
import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React, { useState, useEffect } from 'react'; |
||||
|
||||
import GenericTable from '../../../components/GenericTable'; |
||||
import AddAgent from './AddAgent'; |
||||
import AgentRow from './AgentRow'; |
||||
|
||||
function DepartmentsAgentsTable({ agents, setAgentListFinal, setAgentsAdded, setAgentsRemoved }) { |
||||
const t = useTranslation(); |
||||
const [agentList, setAgentList] = useState((agents && JSON.parse(JSON.stringify(agents))) || []); |
||||
|
||||
useEffect(() => setAgentListFinal(agentList), [agentList, setAgentListFinal]); |
||||
|
||||
const mediaQuery = useMediaQuery('(min-width: 1024px)'); |
||||
|
||||
return ( |
||||
<> |
||||
<AddAgent agentList={agentList} data-qa='DepartmentSelect-AgentsTable' setAgentList={setAgentList} setAgentsAdded={setAgentsAdded} /> |
||||
<GenericTable |
||||
header={ |
||||
<> |
||||
<GenericTable.HeaderCell key={'name'} w='x200'> |
||||
{t('Name')} |
||||
</GenericTable.HeaderCell> |
||||
<GenericTable.HeaderCell key={'Count'} w='x140'> |
||||
{t('Count')} |
||||
</GenericTable.HeaderCell> |
||||
<GenericTable.HeaderCell key={'Order'} w='x120'> |
||||
{t('Order')} |
||||
</GenericTable.HeaderCell> |
||||
<GenericTable.HeaderCell key={'remove'} w='x40'> |
||||
{t('Remove')} |
||||
</GenericTable.HeaderCell> |
||||
</> |
||||
} |
||||
results={agentList} |
||||
total={agentList?.length} |
||||
pi='x24' |
||||
> |
||||
{(props) => ( |
||||
<AgentRow |
||||
key={props._id} |
||||
mediaQuery={mediaQuery} |
||||
agentList={agentList} |
||||
setAgentList={setAgentList} |
||||
setAgentsRemoved={setAgentsRemoved} |
||||
{...props} |
||||
/> |
||||
)} |
||||
</GenericTable> |
||||
</> |
||||
); |
||||
} |
||||
|
||||
export default DepartmentsAgentsTable; |
||||
@ -1,498 +0,0 @@ |
||||
import { |
||||
FieldGroup, |
||||
Field, |
||||
TextInput, |
||||
Chip, |
||||
Box, |
||||
Icon, |
||||
Divider, |
||||
ToggleSwitch, |
||||
TextAreaInput, |
||||
ButtonGroup, |
||||
Button, |
||||
PaginatedSelectFiltered, |
||||
} from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; |
||||
import { useToastMessageDispatch, useRoute, useMethod, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React, { useMemo, useState, useRef, useCallback } from 'react'; |
||||
|
||||
import { validateEmail } from '../../../../lib/emailValidator'; |
||||
import Page from '../../../components/Page'; |
||||
import { useRecordList } from '../../../hooks/lists/useRecordList'; |
||||
import { useComponentDidUpdate } from '../../../hooks/useComponentDidUpdate'; |
||||
import { useForm } from '../../../hooks/useForm'; |
||||
import { useRoomsList } from '../../../hooks/useRoomsList'; |
||||
import { AsyncStatePhase } from '../../../lib/asyncState'; |
||||
import { useFormsSubscription } from '../additionalForms'; |
||||
import DepartmentsAgentsTable from './DepartmentsAgentsTable'; |
||||
|
||||
function withDefault(key, defaultValue) { |
||||
return key || defaultValue; |
||||
} |
||||
|
||||
function EditDepartment({ data, id, title, allowedToForwardData }) { |
||||
const t = useTranslation(); |
||||
const departmentsRoute = useRoute('omnichannel-departments'); |
||||
|
||||
const { |
||||
useEeNumberInput = () => {}, |
||||
useEeTextInput = () => {}, |
||||
useEeTextAreaInput = () => {}, |
||||
useDepartmentForwarding = () => {}, |
||||
useDepartmentBusinessHours = () => {}, |
||||
useSelectForwardDepartment = () => {}, |
||||
} = useFormsSubscription(); |
||||
|
||||
const { agents } = data || { agents: [] }; |
||||
|
||||
const initialAgents = useRef(agents); |
||||
|
||||
const MaxChats = useEeNumberInput(); |
||||
const VisitorInactivity = useEeNumberInput(); |
||||
const WaitingQueueMessageInput = useEeTextAreaInput(); |
||||
const AbandonedMessageInput = useEeTextInput(); |
||||
const DepartmentForwarding = useDepartmentForwarding(); |
||||
const DepartmentBusinessHours = useDepartmentBusinessHours(); |
||||
const AutoCompleteDepartment = useSelectForwardDepartment(); |
||||
const [agentList, setAgentList] = useState([]); |
||||
const [agentsRemoved, setAgentsRemoved] = useState([]); |
||||
const [agentsAdded, setAgentsAdded] = useState([]); |
||||
|
||||
const { department } = data || { department: {} }; |
||||
|
||||
const [initialTags] = useState(() => department?.chatClosingTags ?? []); |
||||
const [[tags, tagsText], setTagsState] = useState(() => [initialTags, '']); |
||||
const hasTagChanges = useMemo(() => tags.toString() !== initialTags.toString(), [tags, initialTags]); |
||||
|
||||
const { values, handlers, hasUnsavedChanges } = useForm({ |
||||
name: withDefault(department?.name, ''), |
||||
email: withDefault(department?.email, ''), |
||||
description: withDefault(department?.description, ''), |
||||
enabled: !!department?.enabled, |
||||
maxNumberSimultaneousChat: department?.maxNumberSimultaneousChat, |
||||
showOnRegistration: !!department?.showOnRegistration, |
||||
showOnOfflineForm: !!department?.showOnOfflineForm, |
||||
abandonedRoomsCloseCustomMessage: withDefault(department?.abandonedRoomsCloseCustomMessage, ''), |
||||
requestTagBeforeClosingChat: !!department?.requestTagBeforeClosingChat, |
||||
offlineMessageChannelName: withDefault(department?.offlineMessageChannelName, ''), |
||||
visitorInactivityTimeoutInSeconds: department?.visitorInactivityTimeoutInSeconds, |
||||
waitingQueueMessage: withDefault(department?.waitingQueueMessage, ''), |
||||
departmentsAllowedToForward: allowedToForwardData?.departments?.map((dep) => ({ label: dep.name, value: dep._id })) || [], |
||||
fallbackForwardDepartment: withDefault(department?.fallbackForwardDepartment, ''), |
||||
}); |
||||
const { |
||||
handleName, |
||||
handleEmail, |
||||
handleDescription, |
||||
handleEnabled, |
||||
handleMaxNumberSimultaneousChat, |
||||
handleShowOnRegistration, |
||||
handleShowOnOfflineForm, |
||||
handleAbandonedRoomsCloseCustomMessage, |
||||
handleRequestTagBeforeClosingChat, |
||||
handleOfflineMessageChannelName, |
||||
handleVisitorInactivityTimeoutInSeconds, |
||||
handleWaitingQueueMessage, |
||||
handleDepartmentsAllowedToForward, |
||||
handleFallbackForwardDepartment, |
||||
} = handlers; |
||||
|
||||
const { |
||||
name, |
||||
email, |
||||
description, |
||||
enabled, |
||||
maxNumberSimultaneousChat, |
||||
showOnRegistration, |
||||
showOnOfflineForm, |
||||
abandonedRoomsCloseCustomMessage, |
||||
requestTagBeforeClosingChat, |
||||
offlineMessageChannelName, |
||||
visitorInactivityTimeoutInSeconds, |
||||
waitingQueueMessage, |
||||
departmentsAllowedToForward, |
||||
fallbackForwardDepartment, |
||||
} = values; |
||||
|
||||
const { itemsList: RoomsList, loadMoreItems: loadMoreRooms } = useRoomsList( |
||||
useMemo(() => ({ text: offlineMessageChannelName }), [offlineMessageChannelName]), |
||||
); |
||||
|
||||
const { phase: roomsPhase, items: roomsItems, itemCount: roomsTotal } = useRecordList(RoomsList); |
||||
|
||||
const handleTagChipClick = (tag) => () => { |
||||
setTagsState(([tags, tagsText]) => [tags.filter((_tag) => _tag !== tag), tagsText]); |
||||
}; |
||||
|
||||
const handleTagTextSubmit = useCallback(() => { |
||||
setTagsState((state) => { |
||||
const [tags, tagsText] = state; |
||||
|
||||
if (tags.includes(tagsText)) { |
||||
return state; |
||||
} |
||||
|
||||
return [[...tags, tagsText], '']; |
||||
}); |
||||
}, []); |
||||
|
||||
const handleTagTextChange = (e) => { |
||||
setTagsState(([tags]) => [tags, e.target.value]); |
||||
}; |
||||
|
||||
const saveDepartmentInfo = useMethod('livechat:saveDepartment'); |
||||
const saveDepartmentAgentsInfoOnEdit = useEndpoint('POST', `/v1/livechat/department/${id}/agents`); |
||||
|
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const [nameError, setNameError] = useState(); |
||||
const [emailError, setEmailError] = useState(); |
||||
const [tagError, setTagError] = useState(); |
||||
|
||||
useComponentDidUpdate(() => { |
||||
setNameError(!name ? t('The_field_is_required', 'name') : ''); |
||||
}, [t, name]); |
||||
useComponentDidUpdate(() => { |
||||
setEmailError(!email ? t('The_field_is_required', 'email') : ''); |
||||
}, [t, email]); |
||||
useComponentDidUpdate(() => { |
||||
setEmailError(!validateEmail(email) ? t('Validate_email_address') : ''); |
||||
}, [t, email]); |
||||
useComponentDidUpdate(() => { |
||||
setTagError(requestTagBeforeClosingChat && (!tags || tags.length === 0) ? t('The_field_is_required', 'name') : ''); |
||||
}, [requestTagBeforeClosingChat, t, tags]); |
||||
|
||||
const handleSubmit = useMutableCallback(async (e) => { |
||||
e.preventDefault(); |
||||
let error = false; |
||||
if (!name) { |
||||
setNameError(t('The_field_is_required', 'name')); |
||||
error = true; |
||||
} |
||||
if (!email) { |
||||
setEmailError(t('The_field_is_required', 'email')); |
||||
error = true; |
||||
} |
||||
if (!validateEmail(email)) { |
||||
setEmailError(t('Validate_email_address')); |
||||
error = true; |
||||
} |
||||
if (requestTagBeforeClosingChat && (!tags || tags.length === 0)) { |
||||
setTagError(t('The_field_is_required', 'tags')); |
||||
error = true; |
||||
} |
||||
|
||||
if (error) { |
||||
return; |
||||
} |
||||
|
||||
const payload = { |
||||
enabled, |
||||
name, |
||||
description, |
||||
showOnRegistration, |
||||
showOnOfflineForm, |
||||
requestTagBeforeClosingChat, |
||||
email, |
||||
chatClosingTags: tags, |
||||
offlineMessageChannelName, |
||||
maxNumberSimultaneousChat, |
||||
visitorInactivityTimeoutInSeconds, |
||||
abandonedRoomsCloseCustomMessage, |
||||
waitingQueueMessage, |
||||
departmentsAllowedToForward: departmentsAllowedToForward?.map((dep) => dep.value), |
||||
fallbackForwardDepartment, |
||||
}; |
||||
|
||||
const agentListPayload = { |
||||
upsert: agentList.filter( |
||||
(agent) => |
||||
!initialAgents.current.some( |
||||
(initialAgent) => initialAgent._id === agent._id && agent.count === initialAgent.count && agent.order === initialAgent.order, |
||||
), |
||||
), |
||||
remove: initialAgents.current.filter((initialAgent) => !agentList.some((agent) => initialAgent._id === agent._id)), |
||||
}; |
||||
|
||||
try { |
||||
if (id) { |
||||
await saveDepartmentInfo(id, payload, []); |
||||
if (agentListPayload.upsert.length > 0 || agentListPayload.remove.length > 0) { |
||||
await saveDepartmentAgentsInfoOnEdit(agentListPayload); |
||||
} |
||||
} else { |
||||
await saveDepartmentInfo(id, payload, agentList); |
||||
} |
||||
dispatchToastMessage({ type: 'success', message: t('Saved') }); |
||||
departmentsRoute.push({}); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}); |
||||
|
||||
const handleReturn = useMutableCallback(() => { |
||||
departmentsRoute.push({}); |
||||
}); |
||||
|
||||
const invalidForm = |
||||
!name || |
||||
!email || |
||||
!validateEmail(email) || |
||||
!(hasUnsavedChanges || hasTagChanges) || |
||||
(requestTagBeforeClosingChat && (!tags || tags.length === 0)); |
||||
|
||||
const formId = useUniqueId(); |
||||
|
||||
const hasNewAgent = useMemo(() => agents.length === agentList.length, [agents, agentList]); |
||||
|
||||
const agentsHaveChanged = () => { |
||||
let hasChanges = false; |
||||
if (agentList.length !== initialAgents.current.length) { |
||||
hasChanges = true; |
||||
} |
||||
|
||||
if (agentsAdded.length > 0 && agentsRemoved.length > 0) { |
||||
hasChanges = true; |
||||
} |
||||
|
||||
agentList.forEach((agent) => { |
||||
const existingAgent = initialAgents.current.find((initial) => initial.agentId === agent.agentId); |
||||
if (existingAgent) { |
||||
if (agent.count !== existingAgent.count) { |
||||
hasChanges = true; |
||||
} |
||||
if (agent.order !== existingAgent.order) { |
||||
hasChanges = true; |
||||
} |
||||
} |
||||
}); |
||||
|
||||
return hasChanges; |
||||
}; |
||||
|
||||
return ( |
||||
<Page flexDirection='row'> |
||||
<Page> |
||||
<Page.Header title={title}> |
||||
<ButtonGroup> |
||||
<Button onClick={handleReturn}> |
||||
<Icon name='back' /> {t('Back')} |
||||
</Button> |
||||
<Button type='submit' form={formId} primary disabled={invalidForm && hasNewAgent && !(id && agentsHaveChanged())}> |
||||
{t('Save')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
</Page.Header> |
||||
<Page.ScrollableContentWithShadow> |
||||
<FieldGroup w='full' alignSelf='center' maxWidth='x600' id={formId} is='form' autoComplete='off' onSubmit={handleSubmit}> |
||||
<Field> |
||||
<Box display='flex' data-qa='DepartmentEditToggle-Enabled' flexDirection='row'> |
||||
<Field.Label>{t('Enabled')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch flexGrow={1} checked={enabled} onChange={handleEnabled} /> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Name')}*</Field.Label> |
||||
<Field.Row> |
||||
<TextInput |
||||
data-qa='DepartmentEditTextInput-Name' |
||||
flexGrow={1} |
||||
error={nameError} |
||||
value={name} |
||||
onChange={handleName} |
||||
placeholder={t('Name')} |
||||
/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Description')}</Field.Label> |
||||
<Field.Row> |
||||
<TextAreaInput |
||||
data-qa='DepartmentEditTextInput-Description' |
||||
flexGrow={1} |
||||
value={description} |
||||
onChange={handleDescription} |
||||
placeholder={t('Description')} |
||||
/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Box data-qa='DepartmentEditToggle-ShowOnRegistrationPage' display='flex' flexDirection='row'> |
||||
<Field.Label>{t('Show_on_registration_page')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch flexGrow={1} checked={showOnRegistration} onChange={handleShowOnRegistration} /> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Email')}*</Field.Label> |
||||
<Field.Row> |
||||
<TextInput |
||||
data-qa='DepartmentEditTextInput-Email' |
||||
flexGrow={1} |
||||
error={emailError} |
||||
value={email} |
||||
addon={<Icon name='mail' size='x20' />} |
||||
onChange={handleEmail} |
||||
placeholder={t('Email')} |
||||
/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Box display='flex' data-qa='DepartmentEditToggle-ShowOnOfflinePage' flexDirection='row'> |
||||
<Field.Label>{t('Show_on_offline_page')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch flexGrow={1} checked={showOnOfflineForm} onChange={handleShowOnOfflineForm} /> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Livechat_DepartmentOfflineMessageToChannel')}</Field.Label> |
||||
<Field.Row> |
||||
<PaginatedSelectFiltered |
||||
data-qa='DepartmentSelect-LivechatDepartmentOfflineMessageToChannel' |
||||
value={offlineMessageChannelName} |
||||
onChange={handleOfflineMessageChannelName} |
||||
flexShrink={0} |
||||
filter={offlineMessageChannelName} |
||||
setFilter={handleOfflineMessageChannelName} |
||||
options={roomsItems} |
||||
placeholder={t('Channel_name')} |
||||
endReached={roomsPhase === AsyncStatePhase.LOADING ? () => {} : (start) => loadMoreRooms(start, Math.min(50, roomsTotal))} |
||||
/> |
||||
</Field.Row> |
||||
</Field> |
||||
{MaxChats && ( |
||||
<Field> |
||||
<MaxChats |
||||
value={maxNumberSimultaneousChat} |
||||
handler={handleMaxNumberSimultaneousChat} |
||||
label={'Max_number_of_chats_per_agent'} |
||||
placeholder='Max_number_of_chats_per_agent_description' |
||||
/> |
||||
</Field> |
||||
)} |
||||
{VisitorInactivity && ( |
||||
<Field> |
||||
<VisitorInactivity |
||||
value={visitorInactivityTimeoutInSeconds} |
||||
handler={handleVisitorInactivityTimeoutInSeconds} |
||||
label={'How_long_to_wait_to_consider_visitor_abandonment_in_seconds'} |
||||
placeholder='Number_in_seconds' |
||||
/> |
||||
</Field> |
||||
)} |
||||
{AbandonedMessageInput && ( |
||||
<Field> |
||||
<AbandonedMessageInput |
||||
value={abandonedRoomsCloseCustomMessage} |
||||
handler={handleAbandonedRoomsCloseCustomMessage} |
||||
label={'Livechat_abandoned_rooms_closed_custom_message'} |
||||
placeholder='Enter_a_custom_message' |
||||
/> |
||||
</Field> |
||||
)} |
||||
{WaitingQueueMessageInput && ( |
||||
<Field> |
||||
<WaitingQueueMessageInput value={waitingQueueMessage} handler={handleWaitingQueueMessage} label={'Waiting_queue_message'} /> |
||||
</Field> |
||||
)} |
||||
{DepartmentForwarding && ( |
||||
<Field> |
||||
<DepartmentForwarding |
||||
departmentId={id} |
||||
value={departmentsAllowedToForward} |
||||
handler={handleDepartmentsAllowedToForward} |
||||
label={'List_of_departments_for_forward'} |
||||
placeholder='Enter_a_department_name' |
||||
/> |
||||
</Field> |
||||
)} |
||||
{AutoCompleteDepartment && ( |
||||
<Field> |
||||
<Field.Label>{t('Fallback_forward_department')}</Field.Label> |
||||
<AutoCompleteDepartment |
||||
haveNone |
||||
excludeDepartmentId={department?._id} |
||||
value={fallbackForwardDepartment} |
||||
onChange={handleFallbackForwardDepartment} |
||||
placeholder={t('Fallback_forward_department')} |
||||
label={t('Fallback_forward_department')} |
||||
onlyMyDepartments |
||||
showArchived |
||||
/> |
||||
</Field> |
||||
)} |
||||
<Field> |
||||
<Box display='flex' data-qa='DiscussionToggle-RequestTagBeforeCLosingChat' flexDirection='row'> |
||||
<Field.Label>{t('Request_tag_before_closing_chat')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch |
||||
data-qa='DiscussionToggle-RequestTagBeforeCLosingChat' |
||||
flexGrow={1} |
||||
checked={requestTagBeforeClosingChat} |
||||
onChange={handleRequestTagBeforeClosingChat} |
||||
/> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
{requestTagBeforeClosingChat && ( |
||||
<Field> |
||||
<Field.Label alignSelf='stretch'>{t('Conversation_closing_tags')}*</Field.Label> |
||||
<Field.Row> |
||||
<TextInput |
||||
data-qa='DepartmentEditTextInput-ConversationClosingTags' |
||||
error={tagError} |
||||
value={tagsText} |
||||
onChange={handleTagTextChange} |
||||
placeholder={t('Enter_a_tag')} |
||||
/> |
||||
<Button |
||||
disabled={Boolean(!tagsText.trim()) || tags.includes(tagsText)} |
||||
data-qa='DepartmentEditAddButton-ConversationClosingTags' |
||||
mis='x8' |
||||
title={t('add')} |
||||
onClick={handleTagTextSubmit} |
||||
> |
||||
{t('Add')} |
||||
</Button> |
||||
</Field.Row> |
||||
<Field.Hint>{t('Conversation_closing_tags_description')}</Field.Hint> |
||||
{tags?.length > 0 && ( |
||||
<Field.Row justifyContent='flex-start'> |
||||
{tags.map((tag, i) => ( |
||||
<Chip key={i} onClick={handleTagChipClick(tag)} mie='x8'> |
||||
{tag} |
||||
</Chip> |
||||
))} |
||||
</Field.Row> |
||||
)} |
||||
</Field> |
||||
)} |
||||
{DepartmentBusinessHours && ( |
||||
<Field> |
||||
<DepartmentBusinessHours bhId={department?.businessHourId} /> |
||||
</Field> |
||||
)} |
||||
<Divider mb='x16' /> |
||||
<Field> |
||||
<Field.Label mb='x4'>{t('Agents')}:</Field.Label> |
||||
<Box display='flex' flexDirection='column' height='50vh'> |
||||
<DepartmentsAgentsTable |
||||
agents={agents} |
||||
setAgentListFinal={setAgentList} |
||||
setAgentsAdded={setAgentsAdded} |
||||
setAgentsRemoved={setAgentsRemoved} |
||||
/> |
||||
</Box> |
||||
</Field> |
||||
</FieldGroup> |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page> |
||||
</Page> |
||||
); |
||||
} |
||||
|
||||
export default EditDepartment; |
||||
@ -0,0 +1,487 @@ |
||||
import type { ILivechatDepartment, ILivechatDepartmentAgents, Serialized } from '@rocket.chat/core-typings'; |
||||
import { |
||||
FieldGroup, |
||||
Field, |
||||
TextInput, |
||||
Box, |
||||
Icon, |
||||
Divider, |
||||
ToggleSwitch, |
||||
TextAreaInput, |
||||
ButtonGroup, |
||||
Button, |
||||
PaginatedSelectFiltered, |
||||
} from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; |
||||
import { useToastMessageDispatch, useRoute, useMethod, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React, { useMemo } from 'react'; |
||||
import { Controller, useForm } from 'react-hook-form'; |
||||
|
||||
import { validateEmail } from '../../../../lib/emailValidator'; |
||||
import Page from '../../../components/Page'; |
||||
import { useRecordList } from '../../../hooks/lists/useRecordList'; |
||||
import { useRoomsList } from '../../../hooks/useRoomsList'; |
||||
import { AsyncStatePhase } from '../../../lib/asyncState'; |
||||
import { useFormsSubscription } from '../additionalForms'; |
||||
import DepartmentsAgentsTable from './DepartmentAgentsTable/DepartmentAgentsTable'; |
||||
import { DepartmentTags } from './DepartmentTags'; |
||||
|
||||
export type EditDepartmentProps = { |
||||
id?: string; |
||||
title: string; |
||||
data?: Serialized<{ |
||||
department: ILivechatDepartment | null; |
||||
agents?: ILivechatDepartmentAgents[]; |
||||
}>; |
||||
allowedToForwardData?: Serialized<{ |
||||
departments: ILivechatDepartment[]; |
||||
}>; |
||||
}; |
||||
|
||||
type InitialValueParams = { |
||||
department?: Serialized<ILivechatDepartment> | null; |
||||
agents?: Serialized<ILivechatDepartmentAgents>[]; |
||||
allowedToForwardData?: EditDepartmentProps['allowedToForwardData']; |
||||
}; |
||||
|
||||
export type IDepartmentAgent = Pick<ILivechatDepartmentAgents, 'agentId' | 'username' | 'count' | 'order'> & { |
||||
_id?: string; |
||||
name?: string; |
||||
}; |
||||
|
||||
export type FormValues = { |
||||
name: string; |
||||
email: string; |
||||
description: string; |
||||
enabled: boolean; |
||||
maxNumberSimultaneousChat: number; |
||||
showOnRegistration: boolean; |
||||
showOnOfflineForm: boolean; |
||||
abandonedRoomsCloseCustomMessage: string; |
||||
requestTagBeforeClosingChat: boolean; |
||||
offlineMessageChannelName: string; |
||||
visitorInactivityTimeoutInSeconds: number; |
||||
waitingQueueMessage: string; |
||||
departmentsAllowedToForward: { label: string; value: string }[]; |
||||
fallbackForwardDepartment: string; |
||||
agentList: IDepartmentAgent[]; |
||||
chatClosingTags: string[]; |
||||
}; |
||||
|
||||
function withDefault<T>(key: T | undefined | null, defaultValue: T) { |
||||
return key || defaultValue; |
||||
} |
||||
|
||||
const getInitialValues = ({ department, agents, allowedToForwardData }: InitialValueParams) => ({ |
||||
name: withDefault(department?.name, ''), |
||||
email: withDefault(department?.email, ''), |
||||
description: withDefault(department?.description, ''), |
||||
enabled: !!department?.enabled, |
||||
maxNumberSimultaneousChat: department?.maxNumberSimultaneousChat, |
||||
showOnRegistration: !!department?.showOnRegistration, |
||||
showOnOfflineForm: !!department?.showOnOfflineForm, |
||||
abandonedRoomsCloseCustomMessage: withDefault(department?.abandonedRoomsCloseCustomMessage, ''), |
||||
requestTagBeforeClosingChat: !!department?.requestTagBeforeClosingChat, |
||||
offlineMessageChannelName: withDefault(department?.offlineMessageChannelName, ''), |
||||
visitorInactivityTimeoutInSeconds: department?.visitorInactivityTimeoutInSeconds, |
||||
waitingQueueMessage: withDefault(department?.waitingQueueMessage, ''), |
||||
departmentsAllowedToForward: allowedToForwardData?.departments?.map((dep) => ({ label: dep.name, value: dep._id })) || [], |
||||
fallbackForwardDepartment: withDefault(department?.fallbackForwardDepartment, ''), |
||||
chatClosingTags: department?.chatClosingTags ?? [], |
||||
agentList: agents || [], |
||||
}); |
||||
|
||||
function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmentProps) { |
||||
const t = useTranslation(); |
||||
const departmentsRoute = useRoute('omnichannel-departments'); |
||||
|
||||
const { |
||||
useEeNumberInput = () => null, |
||||
useEeTextInput = () => null, |
||||
useEeTextAreaInput = () => null, |
||||
useDepartmentForwarding = () => null, |
||||
useDepartmentBusinessHours = () => null, |
||||
useSelectForwardDepartment = () => null, |
||||
} = useFormsSubscription(); |
||||
|
||||
const { department, agents = [] } = data || {}; |
||||
|
||||
const MaxChats = useEeNumberInput(); |
||||
const VisitorInactivity = useEeNumberInput(); |
||||
const WaitingQueueMessageInput = useEeTextAreaInput(); |
||||
const AbandonedMessageInput = useEeTextInput(); |
||||
const DepartmentForwarding = useDepartmentForwarding(); |
||||
const DepartmentBusinessHours = useDepartmentBusinessHours(); |
||||
const AutoCompleteDepartment = useSelectForwardDepartment(); |
||||
|
||||
const initialValues = getInitialValues({ department, agents, allowedToForwardData }); |
||||
|
||||
const { |
||||
register, |
||||
control, |
||||
handleSubmit, |
||||
watch, |
||||
formState: { errors, isValid, isDirty }, |
||||
} = useForm<FormValues>({ mode: 'onChange', defaultValues: initialValues }); |
||||
|
||||
const requestTagBeforeClosingChat = watch('requestTagBeforeClosingChat'); |
||||
const offlineMessageChannelName = watch('offlineMessageChannelName'); |
||||
|
||||
const { itemsList: RoomsList, loadMoreItems: loadMoreRooms } = useRoomsList( |
||||
useMemo(() => ({ text: offlineMessageChannelName }), [offlineMessageChannelName]), |
||||
); |
||||
|
||||
const { phase: roomsPhase, items: roomsItems, itemCount: roomsTotal } = useRecordList(RoomsList); |
||||
|
||||
const saveDepartmentInfo = useMethod('livechat:saveDepartment'); |
||||
const saveDepartmentAgentsInfoOnEdit = useEndpoint('POST', `/v1/livechat/department/:_id/agents`, { _id: id || '' }); |
||||
|
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const handleSave = useMutableCallback(async (data: FormValues) => { |
||||
const { |
||||
agentList, |
||||
enabled, |
||||
name, |
||||
description, |
||||
showOnRegistration, |
||||
showOnOfflineForm, |
||||
email, |
||||
chatClosingTags, |
||||
offlineMessageChannelName, |
||||
maxNumberSimultaneousChat, |
||||
visitorInactivityTimeoutInSeconds, |
||||
abandonedRoomsCloseCustomMessage, |
||||
waitingQueueMessage, |
||||
departmentsAllowedToForward, |
||||
fallbackForwardDepartment, |
||||
} = data; |
||||
|
||||
const payload = { |
||||
enabled, |
||||
name, |
||||
description, |
||||
showOnRegistration, |
||||
showOnOfflineForm, |
||||
requestTagBeforeClosingChat, |
||||
email, |
||||
chatClosingTags, |
||||
offlineMessageChannelName, |
||||
maxNumberSimultaneousChat, |
||||
visitorInactivityTimeoutInSeconds, |
||||
abandonedRoomsCloseCustomMessage, |
||||
waitingQueueMessage, |
||||
departmentsAllowedToForward: departmentsAllowedToForward?.map((dep) => dep.value), |
||||
fallbackForwardDepartment, |
||||
}; |
||||
|
||||
try { |
||||
if (id) { |
||||
const { agentList: initialAgentList } = initialValues; |
||||
|
||||
const agentListPayload = { |
||||
upsert: agentList.filter( |
||||
(agent) => |
||||
!initialAgentList.some( |
||||
(initialAgent) => |
||||
initialAgent._id === agent._id && agent.count === initialAgent.count && agent.order === initialAgent.order, |
||||
), |
||||
), |
||||
remove: initialAgentList.filter((initialAgent) => !agentList.some((agent) => initialAgent._id === agent._id)), |
||||
}; |
||||
|
||||
await saveDepartmentInfo(id, payload, []); |
||||
if (agentListPayload.upsert.length > 0 || agentListPayload.remove.length > 0) { |
||||
await saveDepartmentAgentsInfoOnEdit(agentListPayload); |
||||
} |
||||
} else { |
||||
await saveDepartmentInfo(id ?? null, payload, agentList); |
||||
} |
||||
dispatchToastMessage({ type: 'success', message: t('Saved') }); |
||||
departmentsRoute.push({}); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}); |
||||
|
||||
const handleReturn = useMutableCallback(() => { |
||||
departmentsRoute.push({}); |
||||
}); |
||||
|
||||
const isFormValid = isValid && isDirty; |
||||
|
||||
const formId = useUniqueId(); |
||||
|
||||
return ( |
||||
<Page flexDirection='row'> |
||||
<Page> |
||||
<Page.Header title={title}> |
||||
<ButtonGroup> |
||||
<Button onClick={handleReturn}> |
||||
<Icon name='back' /> {t('Back')} |
||||
</Button> |
||||
<Button type='submit' form={formId} primary disabled={!isFormValid}> |
||||
{t('Save')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
</Page.Header> |
||||
<Page.ScrollableContentWithShadow> |
||||
<FieldGroup |
||||
w='full' |
||||
alignSelf='center' |
||||
maxWidth='x600' |
||||
id={formId} |
||||
is='form' |
||||
autoComplete='off' |
||||
onSubmit={handleSubmit(handleSave)} |
||||
> |
||||
<Field> |
||||
<Box display='flex' data-qa='DepartmentEditToggle-Enabled' flexDirection='row'> |
||||
<Field.Label>{t('Enabled')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch flexGrow={1} {...register('enabled')} /> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
|
||||
<Field> |
||||
<Field.Label>{t('Name')}*</Field.Label> |
||||
<Field.Row> |
||||
<TextInput |
||||
data-qa='DepartmentEditTextInput-Name' |
||||
flexGrow={1} |
||||
error={errors.name?.message as string} |
||||
placeholder={t('Name')} |
||||
{...register('name', { required: t('The_field_is_required', 'name') })} |
||||
/> |
||||
</Field.Row> |
||||
{errors.name && <Field.Error>{errors.name?.message}</Field.Error>} |
||||
</Field> |
||||
|
||||
<Field> |
||||
<Field.Label>{t('Description')}</Field.Label> |
||||
<Field.Row> |
||||
<TextAreaInput |
||||
data-qa='DepartmentEditTextInput-Description' |
||||
flexGrow={1} |
||||
placeholder={t('Description')} |
||||
{...register('description')} |
||||
/> |
||||
</Field.Row> |
||||
</Field> |
||||
|
||||
<Field> |
||||
<Box data-qa='DepartmentEditToggle-ShowOnRegistrationPage' display='flex' flexDirection='row'> |
||||
<Field.Label>{t('Show_on_registration_page')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch flexGrow={1} {...register('showOnRegistration')} /> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
|
||||
<Field> |
||||
<Field.Label>{t('Email')}*</Field.Label> |
||||
<Field.Row> |
||||
<TextInput |
||||
data-qa='DepartmentEditTextInput-Email' |
||||
flexGrow={1} |
||||
error={errors.email?.message as string} |
||||
addon={<Icon name='mail' size='x20' />} |
||||
placeholder={t('Email')} |
||||
{...register('email', { |
||||
required: t('The_field_is_required', 'email'), |
||||
validate: (email) => validateEmail(email) || t('error-invalid-email-address'), |
||||
})} |
||||
/> |
||||
</Field.Row> |
||||
{errors.email && <Field.Error>{errors.email?.message}</Field.Error>} |
||||
</Field> |
||||
|
||||
<Field> |
||||
<Box display='flex' data-qa='DepartmentEditToggle-ShowOnOfflinePage' flexDirection='row'> |
||||
<Field.Label>{t('Show_on_offline_page')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch flexGrow={1} {...register('showOnOfflineForm')} /> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
|
||||
<Field> |
||||
<Field.Label>{t('Livechat_DepartmentOfflineMessageToChannel')}</Field.Label> |
||||
<Field.Row> |
||||
<Controller |
||||
control={control} |
||||
name='offlineMessageChannelName' |
||||
render={({ field: { value, onChange } }) => ( |
||||
<PaginatedSelectFiltered |
||||
data-qa='DepartmentSelect-LivechatDepartmentOfflineMessageToChannel' |
||||
value={value} |
||||
onChange={onChange} |
||||
flexShrink={0} |
||||
filter={value} |
||||
setFilter={onChange} |
||||
options={roomsItems} |
||||
placeholder={t('Channel_name')} |
||||
endReached={ |
||||
roomsPhase === AsyncStatePhase.LOADING ? () => undefined : (start) => loadMoreRooms(start, Math.min(50, roomsTotal)) |
||||
} |
||||
/> |
||||
)} |
||||
/> |
||||
</Field.Row> |
||||
</Field> |
||||
|
||||
{MaxChats && ( |
||||
<Field> |
||||
<Controller |
||||
control={control} |
||||
name='maxNumberSimultaneousChat' |
||||
render={({ field: { value, onChange } }) => ( |
||||
<MaxChats |
||||
value={value} |
||||
handler={onChange} |
||||
label={'Max_number_of_chats_per_agent'} |
||||
placeholder='Max_number_of_chats_per_agent_description' |
||||
/> |
||||
)} |
||||
/> |
||||
</Field> |
||||
)} |
||||
|
||||
{VisitorInactivity && ( |
||||
<Field> |
||||
<Controller |
||||
control={control} |
||||
name='visitorInactivityTimeoutInSeconds' |
||||
render={({ field: { value, onChange } }) => ( |
||||
<VisitorInactivity |
||||
value={value} |
||||
handler={onChange} |
||||
label={'How_long_to_wait_to_consider_visitor_abandonment_in_seconds'} |
||||
placeholder='Number_in_seconds' |
||||
/> |
||||
)} |
||||
/> |
||||
</Field> |
||||
)} |
||||
|
||||
{AbandonedMessageInput && ( |
||||
<Field> |
||||
<Controller |
||||
control={control} |
||||
name='abandonedRoomsCloseCustomMessage' |
||||
render={({ field: { value, onChange } }) => ( |
||||
<AbandonedMessageInput |
||||
value={value} |
||||
handler={onChange} |
||||
label={'Livechat_abandoned_rooms_closed_custom_message'} |
||||
placeholder='Enter_a_custom_message' |
||||
/> |
||||
)} |
||||
/> |
||||
</Field> |
||||
)} |
||||
|
||||
{WaitingQueueMessageInput && ( |
||||
<Field> |
||||
<Controller |
||||
control={control} |
||||
name='waitingQueueMessage' |
||||
render={({ field: { value, onChange } }) => ( |
||||
<WaitingQueueMessageInput |
||||
value={value} |
||||
handler={onChange} |
||||
label={'Waiting_queue_message'} |
||||
placeholder={'Waiting_queue_message'} |
||||
/> |
||||
)} |
||||
/> |
||||
</Field> |
||||
)} |
||||
|
||||
{DepartmentForwarding && ( |
||||
<Field> |
||||
<Controller |
||||
control={control} |
||||
name='departmentsAllowedToForward' |
||||
render={({ field: { value, onChange } }) => ( |
||||
<DepartmentForwarding |
||||
departmentId={id ?? ''} |
||||
value={value} |
||||
handler={onChange} |
||||
label={'List_of_departments_for_forward'} |
||||
/> |
||||
)} |
||||
/> |
||||
</Field> |
||||
)} |
||||
|
||||
{AutoCompleteDepartment && ( |
||||
<Field> |
||||
<Field.Label>{t('Fallback_forward_department')}</Field.Label> |
||||
<Controller |
||||
control={control} |
||||
name='fallbackForwardDepartment' |
||||
render={({ field: { value, onChange } }) => ( |
||||
<AutoCompleteDepartment |
||||
haveNone |
||||
excludeDepartmentId={department?._id} |
||||
value={value} |
||||
onChange={onChange} |
||||
onlyMyDepartments |
||||
showArchived |
||||
/> |
||||
)} |
||||
/> |
||||
</Field> |
||||
)} |
||||
|
||||
<Field> |
||||
<Box display='flex' data-qa='DiscussionToggle-RequestTagBeforeCLosingChat' flexDirection='row'> |
||||
<Field.Label>{t('Request_tag_before_closing_chat')}</Field.Label> |
||||
<Field.Row> |
||||
<ToggleSwitch |
||||
data-qa='DiscussionToggle-RequestTagBeforeCLosingChat' |
||||
flexGrow={1} |
||||
{...register('requestTagBeforeClosingChat')} |
||||
/> |
||||
</Field.Row> |
||||
</Box> |
||||
</Field> |
||||
|
||||
{requestTagBeforeClosingChat && ( |
||||
<Field> |
||||
<Field.Label alignSelf='stretch'>{t('Conversation_closing_tags')}*</Field.Label> |
||||
<Controller |
||||
control={control} |
||||
name='chatClosingTags' |
||||
rules={{ required: t('The_field_is_required', 'tags') }} |
||||
render={({ field: { value, onChange } }) => ( |
||||
<DepartmentTags value={value} onChange={onChange} error={errors.chatClosingTags?.message as string} /> |
||||
)} |
||||
/> |
||||
{errors.chatClosingTags && <Field.Error>{errors.chatClosingTags?.message}</Field.Error>} |
||||
</Field> |
||||
)} |
||||
|
||||
{DepartmentBusinessHours && ( |
||||
<Field> |
||||
<DepartmentBusinessHours bhId={department?.businessHourId} /> |
||||
</Field> |
||||
)} |
||||
|
||||
<Divider mb='x16' /> |
||||
<Field> |
||||
<Field.Label mb='x4'>{t('Agents')}:</Field.Label> |
||||
<Box display='flex' flexDirection='column' height='50vh'> |
||||
<DepartmentsAgentsTable control={control} register={register} /> |
||||
</Box> |
||||
</Field> |
||||
</FieldGroup> |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page> |
||||
</Page> |
||||
); |
||||
} |
||||
|
||||
export default EditDepartment; |
||||
@ -1,36 +0,0 @@ |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React, { useMemo } from 'react'; |
||||
|
||||
import { FormSkeleton } from '../../../components/Skeleton'; |
||||
import { AsyncStatePhase } from '../../../hooks/useAsyncState'; |
||||
import { useEndpointData } from '../../../hooks/useEndpointData'; |
||||
import EditDepartment from './EditDepartment'; |
||||
|
||||
function EditDepartmentWithAllowedForwardData({ data, ...props }) { |
||||
const t = useTranslation(); |
||||
|
||||
const { |
||||
value: allowedToForwardData, |
||||
phase: allowedToForwardState, |
||||
error: allowedToForwardError, |
||||
} = useEndpointData('/v1/livechat/department.listByIds', { |
||||
params: useMemo( |
||||
() => ({ |
||||
ids: data?.department?.departmentsAllowedToForward ?? [], |
||||
}), |
||||
[data], |
||||
), |
||||
}); |
||||
|
||||
if ([allowedToForwardState].includes(AsyncStatePhase.LOADING)) { |
||||
return <FormSkeleton />; |
||||
} |
||||
|
||||
if (allowedToForwardError) { |
||||
return <Box mbs='x16'>{t('Not_Available')}</Box>; |
||||
} |
||||
return <EditDepartment data={data} allowedToForwardData={allowedToForwardData} {...props} />; |
||||
} |
||||
|
||||
export default EditDepartmentWithAllowedForwardData; |
||||
@ -0,0 +1,35 @@ |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import { useQuery } from '@tanstack/react-query'; |
||||
import React from 'react'; |
||||
|
||||
import { FormSkeleton } from '../../../components/Skeleton'; |
||||
import type { EditDepartmentProps } from './EditDepartment'; |
||||
import EditDepartment from './EditDepartment'; |
||||
|
||||
const EditDepartmentWithAllowedForwardData = ({ data, ...props }: Omit<EditDepartmentProps, 'allowedToForwardData'>) => { |
||||
const t = useTranslation(); |
||||
const getDepartmentListByIds = useEndpoint('GET', '/v1/livechat/department.listByIds'); |
||||
|
||||
const { |
||||
data: allowedToForwardData, |
||||
isInitialLoading, |
||||
isError, |
||||
} = useQuery(['/v1/livechat/department.listByIds', data?.department?.departmentsAllowedToForward], () => |
||||
getDepartmentListByIds({ |
||||
ids: data?.department?.departmentsAllowedToForward ?? [], |
||||
}), |
||||
); |
||||
|
||||
if (isInitialLoading) { |
||||
return <FormSkeleton />; |
||||
} |
||||
|
||||
if (isError) { |
||||
return <Box mbs='x16'>{t('Not_Available')}</Box>; |
||||
} |
||||
|
||||
return <EditDepartment data={data} allowedToForwardData={allowedToForwardData} {...props} />; |
||||
}; |
||||
|
||||
export default EditDepartmentWithAllowedForwardData; |
||||
@ -1,39 +0,0 @@ |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React from 'react'; |
||||
|
||||
import { FormSkeleton } from '../../../components/Skeleton'; |
||||
import { AsyncStatePhase } from '../../../hooks/useAsyncState'; |
||||
import { useEndpointData } from '../../../hooks/useEndpointData'; |
||||
import EditDepartment from './EditDepartment'; |
||||
import EditDepartmentWithAllowedForwardData from './EditDepartmentWithAllowedForwardData'; |
||||
|
||||
const params = { onlyMyDepartments: true }; |
||||
function EditDepartmentWithData({ id, title }) { |
||||
const t = useTranslation(); |
||||
const { value: data, phase: state, error } = useEndpointData('/v1/livechat/department/:_id', { keys: { _id: id }, params }); |
||||
|
||||
if ([state].includes(AsyncStatePhase.LOADING)) { |
||||
return <FormSkeleton />; |
||||
} |
||||
|
||||
if (error || (id && !data?.department)) { |
||||
return <Box mbs={16}>{t('Department_not_found')}</Box>; |
||||
} |
||||
|
||||
if (data.department.archived === true) { |
||||
return <Box mbs={16}>{t('Department_archived')}</Box>; |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
{data && data.department && data.department.departmentsAllowedToForward && data.department.departmentsAllowedToForward.length > 0 ? ( |
||||
<EditDepartmentWithAllowedForwardData id={id} data={data} title={title} /> |
||||
) : ( |
||||
<EditDepartment id={id} data={data} title={title} /> |
||||
)} |
||||
</> |
||||
); |
||||
} |
||||
|
||||
export default EditDepartmentWithData; |
||||
@ -0,0 +1,45 @@ |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import { useQuery } from '@tanstack/react-query'; |
||||
import React from 'react'; |
||||
|
||||
import { FormSkeleton } from '../../../components/Skeleton'; |
||||
import EditDepartment from './EditDepartment'; |
||||
import EditDepartmentWithAllowedForwardData from './EditDepartmentWithAllowedForwardData'; |
||||
|
||||
const params = { onlyMyDepartments: 'true' } as const; |
||||
|
||||
type EditDepartmentWithDataProps = { |
||||
id?: string; |
||||
title: string; |
||||
}; |
||||
|
||||
const EditDepartmentWithData = ({ id, title }: EditDepartmentWithDataProps) => { |
||||
const t = useTranslation(); |
||||
const getDepartment = useEndpoint('GET', '/v1/livechat/department/:_id', { _id: id ?? '' }); |
||||
const { data, isInitialLoading, isError } = useQuery(['/v1/livechat/department/:_id'], () => getDepartment(params), { enabled: !!id }); |
||||
|
||||
if (isInitialLoading) { |
||||
return <FormSkeleton />; |
||||
} |
||||
|
||||
if (isError || (id && !data?.department)) { |
||||
return <Box mbs={16}>{t('Department_not_found')}</Box>; |
||||
} |
||||
|
||||
if (data?.department?.archived === true) { |
||||
return <Box mbs={16}>{t('Department_archived')}</Box>; |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
{data?.department?.departmentsAllowedToForward && data.department.departmentsAllowedToForward.length > 0 ? ( |
||||
<EditDepartmentWithAllowedForwardData id={id} data={data} title={title} /> |
||||
) : ( |
||||
<EditDepartment id={id} data={data} title={title} /> |
||||
)} |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default EditDepartmentWithData; |
||||
@ -1,30 +0,0 @@ |
||||
import { Box, NumberInput } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React, { useState } from 'react'; |
||||
|
||||
function Order({ agentId, setAgentList, agentList }) { |
||||
const t = useTranslation(); |
||||
const [agentOrder, setAgentOrder] = useState(agentList.find((agent) => agent.agentId === agentId).order || 0); |
||||
|
||||
const handleOrder = useMutableCallback(async (e) => { |
||||
const orderValue = Number(e.currentTarget.value); |
||||
setAgentOrder(orderValue); |
||||
setAgentList( |
||||
agentList.map((agent) => { |
||||
if (agent.agentId === agentId) { |
||||
agent.order = orderValue; |
||||
} |
||||
return agent; |
||||
}), |
||||
); |
||||
}); |
||||
|
||||
return ( |
||||
<Box display='flex'> |
||||
<NumberInput flexShrink={1} key={`${agentId}-order`} title={t('Order')} value={agentOrder} onChange={handleOrder} /> |
||||
</Box> |
||||
); |
||||
} |
||||
|
||||
export default Order; |
||||
@ -1,23 +0,0 @@ |
||||
import { Field, TextInput } from '@rocket.chat/fuselage'; |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React, { useMemo } from 'react'; |
||||
|
||||
import { useEndpointData } from '../../../../client/hooks/useEndpointData'; |
||||
|
||||
export const DepartmentBusinessHours = ({ bhId }) => { |
||||
const t = useTranslation(); |
||||
const { value: data } = useEndpointData('/v1/livechat/business-hour', { params: useMemo(() => ({ _id: bhId, type: 'custom' }), [bhId]) }); |
||||
|
||||
const name = data && data.businessHour && data.businessHour.name; |
||||
|
||||
return ( |
||||
<Field> |
||||
<Field.Label>{t('Business_Hour')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput disabled value={name || ''} /> |
||||
</Field.Row> |
||||
</Field> |
||||
); |
||||
}; |
||||
|
||||
export default DepartmentBusinessHours; |
||||
@ -0,0 +1,23 @@ |
||||
import { Field, TextInput } from '@rocket.chat/fuselage'; |
||||
import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import { useQuery } from '@tanstack/react-query'; |
||||
import React from 'react'; |
||||
|
||||
export const DepartmentBusinessHours = ({ bhId }: { bhId: string | undefined }) => { |
||||
const t = useTranslation(); |
||||
const getBusinessHour = useEndpoint('GET', '/v1/livechat/business-hour'); |
||||
const { data } = useQuery(['/v1/livechat/business-hour', bhId], () => getBusinessHour({ _id: bhId, type: 'custom' })); |
||||
|
||||
const name = data?.businessHour?.name; |
||||
|
||||
return ( |
||||
<Field> |
||||
<Field.Label>{t('Business_Hour')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput disabled value={name || ''} /> |
||||
</Field.Row> |
||||
</Field> |
||||
); |
||||
}; |
||||
|
||||
export default DepartmentBusinessHours; |
||||
Loading…
Reference in new issue