Skip to content
Snippets Groups Projects
Unverified Commit 369b40a1 authored by Tasso's avatar Tasso
Browse files

Remove collection from `EditableSettingProvider`

parent 89b65559
No related branches found
Tags 7.4.0-rc.1
No related merge requests found
import type { ISettingBase, ISettingColor, ISetting } from '@rocket.chat/core-typings'; import type { ISetting } from '@rocket.chat/core-typings';
import type { SettingsContextQuery } from '@rocket.chat/ui-contexts'; import { createContext, useContext, useMemo } from 'react';
import { createContext, useContext, useMemo, useSyncExternalStore } from 'react';
export type EditableSetting = (ISettingBase | ISettingColor) & { export type EditableSetting = ISetting & {
disabled: boolean; disabled: boolean;
changed: boolean; changed: boolean;
invisible: boolean; invisible: boolean;
}; };
type EditableSettingsContextQuery = SettingsContextQuery & { export const compareSettings = (a: EditableSetting, b: EditableSetting): number => {
changed?: boolean; const sorter = a.sorter - b.sorter;
if (sorter !== 0) return sorter;
const tab = (a.tab ?? '').localeCompare(b.tab ?? '');
if (tab !== 0) return tab;
const i18nLabel = a.i18nLabel.localeCompare(b.i18nLabel);
return i18nLabel;
}; };
type EditableSettingsContextQuery =
| {
group: ISetting['_id'];
}
| {
group: ISetting['_id'];
section: string;
tab?: ISetting['_id'];
}
| {
group: ISetting['_id'];
changed: true;
};
export type EditableSettingsContextValue = { export type EditableSettingsContextValue = {
readonly queryEditableSetting: ( settings: EditableSetting[];
_id: ISetting['_id'], dispatch(changes: Partial<EditableSetting>[]): void;
) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => EditableSetting | undefined];
readonly queryEditableSettings: (
query: EditableSettingsContextQuery,
) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => EditableSetting[]];
readonly queryGroupSections: (
_id: ISetting['_id'],
tab?: ISetting['_id'],
) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => string[]];
readonly queryGroupTabs: (
_id: ISetting['_id'],
) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ISetting['_id'][]];
readonly dispatch: (changes: Partial<EditableSetting>[]) => void;
}; };
export const EditableSettingsContext = createContext<EditableSettingsContextValue>({ export const EditableSettingsContext = createContext<EditableSettingsContextValue>({
queryEditableSetting: () => [(): (() => void) => (): void => undefined, (): undefined => undefined], settings: [],
queryEditableSettings: () => [(): (() => void) => (): void => undefined, (): EditableSetting[] => []],
queryGroupSections: () => [(): (() => void) => (): void => undefined, (): string[] => []],
queryGroupTabs: () => [(): (() => void) => (): void => undefined, (): ISetting['_id'][] => []],
dispatch: () => undefined, dispatch: () => undefined,
}); });
export const useEditableSetting = (_id: ISetting['_id']): EditableSetting | undefined => { export const useEditableSetting = (_id: ISetting['_id']): EditableSetting | undefined => {
const { queryEditableSetting } = useContext(EditableSettingsContext); const { settings } = useContext(EditableSettingsContext);
const [subscribe, getSnapshot] = useMemo(() => queryEditableSetting(_id), [queryEditableSetting, _id]); return useMemo(() => settings.find((x) => x._id === _id), [settings, _id]);
return useSyncExternalStore(subscribe, getSnapshot);
}; };
export const useEditableSettings = (query?: EditableSettingsContextQuery): EditableSetting[] => { export const useEditableSettings = (query: EditableSettingsContextQuery): EditableSetting[] => {
const { queryEditableSettings } = useContext(EditableSettingsContext); const { settings } = useContext(EditableSettingsContext);
const [subscribe, getSnapshot] = useMemo(() => queryEditableSettings(query ?? {}), [queryEditableSettings, query]);
return useSyncExternalStore(subscribe, getSnapshot); return useMemo(
() =>
settings
.filter((x) => {
if ('changed' in query) {
return x.group === query.group && x.changed;
}
if ('section' in query) {
return (
x.group === query.group &&
(query.section ? x.section === query.section : !x.section) &&
(query.tab ? x.tab === query.tab : !x.tab)
);
}
return x.group === query.group;
})
.sort(compareSettings),
[settings, query],
);
}; };
export const useEditableSettingsGroupSections = (_id: ISetting['_id'], tab?: ISetting['_id']): string[] => { export const useEditableSettingsGroupSections = (_id: ISetting['_id'], tab?: ISetting['_id']): string[] => {
const { queryGroupSections } = useContext(EditableSettingsContext); const { settings } = useContext(EditableSettingsContext);
const [subscribe, getSnapshot] = useMemo(() => queryGroupSections(_id, tab), [queryGroupSections, _id, tab]); return useMemo(
return useSyncExternalStore(subscribe, getSnapshot); () =>
Array.from(
new Set(
settings
.filter((x) => x.group === _id && (tab !== undefined ? x.tab === tab : !x.tab))
.sort(compareSettings)
.map(({ section }) => section || ''),
),
),
[_id, settings, tab],
);
}; };
export const useEditableSettingsGroupTabs = (_id: ISetting['_id']): ISetting['_id'][] => { export const useEditableSettingsGroupTabs = (_id: ISetting['_id']): ISetting['_id'][] => {
const { queryGroupTabs } = useContext(EditableSettingsContext); const { settings } = useContext(EditableSettingsContext);
const [subscribe, getSnapshot] = useMemo(() => queryGroupTabs(_id), [queryGroupTabs, _id]); return useMemo(
return useSyncExternalStore(subscribe, getSnapshot); () =>
Array.from(
new Set(
settings
.filter((x) => x.group === _id)
.sort(compareSettings)
.map(({ tab }) => tab || ''),
),
),
[settings, _id],
);
}; };
export const useEditableSettingsDispatch = (): ((changes: Partial<EditableSetting>[]) => void) => export const useEditableSettingsDispatch = (): ((changes: Partial<EditableSetting>[]) => void) => {
useContext(EditableSettingsContext).dispatch; const { dispatch } = useContext(EditableSettingsContext);
return dispatch;
};
import type { ISetting } from '@rocket.chat/core-typings'; import type { ISetting } from '@rocket.chat/core-typings';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import type { SettingsContextQuery } from '@rocket.chat/ui-contexts';
import { useSettings } from '@rocket.chat/ui-contexts'; import { useSettings } from '@rocket.chat/ui-contexts';
import { Mongo } from 'meteor/mongo';
import { Tracker } from 'meteor/tracker'; import { Tracker } from 'meteor/tracker';
import type { FilterOperators } from 'mongodb'; import type { ReactNode } from 'react';
import type { MutableRefObject, ReactNode } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import { createReactiveSubscriptionFactory } from '../../../lib/createReactiveSubscriptionFactory'; import { createFilterFromQuery } from '../../../lib/minimongo';
import type { EditableSetting, EditableSettingsContextValue } from '../EditableSettingsContext'; import type { EditableSetting } from '../EditableSettingsContext';
import { EditableSettingsContext } from '../EditableSettingsContext'; import { EditableSettingsContext } from '../EditableSettingsContext';
const defaultQuery: SettingsContextQuery = {}; const defaultOmit: Array<ISetting['_id']> = ['Cloud_Workspace_AirGapped_Restrictions_Remaining_Days'];
const defaultOmit: Array<ISetting['_id']> = [];
const performQuery = (
query:
| string
| {
_id: string;
value: unknown;
}
| {
_id: string;
value: unknown;
}[]
| undefined,
settings: ISetting[],
) => {
if (!query) {
return true;
}
const queries = [].concat(typeof query === 'string' ? JSON.parse(query) : query);
return queries.every((query) => settings.filter(createFilterFromQuery(query)).length > 0);
};
type EditableSettingsProviderProps = { type EditableSettingsProviderProps = {
children?: ReactNode; children?: ReactNode;
query?: SettingsContextQuery;
omit?: Array<ISetting['_id']>;
}; };
const EditableSettingsProvider = ({ children, query = defaultQuery, omit = defaultOmit }: EditableSettingsProviderProps) => { const EditableSettingsProvider = ({ children }: EditableSettingsProviderProps) => {
const settingsCollectionRef = useRef<Mongo.Collection<EditableSetting>>(null) as MutableRefObject<Mongo.Collection<EditableSetting>>; const persistedSettings = useSettings();
const persistedSettings = useSettings(query);
const [settings, updateSettings] = useState(() =>
const getSettingsCollection = useEffectEvent(() => { persistedSettings
if (!settingsCollectionRef.current) { .filter((x) => !defaultOmit.includes(x._id))
settingsCollectionRef.current = new Mongo.Collection<any>(null); .map(
} (persisted): EditableSetting => ({
...persisted,
return settingsCollectionRef.current; changed: false,
}) as () => Mongo.Collection<EditableSetting>; disabled: persisted.blocked || !performQuery(persisted.enableQuery, persistedSettings),
invisible: !performQuery(persisted.displayQuery, persistedSettings),
useEffect(() => { }),
const settingsCollection = getSettingsCollection();
settingsCollection.remove({ _id: { $nin: persistedSettings.map(({ _id }) => _id) } });
for (const { _id, ...fields } of persistedSettings) {
settingsCollection.upsert(_id, { $set: { ...fields }, $unset: { changed: true } });
}
// TODO: Remove option to omit settings from admin pages manually
// This is a very wacky workaround due to lack of support to omit settings from the
// admin settings page while keeping them public.
if (omit.length > 0) {
settingsCollection.remove({ _id: { $in: omit } });
}
}, [getSettingsCollection, persistedSettings, omit]);
const queryEditableSetting = useMemo(() => {
const validateSettingQueries = (
query: undefined | string | FilterOperators<ISetting> | FilterOperators<ISetting>[],
settingsCollection: Mongo.Collection<EditableSetting>,
): boolean => {
if (!query) {
return true;
}
const queries = [].concat(typeof query === 'string' ? JSON.parse(query) : query);
return queries.every((query) => settingsCollection.find(query).count() > 0);
};
return createReactiveSubscriptionFactory((_id: ISetting['_id']): EditableSetting | undefined => {
const settingsCollection = getSettingsCollection();
const editableSetting = settingsCollection.findOne(_id);
if (!editableSetting) {
return undefined;
}
return {
...editableSetting,
disabled: editableSetting.blocked || !validateSettingQueries(editableSetting.enableQuery, settingsCollection),
invisible: !validateSettingQueries(editableSetting.displayQuery, settingsCollection),
};
});
}, [getSettingsCollection]);
const queryEditableSettings = useMemo(
() =>
createReactiveSubscriptionFactory((query = {}) =>
getSettingsCollection()
.find(
{
...('_id' in query && { _id: { $in: query._id } }),
...('group' in query && { group: query.group }),
...('changed' in query && { changed: query.changed }),
$and: [
{
...('section' in query &&
(query.section
? { section: query.section }
: {
$or: [{ section: { $exists: false } }, { section: '' }],
})),
},
{
...('tab' in query &&
(query.tab
? { tab: query.tab }
: {
$or: [{ tab: { $exists: false } }, { tab: '' }],
})),
},
],
},
{
sort: {
section: 1,
sorter: 1,
i18nLabel: 1,
},
},
)
.fetch(),
), ),
[getSettingsCollection],
); );
const queryGroupSections = useMemo( useEffect(() => {
() => updateSettings((settings) =>
createReactiveSubscriptionFactory((_id: ISetting['_id'], tab?: ISetting['_id']) => persistedSettings
Array.from( .filter((x) => !defaultOmit.includes(x._id))
new Set( .map(
getSettingsCollection() (persisted): EditableSetting => ({
.find( ...settings.find(({ _id }) => _id === persisted._id),
{ ...persisted,
group: _id, changed: false,
...(tab !== undefined disabled: persisted.blocked || !performQuery(persisted.enableQuery, settings),
? { tab } invisible: !performQuery(persisted.displayQuery, settings),
: { }),
$or: [{ tab: { $exists: false } }, { tab: '' }],
}),
},
{
fields: {
section: 1,
},
sort: {
sorter: 1,
section: 1,
i18nLabel: 1,
},
},
)
.fetch()
.map(({ section }) => section || ''),
),
),
),
[getSettingsCollection],
);
const queryGroupTabs = useMemo(
() =>
createReactiveSubscriptionFactory((_id: ISetting['_id']) =>
Array.from(
new Set(
getSettingsCollection()
.find(
{
group: _id,
},
{
fields: {
tab: 1,
},
sort: {
sorter: 1,
tab: 1,
i18nLabel: 1,
},
},
)
.fetch()
.map(({ tab }) => tab || ''),
),
), ),
), );
[getSettingsCollection], }, [persistedSettings]);
);
const dispatch = useEffectEvent((changes: Partial<EditableSetting>[]): void => {
for (const { _id, ...data } of changes) {
if (!_id) {
continue;
}
getSettingsCollection().update(_id, { $set: data }); const dispatch = useEffectEvent((changes: Partial<EditableSetting>[]) => {
if (changes.length === 0) {
return;
} }
updateSettings((settings) =>
persistedSettings
.filter((x) => !defaultOmit.includes(x._id))
.map((persisted): EditableSetting => {
const current = settings.find(({ _id }) => _id === persisted._id);
if (!current) throw new Error(`Setting ${persisted._id} not found`);
const change = changes.find(({ _id }) => _id === current._id);
if (!change) {
return current;
}
const partial = { ...current, ...change };
return {
...partial,
disabled: persisted.blocked || !performQuery(persisted.enableQuery, settings),
invisible: !performQuery(persisted.displayQuery, settings),
};
}),
);
// TODO: why is this here?
Tracker.flush(); Tracker.flush();
}); });
const contextValue = useMemo<EditableSettingsContextValue>( return (
() => ({ <EditableSettingsContext.Provider value={useMemo(() => ({ settings, dispatch }), [settings, dispatch])}>
queryEditableSetting, {children}
queryEditableSettings, </EditableSettingsContext.Provider>
queryGroupSections,
queryGroupTabs,
dispatch,
}),
[queryEditableSetting, queryEditableSettings, queryGroupSections, queryGroupTabs, dispatch],
); );
return <EditableSettingsContext.Provider children={children} value={contextValue} />;
}; };
export default EditableSettingsProvider; export default EditableSettingsProvider;
...@@ -6,8 +6,6 @@ import SettingsGroupSelector from './SettingsGroupSelector'; ...@@ -6,8 +6,6 @@ import SettingsGroupSelector from './SettingsGroupSelector';
import SettingsPage from './SettingsPage'; import SettingsPage from './SettingsPage';
import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage';
const omittedSettings = ['Cloud_Workspace_AirGapped_Restrictions_Remaining_Days'];
export const SettingsRoute = (): ReactElement => { export const SettingsRoute = (): ReactElement => {
const hasPermission = useIsPrivilegedSettingsContext(); const hasPermission = useIsPrivilegedSettingsContext();
const groupId = useRouteParameter('group'); const groupId = useRouteParameter('group');
...@@ -22,7 +20,7 @@ export const SettingsRoute = (): ReactElement => { ...@@ -22,7 +20,7 @@ export const SettingsRoute = (): ReactElement => {
} }
return ( return (
<EditableSettingsProvider omit={omittedSettings}> <EditableSettingsProvider>
<SettingsGroupSelector groupId={groupId} onClickBack={() => router.navigate('/admin/settings')} /> <SettingsGroupSelector groupId={groupId} onClickBack={() => router.navigate('/admin/settings')} />
</EditableSettingsProvider> </EditableSettingsProvider>
); );
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment