snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Code scanning - action / CodeQL-Build (push) Failing after 1s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Code scanning - action / CodeQL-Build (push) Failing after 1s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { Box, Button } from '@rocket.chat/fuselage';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { useToastMessageDispatch } from '@rocket.chat/ui-contexts';
|
||||
import type { AriaAttributes } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useEndpointMutation } from '../../../../hooks/useEndpointMutation';
|
||||
import AutoCompleteAgent from '../../components/AutoCompleteAgent';
|
||||
import type { IDepartmentAgent } from '../definitions';
|
||||
|
||||
export type AddAgentProps = Pick<AriaAttributes, 'aria-labelledby'> & {
|
||||
agentList: IDepartmentAgent[];
|
||||
onAdd: (agent: IDepartmentAgent) => void;
|
||||
};
|
||||
|
||||
function AddAgent({ agentList, onAdd, 'aria-labelledby': ariaLabelledBy }: AddAgentProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [userId, setUserId] = useState('');
|
||||
|
||||
const { mutateAsync: getAgent } = useEndpointMutation('GET', '/v1/livechat/users/agent/:_id', { keys: { _id: userId } });
|
||||
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
|
||||
const handleAgent = useStableCallback((e: string) => setUserId(e));
|
||||
|
||||
const handleSave = useStableCallback(async () => {
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
user: { _id, username, name },
|
||||
} = await getAgent();
|
||||
|
||||
if (!agentList.some(({ agentId }) => agentId === _id)) {
|
||||
setUserId('');
|
||||
onAdd({ agentId: _id, username: username ?? '', name, count: 0, order: 0 });
|
||||
} else {
|
||||
dispatchToastMessage({ type: 'error', message: t('This_agent_was_already_selected') });
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box role='group' aria-labelledby={ariaLabelledBy} display='flex' alignItems='center'>
|
||||
<AutoCompleteAgent value={userId} onChange={handleAgent} />
|
||||
<Button disabled={!userId} onClick={handleSave} marginInlineStart={8} primary>
|
||||
{t('Add')}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddAgent;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Box } from '@rocket.chat/fuselage';
|
||||
import { useMediaQuery } from '@rocket.chat/fuselage-hooks';
|
||||
import { UserAvatar } from '@rocket.chat/ui-avatar';
|
||||
import { memo } from 'react';
|
||||
|
||||
export type AgentAvatarProps = { name: string; username: string; eTag?: string };
|
||||
|
||||
const AgentAvatar = ({ name, username, eTag }: AgentAvatarProps) => {
|
||||
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 marginInline={8}>
|
||||
<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,38 @@
|
||||
import { NumberInput } from '@rocket.chat/fuselage';
|
||||
import { GenericTableRow, GenericTableCell } from '@rocket.chat/ui-client';
|
||||
import { memo } from 'react';
|
||||
import type { UseFormRegister } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { EditDepartmentFormData, IDepartmentAgent } from '../definitions';
|
||||
import AgentAvatar from './AgentAvatar';
|
||||
import RemoveAgentButton from './RemoveAgentButton';
|
||||
|
||||
export type AgentRowProps = {
|
||||
agent: IDepartmentAgent;
|
||||
index: number;
|
||||
register: UseFormRegister<EditDepartmentFormData>;
|
||||
onRemove: (agentId: string) => void;
|
||||
};
|
||||
|
||||
const AgentRow = ({ index, agent, register, onRemove }: AgentRowProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<GenericTableRow key={agent.agentId} tabIndex={0} action>
|
||||
<GenericTableCell withTruncatedText>
|
||||
<AgentAvatar name={agent.name || ''} username={agent.username || ''} />
|
||||
</GenericTableCell>
|
||||
<GenericTableCell fontScale='p2' color='hint' withTruncatedText>
|
||||
<NumberInput title={t('Count')} maxWidth='100%' {...register(`agentList.${index}.count`, { valueAsNumber: true })} />
|
||||
</GenericTableCell>
|
||||
<GenericTableCell fontScale='p2' color='hint' withTruncatedText>
|
||||
<NumberInput title={t('Order')} maxWidth='100%' {...register(`agentList.${index}.order`, { valueAsNumber: true })} />
|
||||
</GenericTableCell>
|
||||
<GenericTableCell fontScale='p2' color='hint'>
|
||||
<RemoveAgentButton agentId={agent.agentId} onRemove={onRemove} />
|
||||
</GenericTableCell>
|
||||
</GenericTableRow>
|
||||
);
|
||||
};
|
||||
export default memo(AgentRow);
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Pagination } from '@rocket.chat/fuselage';
|
||||
import { GenericTable, GenericTableBody, GenericTableHeader, GenericTableHeaderCell, usePagination } from '@rocket.chat/ui-client';
|
||||
import type { AriaAttributes } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import type { Control, UseFormRegister } from 'react-hook-form';
|
||||
import { useWatch, useFieldArray } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { EditDepartmentFormData } from '../definitions';
|
||||
import AddAgent from './AddAgent';
|
||||
import AgentRow from './AgentRow';
|
||||
|
||||
export type DepartmentAgentsTableProps = Pick<AriaAttributes, 'aria-labelledby'> & {
|
||||
control: Control<EditDepartmentFormData>;
|
||||
register: UseFormRegister<EditDepartmentFormData>;
|
||||
};
|
||||
|
||||
function DepartmentAgentsTable({ control, register, 'aria-labelledby': ariaLabelledBy }: DepartmentAgentsTableProps) {
|
||||
const { t } = useTranslation();
|
||||
const { fields, append, remove } = useFieldArray({ control, name: 'agentList' });
|
||||
const agentList = useWatch({ control, name: 'agentList' });
|
||||
|
||||
const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination();
|
||||
const page = useMemo(() => fields.slice(current, current + itemsPerPage), [current, fields, itemsPerPage]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AddAgent aria-labelledby={ariaLabelledBy} agentList={agentList} onAdd={append} />
|
||||
<GenericTable aria-label={t('Agents')}>
|
||||
<GenericTableHeader>
|
||||
<GenericTableHeaderCell width='x200'>{t('Name')}</GenericTableHeaderCell>
|
||||
<GenericTableHeaderCell width='x140'>{t('Count')}</GenericTableHeaderCell>
|
||||
<GenericTableHeaderCell width='x120'>{t('Order')}</GenericTableHeaderCell>
|
||||
<GenericTableHeaderCell width='x40'>{t('Remove')}</GenericTableHeaderCell>
|
||||
</GenericTableHeader>
|
||||
<GenericTableBody>
|
||||
{page.map((agent, index) => (
|
||||
<AgentRow key={agent.id} index={index} agent={agent} register={register} onRemove={() => remove(index)} />
|
||||
))}
|
||||
</GenericTableBody>
|
||||
</GenericTable>
|
||||
<Pagination
|
||||
divider
|
||||
current={current}
|
||||
itemsPerPage={itemsPerPage}
|
||||
count={fields.length}
|
||||
onSetItemsPerPage={onSetItemsPerPage}
|
||||
onSetCurrent={onSetCurrent}
|
||||
{...paginationProps}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DepartmentAgentsTable;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { IconButton } from '@rocket.chat/fuselage';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { GenericModal } from '@rocket.chat/ui-client';
|
||||
import { useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function RemoveAgentButton({ agentId, onRemove }: { agentId: string; onRemove: (agentId: string) => void }) {
|
||||
const setModal = useSetModal();
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleDelete = useStableCallback((e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const onRemoveAgent = async () => {
|
||||
onRemove(agentId);
|
||||
dispatchToastMessage({ type: 'success', message: t('Agent_removed') });
|
||||
setModal();
|
||||
};
|
||||
|
||||
setModal(<GenericModal variant='danger' onConfirm={onRemoveAgent} onCancel={() => setModal()} confirmText={t('Delete')} />);
|
||||
});
|
||||
|
||||
return <IconButton icon='trash' small title={t('Remove')} onClick={handleDelete} />;
|
||||
}
|
||||
|
||||
export default RemoveAgentButton;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Button, Chip, FieldRow, TextInput } from '@rocket.chat/fuselage';
|
||||
import type { ComponentProps, ChangeEvent } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type DepartmentTagsProps = {
|
||||
error: string;
|
||||
value: string[];
|
||||
onChange: (tags: string[]) => void;
|
||||
} & ComponentProps<typeof TextInput>;
|
||||
|
||||
const DepartmentTags = ({ error, value: tags, onChange, ...props }: 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 (
|
||||
<>
|
||||
<FieldRow>
|
||||
<TextInput
|
||||
error={error}
|
||||
placeholder={t('Enter_a_tag')}
|
||||
value={tagText}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setTagText(e.currentTarget.value)}
|
||||
{...props}
|
||||
/>
|
||||
<Button disabled={Boolean(!tagText.trim()) || tags.includes(tagText)} marginInlineStart={8} title={t('Add')} onClick={handleAddTag}>
|
||||
{t('Add')}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
{tags?.length > 0 && (
|
||||
<FieldRow justifyContent='flex-start'>
|
||||
{tags.map((tag, i) => (
|
||||
<Chip key={i} onClick={handleTagChipClick(tag)} marginInlineEnd={8}>
|
||||
{tag}
|
||||
</Chip>
|
||||
))}
|
||||
</FieldRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentTags;
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Tabs, TabsItem, Button } from '@rocket.chat/fuselage';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { Page, PageHeader, PageContent } from '@rocket.chat/ui-client';
|
||||
import { useRoute, useTranslation, useRouteParameter } from '@rocket.chat/ui-contexts';
|
||||
|
||||
import DepartmentsTable from './DepartmentsTable';
|
||||
import EditDepartmentWithData from './EditDepartmentWithData';
|
||||
import NewDepartment from './NewDepartment';
|
||||
|
||||
const DepartmentsPage = () => {
|
||||
const t = useTranslation();
|
||||
const departmentsRoute = useRoute('omnichannel-departments');
|
||||
|
||||
const context = useRouteParameter('context');
|
||||
const id = useRouteParameter('id');
|
||||
|
||||
const handleTabClick = useStableCallback((tab: undefined | 'archived') =>
|
||||
departmentsRoute.push(
|
||||
tab
|
||||
? {
|
||||
context: tab,
|
||||
}
|
||||
: {},
|
||||
),
|
||||
);
|
||||
|
||||
const onAddNew = useStableCallback(() =>
|
||||
departmentsRoute.push({
|
||||
context: 'new',
|
||||
}),
|
||||
);
|
||||
|
||||
if (context === 'new') {
|
||||
return <NewDepartment id={id} />;
|
||||
}
|
||||
|
||||
if (context === 'edit') {
|
||||
return <EditDepartmentWithData id={id} title={t('Edit_Department')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page flexDirection='row'>
|
||||
<Page>
|
||||
<PageHeader title={t('Departments')}>
|
||||
<Button onClick={onAddNew}>{t('Create_department')}</Button>
|
||||
</PageHeader>
|
||||
<Tabs>
|
||||
<TabsItem key='departments' selected={!context} onClick={() => handleTabClick(undefined)}>
|
||||
{t('All')}
|
||||
</TabsItem>
|
||||
<TabsItem key='archived' selected={context === 'archived'} onClick={() => handleTabClick('archived')}>
|
||||
{t('Archived')}
|
||||
</TabsItem>
|
||||
</Tabs>
|
||||
<PageContent>
|
||||
<DepartmentsTable archived={context === 'archived'} />
|
||||
</PageContent>
|
||||
</Page>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentsPage;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { usePermission } from '@rocket.chat/ui-contexts';
|
||||
|
||||
import DepartmentsPage from './DepartmentsPage';
|
||||
import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage';
|
||||
|
||||
const DepartmentsRoute = () => {
|
||||
const canViewDepartments = usePermission('manage-livechat-departments');
|
||||
|
||||
if (!canViewDepartments) {
|
||||
return <NotAuthorizedPage />;
|
||||
}
|
||||
|
||||
return <DepartmentsPage />;
|
||||
};
|
||||
|
||||
export default DepartmentsRoute;
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { GenericMenu } from '@rocket.chat/ui-client';
|
||||
import { useToastMessageDispatch, useEndpoint, useRoute, useSetModal, useSetting } from '@rocket.chat/ui-contexts';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import RemoveDepartmentModal from './RemoveDepartmentModal';
|
||||
|
||||
const ARCHIVE_DEPARTMENT_ENDPOINTS = {
|
||||
archive: '/v1/livechat/department/:_id/archive',
|
||||
unarchive: '/v1/livechat/department/:_id/unarchive',
|
||||
} as const;
|
||||
|
||||
export type DepartmentItemMenuProps = {
|
||||
department: Omit<ILivechatDepartment, '_updatedAt'>;
|
||||
archived: boolean;
|
||||
};
|
||||
|
||||
const DepartmentItemMenu = ({ department, archived }: DepartmentItemMenuProps) => {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const setModal = useSetModal();
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
|
||||
const route = useRoute('omnichannel-departments');
|
||||
const departmentRemovalEnabled = useSetting('Omnichannel_enable_department_removal');
|
||||
const { _id, name } = department;
|
||||
|
||||
const toggleArchive = useEndpoint('POST', archived ? ARCHIVE_DEPARTMENT_ENDPOINTS.unarchive : ARCHIVE_DEPARTMENT_ENDPOINTS.archive, {
|
||||
_id,
|
||||
});
|
||||
|
||||
const handleEdit = useStableCallback(() => {
|
||||
route.push({ context: 'edit', id: _id });
|
||||
});
|
||||
|
||||
const handleReload = useCallback(async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['livechat-departments'],
|
||||
});
|
||||
}, [queryClient]);
|
||||
|
||||
const handleToggleArchive = useStableCallback(async () => {
|
||||
try {
|
||||
await toggleArchive();
|
||||
dispatchToastMessage({ type: 'success', message: archived ? t('Department_unarchived') : t('Department_archived') });
|
||||
queryClient.removeQueries({
|
||||
queryKey: ['/v1/livechat/department/:_id', department._id],
|
||||
});
|
||||
handleReload();
|
||||
} catch (error) {
|
||||
dispatchToastMessage({ type: 'error', message: error });
|
||||
}
|
||||
});
|
||||
|
||||
const handlePermanentDepartmentRemoval = useStableCallback(() => {
|
||||
setModal(<RemoveDepartmentModal _id={_id} reset={handleReload} onClose={() => setModal(null)} name={name} />);
|
||||
});
|
||||
|
||||
const items = useMemo(
|
||||
() => [
|
||||
...(archived
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: 'edit',
|
||||
icon: 'edit' as const,
|
||||
content: t('Edit'),
|
||||
onClick: handleEdit,
|
||||
},
|
||||
]),
|
||||
{
|
||||
id: archived ? 'unarchive' : 'archive',
|
||||
icon: archived ? ('undo' as const) : ('arrow-down-box' as const),
|
||||
content: archived ? t('Unarchive') : t('Archive'),
|
||||
onClick: handleToggleArchive,
|
||||
},
|
||||
{
|
||||
id: 'delete',
|
||||
icon: 'trash' as const,
|
||||
content: t('Delete'),
|
||||
onClick: handlePermanentDepartmentRemoval,
|
||||
disabled: !departmentRemovalEnabled,
|
||||
tooltip: !departmentRemovalEnabled ? t('Department_Removal_Disabled') : undefined,
|
||||
},
|
||||
],
|
||||
[archived, departmentRemovalEnabled, handleEdit, handleToggleArchive, handlePermanentDepartmentRemoval, t],
|
||||
);
|
||||
|
||||
return <GenericMenu title={t('Options')} items={items} />;
|
||||
};
|
||||
|
||||
export default DepartmentItemMenu;
|
||||
@@ -0,0 +1,158 @@
|
||||
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
|
||||
import { Pagination } from '@rocket.chat/fuselage';
|
||||
import { useDebouncedValue, useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import {
|
||||
GenericTable,
|
||||
GenericTableBody,
|
||||
GenericTableCell,
|
||||
GenericTableHeader,
|
||||
GenericTableHeaderCell,
|
||||
GenericTableLoadingTable,
|
||||
GenericTableRow,
|
||||
usePagination,
|
||||
useSort,
|
||||
} from '@rocket.chat/ui-client';
|
||||
import { useTranslation, useEndpoint, useRouter } from '@rocket.chat/ui-contexts';
|
||||
import { useQuery, hashKey, keepPreviousData } from '@tanstack/react-query';
|
||||
import { useState, useMemo } from 'react';
|
||||
|
||||
import DepartmentItemMenu from './DepartmentItemMenu';
|
||||
import FilterByText from '../../../../components/FilterByText';
|
||||
import GenericNoResults from '../../../../components/GenericNoResults/GenericNoResults';
|
||||
import { links } from '../../../../lib/links';
|
||||
|
||||
const DEPARTMENTS_ENDPOINTS = {
|
||||
department: '/v1/livechat/department',
|
||||
archived: '/v1/livechat/departments/archived',
|
||||
} as const;
|
||||
|
||||
export type DepartmentsTableProps = { archived: boolean };
|
||||
|
||||
const DepartmentsTable = ({ archived }: DepartmentsTableProps) => {
|
||||
const t = useTranslation();
|
||||
const router = useRouter();
|
||||
const [text, setText] = useState('');
|
||||
|
||||
const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination();
|
||||
const { sortBy, sortDirection, setSort } = useSort<'name' | 'description' | 'numAgents' | 'enabled' | 'showOnRegistration'>('name');
|
||||
|
||||
const getDepartments = useEndpoint('GET', archived ? DEPARTMENTS_ENDPOINTS.archived : DEPARTMENTS_ENDPOINTS.department);
|
||||
|
||||
const handleAddNew = useStableCallback(() => router.navigate('/omnichannel/departments/new'));
|
||||
|
||||
const query = useDebouncedValue(
|
||||
useMemo(
|
||||
() => ({
|
||||
onlyMyDepartments: 'true' as const,
|
||||
text,
|
||||
sort: JSON.stringify({ [sortBy]: sortDirection === 'asc' ? 1 : -1 }),
|
||||
...(current && { offset: current }),
|
||||
...(itemsPerPage && { count: itemsPerPage }),
|
||||
}),
|
||||
[current, itemsPerPage, sortBy, sortDirection, text],
|
||||
),
|
||||
500,
|
||||
);
|
||||
|
||||
const { data, isSuccess, isLoading } = useQuery({
|
||||
queryKey: ['livechat-departments', query, archived],
|
||||
queryFn: async () => getDepartments(query),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const [defaultQuery] = useState(hashKey([query]));
|
||||
const queryHasChanged = defaultQuery !== hashKey([query]);
|
||||
|
||||
const headers = (
|
||||
<>
|
||||
<GenericTableHeaderCell key='name' direction={sortDirection} active={sortBy === 'name'} onClick={setSort} sort='name'>
|
||||
{t('Name')}
|
||||
</GenericTableHeaderCell>
|
||||
<GenericTableHeaderCell
|
||||
key='description'
|
||||
direction={sortDirection}
|
||||
active={sortBy === 'description'}
|
||||
onClick={setSort}
|
||||
sort='description'
|
||||
>
|
||||
{t('Description')}
|
||||
</GenericTableHeaderCell>
|
||||
<GenericTableHeaderCell key='numAgents' direction={sortDirection} active={sortBy === 'numAgents'} onClick={setSort} sort='numAgents'>
|
||||
{t('Num_Agents')}
|
||||
</GenericTableHeaderCell>
|
||||
<GenericTableHeaderCell key='enabled' direction={sortDirection} active={sortBy === 'enabled'} onClick={setSort} sort='enabled'>
|
||||
{t('Enabled')}
|
||||
</GenericTableHeaderCell>
|
||||
<GenericTableHeaderCell
|
||||
key='showOnRegistration'
|
||||
direction={sortDirection}
|
||||
active={sortBy === 'showOnRegistration'}
|
||||
onClick={setSort}
|
||||
sort='showOnRegistration'
|
||||
>
|
||||
{t('Show_on_registration_page')}
|
||||
</GenericTableHeaderCell>
|
||||
<GenericTableHeaderCell key='spacer' width={40} />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{((isSuccess && data?.departments.length > 0) || queryHasChanged) && (
|
||||
<FilterByText value={text} onChange={(event) => setText(event.target.value)} />
|
||||
)}
|
||||
{isLoading && (
|
||||
<GenericTable>
|
||||
<GenericTableHeader>{headers}</GenericTableHeader>
|
||||
<GenericTableBody>
|
||||
<GenericTableLoadingTable headerCells={6} />
|
||||
</GenericTableBody>
|
||||
</GenericTable>
|
||||
)}
|
||||
{isSuccess && data?.departments.length === 0 && (queryHasChanged || archived) && <GenericNoResults />}
|
||||
{isSuccess && data?.departments.length === 0 && !queryHasChanged && !archived && (
|
||||
<GenericNoResults
|
||||
icon='folder'
|
||||
title={t('No_departments_yet')}
|
||||
description={t('No_departments_yet_description')}
|
||||
buttonAction={handleAddNew}
|
||||
buttonTitle={t('Create_department')}
|
||||
linkHref={links.go.omnichannelDocs}
|
||||
linkText={t('Learn_more_about_departments')}
|
||||
/>
|
||||
)}
|
||||
{isSuccess && data?.departments.length > 0 && (
|
||||
<>
|
||||
<GenericTable aria-label={t('Departments')} aria-busy={isLoading} aria-live='polite'>
|
||||
<GenericTableHeader>{headers}</GenericTableHeader>
|
||||
<GenericTableBody>
|
||||
{data.departments.map((department: Omit<ILivechatDepartment, '_updatedAt'>) => (
|
||||
<GenericTableRow key={department._id} tabIndex={0} width='full'>
|
||||
<GenericTableCell withTruncatedText>{department.name}</GenericTableCell>
|
||||
<GenericTableCell withTruncatedText>{department.description}</GenericTableCell>
|
||||
<GenericTableCell withTruncatedText>{department.numAgents || '0'}</GenericTableCell>
|
||||
<GenericTableCell withTruncatedText>{department.enabled ? t('Yes') : t('No')}</GenericTableCell>
|
||||
<GenericTableCell withTruncatedText>{department.showOnRegistration ? t('Yes') : t('No')}</GenericTableCell>
|
||||
<GenericTableCell withTruncatedText>
|
||||
<DepartmentItemMenu department={department} archived={archived} />
|
||||
</GenericTableCell>
|
||||
</GenericTableRow>
|
||||
))}
|
||||
</GenericTableBody>
|
||||
</GenericTable>
|
||||
<Pagination
|
||||
divider
|
||||
current={current}
|
||||
itemsPerPage={itemsPerPage}
|
||||
count={data?.total || 0}
|
||||
onSetItemsPerPage={onSetItemsPerPage}
|
||||
onSetCurrent={onSetCurrent}
|
||||
{...paginationProps}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentsTable;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Box, Input } from '@rocket.chat/fuselage';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { GenericModal } from '@rocket.chat/ui-client';
|
||||
import { useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import type { ChangeEvent, FormEvent } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type RemoveDepartmentModalProps = {
|
||||
_id: string;
|
||||
name: string;
|
||||
reset: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const RemoveDepartmentModal = ({ _id = '', name, reset, onClose }: RemoveDepartmentModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [text, setText] = useState('');
|
||||
|
||||
const removeDepartment = useEndpoint('DELETE', '/v1/livechat/department/:_id', { _id });
|
||||
const dispatchToast = useToastMessageDispatch();
|
||||
|
||||
const onSubmit = useStableCallback(async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await removeDepartment();
|
||||
dispatchToast({ type: 'success', message: t('Department_removed') });
|
||||
reset();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
dispatchToast({ type: 'error', message: error });
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
wrapperFunction={(props) => <Box is='form' onSubmit={onSubmit} {...props} />}
|
||||
onCancel={onClose}
|
||||
confirmText={t('Delete')}
|
||||
title={t('Delete_Department?')}
|
||||
onClose={onClose}
|
||||
variant='danger'
|
||||
confirmDisabled={text !== name}
|
||||
>
|
||||
<Box marginBlockEnd={16}>{t('Are_you_sure_delete_department')}</Box>
|
||||
<Box marginBlockEnd={16} display='flex' justifyContent='stretch'>
|
||||
<Input
|
||||
value={text}
|
||||
aria-label={t('Department_name')}
|
||||
name='confirmDepartmentName'
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => setText(event.currentTarget.value)}
|
||||
/>
|
||||
</Box>
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemoveDepartmentModal;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './DepartmentsTable';
|
||||
@@ -0,0 +1,436 @@
|
||||
import type { ILivechatDepartment, ILivechatDepartmentAgents, Serialized } from '@rocket.chat/core-typings';
|
||||
import {
|
||||
FieldGroup,
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldRow,
|
||||
FieldError,
|
||||
TextInput,
|
||||
Box,
|
||||
Icon,
|
||||
Divider,
|
||||
ToggleSwitch,
|
||||
TextAreaInput,
|
||||
ButtonGroup,
|
||||
Button,
|
||||
PaginatedSelectFiltered,
|
||||
FieldHint,
|
||||
Option,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
|
||||
import { validateEmail } from '@rocket.chat/tools';
|
||||
import { Page, PageHeader, PageScrollableContentWithShadow } from '@rocket.chat/ui-client';
|
||||
import { useToastMessageDispatch, useEndpoint, useRouter, usePermission } from '@rocket.chat/ui-contexts';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useId, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DepartmentsAgentsTable from './DepartmentAgentsTable/DepartmentAgentsTable';
|
||||
import DepartmentTags from './DepartmentTags';
|
||||
import type { EditDepartmentFormData } from './definitions';
|
||||
import { formatAgentListPayload } from './utils/formatAgentListPayload';
|
||||
import { formatEditDepartmentPayload } from './utils/formatEditDepartmentPayload';
|
||||
import { getFormInitialValues } from './utils/getFormInititalValues';
|
||||
import { useFormSubmitWithDirtyCheck } from '../../../hooks/useFormSubmitWithDirtyCheck';
|
||||
import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule';
|
||||
import { useRoomsList } from '../../../hooks/useRoomsList';
|
||||
import { EeTextInput, EeTextAreaInput, EeNumberInput, DepartmentBusinessHours } from '../additionalForms';
|
||||
import AutoCompleteUnit from '../additionalForms/AutoCompleteUnit';
|
||||
import AutoCompleteDepartment from '../components/AutoCompleteDepartment';
|
||||
import AutoCompleteDepartmentMultiple from '../components/AutoCompleteDepartmentMultiple';
|
||||
|
||||
export type EditDepartmentProps = {
|
||||
id?: string;
|
||||
title: string;
|
||||
data?: Serialized<{
|
||||
department: ILivechatDepartment | null;
|
||||
agents?: ILivechatDepartmentAgents[];
|
||||
}>;
|
||||
allowedToForwardData?: Serialized<{
|
||||
departments: ILivechatDepartment[];
|
||||
}>;
|
||||
};
|
||||
|
||||
function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmentProps) {
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { department, agents = [] } = data || {};
|
||||
|
||||
const { data: hasLicense = false } = useHasLicenseModule('livechat-enterprise');
|
||||
const canManageUnits = usePermission('manage-livechat-units');
|
||||
|
||||
const initialValues = getFormInitialValues({ department, agents, allowedToForwardData });
|
||||
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { errors, isDirty, isSubmitting },
|
||||
} = useForm<EditDepartmentFormData>({ defaultValues: initialValues });
|
||||
|
||||
const [fallbackFilter, setFallbackFilter] = useState<string>('');
|
||||
const [isUnitRequired, setUnitRequired] = useState(false);
|
||||
|
||||
const debouncedFallbackFilter = useDebouncedValue(fallbackFilter, 500);
|
||||
|
||||
const { data: roomItems = [], fetchNextPage } = useRoomsList({ text: debouncedFallbackFilter });
|
||||
|
||||
const createDepartment = useEndpoint('POST', '/v1/livechat/department');
|
||||
const updateDepartmentInfo = useEndpoint('PUT', '/v1/livechat/department/:_id', { _id: id || '' });
|
||||
const saveDepartmentAgentsInfoOnEdit = useEndpoint('POST', `/v1/livechat/department/:_id/agents`, { _id: id || '' });
|
||||
|
||||
const handleSave = useFormSubmitWithDirtyCheck(
|
||||
async (data: EditDepartmentFormData) => {
|
||||
try {
|
||||
const { agentList } = data;
|
||||
const payload = formatEditDepartmentPayload(data);
|
||||
const departmentUnit = data.unit ? { _id: data.unit } : undefined;
|
||||
|
||||
if (id) {
|
||||
await updateDepartmentInfo({
|
||||
department: payload,
|
||||
agents: [],
|
||||
departmentUnit,
|
||||
});
|
||||
|
||||
const { agentList: initialAgentList } = initialValues;
|
||||
const agentListPayload = formatAgentListPayload(initialAgentList, agentList);
|
||||
|
||||
if (agentListPayload.upsert.length > 0 || agentListPayload.remove.length > 0) {
|
||||
await saveDepartmentAgentsInfoOnEdit(agentListPayload);
|
||||
}
|
||||
} else {
|
||||
await createDepartment({
|
||||
department: payload,
|
||||
agents: agentList.map(({ agentId, count, order }) => ({ agentId, count, order })),
|
||||
departmentUnit,
|
||||
});
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ['/v1/livechat/department/:_id', id] });
|
||||
dispatchToastMessage({ type: 'success', message: t('Saved') });
|
||||
router.navigate('/omnichannel/departments');
|
||||
} catch (error) {
|
||||
dispatchToastMessage({ type: 'error', message: error });
|
||||
}
|
||||
},
|
||||
{ isDirty },
|
||||
);
|
||||
|
||||
const formId = useId();
|
||||
const enabledField = useId();
|
||||
const nameField = useId();
|
||||
const descriptionField = useId();
|
||||
const showOnRegistrationField = useId();
|
||||
const emailField = useId();
|
||||
const showOnOfflineFormField = useId();
|
||||
const offlineMessageChannelNameField = useId();
|
||||
const fallbackForwardDepartmentField = useId();
|
||||
const requestTagBeforeClosingChatField = useId();
|
||||
const chatClosingTagsField = useId();
|
||||
const allowReceiveForwardOffline = useId();
|
||||
const unitFieldId = useId();
|
||||
const agentsLabelId = useId();
|
||||
const departmentsAllowedToForwardFieldId = useId();
|
||||
|
||||
return (
|
||||
<Page flexDirection='row'>
|
||||
<Page>
|
||||
<PageHeader title={title} onClickBack={() => router.navigate('/omnichannel/departments')}>
|
||||
<ButtonGroup>
|
||||
<Button type='submit' form={formId} primary loading={isSubmitting}>
|
||||
{t('Save')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</PageHeader>
|
||||
<PageScrollableContentWithShadow>
|
||||
<FieldGroup
|
||||
width='full'
|
||||
alignSelf='center'
|
||||
maxWidth='x600'
|
||||
id={formId}
|
||||
is='form'
|
||||
autoComplete='off'
|
||||
onSubmit={handleSubmit(handleSave)}
|
||||
>
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={enabledField}>{t('Enabled')}</FieldLabel>
|
||||
<ToggleSwitch id={enabledField} {...register('enabled')} />
|
||||
</FieldRow>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={nameField} required>
|
||||
{t('Name')}
|
||||
</FieldLabel>
|
||||
<FieldRow>
|
||||
<TextInput
|
||||
id={nameField}
|
||||
flexGrow={1}
|
||||
error={errors.name?.message as string}
|
||||
placeholder={t('Name')}
|
||||
{...register('name', { required: t('Required_field', { field: t('Name') }) })}
|
||||
/>
|
||||
</FieldRow>
|
||||
{errors.name && (
|
||||
<FieldError role='alert' id={`${nameField}-error`}>
|
||||
{errors.name?.message}
|
||||
</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={descriptionField}>{t('Description')}</FieldLabel>
|
||||
<FieldRow>
|
||||
<TextAreaInput id={descriptionField} placeholder={t('Description')} {...register('description')} />
|
||||
</FieldRow>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={showOnRegistrationField}>{t('Show_on_registration_page')}</FieldLabel>
|
||||
<ToggleSwitch id={showOnRegistrationField} {...register('showOnRegistration')} />
|
||||
</FieldRow>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={emailField} required>
|
||||
{t('Email')}
|
||||
</FieldLabel>
|
||||
<FieldRow>
|
||||
<TextInput
|
||||
id={emailField}
|
||||
error={errors.email?.message as string}
|
||||
endAddon={<Icon name='mail' size='x20' />}
|
||||
placeholder={t('Email')}
|
||||
{...register('email', {
|
||||
required: t('Required_field', { field: t('Email') }),
|
||||
validate: (email) => validateEmail(email) || t('error-invalid-email-address'),
|
||||
})}
|
||||
aria-describedby={`${emailField}-error`}
|
||||
/>
|
||||
</FieldRow>
|
||||
{errors.email && (
|
||||
<FieldError role='alert' id={`${emailField}-error`}>
|
||||
{errors.email?.message}
|
||||
</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={showOnOfflineFormField}>{t('Show_on_offline_page')}</FieldLabel>
|
||||
<ToggleSwitch id={showOnOfflineFormField} {...register('showOnOfflineForm')} />
|
||||
</FieldRow>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={offlineMessageChannelNameField}>{t('Livechat_DepartmentOfflineMessageToChannel')}</FieldLabel>
|
||||
<FieldRow>
|
||||
<Controller
|
||||
control={control}
|
||||
name='offlineMessageChannelName'
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<PaginatedSelectFiltered
|
||||
id={offlineMessageChannelNameField}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
flexShrink={0}
|
||||
filter={fallbackFilter}
|
||||
setFilter={setFallbackFilter as (value?: string | number) => void}
|
||||
options={roomItems}
|
||||
placeholder={t('Channel_name')}
|
||||
endReached={() => fetchNextPage()}
|
||||
aria-busy={fallbackFilter !== debouncedFallbackFilter}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FieldRow>
|
||||
</Field>
|
||||
{hasLicense && (
|
||||
<>
|
||||
<Field>
|
||||
<Controller
|
||||
control={control}
|
||||
name='maxNumberSimultaneousChat'
|
||||
render={({ field }) => (
|
||||
<EeNumberInput
|
||||
{...field}
|
||||
label={t('Max_number_of_chats_per_agent')}
|
||||
placeholder={t('Max_number_of_chats_per_agent_description')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<Controller
|
||||
control={control}
|
||||
name='visitorInactivityTimeoutInSeconds'
|
||||
render={({ field }) => (
|
||||
<EeNumberInput
|
||||
{...field}
|
||||
label={t('How_long_to_wait_to_consider_visitor_abandonment_in_seconds')}
|
||||
placeholder={t('Number_in_seconds')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<Controller
|
||||
control={control}
|
||||
name='abandonedRoomsCloseCustomMessage'
|
||||
render={({ field }) => (
|
||||
<EeTextInput
|
||||
{...field}
|
||||
label={t('Livechat_abandoned_rooms_closed_custom_message')}
|
||||
placeholder={t('Enter_a_custom_message')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<Controller
|
||||
control={control}
|
||||
name='waitingQueueMessage'
|
||||
render={({ field }) => (
|
||||
<EeTextAreaInput {...field} label={t('Waiting_queue_message')} placeholder={t('Waiting_queue_message')} />
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={departmentsAllowedToForwardFieldId}>{t('List_of_departments_for_forward')}</FieldLabel>
|
||||
<FieldRow>
|
||||
<Box width='100%'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='departmentsAllowedToForward'
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<AutoCompleteDepartmentMultiple
|
||||
id={departmentsAllowedToForwardFieldId}
|
||||
withCheckbox
|
||||
showArchived
|
||||
excludeId={id ?? ''}
|
||||
value={value}
|
||||
maxWidth='100%'
|
||||
width='100%'
|
||||
flexGrow={1}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
</FieldRow>
|
||||
<FieldHint>{t('List_of_departments_for_forward_description')}</FieldHint>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={fallbackForwardDepartmentField}>{t('Fallback_forward_department')}</FieldLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name='fallbackForwardDepartment'
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<AutoCompleteDepartment
|
||||
id={fallbackForwardDepartmentField}
|
||||
haveNone
|
||||
excludeId={department?._id}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onlyMyDepartments
|
||||
showArchived
|
||||
withTitle={false}
|
||||
renderItem={({ label, ...props }) => (
|
||||
<Option {...props} label={<span style={{ whiteSpace: 'normal' }}>{label}</span>} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={unitFieldId} required={isUnitRequired}>
|
||||
{t('Unit')}
|
||||
</FieldLabel>
|
||||
<FieldRow>
|
||||
<Controller
|
||||
name='unit'
|
||||
control={control}
|
||||
rules={{ required: isUnitRequired ? t('Required_field', { field: t('Unit') }) : false }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<AutoCompleteUnit
|
||||
disabled={!!initialValues.unit}
|
||||
haveNone
|
||||
id={unitFieldId}
|
||||
error={errors.unit?.message as string}
|
||||
aria-describedby={`${unitFieldId}-error`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onLoadItems={(list) => {
|
||||
// NOTE: list.itemCount > 1 to account for the "None" option
|
||||
setUnitRequired(!canManageUnits && list.length > 1);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FieldRow>
|
||||
{errors.unit && (
|
||||
<FieldError role='alert' id={`${unitFieldId}-error`}>
|
||||
{errors.unit?.message}
|
||||
</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={requestTagBeforeClosingChatField}>{t('Request_tag_before_closing_chat')}</FieldLabel>
|
||||
<ToggleSwitch id={requestTagBeforeClosingChatField} {...register('requestTagBeforeClosingChat')} />
|
||||
</FieldRow>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={chatClosingTagsField}>{t('Conversation_closing_tags')}</FieldLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name='chatClosingTags'
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<DepartmentTags
|
||||
id={chatClosingTagsField}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
error={errors.chatClosingTags?.message as string}
|
||||
aria-describedby={`${chatClosingTagsField}-hint ${chatClosingTagsField}-error`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FieldHint id={`${chatClosingTagsField}-hint`}>{t('Conversation_closing_tags_description')}</FieldHint>
|
||||
{errors.chatClosingTags && (
|
||||
<FieldError aria-live='assertive' id={`${chatClosingTagsField}-error`}>
|
||||
{errors.chatClosingTags?.message}
|
||||
</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={allowReceiveForwardOffline}>{t('Accept_receive_inquiry_no_online_agents')}</FieldLabel>
|
||||
<ToggleSwitch id={allowReceiveForwardOffline} {...register('allowReceiveForwardOffline')} />
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<FieldHint id={`${allowReceiveForwardOffline}-hint`}>{t('Accept_receive_inquiry_no_online_agents_Hint')}</FieldHint>
|
||||
</FieldRow>
|
||||
</Field>
|
||||
<Field>
|
||||
<DepartmentBusinessHours bhId={department?.businessHourId} />
|
||||
</Field>
|
||||
<Divider marginBlock={16} />
|
||||
<Field>
|
||||
<FieldLabel id={agentsLabelId} marginBlock={4}>
|
||||
{t('Agents')}
|
||||
</FieldLabel>
|
||||
<Box display='flex' flexDirection='column' height='50vh'>
|
||||
<DepartmentsAgentsTable aria-labelledby={agentsLabelId} control={control} register={register} />
|
||||
</Box>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</PageScrollableContentWithShadow>
|
||||
</Page>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditDepartment;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Box } from '@rocket.chat/fuselage';
|
||||
import { useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { EditDepartmentProps } from './EditDepartment';
|
||||
import EditDepartment from './EditDepartment';
|
||||
import { FormSkeleton } from '../../../components/Skeleton';
|
||||
|
||||
const EditDepartmentWithAllowedForwardData = ({ data, ...props }: Omit<EditDepartmentProps, 'allowedToForwardData'>) => {
|
||||
const { t } = useTranslation();
|
||||
const getDepartmentListByIds = useEndpoint('GET', '/v1/livechat/department.listByIds');
|
||||
|
||||
const {
|
||||
data: allowedToForwardData,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ['/v1/livechat/department.listByIds', data?.department?.departmentsAllowedToForward],
|
||||
|
||||
queryFn: () =>
|
||||
getDepartmentListByIds({
|
||||
ids: data?.department?.departmentsAllowedToForward ?? [],
|
||||
}),
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <FormSkeleton />;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <Box marginBlockStart={16}>{t('Not_Available')}</Box>;
|
||||
}
|
||||
|
||||
return <EditDepartment data={data} allowedToForwardData={allowedToForwardData} {...props} />;
|
||||
};
|
||||
|
||||
export default EditDepartmentWithAllowedForwardData;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Box } from '@rocket.chat/fuselage';
|
||||
import { useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import EditDepartment from './EditDepartment';
|
||||
import EditDepartmentWithAllowedForwardData from './EditDepartmentWithAllowedForwardData';
|
||||
import { FormSkeleton } from '../../../components/Skeleton';
|
||||
|
||||
const params = { onlyMyDepartments: 'true' } as const;
|
||||
|
||||
export 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, isLoading, isError } = useQuery({
|
||||
queryKey: ['/v1/livechat/department/:_id', id],
|
||||
queryFn: () => getDepartment(params),
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <FormSkeleton padding='1.5rem 1rem' maxWidth='37.5rem' margin='0 auto' />;
|
||||
}
|
||||
|
||||
if (isError || (id && !data?.department)) {
|
||||
return <Box marginBlockStart={16}>{t('Department_not_found')}</Box>;
|
||||
}
|
||||
|
||||
if (data?.department?.archived === true) {
|
||||
return <Box marginBlockStart={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;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Callout } from '@rocket.chat/fuselage';
|
||||
import { useEndpoint, useSetModal } from '@rocket.chat/ui-contexts';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import EditDepartment from './EditDepartment';
|
||||
import PageSkeleton from '../../../components/PageSkeleton';
|
||||
import EnterpriseDepartmentsModal from '../modals/EnterpriseDepartmentsModal';
|
||||
|
||||
export type NewDepartmentProps = {
|
||||
id?: string;
|
||||
};
|
||||
|
||||
const NewDepartment = ({ id }: NewDepartmentProps) => {
|
||||
const { t } = useTranslation();
|
||||
const setModal = useSetModal();
|
||||
const getDepartmentCreationAvailable = useEndpoint('GET', '/v1/livechat/department/isDepartmentCreationAvailable');
|
||||
const { data, isPending, isError } = useQuery({
|
||||
queryKey: ['getDepartments'],
|
||||
queryFn: () => getDepartmentCreationAvailable(),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.isDepartmentCreationAvailable === false) {
|
||||
setModal(<EnterpriseDepartmentsModal closeModal={(): void => setModal(null)} />);
|
||||
}
|
||||
}, [data?.isDepartmentCreationAvailable, setModal]);
|
||||
|
||||
if (isError) {
|
||||
return <Callout type='danger'>{t('Unavailable')}</Callout>;
|
||||
}
|
||||
|
||||
if (!data || isPending || !data.isDepartmentCreationAvailable) {
|
||||
return <PageSkeleton />;
|
||||
}
|
||||
|
||||
return <EditDepartment id={id} title={t('New_Department')} />;
|
||||
};
|
||||
|
||||
export default NewDepartment;
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { ILivechatDepartmentAgents } from '@rocket.chat/core-typings';
|
||||
|
||||
export type IDepartmentAgent = Pick<ILivechatDepartmentAgents, 'agentId' | 'username' | 'count' | 'order'> & {
|
||||
_id?: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type EditDepartmentFormData = {
|
||||
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[];
|
||||
allowReceiveForwardOffline: boolean;
|
||||
unit?: string;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { IDepartmentAgent } from '../definitions';
|
||||
|
||||
export const formatAgentListPayload = (oldAgentList: IDepartmentAgent[], newAgentList: IDepartmentAgent[]) => {
|
||||
const upsert: Pick<IDepartmentAgent, 'agentId' | 'username' | 'count' | 'order'>[] = [];
|
||||
const remove: Pick<IDepartmentAgent, 'agentId' | 'username'>[] = [];
|
||||
|
||||
for (const agent of newAgentList) {
|
||||
const initialAgent = agent._id ? oldAgentList.find((initialAgent) => initialAgent._id === agent._id) : undefined;
|
||||
|
||||
if (!initialAgent || agent.count !== initialAgent.count || agent.order !== initialAgent.order) {
|
||||
upsert.push({ agentId: agent.agentId, username: agent.username, count: agent.count, order: agent.order });
|
||||
}
|
||||
}
|
||||
|
||||
for (const initialAgent of oldAgentList) {
|
||||
if (!newAgentList.some((agent) => initialAgent._id === agent._id)) {
|
||||
remove.push({ agentId: initialAgent.agentId, username: initialAgent.username });
|
||||
}
|
||||
}
|
||||
|
||||
return { upsert, remove };
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { EditDepartmentFormData } from '../definitions';
|
||||
|
||||
export const formatEditDepartmentPayload = (data: EditDepartmentFormData) => {
|
||||
const {
|
||||
enabled,
|
||||
name,
|
||||
description,
|
||||
showOnRegistration,
|
||||
showOnOfflineForm,
|
||||
email,
|
||||
chatClosingTags,
|
||||
offlineMessageChannelName,
|
||||
maxNumberSimultaneousChat,
|
||||
visitorInactivityTimeoutInSeconds,
|
||||
abandonedRoomsCloseCustomMessage,
|
||||
waitingQueueMessage,
|
||||
departmentsAllowedToForward,
|
||||
fallbackForwardDepartment,
|
||||
allowReceiveForwardOffline,
|
||||
requestTagBeforeClosingChat,
|
||||
} = data;
|
||||
|
||||
return {
|
||||
enabled,
|
||||
name,
|
||||
description,
|
||||
showOnRegistration,
|
||||
showOnOfflineForm,
|
||||
requestTagBeforeClosingChat,
|
||||
email,
|
||||
chatClosingTags,
|
||||
offlineMessageChannelName,
|
||||
maxNumberSimultaneousChat,
|
||||
visitorInactivityTimeoutInSeconds,
|
||||
abandonedRoomsCloseCustomMessage,
|
||||
waitingQueueMessage,
|
||||
departmentsAllowedToForward: departmentsAllowedToForward?.map((dep) => dep.value),
|
||||
fallbackForwardDepartment,
|
||||
allowReceiveForwardOffline,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ILivechatDepartment, Serialized, ILivechatDepartmentAgents } from '@rocket.chat/core-typings';
|
||||
|
||||
import type { EditDepartmentProps } from '../EditDepartment';
|
||||
|
||||
type InitialValueParams = {
|
||||
department?: Serialized<ILivechatDepartment> | null;
|
||||
agents?: Serialized<ILivechatDepartmentAgents>[];
|
||||
allowedToForwardData?: EditDepartmentProps['allowedToForwardData'];
|
||||
};
|
||||
|
||||
function withDefault<T>(key: T | undefined | null, defaultValue: T) {
|
||||
return key || defaultValue;
|
||||
}
|
||||
|
||||
export const getFormInitialValues = ({ 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 || [],
|
||||
allowReceiveForwardOffline: withDefault(department?.allowReceiveForwardOffline, false),
|
||||
unit: withDefault(department?.ancestors?.[0], ''), // NOTE: A department should only have one ancestor
|
||||
});
|
||||
Reference in New Issue
Block a user