diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js index b5cac9abccdc8..e2e7d66c7ad80 100644 --- a/.storybook/webpack.config.js +++ b/.storybook/webpack.config.js @@ -42,7 +42,6 @@ module.exports = async ({ config }) => { }, }, }, - 'react-docgen-typescript-loader', ], }); diff --git a/app/ui-sidenav/client/sidebarHeader.js b/app/ui-sidenav/client/sidebarHeader.js index b748f03678358..4ca2d53506bce 100644 --- a/app/ui-sidenav/client/sidebarHeader.js +++ b/app/ui-sidenav/client/sidebarHeader.js @@ -159,8 +159,6 @@ const toolbarButtons = (/* user */) => [{ type: 'open', id: 'administration', action: () => { - SideNav.setFlex('adminFlex'); - SideNav.openFlex(); FlowRouter.go('admin', { group: 'info' }); popover.close(); }, diff --git a/app/ui-utils/client/lib/SideNav.js b/app/ui-utils/client/lib/SideNav.js index 116e9c8bdc502..5f0da9b4cc6f8 100644 --- a/app/ui-utils/client/lib/SideNav.js +++ b/app/ui-utils/client/lib/SideNav.js @@ -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); } diff --git a/app/ui-utils/client/lib/openRoom.js b/app/ui-utils/client/lib/openRoom.js index 71b35a397d0c0..29f05583bc428 100644 --- a/app/ui-utils/client/lib/openRoom.js +++ b/app/ui-utils/client/lib/openRoom.js @@ -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')) { diff --git a/client/admin/AdministrationLayout.tsx b/client/admin/AdministrationLayout.tsx new file mode 100644 index 0000000000000..8cb5090d413d6 --- /dev/null +++ b/client/admin/AdministrationLayout.tsx @@ -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; diff --git a/client/admin/AdministrationRouter.js b/client/admin/AdministrationRouter.js index ed329405bdd6d..0812ca904a6aa 100644 --- a/client/admin/AdministrationRouter.js +++ b/client/admin/AdministrationRouter.js @@ -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 }> - - ; + return + + }> + + + + ; } export default AdministrationRouter; diff --git a/client/admin/PrivateSettingsCachedCollection.js b/client/admin/PrivateSettingsCachedCollection.js deleted file mode 100644 index a2d0bf40ea907..0000000000000 --- a/client/admin/PrivateSettingsCachedCollection.js +++ /dev/null @@ -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(); - }); - } -} diff --git a/client/admin/PrivateSettingsCachedCollection.ts b/client/admin/PrivateSettingsCachedCollection.ts new file mode 100644 index 0000000000000..c1ea4b3dacd4b --- /dev/null +++ b/client/admin/PrivateSettingsCachedCollection.ts @@ -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 { + 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; + } +} diff --git a/client/admin/settings/SettingsState.js b/client/admin/PrivilegedSettingsProvider.js similarity index 63% rename from client/admin/settings/SettingsState.js rename to client/admin/PrivilegedSettingsProvider.js index ce4016cd27864..911cd00e859d8 100644 --- a/client/admin/settings/SettingsState.js +++ b/client/admin/PrivilegedSettingsProvider.js @@ -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 ; + return ; +} + +function PrivilegedSettingsProvider({ children }) { + const hasPermission = useAtLeastOnePermission([ + 'view-privileged-setting', + 'edit-privileged-setting', + 'manage-selected-settings', + ]); + + if (!hasPermission) { + return children; + } + + return ; } -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; diff --git a/client/admin/adminFlex.html b/client/admin/adminFlex.html deleted file mode 100644 index 0402664008c2e..0000000000000 --- a/client/admin/adminFlex.html +++ /dev/null @@ -1,42 +0,0 @@ - diff --git a/client/admin/adminFlex.js b/client/admin/adminFlex.js deleted file mode 100644 index ad4752da223cf..0000000000000 --- a/client/admin/adminFlex.js +++ /dev/null @@ -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); - }, -}); diff --git a/client/admin/index.js b/client/admin/index.js index eb3b7e29f7ae9..a962afc2ff1c9 100644 --- a/client/admin/index.js +++ b/client/admin/index.js @@ -1,4 +1,2 @@ -import './adminFlex'; - export { registerAdminRoute } from './routes'; export { registerAdminSidebarItem } from './sidebarItems'; diff --git a/client/admin/settings/GroupSelector.js b/client/admin/settings/GroupSelector.js index 2b4e9e45e4e66..551765b44d280 100644 --- a/client/admin/settings/GroupSelector.js +++ b/client/admin/settings/GroupSelector.js @@ -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 ; diff --git a/client/admin/settings/Section.js b/client/admin/settings/Section.js index e249c7ca04d95..6adfea97a1c07 100644 --- a/client/admin/settings/Section.js +++ b/client/admin/settings/Section.js @@ -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(); diff --git a/client/admin/settings/Setting.js b/client/admin/settings/Setting.js index 86add049c19f1..38e3b205f059b 100644 --- a/client/admin/settings/Setting.js +++ b/client/admin/settings/Setting.js @@ -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(); diff --git a/client/admin/settings/SettingsRoute.js b/client/admin/settings/SettingsRoute.js index 42f5323f10440..e0c4b80695d6d 100644 --- a/client/admin/settings/SettingsRoute.js +++ b/client/admin/settings/SettingsRoute.js @@ -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 ; } - return - - ; + return ; } export default SettingsRoute; diff --git a/client/admin/sidebar/AdminSidebar.js b/client/admin/sidebar/AdminSidebar.js new file mode 100644 index 0000000000000..935a81dd4751d --- /dev/null +++ b/client/admin/sidebar/AdminSidebar.js @@ -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 + + {icon && } + {label} + + ; +}); + +const SidebarItemsAssembler = React.memo(({ items, currentPath }) => { + const t = useTranslation(); + return items.map(({ + href, + i18nLabel, + name, + icon, + permissionGranted, + pathGroup, + }) => ); +}); + +const AdminSidebarPages = ({ currentPath }) => { + const items = useReactiveValue(() => sidebarItems.get()); + + return + + ; +}; + +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 + {t('Settings')} + + } + className={['asdsads']} + /> + + + {isLoadingGroups && } + {!isLoadingGroups && !!groups.length && ({ + name: t(group.i18nLabel || group._id), + href: 'admin', + pathGroup: group._id, + }))} + currentPath={currentPath} + />} + {!isLoadingGroups && !groups.length && {t('Nothing_found')}} + + ; +}; + +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 + + + {t('Administration')} + + + + + + {canViewSettings && } + + + + ; +} diff --git a/client/admin/sidebarItems.js b/client/admin/sidebarItems.js index c5112049b571b..657069b63a10a 100644 --- a/client/admin/sidebarItems.js +++ b/client/admin/sidebarItems.js @@ -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', diff --git a/client/contexts/PrivateSettingsContext.ts b/client/contexts/PrivilegedSettingsContext.ts similarity index 65% rename from client/contexts/PrivateSettingsContext.ts rename to client/contexts/PrivilegedSettingsContext.ts index e900175f54245..da463d6de2da4 100644 --- a/client/contexts/PrivateSettingsContext.ts +++ b/client/contexts/PrivilegedSettingsContext.ts @@ -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 = (a: T, b: T) => boolean; -type PrivateSettingsContextValue = { - subscribers: Set<(state: PrivateSettingsState) => void>; - stateRef: RefObject; +// TODO: split editing into another context +type PrivilegedSettingsContextValue = { + authorized: boolean; + loading: boolean; + subscribers: Set<(state: PrivilegedSettingsState) => void>; + stateRef: RefObject; hydrate: (changes: any[]) => void; - isDisabled: (setting: Setting) => boolean; + isDisabled: (setting: PrivilegedSetting) => boolean; }; -export const PrivateSettingsContext = createContext({ - subscribers: new Set<(state: PrivateSettingsState) => void>(), +export const PrivilegedSettingsContext = createContext({ + authorized: false, + loading: false, + subscribers: new Set<(state: PrivilegedSettingsState) => void>(), stateRef: { current: { settings: [], @@ -51,14 +59,59 @@ export const PrivateSettingsContext = createContext 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 = ( - selector: (state: PrivateSettingsState) => T, + selector: (state: PrivilegedSettingsState) => T, equalityFunction: EqualityFunction = Object.is, ): T | null => { - const { subscribers, stateRef } = useContext(PrivateSettingsContext); + const { subscribers, stateRef } = useContext(PrivilegedSettingsContext); const [value, setValue] = useState(() => (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 = ( 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; diff --git a/package-lock.json b/package-lock.json index 0427269a54ab3..566d084ec16c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 3e3b71fdd8816..2e50c68c415df 100644 --- a/package.json +++ b/package.json @@ -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",