chore: Refactor Omnichannel Edit Tags UI (#30732)
parent
a19f9c36c3
commit
8db7b92983
@ -1,42 +0,0 @@ |
||||
import type { ILivechatTag } from '@rocket.chat/core-typings'; |
||||
import { IconButton } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import { useSetModal, useToastMessageDispatch, useRoute, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React from 'react'; |
||||
|
||||
import GenericModal from '../../../../client/components/GenericModal'; |
||||
import { GenericTableCell } from '../../../../client/components/GenericTable'; |
||||
|
||||
const RemoveTagButton = ({ _id, reload }: { _id: ILivechatTag['_id']; reload: () => void }) => { |
||||
const t = useTranslation(); |
||||
const setModal = useSetModal(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
const tagsRoute = useRoute('omnichannel-tags'); |
||||
const removeTag = useMethod('livechat:removeTag'); |
||||
|
||||
const handleDelete = useMutableCallback((e) => { |
||||
e.stopPropagation(); |
||||
const onDeleteAgent = async () => { |
||||
try { |
||||
await removeTag(_id); |
||||
dispatchToastMessage({ type: 'success', message: t('Tag_removed') }); |
||||
tagsRoute.push({}); |
||||
reload(); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} finally { |
||||
setModal(); |
||||
} |
||||
}; |
||||
|
||||
setModal(<GenericModal variant='danger' onConfirm={onDeleteAgent} onCancel={() => setModal()} confirmText={t('Delete')} />); |
||||
}); |
||||
|
||||
return ( |
||||
<GenericTableCell fontScale='p2' color='hint' withTruncatedText> |
||||
<IconButton icon='trash' small title={t('Remove')} onClick={handleDelete} /> |
||||
</GenericTableCell> |
||||
); |
||||
}; |
||||
|
||||
export default RemoveTagButton; |
||||
@ -1,99 +0,0 @@ |
||||
import { Field, TextInput, Button, ButtonGroup, FieldGroup } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import { useToastMessageDispatch, useRoute, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React, { useMemo } from 'react'; |
||||
|
||||
import AutoCompleteDepartmentMultiple from '../../../../client/components/AutoCompleteDepartmentMultiple'; |
||||
import Page from '../../../../client/components/Page'; |
||||
import { useForm } from '../../../../client/hooks/useForm'; |
||||
|
||||
function TagEdit({ title, data, tagId, reload, currentDepartments, ...props }) { |
||||
const t = useTranslation(); |
||||
const tagsRoute = useRoute('omnichannel-tags'); |
||||
|
||||
const tag = data || {}; |
||||
|
||||
const { values, handlers, hasUnsavedChanges } = useForm({ |
||||
name: tag.name, |
||||
description: tag.description, |
||||
departments: |
||||
currentDepartments && currentDepartments.departments |
||||
? currentDepartments.departments.map((dep) => ({ label: dep.name, value: dep._id })) |
||||
: [], |
||||
}); |
||||
|
||||
const { handleName, handleDescription, handleDepartments } = handlers; |
||||
const { name, description, departments } = values; |
||||
|
||||
const nameError = useMemo(() => (!name || name.length === 0 ? t('The_field_is_required', 'name') : undefined), [name, t]); |
||||
|
||||
const saveTag = useMethod('livechat:saveTag'); |
||||
|
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const handleReturn = useMutableCallback(() => { |
||||
tagsRoute.push({}); |
||||
}); |
||||
|
||||
const canSave = useMemo(() => !nameError, [nameError]); |
||||
|
||||
const handleSave = useMutableCallback(async () => { |
||||
const tagData = { name, description }; |
||||
|
||||
if (!canSave) { |
||||
return dispatchToastMessage({ type: 'error', message: t('The_field_is_required') }); |
||||
} |
||||
|
||||
const finalDepartments = departments ? departments.map((dep) => dep.value) : ['']; |
||||
|
||||
try { |
||||
await saveTag(tagId, tagData, finalDepartments); |
||||
dispatchToastMessage({ type: 'success', message: t('Saved') }); |
||||
reload(); |
||||
tagsRoute.push({}); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}); |
||||
|
||||
return ( |
||||
<Page flexDirection='row'> |
||||
<Page> |
||||
<Page.Header title={title}> |
||||
<ButtonGroup> |
||||
<Button icon='back' onClick={handleReturn}> |
||||
{t('Back')} |
||||
</Button> |
||||
<Button primary mie='none' flexGrow={1} disabled={!hasUnsavedChanges || !canSave} onClick={handleSave}> |
||||
{t('Save')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
</Page.Header> |
||||
<Page.ScrollableContentWithShadow> |
||||
<FieldGroup w='full' alignSelf='center' maxWidth='x600' is='form' autoComplete='off' {...props}> |
||||
<Field> |
||||
<Field.Label>{t('Name')}*</Field.Label> |
||||
<Field.Row> |
||||
<TextInput placeholder={t('Name')} flexGrow={1} value={name} onChange={handleName} error={hasUnsavedChanges && nameError} /> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Description')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput placeholder={t('Description')} flexGrow={1} value={description} onChange={handleDescription} /> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Departments')}</Field.Label> |
||||
<Field.Row> |
||||
<AutoCompleteDepartmentMultiple value={departments} onChange={handleDepartments} showArchived /> |
||||
</Field.Row> |
||||
</Field> |
||||
</FieldGroup> |
||||
</Page.ScrollableContentWithShadow> |
||||
</Page> |
||||
</Page> |
||||
); |
||||
} |
||||
|
||||
export default TagEdit; |
||||
@ -0,0 +1,141 @@ |
||||
import type { ILivechatDepartment, ILivechatTag, Serialized } from '@rocket.chat/core-typings'; |
||||
import { Field, FieldLabel, FieldRow, FieldError, TextInput, Button, ButtonGroup, FieldGroup, Box } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; |
||||
import { useToastMessageDispatch, useRouter, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import { useQueryClient } from '@tanstack/react-query'; |
||||
import React from 'react'; |
||||
import { useForm, Controller } from 'react-hook-form'; |
||||
|
||||
import AutoCompleteDepartmentMultiple from '../../../../client/components/AutoCompleteDepartmentMultiple'; |
||||
import { |
||||
ContextualbarScrollableContent, |
||||
ContextualbarFooter, |
||||
ContextualbarTitle, |
||||
Contextualbar, |
||||
ContextualbarHeader, |
||||
ContextualbarClose, |
||||
} from '../../../../client/components/Contextualbar'; |
||||
import { useRemoveTag } from './useRemoveTag'; |
||||
|
||||
type TagEditPayload = { |
||||
name: string; |
||||
description: string; |
||||
departments: { label: string; value: string }[]; |
||||
}; |
||||
|
||||
type TagEditProps = { |
||||
tagData?: ILivechatTag; |
||||
currentDepartments?: Serialized<ILivechatDepartment>[]; |
||||
}; |
||||
|
||||
const TagEdit = ({ tagData, currentDepartments }: TagEditProps) => { |
||||
const t = useTranslation(); |
||||
const router = useRouter(); |
||||
const queryClient = useQueryClient(); |
||||
const handleDeleteTag = useRemoveTag(); |
||||
|
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
const saveTag = useMethod('livechat:saveTag'); |
||||
|
||||
const { _id, name, description } = tagData || {}; |
||||
|
||||
const { |
||||
control, |
||||
formState: { isDirty, errors }, |
||||
handleSubmit, |
||||
} = useForm<TagEditPayload>({ |
||||
mode: 'onBlur', |
||||
values: { |
||||
name: name || '', |
||||
description: description || '', |
||||
departments: currentDepartments?.map((dep) => ({ label: dep.name, value: dep._id })) || [], |
||||
}, |
||||
}); |
||||
|
||||
const handleSave = useMutableCallback(async ({ name, description, departments }: TagEditPayload) => { |
||||
const departmentsId = departments?.map((dep) => dep.value) || ['']; |
||||
|
||||
try { |
||||
await saveTag(_id as unknown as string, { name, description }, departmentsId); |
||||
dispatchToastMessage({ type: 'success', message: t('Saved') }); |
||||
queryClient.invalidateQueries(['livechat-tags']); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} finally { |
||||
router.navigate('/omnichannel/tags'); |
||||
} |
||||
}); |
||||
|
||||
const formId = useUniqueId(); |
||||
const nameField = useUniqueId(); |
||||
const descriptionField = useUniqueId(); |
||||
const departmentsField = useUniqueId(); |
||||
|
||||
return ( |
||||
<Contextualbar> |
||||
<ContextualbarHeader> |
||||
<ContextualbarTitle>{_id ? t('Edit_Tag') : t('New_Tag')}</ContextualbarTitle> |
||||
<ContextualbarClose onClick={() => router.navigate('/omnichannel/tags')}></ContextualbarClose> |
||||
</ContextualbarHeader> |
||||
<ContextualbarScrollableContent> |
||||
<Box id={formId} is='form' autoComplete='off' onSubmit={handleSubmit(handleSave)}> |
||||
<FieldGroup> |
||||
<Field> |
||||
<FieldLabel htmlFor={nameField} required> |
||||
{t('Name')} |
||||
</FieldLabel> |
||||
<FieldRow> |
||||
<Controller |
||||
name='name' |
||||
control={control} |
||||
rules={{ required: t('The_field_is_required', 'name') }} |
||||
render={({ field }) => <TextInput {...field} error={errors?.name?.message} aria-describedby={`${nameField}-error`} />} |
||||
/> |
||||
</FieldRow> |
||||
{errors?.name && ( |
||||
<FieldError aria-live='assertive' id={`${nameField}-error`}> |
||||
{errors?.name?.message} |
||||
</FieldError> |
||||
)} |
||||
</Field> |
||||
<Field> |
||||
<FieldLabel htmlFor={descriptionField}>{t('Description')}</FieldLabel> |
||||
<FieldRow> |
||||
<Controller name='description' control={control} render={({ field }) => <TextInput id={descriptionField} {...field} />} /> |
||||
</FieldRow> |
||||
</Field> |
||||
<Field> |
||||
<FieldLabel htmlFor={departmentsField}>{t('Departments')}</FieldLabel> |
||||
<FieldRow> |
||||
<Controller |
||||
name='departments' |
||||
control={control} |
||||
render={({ field: { onChange, value, onBlur } }) => ( |
||||
<AutoCompleteDepartmentMultiple id={departmentsField} onChange={onChange} value={value} onBlur={onBlur} showArchived /> |
||||
)} |
||||
/> |
||||
</FieldRow> |
||||
</Field> |
||||
</FieldGroup> |
||||
</Box> |
||||
</ContextualbarScrollableContent> |
||||
<ContextualbarFooter> |
||||
<ButtonGroup stretch> |
||||
<Button onClick={() => router.navigate('/omnichannel/tags')}>{t('Cancel')}</Button> |
||||
<Button form={formId} disabled={!isDirty} type='submit' primary> |
||||
{t('Save')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
{_id && ( |
||||
<ButtonGroup stretch mbs={8}> |
||||
<Button icon='trash' danger onClick={() => handleDeleteTag(_id)}> |
||||
{t('Delete')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
)} |
||||
</ContextualbarFooter> |
||||
</Contextualbar> |
||||
); |
||||
}; |
||||
|
||||
export default TagEdit; |
||||
@ -1,38 +0,0 @@ |
||||
import { Callout } 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 '../../../../client/components/Skeleton'; |
||||
import TagEdit from './TagEdit'; |
||||
import TagEditWithDepartmentData from './TagEditWithDepartmentData'; |
||||
|
||||
function TagEditWithData({ tagId, reload, title }) { |
||||
const getTag = useEndpoint('GET', '/v1/livechat/tags/:tagId', { tagId }); |
||||
const { data, isLoading, isError } = useQuery(['/v1/livechat/tags/:tagId', tagId], () => getTag(), { enabled: Boolean(tagId) }); |
||||
const t = useTranslation(); |
||||
|
||||
if (isLoading && tagId) { |
||||
return <FormSkeleton />; |
||||
} |
||||
|
||||
if (isError) { |
||||
return ( |
||||
<Callout m={16} type='danger'> |
||||
{t('Not_Available')} |
||||
</Callout> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
{data && data.departments && data.departments.length > 0 ? ( |
||||
<TagEditWithDepartmentData tagId={tagId} data={data} reload={reload} title={title} /> |
||||
) : ( |
||||
<TagEdit tagId={tagId} data={data} reload={reload} title={title} /> |
||||
)} |
||||
</> |
||||
); |
||||
} |
||||
|
||||
export default TagEditWithData; |
||||
@ -0,0 +1,36 @@ |
||||
import type { ILivechatTag } from '@rocket.chat/core-typings'; |
||||
import { Callout } from '@rocket.chat/fuselage'; |
||||
import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import { useQuery } from '@tanstack/react-query'; |
||||
import React from 'react'; |
||||
|
||||
import { ContextualbarSkeleton } from '../../../../client/components/Contextualbar'; |
||||
import TagEdit from './TagEdit'; |
||||
import TagEditWithDepartmentData from './TagEditWithDepartmentData'; |
||||
|
||||
const TagEditWithData = ({ tagId }: { tagId: ILivechatTag['_id'] }) => { |
||||
const t = useTranslation(); |
||||
|
||||
const getTagById = useEndpoint('GET', '/v1/livechat/tags/:tagId', { tagId }); |
||||
const { data, isLoading, isError } = useQuery(['livechat-getTagById', tagId], async () => getTagById(), { refetchOnWindowFocus: false }); |
||||
|
||||
if (isLoading) { |
||||
return <ContextualbarSkeleton />; |
||||
} |
||||
|
||||
if (isError) { |
||||
return ( |
||||
<Callout m={16} type='danger'> |
||||
{t('Not_Available')} |
||||
</Callout> |
||||
); |
||||
} |
||||
|
||||
if (data?.departments && data.departments.length > 0) { |
||||
return <TagEditWithDepartmentData tagData={data} />; |
||||
} |
||||
|
||||
return <TagEdit tagData={data} />; |
||||
}; |
||||
|
||||
export default TagEditWithData; |
||||
@ -1,36 +0,0 @@ |
||||
import { useRouteParameter, usePermission, useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React, { useRef, useCallback } from 'react'; |
||||
|
||||
import NotAuthorizedPage from '../../../../client/views/notAuthorized/NotAuthorizedPage'; |
||||
import TagEdit from './TagEdit'; |
||||
import TagEditWithData from './TagEditWithData'; |
||||
import TagsPage from './TagsPage'; |
||||
|
||||
const TagsRoute = () => { |
||||
const t = useTranslation(); |
||||
const reload = useRef(() => null); |
||||
const canViewTags = usePermission('manage-livechat-tags'); |
||||
|
||||
const handleReload = useCallback(() => { |
||||
reload.current(); |
||||
}, []); |
||||
|
||||
const context = useRouteParameter('context'); |
||||
const id = useRouteParameter('id'); |
||||
|
||||
if (context === 'edit') { |
||||
return <TagEditWithData reload={handleReload} tagId={id} title={t('Edit_Tag')} />; |
||||
} |
||||
|
||||
if (context === 'new') { |
||||
return <TagEdit reload={handleReload} title={t('New_Tag')} />; |
||||
} |
||||
|
||||
if (!canViewTags) { |
||||
return <NotAuthorizedPage />; |
||||
} |
||||
|
||||
return <TagsPage reload={reload} />; |
||||
}; |
||||
|
||||
export default TagsRoute; |
||||
@ -0,0 +1,17 @@ |
||||
import { usePermission } from '@rocket.chat/ui-contexts'; |
||||
import React from 'react'; |
||||
|
||||
import NotAuthorizedPage from '../../../../client/views/notAuthorized/NotAuthorizedPage'; |
||||
import TagsPage from './TagsPage'; |
||||
|
||||
const TagsRoute = () => { |
||||
const canViewTags = usePermission('manage-livechat-tags'); |
||||
|
||||
if (!canViewTags) { |
||||
return <NotAuthorizedPage />; |
||||
} |
||||
|
||||
return <TagsPage />; |
||||
}; |
||||
|
||||
export default TagsRoute; |
||||
@ -0,0 +1,34 @@ |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import { useSetModal, useToastMessageDispatch, useRouter, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import { useQueryClient } from '@tanstack/react-query'; |
||||
import React from 'react'; |
||||
|
||||
import GenericModal from '../../../../client/components/GenericModal'; |
||||
|
||||
export const useRemoveTag = () => { |
||||
const t = useTranslation(); |
||||
const setModal = useSetModal(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
const removeTag = useMethod('livechat:removeTag'); |
||||
const queryClient = useQueryClient(); |
||||
const router = useRouter(); |
||||
|
||||
const handleDeleteTag = useMutableCallback((tagId) => { |
||||
const handleDelete = async () => { |
||||
try { |
||||
await removeTag(tagId); |
||||
dispatchToastMessage({ type: 'success', message: t('Tag_removed') }); |
||||
router.navigate('/omnichannel/tags'); |
||||
queryClient.invalidateQueries(['livechat-tags']); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} finally { |
||||
setModal(); |
||||
} |
||||
}; |
||||
|
||||
setModal(<GenericModal variant='danger' onConfirm={handleDelete} onCancel={() => setModal()} confirmText={t('Delete')} />); |
||||
}); |
||||
|
||||
return handleDeleteTag; |
||||
}; |
||||
Loading…
Reference in new issue