Non-idiomatic React code (#19303)
parent
61e449870b
commit
1e7b31d199
@ -0,0 +1,43 @@ |
||||
import React, { FC, useMemo } from 'react'; |
||||
import { Modal, Box } from '@rocket.chat/fuselage'; |
||||
|
||||
import VerticalBar from '../../../../client/components/basic/VerticalBar'; |
||||
|
||||
type ThreadSkeletonProps = { |
||||
expanded: boolean; |
||||
onClose: () => void; |
||||
}; |
||||
|
||||
const ThreadSkeleton: FC<ThreadSkeletonProps> = ({ expanded, onClose }) => { |
||||
const style = useMemo(() => (document.dir === 'rtl' |
||||
? { |
||||
left: 0, |
||||
borderTopRightRadius: 4, |
||||
} |
||||
: { |
||||
right: 0, |
||||
borderTopLeftRadius: 4, |
||||
}), []); |
||||
|
||||
return <> |
||||
{expanded && <Modal.Backdrop onClick={onClose} />} |
||||
<Box width='380px' flexGrow={1} position={expanded ? 'static' : 'relative'}> |
||||
<VerticalBar.Skeleton |
||||
className='rcx-thread-view' |
||||
position='absolute' |
||||
display='flex' |
||||
flexDirection='column' |
||||
width={expanded ? 'full' : 380} |
||||
maxWidth={855} |
||||
overflow='hidden' |
||||
zIndex={100} |
||||
insetBlock={0} |
||||
// insetInlineEnd={0}
|
||||
// borderStartStartRadius={4}
|
||||
style={style} // workaround due to a RTL bug in Fuselage
|
||||
/> |
||||
</Box> |
||||
</>; |
||||
}; |
||||
|
||||
export default ThreadSkeleton; |
||||
@ -0,0 +1,88 @@ |
||||
import React, { useCallback, useMemo, forwardRef, FC } from 'react'; |
||||
import { Modal, Box } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../../../client/contexts/TranslationContext'; |
||||
import VerticalBar from '../../../../client/components/basic/VerticalBar'; |
||||
|
||||
type ThreadViewProps = { |
||||
title: string; |
||||
expanded: boolean; |
||||
following: boolean; |
||||
onToggleExpand: (expanded: boolean) => void; |
||||
onToggleFollow: (following: boolean) => void; |
||||
onClose: () => void; |
||||
}; |
||||
|
||||
const ThreadView: FC<ThreadViewProps> = forwardRef<Element, ThreadViewProps>(({ |
||||
title, |
||||
expanded, |
||||
following, |
||||
onToggleExpand, |
||||
onToggleFollow, |
||||
onClose, |
||||
}, ref) => { |
||||
const style = useMemo(() => (document.dir === 'rtl' |
||||
? { |
||||
left: 0, |
||||
borderTopRightRadius: 4, |
||||
} |
||||
: { |
||||
right: 0, |
||||
borderTopLeftRadius: 4, |
||||
}), []); |
||||
|
||||
const t = useTranslation(); |
||||
|
||||
const expandLabel = expanded ? t('collapse') : t('expand'); |
||||
const expandIcon = expanded ? 'arrow-collapse' : 'arrow-expand'; |
||||
|
||||
const handleExpandActionClick = useCallback(() => { |
||||
onToggleExpand(expanded); |
||||
}, [expanded, onToggleExpand]); |
||||
|
||||
const followLabel = following ? t('Following') : t('Not_Following'); |
||||
const followIcon = following ? 'bell' : 'bell-off'; |
||||
|
||||
const handleFollowActionClick = useCallback(() => { |
||||
onToggleFollow(following); |
||||
}, [following, onToggleFollow]); |
||||
|
||||
return <> |
||||
{expanded && <Modal.Backdrop onClick={onClose}/>} |
||||
|
||||
<Box width='380px' flexGrow={1} position={expanded ? 'static' : 'relative'}> |
||||
<VerticalBar |
||||
className='rcx-thread-view' |
||||
position='absolute' |
||||
display='flex' |
||||
flexDirection='column' |
||||
width={expanded ? 'full' : 380} |
||||
maxWidth={855} |
||||
overflow='hidden' |
||||
zIndex={100} |
||||
insetBlock={0} |
||||
// insetInlineEnd={0}
|
||||
// borderStartStartRadius={4}
|
||||
style={style} // workaround due to a RTL bug in Fuselage
|
||||
> |
||||
<VerticalBar.Header> |
||||
<VerticalBar.Icon name='thread' /> |
||||
<VerticalBar.Text dangerouslySetInnerHTML={{ __html: title }} /> |
||||
<VerticalBar.Action aria-label={expandLabel} name={expandIcon} onClick={handleExpandActionClick} /> |
||||
<VerticalBar.Action aria-label={followLabel} name={followIcon} onClick={handleFollowActionClick} /> |
||||
<VerticalBar.Close onClick={onClose} /> |
||||
</VerticalBar.Header> |
||||
<VerticalBar.Content |
||||
ref={ref} |
||||
{...{ |
||||
flexShrink: 1, |
||||
flexGrow: 1, |
||||
paddingInline: 0, |
||||
}} |
||||
/> |
||||
</VerticalBar> |
||||
</Box> |
||||
</>; |
||||
}); |
||||
|
||||
export default ThreadView; |
||||
@ -0,0 +1,13 @@ |
||||
import { createContext } from 'react'; |
||||
|
||||
import { App } from './types'; |
||||
|
||||
type AppsContextValue = { |
||||
apps: App[]; |
||||
finishedLoading: boolean; |
||||
} |
||||
|
||||
export const AppsContext = createContext<AppsContextValue>({ |
||||
apps: [], |
||||
finishedLoading: false, |
||||
}); |
||||
@ -1,75 +0,0 @@ |
||||
import { useState, useEffect, useContext } from 'react'; |
||||
|
||||
import { Apps } from '../../../../app/apps/client/orchestrator'; |
||||
import { AppDataContext } from '../AppProvider'; |
||||
import { handleAPIError } from '../helpers'; |
||||
|
||||
const getBundledIn = async (appId, appVersion) => { |
||||
try { |
||||
const { bundledIn } = await Apps.getLatestAppFromMarketplace(appId, appVersion); |
||||
bundledIn && await Promise.all(bundledIn.map((bundle, i) => Apps.getAppsOnBundle(bundle.bundleId).then((value) => { |
||||
bundle.apps = value.slice(0, 4); |
||||
bundledIn[i] = bundle; |
||||
}))); |
||||
return bundledIn; |
||||
} catch (e) { |
||||
handleAPIError(e); |
||||
} |
||||
}; |
||||
|
||||
const getSettings = async (appId, installed) => { |
||||
if (!installed) { return {}; } |
||||
try { |
||||
const settings = await Apps.getAppSettings(appId); |
||||
return settings; |
||||
} catch (e) { |
||||
handleAPIError(e); |
||||
} |
||||
}; |
||||
|
||||
const getApis = async (appId, installed) => { |
||||
if (!installed) { return {}; } |
||||
try { |
||||
return await Apps.getAppApis(appId); |
||||
} catch (e) { |
||||
handleAPIError(e); |
||||
} |
||||
}; |
||||
|
||||
export const useAppInfo = (appId) => { |
||||
const { data, dataCache } = useContext(AppDataContext); |
||||
|
||||
const [appData, setAppData] = useState({}); |
||||
|
||||
useEffect(() => { |
||||
(async () => { |
||||
if (!data.length || !appId) { return; } |
||||
|
||||
let app = data.find(({ id }) => id === appId); |
||||
|
||||
if (!app) { |
||||
const localApp = await Apps.getApp(appId); |
||||
app = { ...localApp, installed: true, marketplace: false }; |
||||
} |
||||
|
||||
if (app.marketplace === false) { |
||||
const [settings, apis] = await Promise.all([getSettings(app.id, app.installed), getApis(app.id, app.installed)]); |
||||
return setAppData({ ...app, settings, apis }); |
||||
} |
||||
|
||||
const [ |
||||
bundledIn, |
||||
settings, |
||||
apis, |
||||
] = await Promise.all([ |
||||
getBundledIn(app.id, app.version), |
||||
getSettings(app.id, app.installed), |
||||
getApis(app.id, app.installed), |
||||
]); |
||||
|
||||
setAppData({ ...app, bundledIn, settings, apis }); |
||||
})(); |
||||
}, [appId, data, dataCache]); |
||||
|
||||
return appData; |
||||
}; |
||||
@ -0,0 +1,88 @@ |
||||
import { useState, useEffect, useContext } from 'react'; |
||||
|
||||
import { Apps } from '../../../../app/apps/client/orchestrator'; |
||||
import { AppsContext } from '../AppsContext'; |
||||
import { handleAPIError } from '../helpers'; |
||||
import { App } from '../types'; |
||||
|
||||
const getBundledIn = async (appId: string, appVersion: string): Promise<App['bundledIn']> => { |
||||
try { |
||||
const { bundledIn } = await Apps.getLatestAppFromMarketplace(appId, appVersion) as App; |
||||
if (!bundledIn) { |
||||
return []; |
||||
} |
||||
|
||||
return await Promise.all( |
||||
bundledIn.map(async (bundle) => { |
||||
const apps = await Apps.getAppsOnBundle(bundle.bundleId); |
||||
bundle.apps = apps.slice(0, 4); |
||||
return bundle; |
||||
}), |
||||
); |
||||
} catch (e) { |
||||
handleAPIError(e); |
||||
return []; |
||||
} |
||||
}; |
||||
|
||||
const getSettings = async (appId: string, installed: boolean): Promise<Record<string, unknown>> => { |
||||
if (!installed) { |
||||
return {}; |
||||
} |
||||
|
||||
try { |
||||
return Apps.getAppSettings(appId); |
||||
} catch (e) { |
||||
handleAPIError(e); |
||||
return {}; |
||||
} |
||||
}; |
||||
|
||||
const getApis = async (appId: string, installed: boolean): Promise<Record<string, unknown>> => { |
||||
if (!installed) { |
||||
return {}; |
||||
} |
||||
|
||||
try { |
||||
return Apps.getAppApis(appId); |
||||
} catch (e) { |
||||
handleAPIError(e); |
||||
return {}; |
||||
} |
||||
}; |
||||
|
||||
type AppInfo = Partial<App & { |
||||
settings: Record<string, unknown>; |
||||
apis: Record<string, unknown>; |
||||
}>; |
||||
|
||||
export const useAppInfo = (appId: string): AppInfo => { |
||||
const { apps } = useContext(AppsContext); |
||||
|
||||
const [appData, setAppData] = useState({}); |
||||
|
||||
useEffect(() => { |
||||
const fetchAppInfo = async (): Promise<void> => { |
||||
if (!apps.length || !appId) { |
||||
return; |
||||
} |
||||
|
||||
const app = apps.find((app) => app.id === appId) ?? { |
||||
...await Apps.getApp(appId), |
||||
installed: true, |
||||
marketplace: false, |
||||
}; |
||||
|
||||
const [bundledIn, settings, apis] = await Promise.all([ |
||||
app.marketplace === false ? [] : getBundledIn(app.id, app.version), |
||||
getSettings(app.id, app.installed), |
||||
getApis(app.id, app.installed), |
||||
]); |
||||
setAppData({ ...app, bundledIn, settings, apis }); |
||||
}; |
||||
|
||||
fetchAppInfo(); |
||||
}, [appId, apps]); |
||||
|
||||
return appData; |
||||
}; |
||||
@ -1,34 +0,0 @@ |
||||
import { useMemo } from 'react'; |
||||
|
||||
export const useFilteredApps = ({ |
||||
filterFunction = (text) => { |
||||
if (!text) { return () => true; } |
||||
return (app) => app.name.toLowerCase().indexOf(text.toLowerCase()) > -1; |
||||
}, |
||||
text, |
||||
sort, |
||||
current, |
||||
itemsPerPage, |
||||
data, |
||||
dataCache, |
||||
}) => { |
||||
const filteredValues = useMemo(() => { |
||||
if (Array.isArray(data)) { |
||||
const dataCopy = data.slice(0); |
||||
let filtered = sort[1] === 'asc' ? dataCopy : dataCopy.reverse(); |
||||
|
||||
filtered = filtered.filter(filterFunction(text)); |
||||
|
||||
const filteredLength = filtered.length; |
||||
|
||||
const sliceStart = current > filteredLength ? 0 : current; |
||||
|
||||
filtered = filtered.slice(sliceStart, current + itemsPerPage); |
||||
|
||||
return [filtered, filteredLength]; |
||||
} |
||||
return [null, 0]; |
||||
}, [text, sort[1], dataCache, current, itemsPerPage]); |
||||
|
||||
return [...filteredValues]; |
||||
}; |
||||
@ -0,0 +1,40 @@ |
||||
import { useContext } from 'react'; |
||||
|
||||
import { AppsContext } from '../AppsContext'; |
||||
import { App } from '../types'; |
||||
|
||||
export const useFilteredApps = ({ |
||||
filterFunction = (text: string): ((app: App) => boolean) => { |
||||
if (!text) { return (): boolean => true; } |
||||
return (app: App): boolean => app.name.toLowerCase().indexOf(text.toLowerCase()) > -1; |
||||
}, |
||||
text, |
||||
sort: [, sortDirection], |
||||
current, |
||||
itemsPerPage, |
||||
}: { |
||||
filterFunction: (text: string) => (app: App) => boolean; |
||||
text: string; |
||||
sort: [string, 'asc' | 'desc']; |
||||
current: number; |
||||
itemsPerPage: number; |
||||
apps: App[]; |
||||
}): [App[] | null, number] => { |
||||
const { apps } = useContext(AppsContext); |
||||
|
||||
if (!Array.isArray(apps) || apps.length === 0) { |
||||
return [null, 0]; |
||||
} |
||||
|
||||
const filtered = apps.filter(filterFunction(text)); |
||||
if (sortDirection === 'desc') { |
||||
filtered.reverse(); |
||||
} |
||||
|
||||
const total = filtered.length; |
||||
const start = current > total ? 0 : current; |
||||
const end = current + itemsPerPage; |
||||
const slice = filtered.slice(start, end); |
||||
|
||||
return [slice, total]; |
||||
}; |
||||
@ -1,106 +1,94 @@ |
||||
// eslint-disable
|
||||
|
||||
import React, { useMemo, useState, useCallback } from 'react'; |
||||
import { Button, Icon } from '@rocket.chat/fuselage'; |
||||
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import Page from '../../components/basic/Page'; |
||||
import VerticalBar from '../../components/basic/VerticalBar'; |
||||
import NotAuthorizedPage from '../../components/NotAuthorizedPage'; |
||||
import { usePermission } from '../../contexts/AuthorizationContext'; |
||||
import { useRoute, useRouteParameter } from '../../contexts/RouterContext'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import Page from '../../components/basic/Page'; |
||||
import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental'; |
||||
import AdminSounds from './AdminSounds'; |
||||
import AddCustomSound from './AddCustomSound'; |
||||
import EditCustomSound from './EditCustomSound'; |
||||
import { useRoute, useRouteParameter } from '../../contexts/RouterContext'; |
||||
import { useEndpointData } from '../../hooks/useEndpointData'; |
||||
import VerticalBar from '../../components/basic/VerticalBar'; |
||||
import NotAuthorizedPage from '../../components/NotAuthorizedPage'; |
||||
|
||||
const sortDir = (sortDir) => (sortDir === 'asc' ? 1 : -1); |
||||
|
||||
export const useQuery = ({ text, itemsPerPage, current }, [column, direction], cache) => useMemo(() => ({ |
||||
query: JSON.stringify({ name: { $regex: text || '', $options: 'i' } }), |
||||
sort: JSON.stringify({ [column]: sortDir(direction) }), |
||||
...itemsPerPage && { count: itemsPerPage }, |
||||
...current && { offset: current }, |
||||
// TODO: remove cache. Is necessary for data invalidation
|
||||
}), [text, itemsPerPage, current, column, direction, cache]); |
||||
|
||||
export default function CustomSoundsRoute({ props }) { |
||||
const t = useTranslation(); |
||||
function CustomSoundsRoute() { |
||||
const route = useRoute('custom-sounds'); |
||||
const context = useRouteParameter('context'); |
||||
const id = useRouteParameter('id'); |
||||
const canManageCustomSounds = usePermission('manage-sounds'); |
||||
|
||||
const routeName = 'custom-sounds'; |
||||
|
||||
const [params, setParams] = useState({ text: '', current: 0, itemsPerPage: 25 }); |
||||
const [sort, setSort] = useState(['name', 'asc']); |
||||
const [cache, setCache] = useState(); |
||||
|
||||
const debouncedParams = useDebouncedValue(params, 500); |
||||
const debouncedSort = useDebouncedValue(sort, 500); |
||||
|
||||
const query = useQuery(debouncedParams, debouncedSort, cache); |
||||
|
||||
const data = useEndpointData('custom-sounds.list', query) || {}; |
||||
const t = useTranslation(); |
||||
|
||||
const [params, setParams] = useState(() => ({ text: '', current: 0, itemsPerPage: 25 })); |
||||
const [sort, setSort] = useState(() => ['name', 'asc']); |
||||
|
||||
const router = useRoute(routeName); |
||||
const { text, itemsPerPage, current } = useDebouncedValue(params, 500); |
||||
const [column, direction] = useDebouncedValue(sort, 500); |
||||
const query = useMemo(() => ({ |
||||
query: JSON.stringify({ name: { $regex: text || '', $options: 'i' } }), |
||||
sort: JSON.stringify({ [column]: direction === 'asc' ? 1 : -1 }), |
||||
...itemsPerPage && { count: itemsPerPage }, |
||||
...current && { offset: current }, |
||||
}), [text, itemsPerPage, current, column, direction]); |
||||
|
||||
const context = useRouteParameter('context'); |
||||
const id = useRouteParameter('id'); |
||||
const { data, reload } = useEndpointDataExperimental('custom-sounds.list', query); |
||||
|
||||
const onClick = useCallback((_id) => () => { |
||||
router.push({ |
||||
const handleItemClick = useCallback((_id) => () => { |
||||
route.push({ |
||||
context: 'edit', |
||||
id: _id, |
||||
}); |
||||
}, [router]); |
||||
}, [route]); |
||||
|
||||
const onHeaderClick = (id) => { |
||||
const [sortBy, sortDirection] = sort; |
||||
const handleHeaderClick = (id) => { |
||||
setSort(([sortBy, sortDirection]) => { |
||||
if (sortBy === id) { |
||||
return [id, sortDirection === 'asc' ? 'desc' : 'asc']; |
||||
} |
||||
|
||||
if (sortBy === id) { |
||||
setSort([id, sortDirection === 'asc' ? 'desc' : 'asc']); |
||||
return; |
||||
} |
||||
setSort([id, 'asc']); |
||||
return [id, 'asc']; |
||||
}); |
||||
}; |
||||
|
||||
const handleHeaderButtonClick = useCallback((context) => () => { |
||||
router.push({ context }); |
||||
}, [router]); |
||||
const handleNewButtonClick = useCallback(() => { |
||||
route.push({ context: 'new' }); |
||||
}, [route]); |
||||
|
||||
const close = useCallback(() => { |
||||
router.push({}); |
||||
}, [router]); |
||||
const handleClose = useCallback(() => { |
||||
route.push({}); |
||||
}, [route]); |
||||
|
||||
const onChange = useCallback(() => { |
||||
setCache(new Date()); |
||||
}, []); |
||||
const handleChange = useCallback(() => { |
||||
reload(); |
||||
}, [reload]); |
||||
|
||||
if (!canManageCustomSounds) { |
||||
return <NotAuthorizedPage />; |
||||
} |
||||
|
||||
return <Page {...props} flexDirection='row'> |
||||
return <Page flexDirection='row'> |
||||
<Page name='admin-custom-sounds'> |
||||
<Page.Header title={t('Custom_Sounds')}> |
||||
<Button small onClick={handleHeaderButtonClick('new')} aria-label={t('New')}> |
||||
<Button small onClick={handleNewButtonClick} aria-label={t('New')}> |
||||
<Icon name='plus'/> |
||||
</Button> |
||||
</Page.Header> |
||||
<Page.Content> |
||||
<AdminSounds setParams={setParams} params={params} onHeaderClick={onHeaderClick} data={data} onClick={onClick} sort={sort}/> |
||||
<AdminSounds setParams={setParams} params={params} onHeaderClick={handleHeaderClick} data={data} onClick={handleItemClick} sort={sort}/> |
||||
</Page.Content> |
||||
</Page> |
||||
{ context |
||||
&& <VerticalBar className='contextual-bar' width='x380' qa-context-name={`admin-user-and-room-context-${ context }`} flexShrink={0}> |
||||
<VerticalBar.Header> |
||||
{ context === 'edit' && t('Custom_Sound_Edit') } |
||||
{ context === 'new' && t('Custom_Sound_Add') } |
||||
<VerticalBar.Close onClick={close}/> |
||||
</VerticalBar.Header> |
||||
{context === 'edit' && <EditCustomSound _id={id} close={close} onChange={onChange} cache={cache}/>} |
||||
{context === 'new' && <AddCustomSound goToNew={onClick} close={close} onChange={onChange}/>} |
||||
</VerticalBar>} |
||||
{context && <VerticalBar className='contextual-bar' width='x380' flexShrink={0}> |
||||
<VerticalBar.Header> |
||||
{context === 'edit' && t('Custom_Sound_Edit')} |
||||
{context === 'new' && t('Custom_Sound_Add')} |
||||
<VerticalBar.Close onClick={handleClose}/> |
||||
</VerticalBar.Header> |
||||
{context === 'edit' && <EditCustomSound _id={id} close={handleClose} onChange={handleChange} />} |
||||
{context === 'new' && <AddCustomSound goToNew={handleItemClick} close={handleClose} onChange={handleChange}/>} |
||||
</VerticalBar>} |
||||
</Page>; |
||||
} |
||||
|
||||
export default CustomSoundsRoute; |
||||
|
||||
@ -1,9 +1,10 @@ |
||||
import { useMemo } from 'react'; |
||||
import { useResizeObserver } from '@rocket.chat/fuselage-hooks'; |
||||
import { useResizeObserver, useStableArray } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
export const useResizeInlineBreakpoint = (sizes = [], debounceDelay = 0) => { |
||||
const { ref, borderBoxSize } = useResizeObserver({ debounceDelay }); |
||||
const inlineSize = borderBoxSize ? borderBoxSize.inlineSize : 0; |
||||
const newSizes = useMemo(() => sizes.map((current) => (inlineSize ? inlineSize > current : true)), [inlineSize]); |
||||
const stableSizes = useStableArray(sizes); |
||||
const newSizes = useMemo(() => stableSizes.map((current) => (inlineSize ? inlineSize > current : true)), [inlineSize, stableSizes]); |
||||
return [ref, ...newSizes]; |
||||
}; |
||||
|
||||
Loading…
Reference in new issue