[IMPROVE] Rewrite admin sidebar in React (#17801)

Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat>
Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>
pull/15932/merge
gabriellsh 6 years ago committed by GitHub
parent 70498a454f
commit 2c41b94284
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .storybook/webpack.config.js
  2. 2
      app/ui-sidenav/client/sidebarHeader.js
  3. 7
      app/ui-utils/client/lib/SideNav.js
  4. 25
      app/ui-utils/client/lib/openRoom.js
  5. 16
      client/admin/AdministrationLayout.tsx
  6. 20
      client/admin/AdministrationRouter.js
  7. 20
      client/admin/PrivateSettingsCachedCollection.js
  8. 29
      client/admin/PrivateSettingsCachedCollection.ts
  9. 102
      client/admin/PrivilegedSettingsProvider.js
  10. 42
      client/admin/adminFlex.html
  11. 92
      client/admin/adminFlex.js
  12. 2
      client/admin/index.js
  13. 4
      client/admin/settings/GroupSelector.js
  14. 10
      client/admin/settings/Section.js
  15. 4
      client/admin/settings/Setting.js
  16. 13
      client/admin/settings/SettingsRoute.js
  17. 154
      client/admin/sidebar/AdminSidebar.js
  18. 3
      client/admin/sidebarItems.js
  19. 109
      client/contexts/PrivilegedSettingsContext.ts
  20. 234
      package-lock.json
  21. 2
      package.json

@ -42,7 +42,6 @@ module.exports = async ({ config }) => {
},
},
},
'react-docgen-typescript-loader',
],
});

@ -159,8 +159,6 @@ const toolbarButtons = (/* user */) => [{
type: 'open',
id: 'administration',
action: () => {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
FlowRouter.go('admin', { group: 'info' });
popover.close();
},

@ -32,11 +32,13 @@ export const SideNav = new class {
}
if (window.DISABLE_ANIMATION === true) {
!this.flexNav.opened && this.setFlex();
this.animating = false;
return typeof callback === 'function' && callback();
}
return setTimeout(() => {
!this.flexNav.opened && this.setFlex();
this.animating = false;
return typeof callback === 'function' && callback();
}, 500);
@ -62,10 +64,7 @@ export const SideNav = new class {
return this.flexNav.opened;
}
setFlex(template, data) {
if (data == null) {
data = {};
}
setFlex(template, data = {}) {
Session.set('flex-nav-template', template);
return Session.set('flex-nav-data', data);
}

@ -31,15 +31,18 @@ const getDomOfLoading = mem(function getDomOfLoading() {
function replaceCenterDomBy(dom) {
document.dispatchEvent(new CustomEvent('main-content-destroyed'));
const mainNode = document.querySelector('.main-content');
if (mainNode) {
for (const child of Array.from(mainNode.children)) {
if (child) { mainNode.removeChild(child); }
}
mainNode.appendChild(dom);
}
return mainNode;
return new Promise((resolve) => {
setTimeout(() => {
const mainNode = document.querySelector('.main-content');
if (mainNode) {
for (const child of Array.from(mainNode.children)) {
if (child) { mainNode.removeChild(child); }
}
mainNode.appendChild(dom);
}
resolve(mainNode);
}, 1);
});
}
const waitUntilRoomBeInserted = async (type, rid) => new Promise((resolve) => {
@ -69,7 +72,7 @@ export const openRoom = async function(type, name) {
if (settings.get('Accounts_AllowAnonymousRead')) {
BlazeLayout.render('main');
}
replaceCenterDomBy(getDomOfLoading());
await replaceCenterDomBy(getDomOfLoading());
return;
}
@ -85,7 +88,7 @@ export const openRoom = async function(type, name) {
}
const roomDom = RoomManager.getDomOfRoom(type + name, room._id, roomTypes.getConfig(type).mainTemplate);
const mainNode = replaceCenterDomBy(roomDom);
const mainNode = await replaceCenterDomBy(roomDom);
if (mainNode) {
if (roomDom.classList.contains('room-container')) {

@ -0,0 +1,16 @@
import React, { useEffect, FC } from 'react';
import { SideNav } from '../../app/ui-utils/client';
const AdministrationLayout: FC = ({ children }) => {
useEffect(() => {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
}, []);
return <>
{children}
</>;
};
export default AdministrationLayout;

@ -1,19 +1,19 @@
import React, { lazy, useMemo, Suspense, useEffect } from 'react';
import React, { lazy, useMemo, Suspense } from 'react';
import { SideNav } from '../../app/ui-utils/client';
import AdministrationLayout from './AdministrationLayout';
import PrivilegedSettingsProvider from './PrivilegedSettingsProvider';
import PageSkeleton from './PageSkeleton';
function AdministrationRouter({ lazyRouteComponent, ...props }) {
useEffect(() => {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
}, []);
const LazyRouteComponent = useMemo(() => lazy(lazyRouteComponent), [lazyRouteComponent]);
return <Suspense fallback={<PageSkeleton />}>
<LazyRouteComponent {...props} />
</Suspense>;
return <PrivilegedSettingsProvider>
<AdministrationLayout>
<Suspense fallback={<PageSkeleton />}>
<LazyRouteComponent {...props} />
</Suspense>
</AdministrationLayout>
</PrivilegedSettingsProvider>;
}
export default AdministrationRouter;

@ -1,20 +0,0 @@
import { CachedCollection } from '../../app/ui-cached-collection';
import { Notifications } from '../../app/notifications/client';
export class PrivateSettingsCachedCollection extends CachedCollection {
constructor() {
super({
name: 'private-settings',
eventType: 'onLogged',
});
}
async setupListener(eventType, eventName) {
// private settings also need to listen to a change of authorizations for the setting-based authorizations
Notifications[eventType || this.eventType](eventName || this.eventName, async (t, { _id, ...record }) => {
this.log('record received', t, { _id, ...record });
this.collection.upsert({ _id }, record);
this.sync();
});
}
}

@ -0,0 +1,29 @@
import { CachedCollection } from '../../app/ui-cached-collection/client';
import { Notifications } from '../../app/notifications/client';
export class PrivateSettingsCachedCollection extends CachedCollection {
constructor() {
super({
name: 'private-settings',
eventType: 'onLogged',
});
}
async setupListener(): Promise<void> {
Notifications.onLogged(this.eventName, async (t: string, { _id, ...record }: { _id: string }) => {
this.log('record received', t, { _id, ...record });
this.collection.upsert({ _id }, record);
this.sync();
});
}
static instance: PrivateSettingsCachedCollection;
static get(): PrivateSettingsCachedCollection {
if (!PrivateSettingsCachedCollection.instance) {
PrivateSettingsCachedCollection.instance = new PrivateSettingsCachedCollection();
}
return PrivateSettingsCachedCollection.instance;
}
}

@ -1,20 +1,11 @@
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { Mongo } from 'meteor/mongo';
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
import { Tracker } from 'meteor/tracker';
import React, { useEffect, useMemo, useReducer, useRef, useState } from 'react';
import { PrivateSettingsCachedCollection } from '../PrivateSettingsCachedCollection';
import { PrivateSettingsContext } from '../../contexts/PrivateSettingsContext';
let privateSettingsCachedCollection; // Remove this singleton (╯°□°)╯︵ ┻━┻
const getPrivateSettingsCachedCollection = () => {
if (privateSettingsCachedCollection) {
return [privateSettingsCachedCollection, Promise.resolve()];
}
privateSettingsCachedCollection = new PrivateSettingsCachedCollection();
return [privateSettingsCachedCollection, privateSettingsCachedCollection.init()];
};
import { PrivilegedSettingsContext } from '../contexts/PrivilegedSettingsContext';
import { useAtLeastOnePermission } from '../contexts/AuthorizationContext';
import { PrivateSettingsCachedCollection } from './PrivateSettingsCachedCollection';
const compareStrings = (a = '', b = '') => {
if (a === b || (!a && !b)) {
@ -79,39 +70,37 @@ const settingsReducer = (states, { type, payload }) => {
return states;
};
export function SettingsState({ children }) {
function AuthorizedPrivilegedSettingsProvider({ cachedCollection, children }) {
const [isLoading, setLoading] = useState(true);
const [subscribers] = useState(new Set());
const subscribersRef = useRef();
if (!subscribersRef.current) {
subscribersRef.current = new Set();
}
const stateRef = useRef({ settings: [], persistedSettings: [] });
const enhancedReducer = useCallback((state, action) => {
const newState = settingsReducer(state, action);
stateRef.current = newState;
subscribers.forEach((subscriber) => {
subscriber(newState);
});
return newState;
}, [settingsReducer, subscribers]);
const [state, dispatch] = useReducer(settingsReducer, { settings: [], persistedSettings: [] });
stateRef.current = state;
const [, dispatch] = useReducer(enhancedReducer, { settings: [], persistedSettings: [] });
subscribersRef.current.forEach((subscriber) => {
subscriber(state);
});
const collectionsRef = useRef({});
useEffect(() => {
const [privateSettingsCachedCollection, loadingPromise] = getPrivateSettingsCachedCollection();
const stopLoading = () => {
setLoading(false);
};
loadingPromise.then(stopLoading, stopLoading);
if (!Tracker.nonreactive(() => cachedCollection.ready.get())) {
cachedCollection.init().then(stopLoading, stopLoading);
} else {
stopLoading();
}
const { collection: persistedSettingsCollection } = privateSettingsCachedCollection;
const { collection: persistedSettingsCollection } = cachedCollection;
const settingsCollection = new Mongo.Collection(null);
collectionsRef.current = {
@ -163,21 +152,21 @@ export function SettingsState({ children }) {
const updateTimersRef = useRef({});
const updateAtCollection = useCallback(({ _id, ...data }) => {
const updateAtCollection = useMutableCallback(({ _id, ...data }) => {
const { current: { settingsCollection } } = collectionsRef;
const { current: updateTimers } = updateTimersRef;
clearTimeout(updateTimers[_id]);
updateTimers[_id] = setTimeout(() => {
settingsCollection.update(_id, { $set: data });
}, 70);
}, [collectionsRef, updateTimersRef]);
});
const hydrate = useCallback((changes) => {
const hydrate = useMutableCallback((changes) => {
changes.forEach(updateAtCollection);
dispatch({ type: 'hydrate', payload: changes });
}, [updateAtCollection, dispatch]);
});
const isDisabled = useCallback(({ blocked, enableQuery }) => {
const isDisabled = useMutableCallback(({ blocked, enableQuery }) => {
if (blocked) {
return true;
}
@ -190,28 +179,39 @@ export function SettingsState({ children }) {
const queries = [].concat(typeof enableQuery === 'string' ? JSON.parse(enableQuery) : enableQuery);
return !queries.every((query) => !!settingsCollection.findOne(query));
}, [collectionsRef]);
});
const contextValue = useMemo(() => ({
subscribers,
authorized: true,
loading: isLoading,
subscribers: subscribersRef.current,
stateRef,
hydrate,
isDisabled,
}), [
subscribers,
stateRef,
isLoading,
hydrate,
isDisabled,
]);
return <PrivateSettingsContext.Provider children={children} value={contextValue} />;
return <PrivilegedSettingsContext.Provider children={children} value={contextValue} />;
}
function PrivilegedSettingsProvider({ children }) {
const hasPermission = useAtLeastOnePermission([
'view-privileged-setting',
'edit-privileged-setting',
'manage-selected-settings',
]);
if (!hasPermission) {
return children;
}
return <AuthorizedPrivilegedSettingsProvider
cachedCollection={PrivateSettingsCachedCollection.get()}
children={children}
/>;
}
export {
usePrivateSettingsGroup as useGroup,
usePrivateSettingsSection as useSection,
usePrivateSettingActions as useSettingActions,
usePrivateSettingDisabledState as useSettingDisabledState,
usePrivateSettingsSectionChangedState as useSectionChangedState,
usePrivateSetting as useSetting,
} from '../../contexts/PrivateSettingsContext';
export default PrivilegedSettingsProvider;

@ -1,42 +0,0 @@
<template name="adminFlex">
<aside class="sidebar-light sidebar--medium" role="navigation">
<header class="sidebar-flex__header">
<h1 class="sidebar-flex__title">{{_ "Administration"}}</h1>
<button class="sidebar-flex__close-button" data-action="close">
{{> icon block="sidebar-flex__close-icon" icon="cross"}}
</button>
</header>
<div class="rooms-list {{#if isEmbedded}}rooms-list--embedded{{/if}}" aria-label="{{_ "Administration"}}">
<ul class="rooms-list__list">
{{#each _sidebarItem in sidebarItems}}
{{> sidebarItem _sidebarItem }}
{{/each}}
</ul>
{{#if hasSettingPermission}}
<h3 class="rooms-list__type">{{_ "Settings"}}</h3>
<div class="rc-input sidebar-flex__search">
<label class="rc-input__label">
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-sv" icon="magnifier"}}
</div>
<input type="text" class="rc-input__element rc-input__element--small" name="settings-search" placeholder="{{_ 'Search'}}">
</div>
</label>
</div>
<ul class="rooms-list__list">
{{#each _group in groups}}
{{> sidebarItem _group}}
{{else}}
<div class="rc-input sidebar-flex__search">
<div class="rc-input__wrapper">
<p>{{_ "Nothing_found"}}.</p>
</div>
</div>
{{/each}}
</ul>
{{/if}}
</div>
</aside>
</template>

@ -1,92 +0,0 @@
import _ from 'underscore';
import s from 'underscore.string';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { settings } from '../../app/settings';
import { menu, SideNav, Layout } from '../../app/ui-utils/client';
import { t } from '../../app/utils/client';
import { PrivateSettingsCachedCollection } from './PrivateSettingsCachedCollection';
import { hasAtLeastOnePermission } from '../../app/authorization/client';
import { sidebarItems } from './sidebarItems';
import './adminFlex.html';
Template.adminFlex.onCreated(function() {
this.settingsFilter = new ReactiveVar('');
if (settings.cachedCollectionPrivate == null) {
settings.cachedCollectionPrivate = new PrivateSettingsCachedCollection();
settings.collectionPrivate = settings.cachedCollectionPrivate.collection;
settings.cachedCollectionPrivate.init();
}
});
Template.adminFlex.helpers({
isEmbedded: () => Layout.isEmbedded(),
sidebarItems: () => sidebarItems.get()
.filter((sidebarItem) => !sidebarItem.permissionGranted || sidebarItem.permissionGranted())
.map(({ _id, i18nLabel, icon, href }) => ({
name: t(i18nLabel || _id),
icon,
pathSection: href,
darken: true,
isLightSidebar: true,
active: href === FlowRouter.getRouteName(),
})),
hasSettingPermission: () =>
hasAtLeastOnePermission(['view-privileged-setting', 'edit-privileged-setting', 'manage-selected-settings']),
groups: () => {
const filter = Template.instance().settingsFilter.get();
const query = {
type: 'group',
};
let groups = [];
if (filter) {
const filterRegex = new RegExp(s.escapeRegExp(filter), 'i');
const records = settings.collectionPrivate.find().fetch();
records.forEach(function(record) {
if (filterRegex.test(TAPi18n.__(record.i18nLabel || record._id))) {
groups.push(record.group || record._id);
}
});
groups = _.unique(groups);
if (groups.length > 0) {
query._id = {
$in: groups,
};
}
}
if (filter && groups.length === 0) {
return [];
}
return settings.collectionPrivate.find(query)
.fetch()
.map((item) => ({ ...item, name: t(item.i18nLabel || item._id) }))
.sort(({ name: a }, { name: b }) => (a.toLowerCase() >= b.toLowerCase() ? 1 : -1))
.map(({ _id, name }) => ({
name,
pathSection: 'admin',
pathGroup: _id,
darken: true,
isLightSidebar: true,
active: _id === FlowRouter.getParam('group'),
}));
},
});
Template.adminFlex.events({
'click [data-action="close"]'() {
if (Layout.isEmbedded()) {
menu.close();
return;
}
SideNav.closeFlex();
},
'keyup [name=settings-search]'(e, t) {
t.settingsFilter.set(e.target.value);
},
});

@ -1,4 +1,2 @@
import './adminFlex';
export { registerAdminRoute } from './routes';
export { registerAdminSidebarItem } from './sidebarItems';

@ -1,13 +1,13 @@
import React from 'react';
import { usePrivateSettingsGroup } from '../../contexts/PrivateSettingsContext';
import { usePrivilegedSettingsGroup } from '../../contexts/PrivilegedSettingsContext';
import { AssetsGroupPage } from './groups/AssetsGroupPage';
import { OAuthGroupPage } from './groups/OAuthGroupPage';
import { GenericGroupPage } from './groups/GenericGroupPage';
import { GroupPage } from './GroupPage';
export function GroupSelector({ groupId }) {
const group = usePrivateSettingsGroup(groupId);
const group = usePrivilegedSettingsGroup(groupId);
if (!group) {
return <GroupPage.Skeleton />;

@ -2,15 +2,15 @@ import { Accordion, Box, Button, FieldGroup, Skeleton } from '@rocket.chat/fusel
import React from 'react';
import {
usePrivateSettingsSection,
usePrivateSettingsSectionChangedState,
} from '../../contexts/PrivateSettingsContext';
usePrivilegedSettingsSection,
usePrivilegedSettingsSectionChangedState,
} from '../../contexts/PrivilegedSettingsContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { Setting } from './Setting';
export function Section({ children, groupId, hasReset = true, help, sectionName, solo }) {
const section = usePrivateSettingsSection(groupId, sectionName);
const changed = usePrivateSettingsSectionChangedState(groupId, sectionName);
const section = usePrivilegedSettingsSection(groupId, sectionName);
const changed = usePrivilegedSettingsSectionChangedState(groupId, sectionName);
const t = useTranslation();

@ -2,7 +2,7 @@ import { Callout, Field, Flex, InputBox, Margins, Skeleton } from '@rocket.chat/
import React, { memo, useEffect, useMemo, useState, useCallback } from 'react';
import MarkdownText from '../../components/basic/MarkdownText';
import { usePrivateSetting } from '../../contexts/PrivateSettingsContext';
import { usePrivilegedSetting } from '../../contexts/PrivilegedSettingsContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { GenericSettingInput } from './inputs/GenericSettingInput';
import { BooleanSettingInput } from './inputs/BooleanSettingInput';
@ -70,7 +70,7 @@ export function Setting({ settingId, sectionChanged }) {
update,
reset,
...setting
} = usePrivateSetting(settingId);
} = usePrivilegedSetting(settingId);
const t = useTranslation();

@ -1,17 +1,12 @@
import React from 'react';
import { useAtLeastOnePermission } from '../../contexts/AuthorizationContext';
import { usePrivilegedSettingsAuthorized } from '../../contexts/PrivilegedSettingsContext';
import { useRouteParameter } from '../../contexts/RouterContext';
import { GroupSelector } from './GroupSelector';
import NotAuthorizedPage from '../NotAuthorizedPage';
import { SettingsState } from './SettingsState';
export function SettingsRoute() {
const hasPermission = useAtLeastOnePermission([
'view-privileged-setting',
'edit-privileged-setting',
'manage-selected-settings',
]);
const hasPermission = usePrivilegedSettingsAuthorized();
const groupId = useRouteParameter('group');
@ -19,9 +14,7 @@ export function SettingsRoute() {
return <NotAuthorizedPage />;
}
return <SettingsState>
<GroupSelector groupId={groupId} />
</SettingsState>;
return <GroupSelector groupId={groupId} />;
}
export default SettingsRoute;

@ -0,0 +1,154 @@
import { css } from '@rocket.chat/css-in-js';
import { Box, Button, Icon, SearchInput, Scrollable, Skeleton } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import React, { useCallback, useState, useMemo, useEffect } from 'react';
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 PrivilegedSettingsProvider from '../PrivilegedSettingsProvider';
import { usePrivilegedSettingsGroups } from '../../contexts/PrivilegedSettingsContext';
const SidebarItem = React.memo(({ permissionGranted, pathGroup, href, icon, label, currentPath }) => {
const params = useMemo(() => ({ group: pathGroup }), [pathGroup]);
const path = useRoutePath(href, params);
const isActive = path === currentPath || false;
if (permissionGranted && !permissionGranted()) { return null; }
return <Box
is='a'
color='default'
pb='x8'
pi='x24'
key={path}
href={path}
className={[
isActive && 'active',
css`
&:hover,
&:focus,
&.active:focus,
&.active:hover {
background-color: var(--sidebar-background-light-hover);
}
&.active {
background-color: var(--sidebar-background-light-active);
}
`,
].filter(Boolean)}
>
<Box
mi='neg-x4'
display='flex'
flexDirection='row'
alignItems='center'>
{icon && <Icon name={icon} size='x20' mi='x4'/>}
<Box withTruncatedText fontScale='p1' mi='x4' color='info'>{label}</Box>
</Box>
</Box>;
});
const SidebarItemsAssembler = React.memo(({ 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 display='flex' flexDirection='column' flexShrink={0} pb='x8'>
<SidebarItemsAssembler items={items} currentPath={currentPath}/>
</Box>;
};
const AdminSidebarSettings = ({ currentPath }) => {
const t = useTranslation();
const [filter, setFilter] = useState('');
const handleChange = useCallback((e) => setFilter(e.currentTarget.value), []);
const groups = usePrivilegedSettingsGroups(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'/>}
className={['asdsads']}
/>
</Box>
<Box pb='x16' display='flex' flexDirection='column'>
{isLoadingGroups && <Skeleton/>}
{!isLoadingGroups && !!groups.length && <SidebarItemsAssembler
items={groups.map((group) => ({
name: t(group.i18nLabel || group._id),
href: 'admin',
pathGroup: group._id,
}))}
currentPath={currentPath}
/>}
{!isLoadingGroups && !groups.length && <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);
useEffect(() => {
if (!currentPath.startsWith('/admin/')) {
SideNav.closeFlex();
}
}, [currentRoute]);
// TODO: uplift this provider
return <PrivilegedSettingsProvider>
<Box display='flex' flexDirection='column' h='100vh'>
<Box is='header' pb='x16' pi='x24' display='flex' flexDirection='row' alignItems='center' justifyContent='space-between'>
<Box color='neutral-800' fontSize='p1' fontWeight='p1' fontWeight='p1' flexShrink={1} withTruncatedText>{t('Administration')}</Box>
<Button square small ghost onClick={closeAdminFlex}><Icon name='cross' size='x20'/></Button>
</Box>
<Scrollable>
<Box display='flex' flexDirection='column' h='full'>
<AdminSidebarPages currentPath={currentPath}/>
{canViewSettings && <AdminSidebarSettings currentPath={currentPath}/>}
</Box>
</Scrollable>
</Box>
</PrivilegedSettingsProvider>;
}

@ -1,6 +1,7 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { hasPermission } from '../../app/authorization/client';
import { createTemplateForComponent } from '../reactAdapters';
export const sidebarItems = new ReactiveVar([]);
@ -8,6 +9,8 @@ export const registerAdminSidebarItem = (itemOptions) => {
sidebarItems.set([...sidebarItems.get(), itemOptions]);
};
createTemplateForComponent('adminFlex', () => import('./sidebar/AdminSidebar'));
registerAdminSidebarItem({
href: 'admin-info',
i18nLabel: 'Info',

@ -1,6 +1,7 @@
import { useDebouncedCallback, useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { Tracker } from 'meteor/tracker';
import { createContext, useContext, RefObject, useState, useEffect, useLayoutEffect } from 'react';
import { createContext, useContext, RefObject, useState, useEffect, useLayoutEffect, useMemo, useCallback } from 'react';
import { useSubscription } from 'use-subscription';
import { useReactiveValue } from '../hooks/useReactiveValue';
import { useBatchSettingsDispatch } from './SettingsContext';
@ -8,8 +9,8 @@ import { useToastMessageDispatch } from './ToastMessagesContext';
import { useTranslation, useLoadLanguage } from './TranslationContext';
import { useUser } from './UserContext';
type Setting = object & {
_id: unknown;
export type PrivilegedSetting = object & {
_id: string;
type: string;
blocked: boolean;
enableQuery: unknown;
@ -20,27 +21,34 @@ type Setting = object & {
packageValue: unknown;
packageEditor: unknown;
editor: unknown;
sorter: string;
i18nLabel: string;
disabled?: boolean;
update?: () => void;
reset?: () => void;
};
type PrivateSettingsState = {
settings: Setting[];
persistedSettings: Setting[];
export type PrivilegedSettingsState = {
settings: PrivilegedSetting[];
persistedSettings: PrivilegedSetting[];
};
type EqualityFunction<T> = (a: T, b: T) => boolean;
type PrivateSettingsContextValue = {
subscribers: Set<(state: PrivateSettingsState) => void>;
stateRef: RefObject<PrivateSettingsState>;
// TODO: split editing into another context
type PrivilegedSettingsContextValue = {
authorized: boolean;
loading: boolean;
subscribers: Set<(state: PrivilegedSettingsState) => void>;
stateRef: RefObject<PrivilegedSettingsState>;
hydrate: (changes: any[]) => void;
isDisabled: (setting: Setting) => boolean;
isDisabled: (setting: PrivilegedSetting) => boolean;
};
export const PrivateSettingsContext = createContext<PrivateSettingsContextValue>({
subscribers: new Set<(state: PrivateSettingsState) => void>(),
export const PrivilegedSettingsContext = createContext<PrivilegedSettingsContextValue>({
authorized: false,
loading: false,
subscribers: new Set<(state: PrivilegedSettingsState) => void>(),
stateRef: {
current: {
settings: [],
@ -51,14 +59,59 @@ export const PrivateSettingsContext = createContext<PrivateSettingsContextValue>
isDisabled: () => false,
});
export const usePrivilegedSettingsAuthorized = (): boolean =>
useContext(PrivilegedSettingsContext).authorized;
export const useIsPrivilegedSettingsLoading = (): boolean =>
useContext(PrivilegedSettingsContext).loading;
export const usePrivilegedSettingsGroups = (filter?: string): any => {
const { stateRef, subscribers } = useContext(PrivilegedSettingsContext);
const t = useTranslation();
const getCurrentValue = useCallback(() => {
const filterRegex = filter ? new RegExp(filter, 'i') : null;
const filterPredicate = (setting: PrivilegedSetting): boolean =>
!filterRegex || filterRegex.test(t(setting.i18nLabel || setting._id));
const groupIds = Array.from(new Set(
(stateRef.current?.persistedSettings ?? [])
.filter(filterPredicate)
.map((setting) => setting.group || setting._id),
));
return (stateRef.current?.persistedSettings ?? [])
.filter(({ type, group, _id }) => type === 'group' && groupIds.includes(group || _id))
.sort((a, b) => t(a.i18nLabel || a._id).localeCompare(t(b.i18nLabel || b._id)));
}, [filter]);
const subscribe = useCallback((cb) => {
const handleUpdate = (): void => {
cb(getCurrentValue());
};
subscribers.add(handleUpdate);
return (): void => {
subscribers.delete(handleUpdate);
};
}, [getCurrentValue]);
return useSubscription(useMemo(() => ({
getCurrentValue,
subscribe,
}), [getCurrentValue, subscribe]));
};
const useSelector = <T>(
selector: (state: PrivateSettingsState) => T,
selector: (state: PrivilegedSettingsState) => T,
equalityFunction: EqualityFunction<T> = Object.is,
): T | null => {
const { subscribers, stateRef } = useContext(PrivateSettingsContext);
const { subscribers, stateRef } = useContext(PrivilegedSettingsContext);
const [value, setValue] = useState<T | null>(() => (stateRef.current ? selector(stateRef.current) : null));
const handleUpdate = useMutableCallback((state: PrivateSettingsState) => {
const handleUpdate = useMutableCallback((state: PrivilegedSettingsState) => {
const newValue = selector(state);
if (!value || !equalityFunction(newValue, value)) {
@ -81,7 +134,7 @@ const useSelector = <T>(
return value;
};
export const usePrivateSettingsGroup = (groupId: string): any => {
export const usePrivilegedSettingsGroup = (groupId: string): any => {
const group = useSelector((state) => state.settings.find(({ _id, type }) => _id === groupId && type === 'group'));
const filterSettings = (settings: any[]): any[] => settings.filter(({ group }) => group === groupId);
@ -90,7 +143,7 @@ export const usePrivateSettingsGroup = (groupId: string): any => {
const sections = useSelector((state) => Array.from(new Set(filterSettings(state.settings).map(({ section }) => section || ''))), (a, b) => a.length === b.length && a.join() === b.join());
const batchSetSettings = useBatchSettingsDispatch();
const { stateRef, hydrate } = useContext(PrivateSettingsContext);
const { stateRef, hydrate } = useContext(PrivilegedSettingsContext);
const dispatchToastMessage = useToastMessageDispatch() as any;
const t = useTranslation() as (key: string, ...args: any[]) => string;
@ -148,7 +201,7 @@ export const usePrivateSettingsGroup = (groupId: string): any => {
return group && { ...group, sections, changed, save, cancel };
};
export const usePrivateSettingsSection = (groupId: string, sectionName?: string): any => {
export const usePrivilegedSettingsSection = (groupId: string, sectionName?: string): any => {
sectionName = sectionName || '';
const filterSettings = (settings: any[]): any[] =>
@ -157,7 +210,7 @@ export const usePrivateSettingsSection = (groupId: string, sectionName?: string)
const canReset = useSelector((state) => filterSettings(state.settings).some(({ value, packageValue }) => JSON.stringify(value) !== JSON.stringify(packageValue)));
const settingsIds = useSelector((state) => filterSettings(state.settings).map(({ _id }) => _id), (a, b) => a.length === b.length && a.join() === b.join());
const { stateRef, hydrate, isDisabled } = useContext(PrivateSettingsContext);
const { stateRef, hydrate, isDisabled } = useContext(PrivilegedSettingsContext);
const reset = useMutableCallback(() => {
const state = stateRef.current;
@ -186,11 +239,11 @@ export const usePrivateSettingsSection = (groupId: string, sectionName?: string)
};
};
export const usePrivateSettingActions = (persistedSetting: Setting | null | undefined): {
export const usePrivilegedSettingActions = (persistedSetting: PrivilegedSetting | null | undefined): {
update: () => void;
reset: () => void;
} => {
const { hydrate } = useContext(PrivateSettingsContext);
const { hydrate } = useContext(PrivilegedSettingsContext);
const update = useDebouncedCallback(({ value, editor }) => {
const changes = [{
@ -217,24 +270,24 @@ export const usePrivateSettingActions = (persistedSetting: Setting | null | unde
return { update, reset };
};
export const usePrivateSettingDisabledState = (setting: Setting | null | undefined): boolean => {
const { isDisabled } = useContext(PrivateSettingsContext);
export const usePrivilegedSettingDisabledState = (setting: PrivilegedSetting | null | undefined): boolean => {
const { isDisabled } = useContext(PrivilegedSettingsContext);
return useReactiveValue(() => (setting ? isDisabled(setting) : false), [setting?.blocked, setting?.enableQuery]) as unknown as boolean;
};
export const usePrivateSettingsSectionChangedState = (groupId: string, sectionName: string): boolean =>
export const usePrivilegedSettingsSectionChangedState = (groupId: string, sectionName: string): boolean =>
!!useSelector((state) =>
state.settings.some(({ group, section, changed }) =>
group === groupId && ((!sectionName && !section) || (sectionName === section)) && changed));
export const usePrivateSetting = (_id: string): Setting | null | undefined => {
const selectSetting = (settings: Setting[]): Setting | undefined => settings.find((setting) => setting._id === _id);
export const usePrivilegedSetting = (_id: string): PrivilegedSetting | null | undefined => {
const selectSetting = (settings: PrivilegedSetting[]): PrivilegedSetting | undefined => settings.find((setting) => setting._id === _id);
const setting = useSelector((state) => selectSetting(state.settings));
const persistedSetting = useSelector((state) => selectSetting(state.persistedSettings));
const { update, reset } = usePrivateSettingActions(persistedSetting);
const disabled = usePrivateSettingDisabledState(persistedSetting);
const { update, reset } = usePrivilegedSettingActions(persistedSetting);
const disabled = usePrivilegedSettingDisabledState(persistedSetting);
if (!setting) {
return null;

234
package-lock.json generated

@ -6206,6 +6206,15 @@
"@types/react": "*"
}
},
"@types/react-dom": {
"version": "16.9.8",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.8.tgz",
"integrity": "sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-syntax-highlighter": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.4.tgz",
@ -6643,93 +6652,6 @@
"@xtuc/long": "4.2.1"
}
},
"@webpack-contrib/schema-utils": {
"version": "1.0.0-beta.0",
"resolved": "https://registry.npmjs.org/@webpack-contrib/schema-utils/-/schema-utils-1.0.0-beta.0.tgz",
"integrity": "sha512-LonryJP+FxQQHsjGBi6W786TQB1Oym+agTpY0c+Kj8alnIw+DLUJb6SI8Y1GHGhLCH1yPRrucjObUmxNICQ1pg==",
"dev": true,
"requires": {
"ajv": "^6.1.0",
"ajv-keywords": "^3.1.0",
"chalk": "^2.3.2",
"strip-ansi": "^4.0.0",
"text-table": "^0.2.0",
"webpack-log": "^1.1.2"
},
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"dev": true
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"dev": true,
"requires": {
"ansi-regex": "^3.0.0"
}
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
},
"webpack-log": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-1.2.0.tgz",
"integrity": "sha512-U9AnICnu50HXtiqiDxuli5gLB5PGBo7VvcHx36jRZHwK4vzOYLbImqT4lwWwoMHdQWwEKw736fCHEekokTEKHA==",
"dev": true,
"requires": {
"chalk": "^2.1.0",
"log-symbols": "^2.1.0",
"loglevelnext": "^1.0.1",
"uuid": "^3.1.0"
}
}
}
},
"@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@ -12811,16 +12733,6 @@
}
}
},
"d": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
"integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
"dev": true,
"requires": {
"es5-ext": "^0.10.50",
"type": "^1.0.1"
}
},
"d3-array": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.4.0.tgz",
@ -13874,34 +13786,12 @@
"is-symbol": "^1.0.2"
}
},
"es5-ext": {
"version": "0.10.53",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz",
"integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==",
"dev": true,
"requires": {
"es6-iterator": "~2.0.3",
"es6-symbol": "~3.1.3",
"next-tick": "~1.0.0"
}
},
"es5-shim": {
"version": "4.5.14",
"resolved": "https://registry.npmjs.org/es5-shim/-/es5-shim-4.5.14.tgz",
"integrity": "sha512-7SwlpL+2JpymWTt8sNLuC2zdhhc+wrfe5cMPI2j0o6WsPdfAiPwmFy2f0AocPB4RQVBOZ9kNTgi5YF7TdhkvEg==",
"dev": true
},
"es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
"dev": true,
"requires": {
"d": "1",
"es5-ext": "^0.10.35",
"es6-symbol": "^3.1.1"
}
},
"es6-promise": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.5.tgz",
@ -13921,16 +13811,6 @@
"integrity": "sha512-E9kK/bjtCQRpN1K28Xh4BlmP8egvZBGJJ+9GtnzOwt7mdqtrjHFuVGr7QJfdjBIKqrlU5duPf3pCBoDrkjVYFg==",
"dev": true
},
"es6-symbol": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
"integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
"dev": true,
"requires": {
"d": "^1.0.1",
"ext": "^1.1.2"
}
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -14996,23 +14876,6 @@
}
}
},
"ext": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz",
"integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==",
"dev": true,
"requires": {
"type": "^2.0.0"
},
"dependencies": {
"type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz",
"integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==",
"dev": true
}
}
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@ -20853,16 +20716,6 @@
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz",
"integrity": "sha1-4PyVEztu8nbNyIh82vJKpvFW+Po="
},
"loglevelnext": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/loglevelnext/-/loglevelnext-1.0.5.tgz",
"integrity": "sha512-V/73qkPuJmx4BcBF19xPBr+0ZRVBhc4POxvZTZdMeXpJ4NItXSJ/MSwuFT0kQJlCbXvdlZoQQ/418bS1y9Jh6A==",
"dev": true,
"requires": {
"es6-symbol": "^3.1.1",
"object.assign": "^4.1.0"
}
},
"long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
@ -22877,12 +22730,6 @@
"integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==",
"dev": true
},
"next-tick": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=",
"dev": true
},
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@ -26672,63 +26519,6 @@
}
}
},
"react-docgen-typescript": {
"version": "1.16.3",
"resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-1.16.3.tgz",
"integrity": "sha512-xYISCr8mFKfV15talgpicOF/e0DudTucf1BXzu/HteMF4RM3KsfxXkhWybZC3LTVbYrdbammDV26Z4Yuk+MoWg==",
"dev": true
},
"react-docgen-typescript-loader": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/react-docgen-typescript-loader/-/react-docgen-typescript-loader-3.7.2.tgz",
"integrity": "sha512-fNzUayyUGzSyoOl7E89VaPKJk9dpvdSgyXg81cUkwy0u+NBvkzQG3FC5WBIlXda0k/iaxS+PWi+OC+tUiGxzPA==",
"dev": true,
"requires": {
"@webpack-contrib/schema-utils": "^1.0.0-beta.0",
"loader-utils": "^1.2.3",
"react-docgen-typescript": "^1.15.0"
},
"dependencies": {
"big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
"dev": true
},
"emojis-list": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"dev": true
},
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
"requires": {
"minimist": "^1.2.0"
}
},
"loader-utils": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^1.0.1"
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
}
}
},
"react-dom": {
"version": "16.8.6",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz",
@ -30635,12 +30425,6 @@
}
}
},
"type": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
"integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==",
"dev": true
},
"type-check": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",

@ -69,6 +69,7 @@
"@types/mocha": "^7.0.2",
"@types/mock-require": "^2.0.0",
"@types/mongodb": "^3.5.8",
"@types/react-dom": "^16.9.8",
"@typescript-eslint/eslint-plugin": "^2.11.0",
"@typescript-eslint/parser": "^2.11.0",
"acorn": "^6.4.1",
@ -106,7 +107,6 @@
"postcss-url": "^8.0.0",
"progress": "^2.0.2",
"proxyquire": "^2.1.0",
"react-docgen-typescript-loader": "^3.7.2",
"simple-git": "^1.107.0",
"source-map": "^0.5.6",
"stylelint": "^9.9.0",

Loading…
Cancel
Save