From d73c7a7219fcd4712575fc078eb09bfc59e18288 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Thu, 18 Jun 2020 01:27:08 -0300 Subject: [PATCH] [IMPROVE] Performance editing Admin settings (#17916) --- app/settings/client/lib/settings.ts | 13 +- client/admin/AdministrationRouter.js | 6 +- client/admin/PrivilegedSettingsProvider.js | 217 ------------- client/admin/settings/GroupPage.js | 87 ++++- client/admin/settings/GroupPage.stories.js | 2 +- client/admin/settings/GroupSelector.js | 25 -- .../admin/settings/GroupSelector.stories.js | 2 +- client/admin/settings/GroupSelector.tsx | 32 ++ client/admin/settings/Section.js | 54 +++- client/admin/settings/Setting.js | 56 ++-- client/admin/settings/SettingsRoute.js | 11 +- .../admin/settings/groups/AssetsGroupPage.js | 10 +- .../admin/settings/groups/GenericGroupPage.js | 12 +- .../admin/settings/groups/OAuthGroupPage.js | 10 +- client/admin/sidebar/AdminSidebar.js | 58 +++- client/admin/users/UserInfo.js | 2 +- client/contexts/EditableSettingsContext.ts | 65 ++++ client/contexts/PrivilegedSettingsContext.ts | 302 ------------------ client/contexts/SettingsContext.js | 24 -- client/contexts/SettingsContext.ts | 66 ++++ client/fuselage-hooks.d.ts | 3 + client/hooks/useQuery.ts | 30 ++ .../hooks/useReactiveSubscriptionFactory.ts | 28 ++ .../PrivateSettingsCachedCollection.ts | 4 +- .../PublicSettingsCachedCollection.ts | 22 ++ client/meteor.d.ts | 19 ++ client/providers/EditableSettingsProvider.tsx | 137 ++++++++ client/providers/MeteorProvider.js | 2 +- client/providers/SettingsProvider.js | 33 -- client/providers/SettingsProvider.tsx | 113 +++++++ client/views/setupWizard/steps/FinalStep.js | 4 +- .../setupWizard/steps/RegisterServerStep.js | 6 +- .../setupWizard/steps/SettingsBasedStep.js | 8 +- definition/ISetting.ts | 42 +++ 34 files changed, 815 insertions(+), 690 deletions(-) delete mode 100644 client/admin/PrivilegedSettingsProvider.js delete mode 100644 client/admin/settings/GroupSelector.js create mode 100644 client/admin/settings/GroupSelector.tsx create mode 100644 client/contexts/EditableSettingsContext.ts delete mode 100644 client/contexts/PrivilegedSettingsContext.ts delete mode 100644 client/contexts/SettingsContext.js create mode 100644 client/contexts/SettingsContext.ts create mode 100644 client/hooks/useQuery.ts create mode 100644 client/hooks/useReactiveSubscriptionFactory.ts rename client/{admin => lib/settings}/PrivateSettingsCachedCollection.ts (83%) create mode 100644 client/lib/settings/PublicSettingsCachedCollection.ts create mode 100644 client/providers/EditableSettingsProvider.tsx delete mode 100644 client/providers/SettingsProvider.js create mode 100644 client/providers/SettingsProvider.tsx create mode 100644 definition/ISetting.ts diff --git a/app/settings/client/lib/settings.ts b/app/settings/client/lib/settings.ts index 712eadc6c37..6a9597e9df4 100644 --- a/app/settings/client/lib/settings.ts +++ b/app/settings/client/lib/settings.ts @@ -1,20 +1,13 @@ import { Meteor } from 'meteor/meteor'; import { ReactiveDict } from 'meteor/reactive-dict'; -import { CachedCollection } from '../../../ui-cached-collection'; +import { PublicSettingsCachedCollection } from '../../../../client/lib/settings/PublicSettingsCachedCollection'; import { SettingsBase, SettingValue } from '../../lib/settings'; -const cachedCollection = new CachedCollection({ - name: 'public-settings', - eventType: 'onAll', - userRelated: false, - listenChangesForLoggedUsersOnly: true, -}); - class Settings extends SettingsBase { - cachedCollection = cachedCollection + cachedCollection = PublicSettingsCachedCollection.get() - collection = cachedCollection.collection; + collection = PublicSettingsCachedCollection.get().collection; dict = new ReactiveDict('settings'); diff --git a/client/admin/AdministrationRouter.js b/client/admin/AdministrationRouter.js index 1abeee9688f..381bfa8ceee 100644 --- a/client/admin/AdministrationRouter.js +++ b/client/admin/AdministrationRouter.js @@ -1,17 +1,17 @@ import React, { lazy, useMemo, Suspense } from 'react'; +import SettingsProvider from '../providers/SettingsProvider'; import AdministrationLayout from './AdministrationLayout'; -import PrivilegedSettingsProvider from './PrivilegedSettingsProvider'; import PageSkeleton from './PageSkeleton'; function AdministrationRouter({ lazyRouteComponent, ...props }) { const LazyRouteComponent = useMemo(() => lazy(lazyRouteComponent), [lazyRouteComponent]); return - + }> - + ; } diff --git a/client/admin/PrivilegedSettingsProvider.js b/client/admin/PrivilegedSettingsProvider.js deleted file mode 100644 index 4cdfbeae0d7..00000000000 --- a/client/admin/PrivilegedSettingsProvider.js +++ /dev/null @@ -1,217 +0,0 @@ -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { Mongo } from 'meteor/mongo'; -import { Tracker } from 'meteor/tracker'; -import React, { useEffect, useMemo, useReducer, useRef, useState } from 'react'; - -import { PrivilegedSettingsContext } from '../contexts/PrivilegedSettingsContext'; -import { useAtLeastOnePermission } from '../contexts/AuthorizationContext'; -import { PrivateSettingsCachedCollection } from './PrivateSettingsCachedCollection'; - -const compareStrings = (a = '', b = '') => { - if (a === b || (!a && !b)) { - return 0; - } - - return a > b ? 1 : -1; -}; - -const compareSettings = (a, b) => - compareStrings(a.section, b.section) - || compareStrings(a.sorter, b.sorter) - || compareStrings(a.i18nLabel, b.i18nLabel); - -const settingsReducer = (states, { type, payload }) => { - const { - settings, - persistedSettings, - } = states; - - switch (type) { - case 'add': { - return { - settings: [...settings, ...payload].sort(compareSettings), - persistedSettings: [...persistedSettings, ...payload].sort(compareSettings), - }; - } - - case 'change': { - const mapping = (setting) => (setting._id !== payload._id ? setting : payload); - - return { - settings: settings.map(mapping), - persistedSettings: settings.map(mapping), - }; - } - - case 'remove': { - const mapping = (setting) => setting._id !== payload; - - return { - settings: settings.filter(mapping), - persistedSettings: persistedSettings.filter(mapping), - }; - } - - case 'hydrate': { - const map = {}; - payload.forEach((setting) => { - map[setting._id] = setting; - }); - - const mapping = (setting) => (map[setting._id] ? { ...setting, ...map[setting._id] } : setting); - - return { - settings: settings.map(mapping), - persistedSettings, - }; - } - } - - return states; -}; - -function AuthorizedPrivilegedSettingsProvider({ cachedCollection, children }) { - const [isLoading, setLoading] = useState(true); - - const subscribersRef = useRef(); - if (!subscribersRef.current) { - subscribersRef.current = new Set(); - } - - const stateRef = useRef({ settings: [], persistedSettings: [] }); - - const [state, dispatch] = useReducer(settingsReducer, { settings: [], persistedSettings: [] }); - stateRef.current = state; - - subscribersRef.current.forEach((subscriber) => { - subscriber(state); - }); - - const collectionsRef = useRef({}); - - useEffect(() => { - const stopLoading = () => { - setLoading(false); - }; - - if (!Tracker.nonreactive(() => cachedCollection.ready.get())) { - cachedCollection.init().then(stopLoading, stopLoading); - } else { - stopLoading(); - } - - const { collection: persistedSettingsCollection } = cachedCollection; - const settingsCollection = new Mongo.Collection(null); - - collectionsRef.current = { - persistedSettingsCollection, - settingsCollection, - }; - }, [collectionsRef]); - - useEffect(() => { - if (isLoading) { - return; - } - - const { current: { persistedSettingsCollection, settingsCollection } } = collectionsRef; - - const query = persistedSettingsCollection.find(); - - const syncCollectionsHandle = query.observe({ - added: (data) => settingsCollection.insert(data), - changed: (data) => settingsCollection.update(data._id, data), - removed: ({ _id }) => settingsCollection.remove(_id), - }); - - const addedQueue = []; - let addedActionTimer; - - const syncStateHandle = query.observe({ - added: (data) => { - addedQueue.push(data); - clearTimeout(addedActionTimer); - addedActionTimer = setTimeout(() => { - dispatch({ type: 'add', payload: addedQueue }); - }, 300); - }, - changed: (data) => { - dispatch({ type: 'change', payload: data }); - }, - removed: ({ _id }) => { - dispatch({ type: 'remove', payload: _id }); - }, - }); - - return () => { - syncCollectionsHandle && syncCollectionsHandle.stop(); - syncStateHandle && syncStateHandle.stop(); - clearTimeout(addedActionTimer); - }; - }, [isLoading, collectionsRef]); - - const updateTimersRef = useRef({}); - - 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); - }); - - const hydrate = useMutableCallback((changes) => { - changes.forEach(updateAtCollection); - dispatch({ type: 'hydrate', payload: changes }); - }); - - const isDisabled = useMutableCallback(({ blocked, enableQuery }) => { - if (blocked) { - return true; - } - - if (!enableQuery) { - return false; - } - - const { current: { settingsCollection } } = collectionsRef; - - const queries = [].concat(typeof enableQuery === 'string' ? JSON.parse(enableQuery) : enableQuery); - return !queries.every((query) => !!settingsCollection.findOne(query)); - }); - - const contextValue = useMemo(() => ({ - authorized: true, - loading: isLoading, - subscribers: subscribersRef.current, - stateRef, - hydrate, - isDisabled, - }), [ - isLoading, - hydrate, - isDisabled, - ]); - - return ; -} - -function PrivilegedSettingsProvider({ children }) { - const hasPermission = useAtLeastOnePermission([ - 'view-privileged-setting', - 'edit-privileged-setting', - 'manage-selected-settings', - ]); - - if (!hasPermission) { - return children; - } - - return ; -} - -export default PrivilegedSettingsProvider; diff --git a/client/admin/settings/GroupPage.js b/client/admin/settings/GroupPage.js index 9ca39d68538..fbfb1129621 100644 --- a/client/admin/settings/GroupPage.js +++ b/client/admin/settings/GroupPage.js @@ -1,13 +1,80 @@ import { Accordion, Box, Button, ButtonGroup, Skeleton } from '@rocket.chat/fuselage'; -import React, { useMemo } from 'react'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import React, { useMemo, memo } from 'react'; import Page from '../../components/basic/Page'; -import { useTranslation } from '../../contexts/TranslationContext'; +import { useEditableSettingsDispatch, useEditableSettings } from '../../contexts/EditableSettingsContext'; +import { useSettingsDispatch, useSettings } from '../../contexts/SettingsContext'; +import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; +import { useTranslation, useLoadLanguage } from '../../contexts/TranslationContext'; +import { useUser } from '../../contexts/UserContext'; import { Section } from './Section'; -const style = { margin: '0 auto', width: '100%', maxWidth: '590px' }; -export function GroupPage({ children, headerButtons, save, cancel, _id, i18nLabel, i18nDescription, changed }) { +function GroupPage({ children, headerButtons, _id, i18nLabel, i18nDescription }) { + const changedEditableSettings = useEditableSettings(useMemo(() => ({ + group: _id, + changed: true, + }), [_id])); + + const originalSettings = useSettings(useMemo(() => ({ + _id: changedEditableSettings.map(({ _id }) => _id), + }), [changedEditableSettings])); + + const dispatch = useSettingsDispatch(); + + const dispatchToastMessage = useToastMessageDispatch(); const t = useTranslation(); + const loadLanguage = useLoadLanguage(); + const user = useUser(); + + const save = useMutableCallback(async () => { + const changes = changedEditableSettings + .map(({ _id, value, editor }) => ({ _id, value, editor })); + + if (changes.length === 0) { + return; + } + + try { + await dispatch(changes); + + if (changes.some(({ _id }) => _id === 'Language')) { + const lng = user?.language + || changes.filter(({ _id }) => _id === 'Language').shift()?.value + || 'en'; + + await loadLanguage(lng); + dispatchToastMessage({ type: 'success', message: t('Settings_updated', { lng }) }); + return; + } + + dispatchToastMessage({ type: 'success', message: t('Settings_updated') }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }); + + const dispatchToEditing = useEditableSettingsDispatch(); + + const cancel = useMutableCallback(() => { + dispatchToEditing( + changedEditableSettings + .map(({ _id }) => originalSettings.find((setting) => setting._id === _id)) + .map((setting) => { + if (!setting) { + return; + } + + return { + _id: setting._id, + value: setting.value, + editor: setting.editor, + changed: false, + }; + }) + .filter(Boolean), + ); + }); const handleSubmit = (event) => { event.preventDefault(); @@ -34,11 +101,11 @@ export function GroupPage({ children, headerButtons, save, cancel, _id, i18nLabe return - {changed && } + {changedEditableSettings.length > 0 && }