Refactor some React Pages and Components (#19202)
parent
9716730901
commit
fe0c27ab38
@ -0,0 +1,48 @@ |
||||
import { ButtonGroup, Button, Box, Icon, PasswordInput, TextInput, Modal } from '@rocket.chat/fuselage'; |
||||
import React, { useState, useCallback, FC } from 'react'; |
||||
|
||||
import { useTranslation } from '../contexts/TranslationContext'; |
||||
|
||||
type ActionConfirmModalProps = { |
||||
title: string; |
||||
text: string; |
||||
isPassword: boolean; |
||||
onSave: (input: string) => void; |
||||
onCancel: () => void; |
||||
}; |
||||
|
||||
const ActionConfirmModal: FC<ActionConfirmModalProps> = ({ |
||||
title, |
||||
text, |
||||
isPassword, |
||||
onSave, |
||||
onCancel, |
||||
...props |
||||
}) => { |
||||
const t = useTranslation(); |
||||
const [inputText, setInputText] = useState(''); |
||||
|
||||
const handleChange = useCallback((e) => setInputText(e.currentTarget.value), [setInputText]); |
||||
const handleSave = useCallback(() => { onSave(inputText); onCancel(); }, [inputText, onSave, onCancel]); |
||||
|
||||
return <Modal {...props}> |
||||
<Modal.Header> |
||||
<Icon color='danger' name='modal-warning' size={20}/> |
||||
<Modal.Title>{title}</Modal.Title> |
||||
<Modal.Close onClick={onCancel}/> |
||||
</Modal.Header> |
||||
<Modal.Content fontScale='p1'> |
||||
<Box mb='x8'>{text}</Box> |
||||
{isPassword && <PasswordInput w='full' value={inputText} onChange={handleChange}/>} |
||||
{!isPassword && <TextInput w='full' value={inputText} onChange={handleChange}/>} |
||||
</Modal.Content> |
||||
<Modal.Footer> |
||||
<ButtonGroup align='end'> |
||||
<Button ghost onClick={onCancel}>{t('Cancel')}</Button> |
||||
<Button primary danger onClick={handleSave}>{t('Continue')}</Button> |
||||
</ButtonGroup> |
||||
</Modal.Footer> |
||||
</Modal>; |
||||
}; |
||||
|
||||
export default ActionConfirmModal; |
||||
@ -0,0 +1,32 @@ |
||||
import React, { FC } from 'react'; |
||||
import { ButtonGroup, Button, Icon, Box, Modal } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
|
||||
type MyDataModalProps = { |
||||
onCancel: () => void; |
||||
title: string; |
||||
text: string; |
||||
}; |
||||
|
||||
const MyDataModal: FC<MyDataModalProps> = ({ onCancel, title, text, ...props }) => { |
||||
const t = useTranslation(); |
||||
|
||||
return <Modal {...props}> |
||||
<Modal.Header> |
||||
<Icon color='success' name='circle-check' size={20}/> |
||||
<Modal.Title>{title}</Modal.Title> |
||||
<Modal.Close onClick={onCancel}/> |
||||
</Modal.Header> |
||||
<Modal.Content fontScale='p1'> |
||||
<Box mb='x8'>{text}</Box> |
||||
</Modal.Content> |
||||
<Modal.Footer> |
||||
<ButtonGroup align='end'> |
||||
<Button primary onClick={onCancel}>{t('Ok')}</Button> |
||||
</ButtonGroup> |
||||
</Modal.Footer> |
||||
</Modal>; |
||||
}; |
||||
|
||||
export default MyDataModal; |
||||
@ -0,0 +1,36 @@ |
||||
import React, { FC, useMemo } from 'react'; |
||||
import { Box, Button, Icon, ButtonGroup, Modal } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import TextCopy from '../../components/basic/TextCopy'; |
||||
|
||||
type BackupCodesModalProps = { |
||||
codes: string[]; |
||||
onClose: () => void; |
||||
}; |
||||
|
||||
const BackupCodesModal: FC<BackupCodesModalProps> = ({ codes, onClose, ...props }) => { |
||||
const t = useTranslation(); |
||||
|
||||
const codesText = useMemo(() => codes.join(' '), [codes]); |
||||
|
||||
return <Modal {...props}> |
||||
<Modal.Header> |
||||
<Icon name='info' size={20}/> |
||||
<Modal.Title>{t('Backup_codes')}</Modal.Title> |
||||
<Modal.Close onClick={onClose}/> |
||||
</Modal.Header> |
||||
<Modal.Content fontScale='p1'> |
||||
<Box mb='x8' withRichContent>{t('Make_sure_you_have_a_copy_of_your_codes_1')}</Box> |
||||
<TextCopy text={codesText} wordBreak='break-word' mb='x8' /> |
||||
<Box mb='x8' withRichContent>{t('Make_sure_you_have_a_copy_of_your_codes_2')}</Box> |
||||
</Modal.Content> |
||||
<Modal.Footer> |
||||
<ButtonGroup align='end'> |
||||
<Button primary onClick={onClose}>{t('Ok')}</Button> |
||||
</ButtonGroup> |
||||
</Modal.Footer> |
||||
</Modal>; |
||||
}; |
||||
|
||||
export default BackupCodesModal; |
||||
@ -0,0 +1,55 @@ |
||||
import React, { FC, useCallback, useEffect, useRef } from 'react'; |
||||
import { Box, Button, TextInput, Icon, ButtonGroup, Modal } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useForm } from '../../hooks/useForm'; |
||||
|
||||
type VerifyCodeModalProps = { |
||||
onVerify: (code: string) => void; |
||||
onCancel: () => void; |
||||
}; |
||||
|
||||
const VerifyCodeModal: FC<VerifyCodeModalProps> = ({ onVerify, onCancel, ...props }) => { |
||||
const t = useTranslation(); |
||||
|
||||
const ref = useRef<HTMLInputElement>(); |
||||
|
||||
useEffect(() => { |
||||
if (typeof ref?.current?.focus === 'function') { |
||||
ref.current.focus(); |
||||
} |
||||
}, [ref]); |
||||
|
||||
const { values, handlers } = useForm({ code: '' }); |
||||
|
||||
const { code } = values as { code: string }; |
||||
const { handleCode } = handlers; |
||||
|
||||
const handleVerify = useCallback((e) => { |
||||
if (e.type === 'click' || (e.type === 'keydown' && e.keyCode === 13)) { |
||||
onVerify(code); |
||||
} |
||||
}, [code, onVerify]); |
||||
|
||||
return <Modal {...props}> |
||||
<Modal.Header> |
||||
<Icon name='info' size={20}/> |
||||
<Modal.Title>{t('Two-factor_authentication')}</Modal.Title> |
||||
<Modal.Close onClick={onCancel}/> |
||||
</Modal.Header> |
||||
<Modal.Content fontScale='p1'> |
||||
<Box mbe='x8'>{t('Open_your_authentication_app_and_enter_the_code')}</Box> |
||||
<Box display='flex' alignItems='stretch'> |
||||
<TextInput ref={ref} placeholder={t('Enter_authentication_code')} value={code} onChange={handleCode} onKeyDown={handleVerify}/> |
||||
</Box> |
||||
</Modal.Content> |
||||
<Modal.Footer> |
||||
<ButtonGroup align='end'> |
||||
<Button onClick={onCancel}>{t('Cancel')}</Button> |
||||
<Button primary onClick={handleVerify}>{t('Verify')}</Button> |
||||
</ButtonGroup> |
||||
</Modal.Footer> |
||||
</Modal>; |
||||
}; |
||||
|
||||
export default VerifyCodeModal; |
||||
@ -0,0 +1,45 @@ |
||||
import { Button, ButtonGroup, Icon, Table } from '@rocket.chat/fuselage'; |
||||
import React, { useCallback, FC } from 'react'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime'; |
||||
|
||||
type AccountTokensRowProps = { |
||||
bypassTwoFactor: unknown; |
||||
createdAt: unknown; |
||||
isMedium: boolean; |
||||
lastTokenPart: string; |
||||
name: string; |
||||
onRegenerate: (name: string) => void; |
||||
onRemove: (name: string) => void; |
||||
}; |
||||
|
||||
const AccountTokensRow: FC<AccountTokensRowProps> = ({ |
||||
bypassTwoFactor, |
||||
createdAt, |
||||
isMedium, |
||||
lastTokenPart, |
||||
name, |
||||
onRegenerate, |
||||
onRemove, |
||||
}) => { |
||||
const t = useTranslation(); |
||||
const formatDateAndTime = useFormatDateAndTime(); |
||||
const handleRegenerate = useCallback(() => onRegenerate(name), [name, onRegenerate]); |
||||
const handleRemove = useCallback(() => onRemove(name), [name, onRemove]); |
||||
|
||||
return <Table.Row key={name} tabIndex={0} role='link' action qa-token-name={name}> |
||||
<Table.Cell withTruncatedText color='default' fontScale='p2'>{name}</Table.Cell> |
||||
{isMedium && <Table.Cell withTruncatedText>{formatDateAndTime(createdAt)}</Table.Cell>} |
||||
<Table.Cell withTruncatedText>...{lastTokenPart}</Table.Cell> |
||||
<Table.Cell withTruncatedText>{bypassTwoFactor ? t('Ignore') : t('Require')}</Table.Cell> |
||||
<Table.Cell withTruncatedText> |
||||
<ButtonGroup> |
||||
<Button onClick={handleRegenerate} small><Icon name='refresh' size='x16'/></Button> |
||||
<Button onClick={handleRemove} small><Icon name='trash' size='x16'/></Button> |
||||
</ButtonGroup> |
||||
</Table.Cell> |
||||
</Table.Row>; |
||||
}; |
||||
|
||||
export default AccountTokensRow; |
||||
@ -1,7 +1,26 @@ |
||||
import React from 'react'; |
||||
import React, { FC, ReactNode } from 'react'; |
||||
import { Button, ButtonGroup, Modal } from '@rocket.chat/fuselage'; |
||||
|
||||
const InfoModal = ({ title, content, icon, onConfirm, onClose, confirmText, cancelText, ...props }) => |
||||
type InfoModalProps = { |
||||
title: string; |
||||
content: ReactNode; |
||||
icon: ReactNode; |
||||
confirmText: string; |
||||
cancelText: string; |
||||
onConfirm: () => void; |
||||
onClose: () => void; |
||||
}; |
||||
|
||||
const InfoModal: FC<InfoModalProps> = ({ |
||||
title, |
||||
content, |
||||
icon, |
||||
confirmText, |
||||
cancelText, |
||||
onConfirm, |
||||
onClose, |
||||
...props |
||||
}) => |
||||
<Modal {...props}> |
||||
<Modal.Header> |
||||
{icon} |
||||
@ -0,0 +1,40 @@ |
||||
import { Box, Divider } from '@rocket.chat/fuselage'; |
||||
import React, { FC } from 'react'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useAbsoluteUrl } from '../../contexts/ServerContext'; |
||||
import { apiCurlGetter } from './helpers'; |
||||
|
||||
type APIsDisplayProps = { |
||||
apis: { |
||||
path: string; |
||||
computedPath: string; |
||||
methods: unknown[]; |
||||
examples: Record<string, unknown>; |
||||
}[]; |
||||
}; |
||||
|
||||
const APIsDisplay: FC<APIsDisplayProps> = ({ apis }) => { |
||||
const t = useTranslation(); |
||||
|
||||
const absoluteUrl = useAbsoluteUrl(); |
||||
|
||||
const getApiCurl = apiCurlGetter(absoluteUrl); |
||||
|
||||
return <> |
||||
<Divider /> |
||||
<Box display='flex' flexDirection='column'> |
||||
<Box fontScale='s2' mb='x12'>{t('APIs')}</Box> |
||||
{apis.map((api) => <Box key={api.path} mb='x8'> |
||||
<Box fontScale='p2'>{api.methods.join(' | ').toUpperCase()} {api.path}</Box> |
||||
{api.methods.map((method) => <Box> |
||||
<Box withRichContent><pre><code> |
||||
{getApiCurl(method, api).map((curlAddress) => <>{curlAddress}<br /></>)} |
||||
</code></pre></Box> |
||||
</Box>)} |
||||
</Box>)} |
||||
</Box> |
||||
</>; |
||||
}; |
||||
|
||||
export default APIsDisplay; |
||||
@ -0,0 +1,106 @@ |
||||
import React, { FC } from 'react'; |
||||
import { Box, Chip, Divider, Margins } from '@rocket.chat/fuselage'; |
||||
|
||||
import AppAvatar from '../../components/basic/avatar/AppAvatar'; |
||||
import ExternalLink from '../../components/basic/ExternalLink'; |
||||
import PriceDisplay from './PriceDisplay'; |
||||
import AppStatus from './AppStatus'; |
||||
import AppMenu from './AppMenu'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { App } from './types'; |
||||
|
||||
type AppDetailsPageContentProps = { |
||||
data: App; |
||||
}; |
||||
|
||||
const AppDetailsPageContent: FC<AppDetailsPageContentProps> = ({ data }) => { |
||||
const t = useTranslation(); |
||||
|
||||
const { |
||||
iconFileData = '', |
||||
name, |
||||
author: { name: authorName, homepage, support }, |
||||
description, |
||||
categories = [], |
||||
version, |
||||
price, |
||||
purchaseType, |
||||
pricingPlans, |
||||
iconFileContent, |
||||
installed, |
||||
bundledIn, |
||||
} = data; |
||||
|
||||
return <> |
||||
<Box display='flex' flexDirection='row' mbe='x20' w='full'> |
||||
<AppAvatar size='x120' mie='x20' iconFileContent={iconFileContent} iconFileData={iconFileData}/> |
||||
<Box display='flex' flexDirection='column' justifyContent='space-between' flexGrow={1}> |
||||
<Box fontScale='h1'>{name}</Box> |
||||
<Box display='flex' flexDirection='row' color='hint' alignItems='center'> |
||||
<Box fontScale='p2' mie='x4'>{t('By_author', { author: authorName })}</Box> |
||||
| |
||||
<Box mis='x4'>{t('Version_version', { version })}</Box> |
||||
</Box> |
||||
<Box display='flex' flexDirection='row' alignItems='center' justifyContent='space-between'> |
||||
<Box flexGrow={1} display='flex' flexDirection='row' alignItems='center' marginInline='neg-x8'> |
||||
<AppStatus app={data} marginInline='x8'/> |
||||
{!installed && <PriceDisplay |
||||
purchaseType={purchaseType} |
||||
pricingPlans={pricingPlans} |
||||
price={price} |
||||
showType={false} |
||||
marginInline='x8' |
||||
/>} |
||||
</Box> |
||||
{installed && <AppMenu app={data} />} |
||||
</Box> |
||||
</Box> |
||||
</Box> |
||||
<Divider /> |
||||
<Box display='flex' flexDirection='column'> |
||||
<Margins block='x12'> |
||||
<Box fontScale='s2'>{t('Categories')}</Box> |
||||
<Box display='flex' flexDirection='row'> |
||||
{categories && categories.map((current) => |
||||
<Chip key={current} textTransform='uppercase' mie='x8'> |
||||
<Box color='hint'>{current}</Box> |
||||
</Chip>)} |
||||
</Box> |
||||
|
||||
<Box fontScale='s2'>{t('Contact')}</Box> |
||||
<Box display='flex' flexDirection='row' flexGrow={1} justifyContent='space-around' flexWrap='wrap'> |
||||
<Box display='flex' flexDirection='column' mie='x12' flexGrow={1}> |
||||
<Box fontScale='s1' color='hint'>{t('Author_Site')}</Box> |
||||
<ExternalLink to={homepage} /> |
||||
</Box> |
||||
<Box display='flex' flexDirection='column' flexGrow={1}> |
||||
<Box fontScale='s1' color='hint'>{t('Support')}</Box> |
||||
<ExternalLink to={support} /> |
||||
</Box> |
||||
</Box> |
||||
|
||||
<Box fontScale='s2'>{t('Details')}</Box> |
||||
<Box display='flex' flexDirection='row'>{description}</Box> |
||||
</Margins> |
||||
</Box> |
||||
{bundledIn && <> |
||||
<Divider /> |
||||
<Box display='flex' flexDirection='column'> |
||||
<Margins block='x12'> |
||||
<Box fontScale='s2'>{t('Bundles')}</Box> |
||||
{bundledIn.map((bundle) => <Box key={bundle.bundleId} display='flex' flexDirection='row' alignItems='center'> |
||||
<Box width='x80' height='x80' display='flex' flexDirection='row' justifyContent='space-around' flexWrap='wrap' flexShrink={0}> |
||||
{bundle.apps.map((app) => <AppAvatar size='x36' key={app.latest.name} iconFileContent={app.latest.iconFileContent} iconFileData={app.latest.iconFileData}/>)} |
||||
</Box> |
||||
<Box display='flex' flexDirection='column' mis='x12'> |
||||
<Box fontScale='p2'>{bundle.bundleName}</Box> |
||||
{bundle.apps.map((app) => <Box key={app.latest.name}>{app.latest.name},</Box>)} |
||||
</Box> |
||||
</Box>)} |
||||
</Margins> |
||||
</Box> |
||||
</>} |
||||
</>; |
||||
}; |
||||
|
||||
export default AppDetailsPageContent; |
||||
@ -0,0 +1,94 @@ |
||||
import { Box, Table, Tag } from '@rocket.chat/fuselage'; |
||||
import React, { FC, useState, memo, KeyboardEvent, MouseEvent } from 'react'; |
||||
|
||||
import AppAvatar from '../../components/basic/avatar/AppAvatar'; |
||||
import { useRoute } from '../../contexts/RouterContext'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import AppMenu from './AppMenu'; |
||||
import AppStatus from './AppStatus'; |
||||
import { App } from './types'; |
||||
|
||||
type AppRowProps = App & { |
||||
medium: boolean; |
||||
}; |
||||
|
||||
const AppRow: FC<AppRowProps> = ({ |
||||
medium, |
||||
...props |
||||
}) => { |
||||
const { |
||||
author: { name: authorName }, |
||||
name, |
||||
id, |
||||
description, |
||||
categories, |
||||
iconFileData, |
||||
marketplaceVersion, |
||||
iconFileContent, |
||||
installed, |
||||
} = props; |
||||
const t = useTranslation(); |
||||
|
||||
const [isFocused, setFocused] = useState(false); |
||||
const [isHovered, setHovered] = useState(false); |
||||
const isStatusVisible = isFocused || isHovered; |
||||
|
||||
const appsRoute = useRoute('admin-apps'); |
||||
|
||||
const handleClick = (): void => { |
||||
appsRoute.push({ |
||||
context: 'details', |
||||
version: marketplaceVersion, |
||||
id, |
||||
}); |
||||
}; |
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLOrSVGElement>): void => { |
||||
if (!['Enter', 'Space'].includes(e.nativeEvent.code)) { |
||||
return; |
||||
} |
||||
|
||||
handleClick(); |
||||
}; |
||||
|
||||
const preventClickPropagation = (e: MouseEvent<HTMLOrSVGElement>): void => { |
||||
e.stopPropagation(); |
||||
}; |
||||
|
||||
return <Table.Row |
||||
key={id} |
||||
role='link' |
||||
action |
||||
tabIndex={0} |
||||
onClick={handleClick} |
||||
onKeyDown={handleKeyDown} |
||||
onFocus={(): void => setFocused(true)} |
||||
onBlur={(): void => setFocused(false)} |
||||
onMouseEnter={(): void => setHovered(true)} |
||||
onMouseLeave={(): void => setHovered(false)} |
||||
> |
||||
<Table.Cell withTruncatedText display='flex' flexDirection='row'> |
||||
<AppAvatar size='x40' mie='x8' alignSelf='center' iconFileContent={iconFileContent} iconFileData={iconFileData}/> |
||||
<Box display='flex' flexDirection='column' alignSelf='flex-start'> |
||||
<Box color='default' fontScale='p2'>{name}</Box> |
||||
<Box color='default' fontScale='p2'>{`${ t('By') } ${ authorName }`}</Box> |
||||
</Box> |
||||
</Table.Cell> |
||||
{medium && <Table.Cell> |
||||
<Box display='flex' flexDirection='column'> |
||||
<Box color='default' withTruncatedText>{description}</Box> |
||||
{categories && <Box color='hint' display='flex' flex-direction='row' withTruncatedText> |
||||
{categories.map((current) => <Tag disabled key={current} mie='x4'>{current}</Tag>)} |
||||
</Box>} |
||||
</Box> |
||||
</Table.Cell>} |
||||
<Table.Cell withTruncatedText> |
||||
<Box display='flex' flexDirection='row' alignItems='center' marginInline='neg-x8' onClick={preventClickPropagation}> |
||||
<AppStatus app={props} showStatus={isStatusVisible} marginInline='x8'/> |
||||
{installed && <AppMenu app={props} invisible={!isStatusVisible} marginInline='x8'/>} |
||||
</Box> |
||||
</Table.Cell> |
||||
</Table.Row>; |
||||
}; |
||||
|
||||
export default memo(AppRow); |
||||
@ -0,0 +1,14 @@ |
||||
import React, { FC } from 'react'; |
||||
import { Box, Skeleton } from '@rocket.chat/fuselage'; |
||||
|
||||
const LoadingDetails: FC = () => |
||||
<Box display='flex' flexDirection='row' mbe='x20' w='full'> |
||||
<Skeleton variant='rect' w='x120' h='x120' mie='x20'/> |
||||
<Box display='flex' flexDirection='column' justifyContent='space-between' flexGrow={1}> |
||||
<Skeleton variant='rect' w='full' h='x32'/> |
||||
<Skeleton variant='rect' w='full' h='x32'/> |
||||
<Skeleton variant='rect' w='full' h='x32'/> |
||||
</Box> |
||||
</Box>; |
||||
|
||||
export default LoadingDetails; |
||||
@ -0,0 +1,31 @@ |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import React, { FC } from 'react'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useHighlightedCode } from '../../hooks/useHighlightedCode'; |
||||
|
||||
type LogEntryProps = { |
||||
severity: string; |
||||
timestamp: string; |
||||
caller: string; |
||||
args: unknown; |
||||
}; |
||||
|
||||
const LogEntry: FC<LogEntryProps> = ({ severity, timestamp, caller, args }) => { |
||||
const t = useTranslation(); |
||||
|
||||
return <Box> |
||||
<Box>{severity}: {timestamp} {t('Caller')}: {caller}</Box> |
||||
<Box withRichContent width='full'> |
||||
<pre> |
||||
<code |
||||
dangerouslySetInnerHTML={{ |
||||
__html: useHighlightedCode('json', JSON.stringify(args, null, 2)), |
||||
}} |
||||
/> |
||||
</pre> |
||||
</Box> |
||||
</Box>; |
||||
}; |
||||
|
||||
export default LogEntry; |
||||
@ -0,0 +1,33 @@ |
||||
import { Box, Accordion } from '@rocket.chat/fuselage'; |
||||
import React, { FC } from 'react'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import LogEntry from './LogEntry'; |
||||
|
||||
type LogItemProps = { |
||||
entries: { |
||||
severity: string; |
||||
timestamp: string; |
||||
caller: string; |
||||
args: unknown; |
||||
}[]; |
||||
instanceId: string; |
||||
title: string; |
||||
}; |
||||
|
||||
const LogItem: FC<LogItemProps> = ({ entries, instanceId, title, ...props }) => { |
||||
const t = useTranslation(); |
||||
|
||||
return <Accordion.Item title={title} {...props}> |
||||
{instanceId && <Box>{t('Instance')}: {instanceId}</Box>} |
||||
{entries.map(({ severity, timestamp, caller, args }, i) => <LogEntry |
||||
key={i} |
||||
severity={severity} |
||||
timestamp={timestamp} |
||||
caller={caller} |
||||
args={args} |
||||
/>)} |
||||
</Accordion.Item>; |
||||
}; |
||||
|
||||
export default LogItem; |
||||
@ -0,0 +1,13 @@ |
||||
import { Box, Skeleton, Margins } from '@rocket.chat/fuselage'; |
||||
import React, { FC } from 'react'; |
||||
|
||||
const LogsLoading: FC = () => |
||||
<Box maxWidth='x600' w='full' alignSelf='center'> |
||||
<Margins block='x2'> |
||||
<Skeleton variant='rect' width='100%' height='x80' /> |
||||
<Skeleton variant='rect' width='100%' height='x80' /> |
||||
<Skeleton variant='rect' width='100%' height='x80' /> |
||||
</Margins> |
||||
</Box>; |
||||
|
||||
export default LogsLoading; |
||||
@ -0,0 +1,103 @@ |
||||
import { Box, Table, Tag } from '@rocket.chat/fuselage'; |
||||
import React, { useState, memo, FC, KeyboardEvent, MouseEvent } from 'react'; |
||||
|
||||
import AppAvatar from '../../components/basic/avatar/AppAvatar'; |
||||
import { useRoute } from '../../contexts/RouterContext'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import AppMenu from './AppMenu'; |
||||
import AppStatus from './AppStatus'; |
||||
import PriceDisplay from './PriceDisplay'; |
||||
import { App } from './types'; |
||||
|
||||
type MarketplaceRowProps = { |
||||
medium?: boolean; |
||||
large?: boolean; |
||||
} & App; |
||||
|
||||
const MarketplaceRow: FC<MarketplaceRowProps> = ({ |
||||
medium, |
||||
large, |
||||
...props |
||||
}) => { |
||||
const { |
||||
author: { name: authorName }, |
||||
name, |
||||
id, |
||||
description, |
||||
categories, |
||||
purchaseType, |
||||
pricingPlans, |
||||
price, |
||||
iconFileData, |
||||
marketplaceVersion, |
||||
iconFileContent, |
||||
installed, |
||||
} = props; |
||||
const t = useTranslation(); |
||||
|
||||
const [isFocused, setFocused] = useState(false); |
||||
const [isHovered, setHovered] = useState(false); |
||||
const isStatusVisible = isFocused || isHovered; |
||||
|
||||
const marketplaceRoute = useRoute('admin-marketplace'); |
||||
|
||||
const handleClick = (): void => { |
||||
marketplaceRoute.push({ |
||||
context: 'details', |
||||
version: marketplaceVersion, |
||||
id, |
||||
}); |
||||
}; |
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLOrSVGElement>): void => { |
||||
if (!['Enter', 'Space'].includes(e.nativeEvent.code)) { |
||||
return; |
||||
} |
||||
|
||||
handleClick(); |
||||
}; |
||||
|
||||
const preventClickPropagation = (e: MouseEvent<HTMLOrSVGElement>): void => { |
||||
e.stopPropagation(); |
||||
}; |
||||
|
||||
return <Table.Row |
||||
key={id} |
||||
role='link' |
||||
action |
||||
tabIndex={0} |
||||
onClick={handleClick} |
||||
onKeyDown={handleKeyDown} |
||||
onFocus={(): void => setFocused(true)} |
||||
onBlur={(): void => setFocused(false)} |
||||
onMouseEnter={(): void => setHovered(true)} |
||||
onMouseLeave={(): void => setHovered(false)} |
||||
> |
||||
<Table.Cell withTruncatedText display='flex' flexDirection='row'> |
||||
<AppAvatar size='x40' mie='x8' alignSelf='center' iconFileContent={iconFileContent} iconFileData={iconFileData}/> |
||||
<Box display='flex' flexDirection='column' alignSelf='flex-start'> |
||||
<Box color='default' fontScale='p2'>{name}</Box> |
||||
<Box color='default' fontScale='p2'>{`${ t('By') } ${ authorName }`}</Box> |
||||
</Box> |
||||
</Table.Cell> |
||||
{large && <Table.Cell> |
||||
<Box display='flex' flexDirection='column'> |
||||
<Box color='default' withTruncatedText>{description}</Box> |
||||
{categories && <Box color='hint' display='flex' flex-direction='row' withTruncatedText> |
||||
{categories.map((current) => <Tag disabled key={current} mie='x4'>{current}</Tag>)} |
||||
</Box>} |
||||
</Box> |
||||
</Table.Cell>} |
||||
{medium && <Table.Cell> |
||||
<PriceDisplay {...{ purchaseType, pricingPlans, price }} /> |
||||
</Table.Cell>} |
||||
<Table.Cell withTruncatedText> |
||||
<Box display='flex' flexDirection='row' alignItems='center' marginInline='neg-x8' onClick={preventClickPropagation}> |
||||
<AppStatus app={props} showStatus={isStatusVisible} marginInline='x8'/> |
||||
{installed && <AppMenu app={props} invisible={!isStatusVisible} marginInline='x8'/>} |
||||
</Box> |
||||
</Table.Cell> |
||||
</Table.Row>; |
||||
}; |
||||
|
||||
export default memo(MarketplaceRow); |
||||
@ -0,0 +1,50 @@ |
||||
import React, { FC, useMemo, useEffect, MutableRefObject } from 'react'; |
||||
import { Box, Divider } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useForm } from '../../hooks/useForm'; |
||||
import { AppSettingsAssembler } from './AppSettings'; |
||||
import { ISetting } from '../../../definition/ISetting'; |
||||
|
||||
type SettingsDisplayProps = { |
||||
settings: { |
||||
[id: string]: ISetting & { id: ISetting['_id'] }; |
||||
}; |
||||
setHasUnsavedChanges: (hasUnsavedChanges: boolean) => void; |
||||
settingsRef: MutableRefObject<Record<string, ISetting['value']>>; |
||||
}; |
||||
|
||||
const SettingsDisplay: FC<SettingsDisplayProps> = ({ |
||||
settings, |
||||
setHasUnsavedChanges, |
||||
settingsRef, |
||||
}) => { |
||||
const t = useTranslation(); |
||||
|
||||
const stringifiedSettings = JSON.stringify(settings); |
||||
|
||||
const reducedSettings = useMemo(() => { |
||||
const settings: SettingsDisplayProps['settings'] = JSON.parse(stringifiedSettings); |
||||
return Object.values(settings) |
||||
.reduce((ret, { id, value, packageValue }) => ({ ...ret, [id]: value ?? packageValue }), {}); |
||||
}, [stringifiedSettings]); |
||||
|
||||
const { values, handlers, hasUnsavedChanges } = useForm(reducedSettings); |
||||
const stringifiedValues = JSON.stringify(values); |
||||
|
||||
useEffect(() => { |
||||
const values = JSON.parse(stringifiedValues); |
||||
setHasUnsavedChanges(hasUnsavedChanges); |
||||
settingsRef.current = values; |
||||
}, [hasUnsavedChanges, stringifiedValues, setHasUnsavedChanges, settingsRef]); |
||||
|
||||
return <> |
||||
<Divider /> |
||||
<Box display='flex' flexDirection='column'> |
||||
<Box fontScale='s2' mb='x12'>{t('Settings')}</Box> |
||||
<AppSettingsAssembler settings={settings} values={values} handlers={handlers}/> |
||||
</Box> |
||||
</>; |
||||
}; |
||||
|
||||
export default SettingsDisplay; |
||||
@ -0,0 +1,27 @@ |
||||
export type App = { |
||||
id: string; |
||||
iconFileData: string; |
||||
name: string; |
||||
author: { |
||||
name: string; |
||||
homepage: string; |
||||
support: string; |
||||
}; |
||||
description: string; |
||||
categories: string[]; |
||||
version: string; |
||||
price: string; |
||||
purchaseType: unknown[]; |
||||
pricingPlans: unknown[]; |
||||
iconFileContent: unknown; |
||||
installed?: boolean; |
||||
bundledIn: { |
||||
bundleId: string; |
||||
bundleName: string; |
||||
apps: App[]; |
||||
}[]; |
||||
marketplaceVersion: string; |
||||
latest: App; |
||||
status: unknown; |
||||
marketplace: unknown; |
||||
}; |
||||
@ -0,0 +1,87 @@ |
||||
import { Box, Button, ButtonGroup, Icon, Scrollable, Modal } from '@rocket.chat/fuselage'; |
||||
import Clipboard from 'clipboard'; |
||||
import React, { useEffect, useState, useRef, FC } from 'react'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import MarkdownText from '../../components/basic/MarkdownText'; |
||||
import { cloudConsoleUrl } from './constants'; |
||||
|
||||
type CopyStepProps = { |
||||
onNextButtonClick: () => void; |
||||
}; |
||||
|
||||
const CopyStep: FC<CopyStepProps> = ({ onNextButtonClick }) => { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const [clientKey, setClientKey] = useState(''); |
||||
|
||||
const getWorkspaceRegisterData = useMethod('cloud:getWorkspaceRegisterData'); |
||||
|
||||
useEffect(() => { |
||||
const loadWorkspaceRegisterData = async (): Promise<void> => { |
||||
const clientKey = await getWorkspaceRegisterData(); |
||||
setClientKey(clientKey); |
||||
}; |
||||
|
||||
loadWorkspaceRegisterData(); |
||||
}, [getWorkspaceRegisterData]); |
||||
|
||||
const copyRef = useRef<Element>(); |
||||
|
||||
useEffect(() => { |
||||
if (!copyRef.current) { |
||||
return; |
||||
} |
||||
|
||||
const clipboard = new Clipboard(copyRef.current); |
||||
clipboard.on('success', () => { |
||||
dispatchToastMessage({ type: 'success', message: t('Copied') }); |
||||
}); |
||||
|
||||
return (): void => { |
||||
clipboard.destroy(); |
||||
}; |
||||
}, [dispatchToastMessage, t]); |
||||
|
||||
return <> |
||||
<Modal.Content> |
||||
<Box withRichContent> |
||||
<p>{t('Cloud_register_offline_helper')}</p> |
||||
</Box> |
||||
<Box |
||||
display='flex' |
||||
flexDirection='column' |
||||
alignItems='stretch' |
||||
padding='x16' |
||||
flexGrow={1} |
||||
backgroundColor='neutral-800' |
||||
> |
||||
<Scrollable vertical> |
||||
<Box |
||||
height='x108' |
||||
fontFamily='mono' |
||||
fontScale='p1' |
||||
color='alternative' |
||||
style={{ wordBreak: 'break-all' }} |
||||
> |
||||
{clientKey} |
||||
</Box> |
||||
</Scrollable> |
||||
<Button ref={copyRef} primary data-clipboard-text={clientKey}> |
||||
<Icon name='copy' /> {t('Copy')} |
||||
</Button> |
||||
</Box> |
||||
<MarkdownText is='p' preserveHtml={true} withRichContent content={t('Cloud_click_here', { cloudConsoleUrl })} /> |
||||
</Modal.Content> |
||||
<Modal.Footer> |
||||
<ButtonGroup> |
||||
<Button primary onClick={onNextButtonClick}>{t('Next')}</Button> |
||||
</ButtonGroup> |
||||
</Modal.Footer> |
||||
</>; |
||||
}; |
||||
|
||||
export default CopyStep; |
||||
@ -0,0 +1,84 @@ |
||||
import { Box, Button, ButtonGroup, Scrollable, Throbber, Modal } from '@rocket.chat/fuselage'; |
||||
import React, { ChangeEvent, FC, useState } from 'react'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useEndpoint } from '../../contexts/ServerContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
|
||||
type PasteStepProps = { |
||||
onBackButtonClick: () => void; |
||||
onFinish: () => void; |
||||
}; |
||||
|
||||
const PasteStep: FC<PasteStepProps> = ({ onBackButtonClick, onFinish }) => { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
|
||||
const [isLoading, setLoading] = useState(false); |
||||
const [cloudKey, setCloudKey] = useState(''); |
||||
|
||||
const handleCloudKeyChange = (e: ChangeEvent<HTMLInputElement>): void => { |
||||
setCloudKey(e.currentTarget.value); |
||||
}; |
||||
|
||||
const registerManually = useEndpoint('POST', 'cloud.manualRegister'); |
||||
|
||||
const handleFinishButtonClick = async (): Promise<void> => { |
||||
setLoading(true); |
||||
|
||||
try { |
||||
await registerManually({}, { cloudBlob: cloudKey }); |
||||
dispatchToastMessage({ type: 'success', message: t('Cloud_register_success') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: t('Cloud_register_error') }); |
||||
} finally { |
||||
setLoading(false); |
||||
onFinish && onFinish(); |
||||
} |
||||
}; |
||||
|
||||
return <> |
||||
<Modal.Content> |
||||
<Box withRichContent> |
||||
<p>{t('Cloud_register_offline_finish_helper')}</p> |
||||
</Box> |
||||
<Box |
||||
display='flex' |
||||
flexDirection='column' |
||||
alignItems='stretch' |
||||
padding='x16' |
||||
flexGrow={1} |
||||
backgroundColor='neutral-800' |
||||
> |
||||
<Scrollable vertical> |
||||
<Box |
||||
is='textarea' |
||||
height='x108' |
||||
fontFamily='mono' |
||||
fontScale='p1' |
||||
color='alternative' |
||||
style={{ wordBreak: 'break-all', resize: 'none' }} |
||||
placeholder={t('Paste_here')} |
||||
disabled={isLoading} |
||||
value={cloudKey} |
||||
autoComplete='off' |
||||
autoCorrect='off' |
||||
autoCapitalize='off' |
||||
spellCheck='false' |
||||
onChange={handleCloudKeyChange} |
||||
/> |
||||
</Scrollable> |
||||
</Box> |
||||
</Modal.Content> |
||||
<Modal.Footer> |
||||
<ButtonGroup> |
||||
<Button disabled={isLoading} onClick={onBackButtonClick}>{t('Back')}</Button> |
||||
<Button primary disabled={isLoading || !cloudKey.trim()} marginInlineStart='auto' onClick={handleFinishButtonClick}> |
||||
{isLoading ? <Throbber inheritColor /> : t('Finish Registration')} |
||||
</Button> |
||||
</ButtonGroup> |
||||
</Modal.Footer> |
||||
</>; |
||||
}; |
||||
|
||||
export default PasteStep; |
||||
@ -0,0 +1,60 @@ |
||||
import React, { useMemo, FC } from 'react'; |
||||
import { Box, Button, ButtonGroup, Skeleton, Throbber, InputBox } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental'; |
||||
import EditCustomEmoji from './EditCustomEmoji'; |
||||
import { EmojiDescriptor } from './types'; |
||||
|
||||
type EditCustomEmojiWithDataProps = { |
||||
_id: string; |
||||
cache: unknown; |
||||
close: () => void; |
||||
onChange: () => void; |
||||
}; |
||||
|
||||
const EditCustomEmojiWithData: FC<EditCustomEmojiWithDataProps> = ({ _id, cache, onChange, ...props }) => { |
||||
const t = useTranslation(); |
||||
const query = useMemo(() => ({ |
||||
query: JSON.stringify({ _id }), |
||||
// TODO: remove cache. Is necessary for data invalidation
|
||||
}), [_id, cache]); |
||||
|
||||
const { |
||||
data = { |
||||
emojis: { |
||||
update: [], |
||||
}, |
||||
}, |
||||
state, |
||||
error, |
||||
} = useEndpointDataExperimental<{ |
||||
emojis?: { |
||||
update: EmojiDescriptor[]; |
||||
}; |
||||
}>('emoji-custom.list', query); |
||||
|
||||
if (state === ENDPOINT_STATES.LOADING) { |
||||
return <Box pb='x20'> |
||||
<Skeleton mbs='x8'/> |
||||
<InputBox.Skeleton w='full'/> |
||||
<Skeleton mbs='x8'/> |
||||
<InputBox.Skeleton w='full'/> |
||||
<ButtonGroup stretch w='full' mbs='x8'> |
||||
<Button disabled><Throbber inheritColor/></Button> |
||||
<Button primary disabled><Throbber inheritColor/></Button> |
||||
</ButtonGroup> |
||||
<ButtonGroup stretch w='full' mbs='x8'> |
||||
<Button primary danger disabled><Throbber inheritColor/></Button> |
||||
</ButtonGroup> |
||||
</Box>; |
||||
} |
||||
|
||||
if (error || !data || !data.emojis || data.emojis.update.length < 1) { |
||||
return <Box fontScale='h1' pb='x20'>{t('Custom_User_Status_Error_Invalid_User_Status')}</Box>; |
||||
} |
||||
|
||||
return <EditCustomEmoji data={data.emojis.update[0]} onChange={onChange} {...props}/>; |
||||
}; |
||||
|
||||
export default EditCustomEmojiWithData; |
||||
@ -0,0 +1,6 @@ |
||||
export type EmojiDescriptor = { |
||||
_id: string; |
||||
name: string; |
||||
aliases: string[]; |
||||
extension: string; |
||||
}; |
||||
@ -0,0 +1,49 @@ |
||||
import React, { useMemo, FC } from 'react'; |
||||
import { Box, Button, ButtonGroup, Skeleton, Throbber, InputBox } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental'; |
||||
import EditCustomUserStatus from './EditCustomUserStatus'; |
||||
|
||||
type EditCustomUserStatusWithDataProps = { |
||||
_id: string; |
||||
cache: unknown; |
||||
close: () => void; |
||||
onChange: () => void; |
||||
}; |
||||
|
||||
export const EditCustomUserStatusWithData: FC<EditCustomUserStatusWithDataProps> = ({ _id, cache, ...props }) => { |
||||
const t = useTranslation(); |
||||
const query = useMemo(() => ({ |
||||
query: JSON.stringify({ _id }), |
||||
// TODO: remove cache. Is necessary for data invalidation
|
||||
}), [_id, cache]); |
||||
|
||||
const { data, state, error } = useEndpointDataExperimental<{ |
||||
statuses: unknown[]; |
||||
}>('custom-user-status.list', query); |
||||
|
||||
if (state === ENDPOINT_STATES.LOADING) { |
||||
return <Box pb='x20'> |
||||
<Skeleton mbs='x8'/> |
||||
<InputBox.Skeleton w='full'/> |
||||
<Skeleton mbs='x8'/> |
||||
<InputBox.Skeleton w='full'/> |
||||
<ButtonGroup stretch w='full' mbs='x8'> |
||||
<Button disabled><Throbber inheritColor/></Button> |
||||
<Button primary disabled><Throbber inheritColor/></Button> |
||||
</ButtonGroup> |
||||
<ButtonGroup stretch w='full' mbs='x8'> |
||||
<Button primary danger disabled><Throbber inheritColor/></Button> |
||||
</ButtonGroup> |
||||
</Box>; |
||||
} |
||||
|
||||
if (error || !data || data.statuses.length < 1) { |
||||
return <Box fontScale='h1' pb='x20'>{t('Custom_User_Status_Error_Invalid_User_Status')}</Box>; |
||||
} |
||||
|
||||
return <EditCustomUserStatus data={data.statuses[0]} {...props}/>; |
||||
}; |
||||
|
||||
export default EditCustomUserStatusWithData; |
||||
@ -0,0 +1,95 @@ |
||||
/* eslint-disable @typescript-eslint/camelcase */ |
||||
import { |
||||
CheckBox, |
||||
Table, |
||||
Tag, |
||||
Pagination, |
||||
} from '@rocket.chat/fuselage'; |
||||
import React, { useState, useCallback, FC, Dispatch, SetStateAction, ChangeEvent } from 'react'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
|
||||
type ChannelDescriptor = { |
||||
channel_id: string; |
||||
name: string; |
||||
is_archived: boolean; |
||||
do_import: boolean; |
||||
}; |
||||
|
||||
type PrepareChannelsProps = { |
||||
channelsCount: number; |
||||
channels: ChannelDescriptor[]; |
||||
setChannels: Dispatch<SetStateAction<ChannelDescriptor[]>>; |
||||
}; |
||||
|
||||
const PrepareChannels: FC<PrepareChannelsProps> = ({ channels, channelsCount, setChannels }) => { |
||||
const t = useTranslation(); |
||||
const [current, setCurrent] = useState(0); |
||||
const [itemsPerPage, setItemsPerPage] = useState<25 | 50 | 100>(25); |
||||
const showingResultsLabel = useCallback(({ count, current, itemsPerPage }) => t('Showing results %s - %s of %s', current + 1, Math.min(current + itemsPerPage, count), count), [t]); |
||||
const itemsPerPageLabel = useCallback(() => t('Items_per_page:'), [t]); |
||||
|
||||
if (!channels.length) { |
||||
return null; |
||||
} |
||||
|
||||
return <> |
||||
<Table> |
||||
<Table.Head> |
||||
<Table.Row> |
||||
<Table.Cell width='x36'> |
||||
<CheckBox |
||||
checked={channelsCount > 0} |
||||
indeterminate={channelsCount > 0 && channelsCount !== channels.length} |
||||
onChange={(): void => { |
||||
setChannels((channels) => { |
||||
const hasCheckedArchivedChannels = channels.some(({ is_archived, do_import }) => is_archived && do_import); |
||||
const isChecking = channelsCount === 0; |
||||
|
||||
if (isChecking) { |
||||
return channels.map((channel) => ({ ...channel, do_import: true })); |
||||
} |
||||
|
||||
if (hasCheckedArchivedChannels) { |
||||
return channels.map((channel) => (channel.is_archived ? { ...channel, do_import: false } : channel)); |
||||
} |
||||
|
||||
return channels.map((channel) => ({ ...channel, do_import: false })); |
||||
}); |
||||
}} |
||||
/> |
||||
</Table.Cell> |
||||
<Table.Cell is='th'>{t('Name')}</Table.Cell> |
||||
<Table.Cell is='th' align='end'></Table.Cell> |
||||
</Table.Row> |
||||
</Table.Head> |
||||
<Table.Body> |
||||
{channels.slice(current, current + itemsPerPage).map((channel) => <Table.Row key={channel.channel_id}> |
||||
<Table.Cell width='x36'> |
||||
<CheckBox |
||||
checked={channel.do_import} |
||||
onChange={(event: ChangeEvent<HTMLInputElement>): void => { |
||||
const { checked } = event.currentTarget; |
||||
setChannels((channels) => |
||||
channels.map((_channel) => (_channel === channel ? { ..._channel, do_import: checked } : _channel))); |
||||
}} |
||||
/> |
||||
</Table.Cell> |
||||
<Table.Cell>{channel.name}</Table.Cell> |
||||
<Table.Cell align='end'>{channel.is_archived && <Tag variant='danger'>{t('Importer_Archived')}</Tag>}</Table.Cell> |
||||
</Table.Row>)} |
||||
</Table.Body> |
||||
</Table> |
||||
<Pagination |
||||
current={current} |
||||
itemsPerPage={itemsPerPage} |
||||
itemsPerPageLabel={itemsPerPageLabel} |
||||
showingResultsLabel={showingResultsLabel} |
||||
count={channels.length || 0} |
||||
onSetItemsPerPage={setItemsPerPage} |
||||
onSetCurrent={setCurrent} |
||||
/> |
||||
</>; |
||||
}; |
||||
|
||||
export default PrepareChannels; |
||||
@ -0,0 +1,94 @@ |
||||
/* eslint-disable @typescript-eslint/camelcase */ |
||||
import { |
||||
CheckBox, |
||||
Table, |
||||
Tag, |
||||
Pagination, |
||||
} from '@rocket.chat/fuselage'; |
||||
import React, { useState, useCallback, FC, Dispatch, SetStateAction, ChangeEvent } from 'react'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
|
||||
type UserDescriptor = { |
||||
user_id: string; |
||||
username: string; |
||||
email: string; |
||||
is_deleted: boolean; |
||||
do_import: boolean; |
||||
}; |
||||
|
||||
type PrepareUsersProps = { |
||||
usersCount: number; |
||||
users: UserDescriptor[]; |
||||
setUsers: Dispatch<SetStateAction<UserDescriptor[]>>; |
||||
}; |
||||
|
||||
const PrepareUsers: FC<PrepareUsersProps> = ({ usersCount, users, setUsers }) => { |
||||
const t = useTranslation(); |
||||
const [current, setCurrent] = useState(0); |
||||
const [itemsPerPage, setItemsPerPage] = useState<25 | 50 | 100>(25); |
||||
const showingResultsLabel = useCallback(({ count, current, itemsPerPage }) => t('Showing results %s - %s of %s', current + 1, Math.min(current + itemsPerPage, count), count), [t]); |
||||
const itemsPerPageLabel = useCallback(() => t('Items_per_page:'), [t]); |
||||
|
||||
return <> |
||||
<Table> |
||||
<Table.Head> |
||||
<Table.Row> |
||||
<Table.Cell width='x36'> |
||||
<CheckBox |
||||
checked={usersCount > 0} |
||||
indeterminate={usersCount > 0 && usersCount !== users.length} |
||||
onChange={(): void => { |
||||
setUsers((users) => { |
||||
const hasCheckedDeletedUsers = users.some(({ is_deleted, do_import }) => is_deleted && do_import); |
||||
const isChecking = usersCount === 0; |
||||
|
||||
if (isChecking) { |
||||
return users.map((user) => ({ ...user, do_import: true })); |
||||
} |
||||
|
||||
if (hasCheckedDeletedUsers) { |
||||
return users.map((user) => (user.is_deleted ? { ...user, do_import: false } : user)); |
||||
} |
||||
|
||||
return users.map((user) => ({ ...user, do_import: false })); |
||||
}); |
||||
}} |
||||
/> |
||||
</Table.Cell> |
||||
<Table.Cell is='th'>{t('Username')}</Table.Cell> |
||||
<Table.Cell is='th'>{t('Email')}</Table.Cell> |
||||
<Table.Cell is='th'></Table.Cell> |
||||
</Table.Row> |
||||
</Table.Head> |
||||
<Table.Body> |
||||
{users.slice(current, current + itemsPerPage).map((user) => <Table.Row key={user.user_id}> |
||||
<Table.Cell width='x36'> |
||||
<CheckBox |
||||
checked={user.do_import} |
||||
onChange={(event: ChangeEvent<HTMLInputElement>): void => { |
||||
const { checked } = event.currentTarget; |
||||
setUsers((users) => |
||||
users.map((_user) => (_user === user ? { ..._user, do_import: checked } : _user))); |
||||
}} |
||||
/> |
||||
</Table.Cell> |
||||
<Table.Cell>{user.username}</Table.Cell> |
||||
<Table.Cell>{user.email}</Table.Cell> |
||||
<Table.Cell align='end'>{user.is_deleted && <Tag variant='danger'>{t('Deleted')}</Tag>}</Table.Cell> |
||||
</Table.Row>)} |
||||
</Table.Body> |
||||
</Table> |
||||
<Pagination |
||||
current={current} |
||||
itemsPerPage={itemsPerPage} |
||||
count={users.length || 0} |
||||
onSetItemsPerPage={setItemsPerPage} |
||||
onSetCurrent={setCurrent} |
||||
itemsPerPageLabel={itemsPerPageLabel} |
||||
showingResultsLabel={showingResultsLabel} |
||||
/> |
||||
</>; |
||||
}; |
||||
|
||||
export default PrepareUsers; |
||||
@ -0,0 +1,20 @@ |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import React, { memo, FC } from 'react'; |
||||
import { useSubscription } from 'use-subscription'; |
||||
|
||||
import Sidebar from '../../components/basic/Sidebar'; |
||||
import { itemsSubscription } from '../sidebarItems'; |
||||
|
||||
type AdminSidebarPagesProps = { |
||||
currentPath: string; |
||||
}; |
||||
|
||||
const AdminSidebarPages: FC<AdminSidebarPagesProps> = ({ currentPath }) => { |
||||
const items = useSubscription(itemsSubscription); |
||||
|
||||
return <Box display='flex' flexDirection='column' flexShrink={0} pb='x8'> |
||||
<Sidebar.ItemsAssembler items={items} currentPath={currentPath}/> |
||||
</Box>; |
||||
}; |
||||
|
||||
export default memo(AdminSidebarPages); |
||||
@ -0,0 +1,92 @@ |
||||
import { Box, Icon, SearchInput, Skeleton } from '@rocket.chat/fuselage'; |
||||
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { useCallback, useState, useMemo, FC } from 'react'; |
||||
|
||||
import { ISetting, SettingType } from '../../../definition/ISetting'; |
||||
import { useSettings } from '../../contexts/SettingsContext'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import Sidebar from '../../components/basic/Sidebar'; |
||||
|
||||
const useSettingsGroups = (filter: string): ISetting[] => { |
||||
const settings = useSettings(); |
||||
|
||||
const t = useTranslation(); |
||||
|
||||
const filterPredicate = useMemo(() => { |
||||
if (!filter) { |
||||
return (): boolean => true; |
||||
} |
||||
|
||||
const getMatchableStrings = (setting: ISetting): string[] => [ |
||||
setting.i18nLabel && t(setting.i18nLabel), |
||||
t(setting._id), |
||||
setting._id, |
||||
].filter(Boolean); |
||||
|
||||
try { |
||||
const filterRegex = new RegExp(filter, 'i'); |
||||
return (setting: ISetting): boolean => |
||||
getMatchableStrings(setting).some((text) => filterRegex.test(text)); |
||||
} catch (e) { |
||||
return (setting: ISetting): boolean => |
||||
getMatchableStrings(setting).some((text) => text.slice(0, filter.length) === filter); |
||||
} |
||||
}, [filter, t]); |
||||
|
||||
return useMemo(() => { |
||||
const groupIds = Array.from(new Set( |
||||
settings |
||||
.filter(filterPredicate) |
||||
.map((setting) => { |
||||
if (setting.type === SettingType.GROUP) { |
||||
return setting._id; |
||||
} |
||||
|
||||
return setting.group; |
||||
}), |
||||
)); |
||||
|
||||
return settings |
||||
.filter(({ type, group, _id }) => type === SettingType.GROUP && groupIds.includes(group || _id)) |
||||
.sort((a, b) => t(a.i18nLabel || a._id).localeCompare(t(b.i18nLabel || b._id))); |
||||
}, [settings, filterPredicate, t]); |
||||
}; |
||||
|
||||
type AdminSidebarSettingsProps = { |
||||
currentPath: string; |
||||
}; |
||||
|
||||
const AdminSidebarSettings: FC<AdminSidebarSettingsProps> = ({ currentPath }) => { |
||||
const t = useTranslation(); |
||||
const [filter, setFilter] = useState(''); |
||||
const handleChange = useCallback((e) => setFilter(e.currentTarget.value), []); |
||||
|
||||
const groups = useSettingsGroups(useDebouncedValue(filter, 400)); |
||||
const isLoadingGroups = false; // TODO: get from PrivilegedSettingsContext
|
||||
|
||||
return <Box is='section' display='flex' flexDirection='column' flexShrink={0} pb='x24'> |
||||
<Box pi='x24' pb='x8' fontScale='p2' color='info'>{t('Settings')}</Box> |
||||
<Box pi='x24' pb='x8' display='flex'> |
||||
<SearchInput |
||||
value={filter} |
||||
placeholder={t('Search')} |
||||
onChange={handleChange} |
||||
addon={<Icon name='magnifier' size='x20'/>} |
||||
/> |
||||
</Box> |
||||
<Box pb='x16' display='flex' flexDirection='column'> |
||||
{isLoadingGroups && <Skeleton/>} |
||||
{!isLoadingGroups && !!groups.length && <Sidebar.ItemsAssembler |
||||
items={groups.map((group) => ({ |
||||
name: t(group.i18nLabel || group._id), |
||||
pathSection: 'admin', |
||||
pathGroup: group._id, |
||||
}))} |
||||
currentPath={currentPath} |
||||
/>} |
||||
{!isLoadingGroups && !groups.length && <Box pi='x28' mb='x4' color='hint'>{t('Nothing_found')}</Box>} |
||||
</Box> |
||||
</Box>; |
||||
}; |
||||
|
||||
export default AdminSidebarSettings; |
||||
@ -0,0 +1,33 @@ |
||||
import React, { FC } from 'react'; |
||||
import { Button, ButtonGroup, Icon, Modal } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../contexts/TranslationContext'; |
||||
|
||||
type DeleteSuccessModalProps = { |
||||
onClose: () => void; |
||||
}; |
||||
|
||||
const DeleteSuccessModal: FC<DeleteSuccessModalProps> = ({ |
||||
children, |
||||
onClose, |
||||
...props |
||||
}) => { |
||||
const t = useTranslation(); |
||||
return <Modal {...props}> |
||||
<Modal.Header> |
||||
<Icon color='success' name='checkmark-circled' size={20}/> |
||||
<Modal.Title>{t('Deleted')}</Modal.Title> |
||||
<Modal.Close onClick={onClose}/> |
||||
</Modal.Header> |
||||
<Modal.Content fontScale='p1'> |
||||
{children} |
||||
</Modal.Content> |
||||
<Modal.Footer> |
||||
<ButtonGroup align='end'> |
||||
<Button primary onClick={onClose}>{t('Ok')}</Button> |
||||
</ButtonGroup> |
||||
</Modal.Footer> |
||||
</Modal>; |
||||
}; |
||||
|
||||
export default DeleteSuccessModal; |
||||
@ -1,26 +0,0 @@ |
||||
import { Icon, Button, Modal, ButtonGroup } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useTranslation } from '../contexts/TranslationContext'; |
||||
|
||||
const DeleteWarningModal = ({ onDelete, onCancel, children, ...props }) => { |
||||
const t = useTranslation(); |
||||
return <Modal {...props}> |
||||
<Modal.Header> |
||||
<Icon color='danger' name='modal-warning' size={20}/> |
||||
<Modal.Title>{t('Are_you_sure')}</Modal.Title> |
||||
<Modal.Close onClick={onCancel}/> |
||||
</Modal.Header> |
||||
<Modal.Content> |
||||
{children} |
||||
</Modal.Content> |
||||
<Modal.Footer> |
||||
<ButtonGroup align='end'> |
||||
<Button ghost onClick={onCancel}>{t('Cancel')}</Button> |
||||
<Button primary danger onClick={onDelete}>{t('Delete')}</Button> |
||||
</ButtonGroup> |
||||
</Modal.Footer> |
||||
</Modal>; |
||||
}; |
||||
|
||||
export default DeleteWarningModal; |
||||
@ -0,0 +1,41 @@ |
||||
import { Button, ButtonGroup, Icon, Modal } from '@rocket.chat/fuselage'; |
||||
import React, { FC } from 'react'; |
||||
|
||||
import { useTranslation } from '../contexts/TranslationContext'; |
||||
|
||||
type DeleteWarningModalProps = { |
||||
cancelText?: string; |
||||
deleteText?: string; |
||||
onDelete: () => void; |
||||
onCancel: () => void; |
||||
}; |
||||
|
||||
const DeleteWarningModal: FC<DeleteWarningModalProps> = ({ |
||||
children, |
||||
cancelText, |
||||
deleteText, |
||||
onCancel, |
||||
onDelete, |
||||
...props |
||||
}) => { |
||||
const t = useTranslation(); |
||||
|
||||
return <Modal {...props}> |
||||
<Modal.Header> |
||||
<Icon color='danger' name='modal-warning' size={20}/> |
||||
<Modal.Title>{t('Are_you_sure')}</Modal.Title> |
||||
<Modal.Close onClick={onCancel}/> |
||||
</Modal.Header> |
||||
<Modal.Content fontScale='p1'> |
||||
{children} |
||||
</Modal.Content> |
||||
<Modal.Footer> |
||||
<ButtonGroup align='end'> |
||||
<Button ghost onClick={onCancel}>{cancelText ?? t('Cancel')}</Button> |
||||
<Button primary danger onClick={onDelete}>{deleteText ?? t('Delete')}</Button> |
||||
</ButtonGroup> |
||||
</Modal.Footer> |
||||
</Modal>; |
||||
}; |
||||
|
||||
export default DeleteWarningModal; |
||||
@ -0,0 +1,37 @@ |
||||
import { Box, Icon, TextInput } from '@rocket.chat/fuselage'; |
||||
import React, { FC, ChangeEvent, FormEvent, memo, useCallback, useEffect, useState } from 'react'; |
||||
|
||||
import { useTranslation } from '../contexts/TranslationContext'; |
||||
|
||||
type FilterByTextProps = { |
||||
placeholder?: string; |
||||
onChange: (filter: { text: string }) => void; |
||||
}; |
||||
|
||||
const FilterByText: FC<FilterByTextProps> = ({ |
||||
placeholder, |
||||
onChange: setFilter, |
||||
...props |
||||
}) => { |
||||
const t = useTranslation(); |
||||
|
||||
const [text, setText] = useState(''); |
||||
|
||||
const handleInputChange = useCallback((event: ChangeEvent<HTMLInputElement>) => { |
||||
setText(event.currentTarget.value); |
||||
}, []); |
||||
|
||||
useEffect(() => { |
||||
setFilter({ text }); |
||||
}, [setFilter, text]); |
||||
|
||||
const handleFormSubmit = useCallback((event: FormEvent<HTMLFormElement>) => { |
||||
event.preventDefault(); |
||||
}, []); |
||||
|
||||
return <Box mb='x16' is='form' onSubmit={handleFormSubmit} display='flex' flexDirection='column' {...props}> |
||||
<TextInput placeholder={placeholder ?? t('Search')} addon={<Icon name='magnifier' size='x20'/>} onChange={handleInputChange} value={text} /> |
||||
</Box>; |
||||
}; |
||||
|
||||
export default memo(FilterByText); |
||||
@ -0,0 +1,30 @@ |
||||
import { Box, Table } from '@rocket.chat/fuselage'; |
||||
import React, { FC, useCallback } from 'react'; |
||||
|
||||
import SortIcon from './SortIcon'; |
||||
|
||||
type HeaderCellProps = { |
||||
active?: boolean; |
||||
direction?: 'asc' | 'desc'; |
||||
sort?: boolean; |
||||
onClick?: (sort: boolean) => void; |
||||
}; |
||||
|
||||
const HeaderCell: FC<HeaderCellProps> = ({ |
||||
children, |
||||
active, |
||||
direction, |
||||
sort, |
||||
onClick, |
||||
...props |
||||
}) => { |
||||
const fn = useCallback(() => onClick && onClick(!!sort), [sort, onClick]); |
||||
return <Table.Cell clickable={!!sort} onClick={fn} { ...props }> |
||||
<Box display='flex' alignItems='center' wrap='no-wrap'> |
||||
{children} |
||||
{sort && <SortIcon direction={active ? direction : undefined} />} |
||||
</Box> |
||||
</Table.Cell>; |
||||
}; |
||||
|
||||
export default HeaderCell; |
||||
@ -0,0 +1,24 @@ |
||||
import { Box, Skeleton, Table } from '@rocket.chat/fuselage'; |
||||
import React, { FC } from 'react'; |
||||
|
||||
type LoadingRowProps = { |
||||
cols: number; |
||||
}; |
||||
|
||||
const LoadingRow: FC<LoadingRowProps> = ({ cols }) => |
||||
<Table.Row> |
||||
<Table.Cell> |
||||
<Box display='flex'> |
||||
<Skeleton variant='rect' height={40} width={40} /> |
||||
<Box mi='x8' flexGrow={1}> |
||||
<Skeleton width='100%' /> |
||||
<Skeleton width='100%' /> |
||||
</Box> |
||||
</Box> |
||||
</Table.Cell> |
||||
{Array.from({ length: cols - 1 }, (_, i) => <Table.Cell key={i}> |
||||
<Skeleton width='100%' /> |
||||
</Table.Cell>)} |
||||
</Table.Row>; |
||||
|
||||
export default LoadingRow; |
||||
@ -0,0 +1,14 @@ |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import React, { FC } from 'react'; |
||||
|
||||
type SortIconProps = { |
||||
direction?: 'asc' | 'desc'; |
||||
}; |
||||
|
||||
const SortIcon: FC<SortIconProps> = ({ direction }) => |
||||
<Box is='svg' width='x16' height='x16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'> |
||||
<path d='M5.33337 5.99999L8.00004 3.33333L10.6667 5.99999' stroke={direction === 'desc' ? '#9EA2A8' : '#E4E7EA' } strokeWidth='1.33333' strokeLinecap='round' strokeLinejoin='round'/> |
||||
<path d='M5.33337 10L8.00004 12.6667L10.6667 10' stroke={direction === 'asc' ? '#9EA2A8' : '#E4E7EA'} strokeWidth='1.33333' strokeLinecap='round' strokeLinejoin='round'/> |
||||
</Box>; |
||||
|
||||
export default SortIcon; |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue