-
Notifications
You must be signed in to change notification settings - Fork 13.7k
Improve: Rewrite admin sidebar in React #17801
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
09abe69
a894a4b
1b301b4
541ba80
a259ed8
147908f
2a8f351
b50b435
b6f4e83
86f6921
a3184a8
0aefa52
18e48c8
206c114
97fd3d6
109b477
808a2cd
8d6f8eb
c15d840
0f49120
3a2efa2
12b250f
b21cbe4
42063ee
54fd0cd
d27ac40
0a08bc5
f6cbe6d
8ab38b7
a17473d
d94ade6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,7 +42,6 @@ module.exports = async ({ config }) => { | |
| }, | ||
| }, | ||
| }, | ||
| 'react-docgen-typescript-loader', | ||
| ], | ||
| }); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| import React, { useCallback, useState, useMemo } from 'react'; | ||
| import { Box, Button, Icon, SearchInput, Scrollable, Skeleton } from '@rocket.chat/fuselage'; | ||
| import { css } from '@rocket.chat/css-in-js'; | ||
|
|
||
| import { menu, SideNav, Layout } from '../../../app/ui-utils/client'; | ||
| import { useReactiveValue } from '../../hooks/useReactiveValue'; | ||
| import { useTranslation } from '../../contexts/TranslationContext'; | ||
| import { useRoutePath, useCurrentRoute } from '../../contexts/RouterContext'; | ||
| import { useAtLeastOnePermission } from '../../contexts/AuthorizationContext'; | ||
| import { sidebarItems } from '../sidebarItems'; | ||
| import { useSettingsGroupsFiltered } from './useSettingsGroupsFiltered'; | ||
|
|
||
| const SidebarItem = ({ permissionGranted, pathGroup, href, icon, label, currentPath }) => { | ||
| if (permissionGranted && !permissionGranted()) { return null; } | ||
| const params = useMemo(() => ({ group: pathGroup }), [pathGroup]); | ||
| const path = useRoutePath(href, params); | ||
| const isActive = path === currentPath || false; | ||
| return useMemo(() => <Box | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not React.memo?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ggazzo
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sure I know , but why not wrap the component as a memoized component instead if this approach? |
||
| is='a' | ||
| color='default' | ||
| pb='x8' | ||
| pi='x24' | ||
| key={path} | ||
| href={path} | ||
| display='flex' | ||
| flexDirection='row' | ||
| alignItems='center' | ||
| className={[ | ||
| isActive && 'active', | ||
| css` | ||
| &:hover, | ||
| &.active:hover { | ||
| background-color: var(--sidebar-background-light-hover); | ||
| } | ||
|
|
||
| &.active { | ||
| background-color: var(--sidebar-background-light-active); | ||
| } | ||
| `, | ||
| ].filter(Boolean)} | ||
| > | ||
| {icon && <Icon name={icon} size='x16' mi='x2'/>} | ||
| <Box withTruncatedText fontScale='p1' mi='x4'>{label}</Box> | ||
| </Box>, [path, label, name, icon, isActive]); | ||
| }; | ||
|
|
||
| const SidebarItemsAssembler = ({ items, currentPath }) => { | ||
| const t = useTranslation(); | ||
| return items.map(({ | ||
| href, | ||
| i18nLabel, | ||
| name, | ||
| icon, | ||
| permissionGranted, | ||
| pathGroup, | ||
| }) => <SidebarItem | ||
| permissionGranted={permissionGranted} | ||
| pathGroup={pathGroup} | ||
| href={href} | ||
| icon={icon} | ||
| label={t(i18nLabel || name)} | ||
| key={i18nLabel || name} | ||
| currentPath={currentPath} | ||
| />); | ||
| }; | ||
|
|
||
| const AdminSidebarPages = ({ currentPath }) => { | ||
| const items = useReactiveValue(() => sidebarItems.get()); | ||
|
|
||
| return <Box is='ul' display='flex' flexDirection='column' flexShrink={0}> | ||
| {useMemo(() => <SidebarItemsAssembler items={items} currentPath={currentPath}/>, [items, currentPath])} | ||
| </Box>; | ||
| }; | ||
|
|
||
| const AdminSidebarSettings = ({ currentPath }) => { | ||
| const t = useTranslation(); | ||
| const [filter, setFilter] = useState(''); | ||
| const handleChange = useCallback((e) => setFilter(e.currentTarget.value), []); | ||
|
|
||
| const [groups, loading] = useSettingsGroupsFiltered(filter); | ||
|
|
||
| const showGroups = !!groups.length; | ||
|
|
||
| return <Box is='section' display='flex' flexDirection='column' flexShrink={0}> | ||
| <Box mi='x24' mb='x16' fontScale='p2' color='hint'>{t('Settings')}</Box> | ||
| <Box pi='x24' mb='x8' display='flex'> | ||
| <Box is={SearchInput} border='0' value={filter} onChange={handleChange} addon={<Icon name='magnifier' size='x20'/>}/> | ||
| </Box> | ||
| <Box is='ul' display='flex' flexDirection='column'> | ||
| {loading && <Skeleton/>} | ||
| {!loading && showGroups && <SidebarItemsAssembler items={groups} currentPath={currentPath}/>} | ||
| {!loading && !showGroups && <Box pi='x28' mb='x4' color='hint'>{t('Nothing_found')}</Box>} | ||
| </Box> | ||
| </Box>; | ||
| }; | ||
|
|
||
| export default function AdminSidebar() { | ||
| const t = useTranslation(); | ||
|
|
||
| const canViewSettings = useAtLeastOnePermission(['view-privileged-setting', 'edit-privileged-setting', 'manage-selected-settings']); | ||
|
|
||
| const closeAdminFlex = useCallback(() => { | ||
| if (Layout.isEmbedded()) { | ||
| menu.close(); | ||
| return; | ||
| } | ||
|
|
||
| SideNav.closeFlex(); | ||
| }, []); | ||
|
|
||
| const currentRoute = useCurrentRoute(); | ||
| const currentPath = useRoutePath(...currentRoute); | ||
|
|
||
| return <Box display='flex' flexDirection='column' h='100vh'> | ||
| <Box is='header' padding='x24' display='flex' flexDirection='row' justifyContent='space-between'> | ||
| <Box fontScale='s1'>{t('Administration')}</Box> | ||
| <Button square small ghost onClick={closeAdminFlex}> | ||
| <Icon name='cross' size='x16'/> | ||
| </Button> | ||
| </Box> | ||
| <Scrollable> | ||
| <Box display='flex' flexDirection='column' h='full'> | ||
| <AdminSidebarPages currentPath={currentPath}/> | ||
| {canViewSettings && <AdminSidebarSettings currentPath={currentPath}/>} | ||
| </Box> | ||
| </Scrollable> | ||
| </Box>; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import { useMemo, useState, useEffect } from 'react'; | ||
| import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; | ||
|
|
||
| import { settings } from '../../../app/settings'; | ||
| import { useTranslation } from '../../contexts/TranslationContext'; | ||
| import { PrivateSettingsCachedCollection } from '../PrivateSettingsCachedCollection'; | ||
|
|
||
| export const useSettingsGroupsFiltered = (textFilter) => { | ||
|
tassoevan marked this conversation as resolved.
Outdated
|
||
| const t = useTranslation(); | ||
|
|
||
| const [collection, setCollection] = useState(settings.collectionPrivate); | ||
|
|
||
| const [loading, setLoading] = useState(true); | ||
|
|
||
| const filter = useDebouncedValue(textFilter, 400); | ||
|
|
||
| useEffect(() => { | ||
| (async function getCollection() { | ||
| if (!settings.cachedCollectionPrivate) { | ||
| settings.cachedCollectionPrivate = new PrivateSettingsCachedCollection(); | ||
| settings.collectionPrivate = settings.cachedCollectionPrivate.collection; | ||
| await settings.cachedCollectionPrivate.init(); | ||
| } | ||
| setCollection(settings.collectionPrivate); | ||
| setLoading(false); | ||
| }()); | ||
| }, []); | ||
|
|
||
| return useMemo(() => { | ||
| if (loading) { return [[], loading]; } | ||
|
|
||
| const query = { | ||
| type: 'group', | ||
| }; | ||
|
|
||
| const groups = []; | ||
| if (filter) { | ||
| const filterRegex = new RegExp(filter, 'i'); | ||
| const records = collection.find().fetch(); | ||
| records.forEach(function(record) { | ||
| if (filterRegex.test(t(record.i18nLabel || record._id))) { | ||
| !groups.includes(record.group || record._id) && groups.push(record.group || record._id); | ||
| } | ||
| }); | ||
|
|
||
| if (groups.length > 0) { | ||
| query._id = { | ||
| $in: groups, | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| if (filter && groups.length === 0) { | ||
| return [[], loading]; | ||
| } | ||
|
|
||
| const result = collection.find(query) | ||
| .fetch() | ||
| .map((item) => ({ name: t(item.i18nLabel || item._id), href: 'admin', pathGroup: item._id })) | ||
| .sort(({ name: a }, { name: b }) => (a.toLowerCase() >= b.toLowerCase() ? 1 : -1)); | ||
|
|
||
| return [result, loading]; | ||
| }, [filter, collection, loading]); | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.