Refactor components and views to Storybook compatibility (#17800)

pull/17860/head
Tasso Evangelista 5 years ago committed by GitHub
parent 6828007669
commit 6be2861f0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 20
      .storybook/decorators.js
  2. 15
      .storybook/hooks.js
  3. 1
      .storybook/main.js
  4. 10
      .storybook/preview.js
  5. 4
      .storybook/webpack.config.js
  6. 2
      app/2fa/server/code/EmailCheck.ts
  7. 2
      app/2fa/server/code/index.ts
  8. 2
      app/authorization/server/functions/hasPermission.js
  9. 20
      app/livechat/server/lib/stream/agentStatus.ts
  10. 3
      app/ui-message/client/blocks/MessageBlock.js
  11. 3
      app/ui-message/client/blocks/ModalBlock.js
  12. 2
      app/ui/client/components/GenericTable.stories.js
  13. 4
      app/ui/client/views/app/components/Directory/ChannelsTab.js
  14. 6
      client/admin/cloud/ManualWorkspaceRegistrationModal.js
  15. 2
      client/admin/invites/InvitesPage.js
  16. 5
      client/admin/mailer/Mailer.js
  17. 2
      client/admin/settings/GroupPage.js
  18. 6
      client/admin/settings/GroupPage.stories.js
  19. 4
      client/admin/settings/GroupSelector.js
  20. 4
      client/admin/settings/GroupSelector.stories.js
  21. 9
      client/admin/settings/Section.js
  22. 4
      client/admin/settings/Section.stories.js
  23. 9
      client/admin/settings/Setting.js
  24. 11
      client/admin/settings/Setting.stories.js
  25. 210
      client/admin/settings/SettingsState.js
  26. 5
      client/admin/settings/groups/OAuthGroupPage.js
  27. 2
      client/admin/users/UserInfo.js
  28. 3
      client/admin/viewLogs/ViewLogs.js
  29. 95
      client/components/basic/BurgerMenuButton.css
  30. 38
      client/components/basic/BurgerMenuButton.js
  31. 46
      client/components/basic/BurgerMenuButton.stories.js
  32. 5
      client/components/basic/Logo.js
  33. 4
      client/components/basic/Logo.stories.js
  34. 20
      client/components/basic/MarkdownText.js
  35. 39
      client/components/basic/MarkdownText.stories.js
  36. 20
      client/components/basic/Page.js
  37. 46
      client/components/basic/Page.stories.js
  38. 15
      client/components/basic/burger/BurgerBadge.js
  39. 21
      client/components/basic/burger/BurgerBadge.stories.js
  40. 69
      client/components/basic/burger/BurgerIcon.js
  41. 22
      client/components/basic/burger/BurgerIcon.stories.js
  42. 27
      client/components/basic/burger/BurgerMenuButton.js
  43. 22
      client/components/basic/burger/BurgerMenuButton.stories.js
  44. 4
      client/components/connectionStatus/ConnectionStatusAlert.js
  45. 70
      client/components/connectionStatus/ConnectionStatusAlert.stories.js
  46. 10
      client/components/pageNotFound/PageNotFound.stories.js
  47. 7
      client/contexts/ModalContext.ts
  48. 249
      client/contexts/PrivateSettingsContext.ts
  49. 4
      client/fuselage-hooks.d.ts
  50. 3
      client/hooks/useIsReducedMotionPreferred.js
  51. 3
      client/hooks/useModal.js
  52. 25
      client/providers/MeteorProvider.js
  53. 10
      client/providers/ModalProvider.js
  54. 4
      client/routes.js
  55. 8
      client/views/notFound/NotFoundPage.js
  56. 10
      client/views/notFound/NotFoundPage.stories.js
  57. 0
      client/views/setupWizard/Pager.js
  58. 2
      client/views/setupWizard/Pager.stories.js
  59. 2
      client/views/setupWizard/SetupWizardPage.js
  60. 2
      client/views/setupWizard/SetupWizardPage.stories.js
  61. 0
      client/views/setupWizard/SetupWizardRoute.js
  62. 0
      client/views/setupWizard/SetupWizardState.js
  63. 0
      client/views/setupWizard/SideBar.css
  64. 2
      client/views/setupWizard/SideBar.js
  65. 2
      client/views/setupWizard/SideBar.stories.js
  66. 0
      client/views/setupWizard/Step.css
  67. 0
      client/views/setupWizard/Step.js
  68. 0
      client/views/setupWizard/StepHeader.js
  69. 2
      client/views/setupWizard/StepHeader.stories.js
  70. 0
      client/views/setupWizard/steps/AdminUserInformationStep.js
  71. 2
      client/views/setupWizard/steps/AdminUserInformationStep.stories.js
  72. 0
      client/views/setupWizard/steps/FinalStep.js
  73. 2
      client/views/setupWizard/steps/FinalStep.stories.js
  74. 0
      client/views/setupWizard/steps/RegisterServerStep.js
  75. 2
      client/views/setupWizard/steps/RegisterServerStep.stories.js
  76. 0
      client/views/setupWizard/steps/SettingsBasedStep.js
  77. 2
      client/views/setupWizard/steps/SettingsBasedStep.stories.js
  78. 2
      ee/app/engagement-dashboard/client/components/ChannelsTab/index.stories.js
  79. 2
      ee/app/engagement-dashboard/client/components/EngagementDashboardPage.stories.js
  80. 2
      ee/app/engagement-dashboard/client/components/MessagesTab/index.stories.js
  81. 2
      ee/app/engagement-dashboard/client/components/UsersTab/index.stories.js
  82. 2
      ee/app/engagement-dashboard/client/components/data/Counter.stories.js
  83. 2
      ee/app/engagement-dashboard/client/components/data/CounterSet.stories.js
  84. 2
      ee/app/engagement-dashboard/client/components/data/Growth.stories.js
  85. 2
      ee/app/engagement-dashboard/client/components/data/Histogram.stories.js
  86. 2
      ee/app/engagement-dashboard/client/components/data/LegendSymbol.stories.js
  87. 2
      ee/app/engagement-dashboard/client/components/data/NegativeGrowthSymbol.stories.js
  88. 2
      ee/app/engagement-dashboard/client/components/data/PositiveGrowthSymbol.stories.js
  89. 2
      ee/app/license/server/license.ts
  90. 1554
      package-lock.json
  91. 13
      package.json
  92. 2
      server/main.d.ts

@ -1,13 +1,13 @@
import React from 'react';
import { MeteorProviderMock } from './providers';
import { MeteorProviderMock } from './mocks/providers';
export const rocketChatDecorator = (fn) => {
const linkElement = document.getElementById('theme-styles') || document.createElement('link');
if (linkElement.id !== 'theme-styles') {
require('../../app/theme/client/main.css');
require('../../app/theme/client/vendor/fontello/css/fontello.css');
require('../../app/theme/client/rocketchat.font.css');
require('../app/theme/client/main.css');
require('../app/theme/client/vendor/fontello/css/fontello.css');
require('../app/theme/client/rocketchat.font.css');
linkElement.setAttribute('id', 'theme-styles');
linkElement.setAttribute('rel', 'stylesheet');
linkElement.setAttribute('href', 'https://open.rocket.chat/theme.css');
@ -15,7 +15,7 @@ export const rocketChatDecorator = (fn) => {
}
// eslint-disable-next-line import/no-unresolved
const { default: icons } = require('!!raw-loader!../../private/public/icons.svg');
const { default: icons } = require('!!raw-loader!../private/public/icons.svg');
return <MeteorProviderMock>
<style>{`
@ -29,3 +29,13 @@ export const rocketChatDecorator = (fn) => {
</div>
</MeteorProviderMock>;
};
export const fullHeightDecorator = (storyFn) =>
<div style={{ display: 'flex', flexDirection: 'column', maxHeight: '100vh' }}>
{storyFn()}
</div>;
export const centeredDecorator = (storyFn) =>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh' }}>
{storyFn()}
</div>;

@ -0,0 +1,15 @@
import { useEffect, useState } from 'react';
export const useAutoToggle = (initialValue = false, ms = 1000) => {
const [value, setValue] = useState(initialValue);
useEffect(() => {
const timer = setInterval(() => setValue((value) => !value), ms);
return () => {
clearInterval(timer);
};
}, []);
return value;
};

@ -7,5 +7,6 @@ module.exports = {
addons: [
'@storybook/addon-actions',
'@storybook/addon-knobs',
'@storybook/addon-viewport',
],
};

@ -1,7 +1,13 @@
import { withKnobs } from '@storybook/addon-knobs';
import { addDecorator } from '@storybook/react';
import { addDecorator, addParameters } from '@storybook/react';
import { rocketChatDecorator } from './mocks/decorators';
import { rocketChatDecorator } from './decorators';
addDecorator(rocketChatDecorator);
addDecorator(withKnobs);
addParameters({
options: {
showRoots: true,
},
});

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

@ -99,7 +99,7 @@ ${ t('If_you_didnt_try_to_login_in_your_account_please_ignore_this_email') }
const random = Random._randomString(6, '0123456789');
const encryptedRandom = bcrypt.hashSync(random, Accounts._bcryptRounds());
const expire = new Date();
const expirationInSeconds = parseInt(settings.get('Accounts_TwoFactorAuthentication_By_Email_Code_Expiration'));
const expirationInSeconds = parseInt(settings.get('Accounts_TwoFactorAuthentication_By_Email_Code_Expiration') as string, 10);
expire.setSeconds(expire.getSeconds() + expirationInSeconds);

@ -95,7 +95,7 @@ export function isAuthorizedForToken(connection: IMethodConnection, user: IUser,
export function rememberAuthorization(connection: IMethodConnection, user: IUser): void {
const currentToken = Accounts._getLoginToken(connection.id);
const rememberFor = parseInt(settings.get('Accounts_TwoFactorAuthentication_RememberFor'));
const rememberFor = parseInt(settings.get('Accounts_TwoFactorAuthentication_RememberFor') as string, 10);
if (rememberFor <= 0) {
return;

@ -12,7 +12,7 @@ const rolesHasPermission = mem(async (permission, roles) => {
return !!result;
}, {
cacheKey: JSON.stringify,
...process.env.TEST_MODE === 'true' && { maxAge: 0 },
...process.env.TEST_MODE === 'true' && { maxAge: 1 },
});
const getRoles = mem(async (uid, scope) => {

@ -9,19 +9,31 @@ let actionTimeout = 60000;
let action = 'none';
let comment = '';
settings.get('Livechat_agent_leave_action_timeout', function(_key: string, value: number) {
settings.get('Livechat_agent_leave_action_timeout', (_key, value) => {
if (typeof value !== 'number') {
return;
}
actionTimeout = value * 1000;
});
settings.get('Livechat_agent_leave_action', function(_key: string, value: boolean) {
settings.get('Livechat_agent_leave_action', (_key, value) => {
if (typeof value !== 'boolean') {
return;
}
monitorAgents = value;
});
settings.get('Livechat_agent_leave_action', function(_key: string, value: string) {
settings.get('Livechat_agent_leave_action', (_key, value) => {
if (typeof value !== 'string') {
return;
}
action = value;
});
settings.get('Livechat_agent_leave_comment', function(_key: string, value: string) {
settings.get('Livechat_agent_leave_comment', (_key, value) => {
if (typeof value !== 'string') {
return;
}
comment = value;
});

@ -2,7 +2,6 @@ import { UIKitIncomingInteractionContainerType } from '@rocket.chat/apps-engine/
import { UiKitMessage, UiKitComponent, kitContext, messageParser } from '@rocket.chat/fuselage-ui-kit';
import React, { useRef, useEffect } from 'react';
import RawText from '../../../../client/components/basic/RawText';
import { renderMessageBody } from '../../../ui-utils/client';
import * as ActionManager from '../ActionManager';
@ -12,7 +11,7 @@ messageParser.text = ({ text, type } = {}) => {
return text;
}
return <RawText>{renderMessageBody({ msg: text })}</RawText>;
return <span dangerouslySetInnerHTML={{ __html: renderMessageBody({ msg: text }) }} />;
};
export function MessageBlock({ mid: _mid, rid, blocks, appId }) {

@ -5,7 +5,6 @@ import { kitContext, UiKitComponent, UiKitModal, modalParser } from '@rocket.cha
import { uiKitText } from '@rocket.chat/ui-kit';
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
import RawText from '../../../../client/components/basic/RawText';
import { renderMessageBody } from '../../../ui-utils/client';
import { getURL } from '../../../utils/lib/getURL';
import * as ActionManager from '../ActionManager';
@ -16,7 +15,7 @@ modalParser.text = ({ text, type } = {}) => {
return text;
}
return <RawText>{renderMessageBody({ msg: text })}</RawText>;
return <span dangerouslySetInnerHTML={{ __html: renderMessageBody({ msg: text }) }} />;
};
const textParser = uiKitText({

@ -5,7 +5,7 @@ import { GenericTable, Th } from './GenericTable';
export default {
title: 'directory/table',
title: 'uncategorized/GenericTable',
component: GenericTable,
decorators: [(fn) => <div children={fn()} style={{ height: '100vh' }} />],
};

@ -88,9 +88,7 @@ export function ChannelsTab() {
<Box display='flex' alignItems='center'>
<Icon name={roomTypes.getIcon(room)} color='hint' /> <Box fontScale='p2' mi='x4'>{fname || name}</Box><RoomTags room={room} style={style} />
</Box>
{topic && <Box fontScale='p1' color='hint' style={style}>
<MarkdownText>{topic}</MarkdownText>
</Box>}
{topic && <MarkdownText fontScale='p1' color='hint' style={style} content={topic} />}
</Box>
</Box>
</Table.Cell>

@ -67,11 +67,7 @@ function CopyStep({ onNextButtonClick }) {
<Icon name='copy' /> {t('Copy')}
</Button>
</Box>
<Box withRichContent>
<p>
<MarkdownText>{t('Cloud_click_here', { cloudConsoleUrl })}</MarkdownText>
</p>
</Box>
<MarkdownText is='p' withRichContent content={t('Cloud_click_here', { cloudConsoleUrl })} />
</Modal.Content>
<Modal.Footer>
<ButtonGroup>

@ -9,9 +9,9 @@ import React, { useState, useEffect } from 'react';
import moment from 'moment';
import Page from '../../components/basic/Page';
import { useModal } from '../../contexts/ModalContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { useEndpoint } from '../../contexts/ServerContext';
import { useModal } from '../../hooks/useModal';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { GenericTable } from '../../../app/ui/client/components/GenericTable';
import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime';

@ -4,7 +4,6 @@ import { TextInput, TextAreaInput, Field, FieldGroup, CheckBox, Button, Icon, Bu
import { isEmail } from '../../../app/utils/lib/isEmail.js';
import { isJSON } from '../../../app/utils/lib/isJSON.js';
import Page from '../../components/basic/Page';
import RawText from '../../components/basic/RawText';
import { useTranslation } from '../../contexts/TranslationContext';
export function Mailer({ sendMail = () => {} }) {
@ -95,9 +94,7 @@ export function Mailer({ sendMail = () => {} }) {
onChange={(e) => setEmailBody(e.currentTarget.value)}
/>
</Field.Row>
<Field.Hint>
<RawText>{t('Mailer_body_tags')}</RawText>
</Field.Hint>
<Field.Hint dangerouslySetInnerHTML={{ __html: t('Mailer_body_tags') }} />
</Field>
</FieldGroup>
</Page.ScrollableContentWithShadow>

@ -1,9 +1,9 @@
import { Accordion, Box, Button, ButtonGroup, Skeleton } from '@rocket.chat/fuselage';
import React, { useMemo } from 'react';
import Page from '../../components/basic/Page';
import { useTranslation } from '../../contexts/TranslationContext';
import { Section } from './Section';
import Page from '../../components/basic/Page';
export function GroupPage({ children, headerButtons, save, cancel, _id, i18nLabel, i18nDescription, changed }) {
const t = useTranslation();

@ -1,16 +1,10 @@
import React from 'react';
import { GroupPage } from './GroupPage';
import { SettingsState } from './SettingsState';
export default {
title: 'admin/settings/GroupPage',
component: GroupPage,
decorators: [
(storyFn) => <SettingsState>
{storyFn()}
</SettingsState>,
],
};
export const _default = () =>

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

@ -1,14 +1,10 @@
import React from 'react';
import { GroupSelector } from './GroupSelector';
import { SettingsState } from './SettingsState';
export default {
title: 'admin/settings/GroupSelector',
component: GroupSelector,
decorators: [
(storyFn) => <SettingsState>{storyFn()}</SettingsState>,
],
};
export const _default = () => <GroupSelector />;

@ -1,13 +1,16 @@
import { Accordion, Box, Button, FieldGroup, Skeleton } from '@rocket.chat/fuselage';
import React from 'react';
import {
usePrivateSettingsSection,
usePrivateSettingsSectionChangedState,
} from '../../contexts/PrivateSettingsContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { Setting } from './Setting';
import { useSection, useSectionChangedState } from './SettingsState';
export function Section({ children, groupId, hasReset = true, help, sectionName, solo }) {
const section = useSection(groupId, sectionName);
const changed = useSectionChangedState(groupId, sectionName);
const section = usePrivateSettingsSection(groupId, sectionName);
const changed = usePrivateSettingsSectionChangedState(groupId, sectionName);
const t = useTranslation();

@ -1,14 +1,10 @@
import React from 'react';
import { Section } from './Section';
import { SettingsState } from './SettingsState';
export default {
title: 'admin/settings/Section',
component: Section,
decorators: [
(storyFn) => <SettingsState>{storyFn()}</SettingsState>,
],
};
export const _default = () => <Section groupId='General' />;

@ -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 RawText from '../../components/basic/RawText';
import { usePrivateSetting } from '../../contexts/PrivateSettingsContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { GenericSettingInput } from './inputs/GenericSettingInput';
import { BooleanSettingInput } from './inputs/BooleanSettingInput';
@ -19,7 +19,6 @@ import { CodeSettingInput } from './inputs/CodeSettingInput';
import { ActionSettingInput } from './inputs/ActionSettingInput';
import { AssetSettingInput } from './inputs/AssetSettingInput';
import { RoomPickSettingInput } from './inputs/RoomPickSettingInput';
import { useSetting } from './SettingsState';
export const MemoizedSetting = memo(function MemoizedSetting({
type,
@ -71,7 +70,7 @@ export function Setting({ settingId, sectionChanged }) {
update,
reset,
...setting
} = useSetting(settingId);
} = usePrivateSetting(settingId);
const t = useTranslation();
@ -116,8 +115,8 @@ export function Setting({ settingId, sectionChanged }) {
} = setting;
const label = (i18nLabel && t(i18nLabel)) || (_id || t(_id));
const hint = useMemo(() => t.has(i18nDescription) && <MarkdownText>{t(i18nDescription)}</MarkdownText>, [i18nDescription]);
const callout = useMemo(() => alert && <RawText>{t(alert)}</RawText>, [alert]);
const hint = useMemo(() => t.has(i18nDescription) && <MarkdownText content={t(i18nDescription)} />, [i18nDescription]);
const callout = useMemo(() => alert && <span dangerouslySetInnerHTML={{ __html: t(alert) }} />, [alert]);
const hasResetButton = !disableReset && !readonly && type !== 'asset' && (JSON.stringify(packageEditor) !== JSON.stringify(editor) || JSON.stringify(value) !== JSON.stringify(packageValue)) && !disabled;
return <MemoizedSetting

@ -2,19 +2,16 @@ import { FieldGroup } from '@rocket.chat/fuselage';
import React from 'react';
import { Setting } from './Setting';
import { SettingsState } from './SettingsState';
export default {
title: 'admin/settings/Setting',
component: Setting,
decorators: [
(storyFn) => <SettingsState>
<div className='rc-old'>
<div className='page-settings'>
{storyFn()}
</div>
(storyFn) => <div className='rc-old'>
<div className='page-settings'>
{storyFn()}
</div>
</SettingsState>,
</div>,
],
};

@ -1,16 +1,8 @@
import { useDebouncedCallback, useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { Mongo } from 'meteor/mongo';
import { Tracker } from 'meteor/tracker';
import React, { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
import { PrivateSettingsCachedCollection } from '../PrivateSettingsCachedCollection';
import { useBatchSettingsDispatch } from '../../contexts/SettingsContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { useReactiveValue } from '../../hooks/useReactiveValue';
import { useTranslation, useLoadLanguage } from '../../contexts/TranslationContext';
import { useUser } from '../../contexts/UserContext';
const SettingsContext = createContext({});
import { PrivateSettingsContext } from '../../contexts/PrivateSettingsContext';
let privateSettingsCachedCollection; // Remove this singleton (╯°□°)╯︵ ┻━┻
@ -212,194 +204,14 @@ export function SettingsState({ children }) {
isDisabled,
]);
return <SettingsContext.Provider children={children} value={contextValue} />;
return <PrivateSettingsContext.Provider children={children} value={contextValue} />;
}
const useSelector = (selector, equalityFunction = (a, b) => a === b) => {
const { subscribers, stateRef } = useContext(SettingsContext);
const [value, setValue] = useState(() => selector(stateRef.current));
const handleUpdate = useMutableCallback((state) => {
const newValue = selector(state);
if (!equalityFunction(newValue, value)) {
setValue(newValue);
}
});
useEffect(() => {
subscribers.add(handleUpdate);
return () => {
subscribers.delete(handleUpdate);
};
}, [handleUpdate]);
useLayoutEffect(() => {
handleUpdate(stateRef.current);
});
return value;
};
export const useGroup = (groupId) => {
const group = useSelector((state) => state.settings.find(({ _id, type }) => _id === groupId && type === 'group'));
const filterSettings = (settings) => settings.filter(({ group }) => group === groupId);
const changed = useSelector((state) => filterSettings(state.settings).some(({ changed }) => changed));
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(SettingsContext);
const dispatchToastMessage = useToastMessageDispatch();
const t = useTranslation();
const loadLanguage = useLoadLanguage();
const user = useUser();
const save = useMutableCallback(async () => {
const state = stateRef.current;
const settings = filterSettings(state.settings);
const changes = settings.filter(({ changed }) => changed)
.map(({ _id, value, editor }) => ({ _id, value, editor }));
if (changes.length === 0) {
return;
}
try {
await batchSetSettings(changes);
if (changes.some(({ _id }) => _id === 'Language')) {
const lng = user.language
|| changes.filter(({ _id }) => _id === 'Language').shift().value
|| 'en';
try {
await loadLanguage(lng);
dispatchToastMessage({ type: 'success', message: t('Settings_updated', { lng }) });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
return;
}
dispatchToastMessage({ type: 'success', message: t('Settings_updated') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
});
const cancel = useMutableCallback(() => {
const state = stateRef.current;
const settings = filterSettings(state.settings);
const persistedSettings = filterSettings(state.persistedSettings);
const changes = settings.filter(({ changed }) => changed)
.map((field) => {
const { _id, value, editor } = persistedSettings.find(({ _id }) => _id === field._id);
return { _id, value, editor, changed: false };
});
hydrate(changes);
});
return group && { ...group, sections, changed, save, cancel };
};
export const useSection = (groupId, sectionName) => {
sectionName = sectionName || '';
const filterSettings = (settings) =>
settings.filter(({ group, section }) => group === groupId && ((!sectionName && !section) || (sectionName === section)));
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(SettingsContext);
const reset = useMutableCallback(() => {
const state = stateRef.current;
const settings = filterSettings(state.settings)
.filter((setting) => Tracker.nonreactive(() => !isDisabled(setting))); // Ignore disabled settings
const persistedSettings = filterSettings(state.persistedSettings);
const changes = settings.map((setting) => {
const { _id, value, packageValue, packageEditor } = persistedSettings.find(({ _id }) => _id === setting._id);
return {
_id,
value: packageValue,
editor: packageEditor,
changed: JSON.stringify(packageValue) !== JSON.stringify(value),
};
});
hydrate(changes);
});
return {
name: sectionName,
canReset,
settings: settingsIds,
reset,
};
};
export const useSettingActions = (persistedSetting) => {
const { hydrate } = useContext(SettingsContext);
const update = useDebouncedCallback(({ value, editor }) => {
const changes = [{
_id: persistedSetting._id,
...value !== undefined && { value },
...editor !== undefined && { editor },
changed: JSON.stringify(persistedSetting.value) !== JSON.stringify(value) || JSON.stringify(editor) !== JSON.stringify(persistedSetting.editor),
}];
hydrate(changes);
}, 100, [hydrate, persistedSetting]);
const reset = useDebouncedCallback(() => {
const { _id, value, packageValue, packageEditor, editor } = persistedSetting;
const changes = [{
_id,
value: packageValue,
editor: packageEditor,
changed: JSON.stringify(packageValue) !== JSON.stringify(value) || JSON.stringify(packageEditor) !== JSON.stringify(editor),
}];
hydrate(changes);
}, 100, [hydrate, persistedSetting]);
return { update, reset };
};
export const useSettingDisabledState = ({ blocked, enableQuery }) => {
const { isDisabled } = useContext(SettingsContext);
return useReactiveValue(() => isDisabled({ blocked, enableQuery }), [blocked, enableQuery]);
};
export const useSectionChangedState = (groupId, sectionName) =>
useSelector((state) =>
state.settings.some(({ group, section, changed }) =>
group === groupId && ((!sectionName && !section) || (sectionName === section)) && changed));
export const useSetting = (_id) => {
const selectSetting = (settings) => settings.find((setting) => setting._id === _id);
const setting = useSelector((state) => selectSetting(state.settings));
const persistedSetting = useSelector((state) => selectSetting(state.persistedSettings));
const { update, reset } = useSettingActions(persistedSetting);
const disabled = useSettingDisabledState(persistedSetting);
return {
...setting,
disabled,
update,
reset,
};
};
export {
usePrivateSettingsGroup as useGroup,
usePrivateSettingsSection as useSection,
usePrivateSettingActions as useSettingActions,
usePrivateSettingDisabledState as useSettingDisabledState,
usePrivateSettingsSectionChangedState as useSectionChangedState,
usePrivateSetting as useSetting,
} from '../../contexts/PrivateSettingsContext';

@ -2,11 +2,10 @@ import { Button } from '@rocket.chat/fuselage';
import React from 'react';
import s from 'underscore.string';
import RawText from '../../../components/basic/RawText';
import { useModal } from '../../../contexts/ModalContext';
import { useAbsoluteUrl, useMethod } from '../../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useModal } from '../../../hooks/useModal';
import { GroupPage } from '../GroupPage';
import { Section } from '../Section';
@ -92,7 +91,7 @@ export function OAuthGroupPage({ _id, sections, ...group }) {
return <Section
key={sectionName}
groupId={_id}
help={<RawText>{t('Custom_oauth_helper', callbackURL(sectionName))}</RawText>}
help={<span dangerouslySetInnerHTML={{ __html: t('Custom_oauth_helper', callbackURL(sectionName)) }} />}
sectionName={sectionName}
solo={solo}
>

@ -71,7 +71,7 @@ export function UserInfo({ data, onChange, ...props }) {
<UserInfoActions isActive={data.active} isAdmin={data.roles.includes('admin')} _id={data._id} username={data.username} onChange={onChange}/>
<Box display='flex' flexDirection='column' w='full' backgroundColor='neutral-200' p='x16' withTruncatedTex flexShrink={0}t>
<Margins blockEnd='x4'>
{data.bio && data.bio.trim().length > 0 && <Box withTruncatedText> <MarkdownText fontScale='s1'>{data.bio}</MarkdownText></Box>}
{data.bio && data.bio.trim().length > 0 && <MarkdownText withTruncatedText fontScale='s1' content={data.bio} />}
{!!data.roles.length && <>
<Box fontScale='micro' color='hint' mbs='none'>{t('Roles')}</Box>
<Box display='flex' flexDirection='row' flexWrap='wrap'>

@ -3,7 +3,6 @@ import React, { useEffect, useRef, useState, useCallback } from 'react';
import { Box, Icon, Scrollable } from '@rocket.chat/fuselage';
import Page from '../../components/basic/Page';
import RawText from '../../components/basic/RawText';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { useEndpoint } from '../../contexts/ServerContext';
import { useTranslation } from '../../contexts/TranslationContext';
@ -212,7 +211,7 @@ function ViewLogs() {
onScroll={handleScroll}
>
{lines.sort((a, b) => a.ts - b.ts).map(({ string }, i) =>
<RawText key={i}>{ansispan(string)}</RawText>)}
<span key={i} dangerouslySetInnerHTML={{ __html: ansispan(string) }} />)}
</Box>
</Scrollable>
<Box

@ -1,95 +0,0 @@
.burger {
position: relative;
display: none;
visibility: hidden;
cursor: pointer;
transition: transform 0.2s ease-out 0.1s;
will-change: transform;
& .burger__line {
display: block;
width: 20px;
height: 2px;
margin: 5px 0;
transition: transform 0.2s ease-out;
opacity: 0.8;
background-color: var(--rc-color-primary);
}
/* TODO: unread-burger-alert -> burger__unread-badge */
& .unread-burger-alert {
position: absolute;
z-index: 3;
bottom: 13px;
left: 10px;
min-width: 18px;
height: 18px;
padding: 0 4px;
text-align: center;
color: var(--rc-color-content);
border-radius: 20px;
background-color: var(--rc-color-error-light);
font-size: 12px;
font-weight: bold;
line-height: 18px;
}
/* TODO: .menu-opened -> .burger--open */
&.menu-opened .burger__line {
&:nth-child(1),
&:nth-child(3) {
transform-origin: 50%, 50%, 0;
opacity: 1;
}
&:nth-child(1) {
transform: translate(-25%, 3px) rotate(-45deg) scale(0.5, 1);
}
&:nth-child(3) {
transform: translate(-25%, -3px) rotate(45deg) scale(0.5, 1);
}
}
.rtl & {
right: 0;
left: auto;
margin-right: 7px;
margin-left: auto;
& .unread-burger-alert {
right: auto;
left: 4px;
}
&.menu-opened .burger__line {
&:nth-child(1) {
transform: translate(25%, 3px) rotate(45deg) scale(0.5, 1);
}
&:nth-child(3) {
transform: translate(25%, -3px) rotate(-45deg) scale(0.5, 1);
}
}
}
}
@media (max-width: 780px) {
.burger {
display: inline-block;
visibility: visible;
}
}

@ -1,38 +0,0 @@
import { Box } from '@rocket.chat/fuselage';
import React from 'react';
import './BurgerMenuButton.css';
import { useSession } from '../../contexts/SessionContext';
import { useSidebar } from '../../contexts/SidebarContext';
import { useEmbeddedLayout } from '../../hooks/useEmbeddedLayout';
export const BurgerMenuButton = (props) => {
const [isSidebarOpen, setSidebarOpen] = useSidebar();
const isLayoutEmbedded = useEmbeddedLayout();
const unreadMessagesBadge = useSession('unread');
const handleClick = () => {
setSidebarOpen(!isSidebarOpen);
};
return <Box
is='button'
aria-label={isSidebarOpen ? 'Close menu' : 'Open menu'}
className={[
'rc-old',
'burger',
!!isSidebarOpen && 'menu-opened',
].filter(Boolean).join(' ')}
type='button'
onClick={handleClick}
{...props}
>
<Box is='i' className='burger__line' aria-hidden='true' />
<Box is='i' className='burger__line' aria-hidden='true' />
<Box is='i' className='burger__line' aria-hidden='true' />
{!isLayoutEmbedded && unreadMessagesBadge
&& <Box className='unread-burger-alert color-error-contrast background-error-color'>
{unreadMessagesBadge}
</Box>}
</Box>;
};

@ -1,46 +0,0 @@
import { action } from '@storybook/addon-actions';
import { boolean, text } from '@storybook/addon-knobs/react';
import React from 'react';
import { BurgerMenuButton } from './BurgerMenuButton';
import { SidebarContext } from '../../contexts/SidebarContext';
import { SessionContext } from '../../contexts/SessionContext';
export default {
title: 'basic/BurgerMenuButton',
component: BurgerMenuButton,
decorators: [(fn) => <div style={{ margin: '1rem' }}>{fn()}</div>],
parameters: {
viewport: { defaultViewport: 'mobile1' },
},
};
export const _default = () => <BurgerMenuButton
isSidebarOpen={boolean('isSidebarOpen')}
isLayoutEmbedded={boolean('isLayoutEmbedded')}
unreadMessagesBadge={text('unreadMessagesBadge')}
onClick={action('click')}
/>;
_default.story = {
decorators: [
(fn) => <SidebarContext.Provider children={fn()} value={[false, action('setSidebarOpen')]} />,
],
};
export const whenSidebarOpen = () => <BurgerMenuButton />;
whenSidebarOpen.story = {
decorators: [
(fn) => <SidebarContext.Provider children={fn()} value={[true, action('setSidebarOpen')]} />,
],
};
export const unreadMessagesBadge = () => <BurgerMenuButton />;
unreadMessagesBadge.story = {
decorators: [
(fn) => <SessionContext.Provider value={{
get: (name) => name === 'unread' && '99',
}}>
<SidebarContext.Provider children={fn()} value={[false, action('setSidebarOpen')]} />
</SessionContext.Provider>,
],
};

@ -3,6 +3,7 @@ import React, { useState } from 'react';
function Logo({ src = 'images/logo/logo.svg', ...props }) {
const [isLoaded, setLoaded] = useState(false);
const isPlaceholderVisible = !src || !isLoaded;
const handleLoad = () => {
setLoaded(true);
@ -13,7 +14,7 @@ function Logo({ src = 'images/logo/logo.svg', ...props }) {
};
return <>
{!isLoaded && <Box is='svg' viewBox='0 0 1000 182' xmlns='http://www.w3.org/2000/svg' {...props}>
{isPlaceholderVisible && <Box {...props} is='svg' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1000 182'>
<g fill='none' fillRule='evenodd'>
<g fill='#9ea2a8' fillRule='nonzero' transform='translate(245.154709 29.581775)'>
<path d='m62.5717573 58.8485839c0 10.1578288-3.791296 16.8288535-11.0711519 20.01071l10.4656798 39.7212001c.4541041 1.822594-.454104 2.727981-2.1238826 2.727981h-15.7753863c-1.5160454 0-2.2728855-.756459-2.5779867-2.122813l-10.1629438-38.5061381h-10.4656799v38.2059181c0 1.517646-.9082081 2.423033-2.4266186 2.423033h-15.77538636c-1.51604538 0-2.42661865-.910115-2.42661865-2.423033v-116.43561996c0-1.51528236.91057327-2.42539734 2.42661865-2.42539734h38.07142346c14.2569758 0 21.8419329 7.58113963 21.8419329 21.83094zm-26.6951702 1.817866c3.942664 0 6.0665466-2.1228136 6.0665466-6.0634933v-28.5019646c0-3.9406797-2.1238826-6.0611294-6.0665466-6.0611294h-15.0161812v40.6289513z'/>
@ -33,7 +34,7 @@ function Logo({ src = 'images/logo/logo.svg', ...props }) {
<path d='m65.7703624 103.026182c-6.6534511 0-12.047139-5.4803639-12.047139-12.2407349 0-6.7603709 5.3936879-12.2407344 12.047139-12.2407344 6.6534512 0 12.047139 5.4803635 12.047139 12.2407344 0 6.760371-5.3936878 12.2407349-12.047139 12.2407349zm38.7859106 0c-6.6534508 0-12.0471386-5.4803639-12.0471386-12.2407349 0-6.7603709 5.3936878-12.2407344 12.0471386-12.2407344 6.653452 0 12.047139 5.4803635 12.047139 12.2407344 0 6.760371-5.393687 12.2407349-12.047139 12.2407349zm38.785911 0c-6.653451 0-12.047139-5.4803639-12.047139-12.2407349 0-6.7603709 5.393688-12.2407344 12.047139-12.2407344 6.653452 0 12.047139 5.4803635 12.047139 12.2407344 0 6.760371-5.393687 12.2407349-12.047139 12.2407349z' fill='#db2323' fillRule='nonzero'/>
</g>
</Box>}
<Box is='img' src={src} hidden={!isLoaded} onLoad={handleLoad} onError={handleError} {...props} />
{src && <Box {...props} is='img' src={src} hidden={!isLoaded} onLoad={handleLoad} onError={handleError} />}
</>;
}

@ -3,8 +3,8 @@ import React from 'react';
import Logo from './Logo';
export default {
title: 'setupWizard/Logo',
title: 'components/basic/Logo',
component: Logo,
};
export const _default = () => <Logo />;
export const AsPlaceholder = () => <Logo />;

@ -1,9 +1,19 @@
import React from 'react';
import { Box } from '@rocket.chat/fuselage';
import React, { useMemo } from 'react';
import marked from 'marked';
import { Markdown } from '../../../app/markdown/client';
import RawText from './RawText';
marked.InlineLexer.rules.gfm.strong = /^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/;
marked.InlineLexer.rules.gfm.em = /^__(?=\S)([\s\S]*?\S)__(?!_)|^_(?=\S)([\s\S]*?\S)_(?!_)/;
const MarkdownText = ({ children }) =>
<RawText>{Markdown.parse(children)}</RawText>;
function MarkdownText({ content, ...props }) {
const options = useMemo(() => ({
gfm: true,
headerIds: false,
}), []);
const __html = useMemo(() => marked(content, options), [content, options]);
return <Box dangerouslySetInnerHTML={{ __html }} withRichContent {...props} />;
}
export default MarkdownText;

@ -0,0 +1,39 @@
import React from 'react';
import MarkdownText from './MarkdownText';
export default {
title: 'components/basic/MarkdownText',
component: MarkdownText,
};
export const Example = () =>
<MarkdownText
content={`
# h1 Heading
## h2 Heading
### h3 Heading
#### h4 Heading
##### h5 Heading
###### h6 Heading
___
*This is bold text*
_This is italic text_
~Strikethrough~
+ Lorem ipsum dolor sit amet
+ Consectetur adipiscing elit
+ Integer molestie lorem at massa
1. Lorem ipsum dolor sit amet
2. Consectetur adipiscing elit
3. Integer molestie lorem at massa
\`rocket.chat();\`
https://rocket.chat`}
/>;

@ -1,7 +1,10 @@
import { Box, Scrollable } from '@rocket.chat/fuselage';
import { useMediaQuery } from '@rocket.chat/fuselage-hooks';
import React, { createContext, useContext, useState } from 'react';
import { BurgerMenuButton } from './BurgerMenuButton';
import { useSidebar } from '../../contexts/SidebarContext';
import BurgerMenuButton from './burger/BurgerMenuButton';
import { useSession } from '../../contexts/SessionContext';
const PageContext = createContext();
@ -23,6 +26,14 @@ function Page(props) {
function PageHeader({ children, title, ...props }) {
const [border] = useContext(PageContext);
const hasBurgerMenuButton = useMediaQuery('(max-width: 780px)');
const [isSidebarOpen, setSidebarOpen] = useSidebar();
const unreadMessagesBadge = useSession('unread');
const handleBurgerMenuButtonClick = () => {
setSidebarOpen((isSidebarOpen) => !isSidebarOpen);
};
return <Box borderBlockEndWidth='x2' borderBlockEndColor={border ? 'neutral-200' : 'transparent'}>
<Box
marginBlock='x16'
@ -34,7 +45,12 @@ function PageHeader({ children, title, ...props }) {
alignItems='center'
{...props}
>
<BurgerMenuButton marginInlineEnd='x8' />
{hasBurgerMenuButton && <BurgerMenuButton
open={isSidebarOpen}
badge={unreadMessagesBadge}
marginInlineEnd='x8'
onClick={handleBurgerMenuButtonClick}
/>}
<Box is='h1' fontScale='h1' flexGrow={1}>{title}</Box>
{children}
</Box>

@ -1,27 +1,27 @@
import { Button, ButtonGroup, Margins, Tile } from '@rocket.chat/fuselage';
import { Button, ButtonGroup, Tile } from '@rocket.chat/fuselage';
import React from 'react';
import { fullHeightDecorator } from '../../../.storybook/decorators';
import Page from './Page';
export default {
title: 'basic/Page',
title: 'components/basic/Page',
component: Page,
decorators: [
(fn) => <div className='rc-old'>{fn()}</div>,
],
};
export const _default = () =>
const DummyContent = () => <>
{Array.from({ length: 60 }, (_, i) => <Tile key={i} children='Content slice' marginBlock='x16' />)}
</>;
export const Basic = () =>
<Page>
<Page.Header title='Header' />
<Page.Content>
<Margins block='x16'>
{Array.from({ length: 60 }, (_, i) => <Tile key={i} children='Content slice' />)}
</Margins>
<DummyContent />
</Page.Content>
</Page>;
export const withButtonsAtTheHeader = () =>
export const WithButtonsAtTheHeader = () =>
<Page>
<Page.Header title='Header'>
<ButtonGroup>
@ -29,8 +29,28 @@ export const withButtonsAtTheHeader = () =>
</ButtonGroup>
</Page.Header>
<Page.Content>
<Margins block='x16'>
{Array.from({ length: 60 }, (_, i) => <Tile key={i} children='Content slice' />)}
</Margins>
<DummyContent />
</Page.Content>
</Page>;
export const WithScrollableContent = () =>
<Page>
<Page.Header title='Header' />
<Page.ScrollableContent>
<DummyContent />
</Page.ScrollableContent>
</Page>;
WithScrollableContent.story = {
decorators: [fullHeightDecorator],
};
export const WithScrollableContentWithShadow = () =>
<Page>
<Page.Header title='Header' />
<Page.ScrollableContentWithShadow>
<DummyContent />
</Page.ScrollableContentWithShadow>
</Page>;
WithScrollableContentWithShadow.story = {
decorators: [fullHeightDecorator],
};

@ -0,0 +1,15 @@
import { Badge } from '@rocket.chat/fuselage';
import React from 'react';
function BurgerBadge({ children }) {
return <Badge
position='absolute'
insetInlineEnd='neg-x8'
insetBlockStart='neg-x4'
zIndex='3'
variant='danger'
children={children}
/>;
}
export default BurgerBadge;

@ -0,0 +1,21 @@
import { Box } from '@rocket.chat/fuselage';
import React from 'react';
import BurgerBadge from './BurgerBadge';
import { centeredDecorator } from '../../../../.storybook/decorators';
export default {
title: 'components/basic/burger/BurgerBadge',
component: BurgerBadge,
decorators: [
(storyFn) => <Box size='x24' borderWidth='x1' borderStyle='dashed' position='relative'>
{storyFn()}
</Box>,
centeredDecorator,
],
};
export const Basic = () =>
<BurgerBadge>
99
</BurgerBadge>;

@ -0,0 +1,69 @@
import { css } from '@rocket.chat/css-in-js';
import { Box } from '@rocket.chat/fuselage';
import React from 'react';
import { useIsReducedMotionPreferred } from '../../../hooks/useIsReducedMotionPreferred';
const Wrapper = ({ children }) =>
<Box
is='span'
display='inline-flex'
flexDirection='column'
alignItems='center'
justifyContent='space-between'
size='x24'
paddingBlock='x4'
paddingInline='x2'
verticalAlign='middle'
children={children}
/>;
const Line = ({ animated, moved }) =>
<Box
is='span'
width='x20'
height='x2'
backgroundColor='currentColor'
className={[
animated && css`
will-change: transform;
transition: transform 0.2s ease-out;
`,
moved && css`
&:nth-child(1),
&:nth-child(3) {
transform-origin: 50%, 50%, 0;
}
&:nth-child(1) {
transform: translate(-25%, 3px) rotate(-45deg) scale(0.5, 1);
}
[dir=rtl] &:nth-child(1) {
transform: translate(25%, 3px) rotate(45deg) scale(0.5, 1);
}
&:nth-child(3) {
transform: translate(-25%, -3px) rotate(45deg) scale(0.5, 1);
}
[dir=rtl] &:nth-child(3) {
transform: translate(25%, -3px) rotate(-45deg) scale(0.5, 1);
}
`,
]}
aria-hidden='true'
/>;
function BurgerIcon({ children, open }) {
const isReducedMotionPreferred = useIsReducedMotionPreferred();
return <Wrapper>
<Line animated={!isReducedMotionPreferred} moved={open} />
<Line animated={!isReducedMotionPreferred} moved={open} />
<Line animated={!isReducedMotionPreferred} moved={open} />
{children}
</Wrapper>;
}
export default BurgerIcon;

@ -0,0 +1,22 @@
import React from 'react';
import { centeredDecorator } from '../../../../.storybook/decorators';
import { useAutoToggle } from '../../../../.storybook/hooks';
import BurgerIcon from './BurgerIcon';
export default {
title: 'components/basic/burger/BurgerIcon',
component: BurgerIcon,
decorators: [
centeredDecorator,
],
};
export const Normal = () =>
<BurgerIcon />;
export const Open = () =>
<BurgerIcon open />;
export const Transitioning = () =>
<BurgerIcon open={useAutoToggle()} />;

@ -0,0 +1,27 @@
import { css } from '@rocket.chat/css-in-js';
import { Box } from '@rocket.chat/fuselage';
import React from 'react';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useEmbeddedLayout } from '../../../hooks/useEmbeddedLayout';
import BurgerIcon from './BurgerIcon';
import BurgerBadge from './BurgerBadge';
function BurgerMenuButton({ open, badge, ...props }) {
const isLayoutEmbedded = useEmbeddedLayout();
const t = useTranslation();
return <Box
is='button'
aria-label={open ? t('Close menu') : t('Open menu')}
type='button'
position='relative'
className={css`cursor: pointer;`}
{...props}
>
<BurgerIcon open={open} />
{!isLayoutEmbedded && badge && <BurgerBadge>{badge}</BurgerBadge>}
</Box>;
}
export default BurgerMenuButton;

@ -0,0 +1,22 @@
import { action } from '@storybook/addon-actions';
import React from 'react';
import { centeredDecorator } from '../../../../.storybook/decorators';
import BurgerMenuButton from './BurgerMenuButton';
export default {
title: 'components/basic/burger/BurgerMenuButton',
component: BurgerMenuButton,
decorators: [
centeredDecorator,
],
};
export const Basic = () =>
<BurgerMenuButton onClick={action('click')} />;
export const Open = () =>
<BurgerMenuButton open onClick={action('click')} />;
export const WithBadge = () =>
<BurgerMenuButton badge='99' onClick={action('click')} />;

@ -37,7 +37,7 @@ const useReconnectCountdown = (retryTime, status) => {
return reconnectCountdown;
};
export function ConnectionStatusAlert() {
function ConnectionStatusAlert() {
const { connected, retryTime, status, reconnect } = useConnectionStatus();
const reconnectCountdown = useReconnectCountdown(retryTime, status);
const t = useTranslation();
@ -69,3 +69,5 @@ export function ConnectionStatusAlert() {
</>}
</div>;
}
export default ConnectionStatusAlert;

@ -2,65 +2,59 @@ import { action } from '@storybook/addon-actions';
import React from 'react';
import { ConnectionStatusContext } from '../../contexts/ConnectionStatusContext';
import { ConnectionStatusAlert } from './ConnectionStatusAlert';
import ConnectionStatusAlert from './ConnectionStatusAlert';
const stateDecorator = ({
status = 'connected',
} = {}) =>
(storyFn) =>
<ConnectionStatusContext.Provider
value={{
connected: status === 'connected',
status,
retryTime: status === 'waiting' && Date.now() + 300000,
reconnect: action('reconnect'),
}}
>
{storyFn()}
</ConnectionStatusContext.Provider>;
export default {
title: 'connectionStatus/ConnectionStatusAlert',
title: 'components/connectionStatus/ConnectionStatusAlert',
component: ConnectionStatusAlert,
};
export const connected = () => <ConnectionStatusAlert />;
connected.story = {
export const Connected = () => <ConnectionStatusAlert />;
Connected.story = {
decorators: [
(fn) => <ConnectionStatusContext.Provider children={fn()} value={{
connected: true,
status: 'connected',
reconnect: action('reconnect'),
}} />,
stateDecorator(),
],
};
export const connecting = () => <ConnectionStatusAlert />;
connecting.story = {
export const Connecting = () => <ConnectionStatusAlert />;
Connecting.story = {
decorators: [
(fn) => <ConnectionStatusContext.Provider children={fn()} value={{
connected: false,
status: 'connecting',
reconnect: action('reconnect'),
}} />,
stateDecorator({ status: 'connecting' }),
],
};
export const failed = () => <ConnectionStatusAlert />;
failed.story = {
export const Failed = () => <ConnectionStatusAlert />;
Failed.story = {
decorators: [
(fn) => <ConnectionStatusContext.Provider children={fn()} value={{
connected: false,
status: 'failed',
reconnect: action('reconnect'),
}} />,
stateDecorator({ status: 'failed' }),
],
};
export const waiting = () => <ConnectionStatusAlert />;
waiting.story = {
export const Waiting = () => <ConnectionStatusAlert />;
Waiting.story = {
decorators: [
(fn) => <ConnectionStatusContext.Provider children={fn()} value={{
connected: false,
status: 'waiting',
retryTime: Date.now() + 300000,
reconnect: action('reconnect'),
}} />,
stateDecorator({ status: 'waiting' }),
],
};
export const offline = () => <ConnectionStatusAlert />;
offline.story = {
export const Offline = () => <ConnectionStatusAlert />;
Offline.story = {
decorators: [
(fn) => <ConnectionStatusContext.Provider children={fn()} value={{
connected: false,
status: 'offline',
reconnect: action('reconnect'),
}} />,
stateDecorator({ status: 'offline' }),
],
};

@ -1,10 +0,0 @@
import React from 'react';
import PageNotFound from './PageNotFound';
export default {
title: 'pageNotFound/PageNotFound',
component: PageNotFound,
};
export const _default = () => <PageNotFound />;

@ -0,0 +1,7 @@
import { createContext, useContext } from 'react';
type ModalContextValue = unknown;
export const ModalContext = createContext<ModalContextValue>({});
export const useModal = (): ModalContextValue => useContext(ModalContext);

@ -0,0 +1,249 @@
import { useDebouncedCallback, useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { Tracker } from 'meteor/tracker';
import { createContext, useContext, RefObject, useState, useEffect, useLayoutEffect } from 'react';
import { useReactiveValue } from '../hooks/useReactiveValue';
import { useBatchSettingsDispatch } from './SettingsContext';
import { useToastMessageDispatch } from './ToastMessagesContext';
import { useTranslation, useLoadLanguage } from './TranslationContext';
import { useUser } from './UserContext';
type Setting = object & {
_id: unknown;
type: string;
blocked: boolean;
enableQuery: unknown;
group: string;
section: string;
changed: boolean;
value: unknown;
packageValue: unknown;
packageEditor: unknown;
editor: unknown;
disabled?: boolean;
update?: () => void;
reset?: () => void;
};
type PrivateSettingsState = {
settings: Setting[];
persistedSettings: Setting[];
};
type EqualityFunction<T> = (a: T, b: T) => boolean;
type PrivateSettingsContextValue = {
subscribers: Set<(state: PrivateSettingsState) => void>;
stateRef: RefObject<PrivateSettingsState>;
hydrate: (changes: any[]) => void;
isDisabled: (setting: Setting) => boolean;
};
export const PrivateSettingsContext = createContext<PrivateSettingsContextValue>({
subscribers: new Set<(state: PrivateSettingsState) => void>(),
stateRef: {
current: {
settings: [],
persistedSettings: [],
},
},
hydrate: () => undefined,
isDisabled: () => false,
});
const useSelector = <T>(
selector: (state: PrivateSettingsState) => T,
equalityFunction: EqualityFunction<T> = Object.is,
): T | null => {
const { subscribers, stateRef } = useContext(PrivateSettingsContext);
const [value, setValue] = useState<T | null>(() => (stateRef.current ? selector(stateRef.current) : null));
const handleUpdate = useMutableCallback((state: PrivateSettingsState) => {
const newValue = selector(state);
if (!value || !equalityFunction(newValue, value)) {
setValue(newValue);
}
});
useEffect(() => {
subscribers.add(handleUpdate);
return (): void => {
subscribers.delete(handleUpdate);
};
}, [handleUpdate]);
useLayoutEffect(() => {
handleUpdate(stateRef.current);
});
return value;
};
export const usePrivateSettingsGroup = (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);
const changed = useSelector((state) => filterSettings(state.settings).some(({ changed }) => changed));
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 dispatchToastMessage = useToastMessageDispatch() as any;
const t = useTranslation() as (key: string, ...args: any[]) => string;
const loadLanguage = useLoadLanguage() as any;
const user = useUser() as any;
const save = useMutableCallback(async () => {
const state = stateRef.current;
const settings = filterSettings(state?.settings ?? []);
const changes = settings.filter(({ changed }) => changed)
.map(({ _id, value, editor }) => ({ _id, value, editor }));
if (changes.length === 0) {
return;
}
try {
await batchSetSettings(changes);
if (changes.some(({ _id }) => _id === 'Language')) {
const lng = user?.language
|| changes.filter(({ _id }) => _id === 'Language').shift()?.value
|| 'en';
try {
await loadLanguage(lng);
dispatchToastMessage({ type: 'success', message: t('Settings_updated', { lng }) });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
return;
}
dispatchToastMessage({ type: 'success', message: t('Settings_updated') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
});
const cancel = useMutableCallback(() => {
const state = stateRef.current;
const settings = filterSettings(state?.settings ?? []);
const persistedSettings = filterSettings(state?.persistedSettings ?? []);
const changes = settings.filter(({ changed }) => changed)
.map((field) => {
const { _id, value, editor } = persistedSettings.find(({ _id }) => _id === field._id);
return { _id, value, editor, changed: false };
});
hydrate(changes);
});
return group && { ...group, sections, changed, save, cancel };
};
export const usePrivateSettingsSection = (groupId: string, sectionName?: string): any => {
sectionName = sectionName || '';
const filterSettings = (settings: any[]): any[] =>
settings.filter(({ group, section }) => group === groupId && ((!sectionName && !section) || (sectionName === section)));
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 reset = useMutableCallback(() => {
const state = stateRef.current;
const settings = filterSettings(state?.settings ?? [])
.filter((setting) => Tracker.nonreactive(() => !isDisabled(setting))); // Ignore disabled settings
const persistedSettings = filterSettings(state?.persistedSettings ?? []);
const changes = settings.map((setting) => {
const { _id, value, packageValue, packageEditor } = persistedSettings.find(({ _id }) => _id === setting._id);
return {
_id,
value: packageValue,
editor: packageEditor,
changed: JSON.stringify(packageValue) !== JSON.stringify(value),
};
});
hydrate(changes);
});
return {
name: sectionName,
canReset,
settings: settingsIds,
reset,
};
};
export const usePrivateSettingActions = (persistedSetting: Setting | null | undefined): {
update: () => void;
reset: () => void;
} => {
const { hydrate } = useContext(PrivateSettingsContext);
const update = useDebouncedCallback(({ value, editor }) => {
const changes = [{
_id: persistedSetting?._id,
...value !== undefined && { value },
...editor !== undefined && { editor },
changed: JSON.stringify(persistedSetting?.value) !== JSON.stringify(value) || JSON.stringify(editor) !== JSON.stringify(persistedSetting?.editor),
}];
hydrate(changes);
}, 100, [hydrate, persistedSetting]) as () => void;
const reset = useDebouncedCallback(() => {
const changes = [{
_id: persistedSetting?._id,
value: persistedSetting?.packageValue,
editor: persistedSetting?.packageEditor,
changed: JSON.stringify(persistedSetting?.packageValue) !== JSON.stringify(persistedSetting?.value) || JSON.stringify(persistedSetting?.packageEditor) !== JSON.stringify(persistedSetting?.editor),
}];
hydrate(changes);
}, 100, [hydrate, persistedSetting]) as () => void;
return { update, reset };
};
export const usePrivateSettingDisabledState = (setting: Setting | null | undefined): boolean => {
const { isDisabled } = useContext(PrivateSettingsContext);
return useReactiveValue(() => (setting ? isDisabled(setting) : false), [setting?.blocked, setting?.enableQuery]) as unknown as boolean;
};
export const usePrivateSettingsSectionChangedState = (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);
const setting = useSelector((state) => selectSetting(state.settings));
const persistedSetting = useSelector((state) => selectSetting(state.persistedSettings));
const { update, reset } = usePrivateSettingActions(persistedSetting);
const disabled = usePrivateSettingDisabledState(persistedSetting);
if (!setting) {
return null;
}
return {
...setting,
disabled,
update,
reset,
};
};

@ -0,0 +1,4 @@
declare module '@rocket.chat/fuselage-hooks' {
export const useDebouncedCallback: (fn: (...args: any[]) => any, ms: number, deps: any[]) => (...args: any[]) => any;
export const useMutableCallback: (fn: (...args: any[]) => any) => (...args: any[]) => any;
}

@ -0,0 +1,3 @@
import { useMediaQuery } from '@rocket.chat/fuselage-hooks';
export const useIsReducedMotionPreferred = () => useMediaQuery('(prefers-reduced-motion: reduce)');

@ -1,3 +0,0 @@
import { modal } from '../../app/ui-utils/client/lib/modal';
export const useModal = () => modal;

@ -12,6 +12,7 @@ import { ToastMessagesProvider } from './ToastMessagesProvider';
import { UserProvider } from './UserProvider';
import { AvatarUrlProvider } from './AvatarUrlProvider';
import { CustomSoundProvider } from './CustomSoundProvides';
import ModalProvider from './ModalProvider';
export function MeteorProvider({ children }) {
return <ConnectionStatusProvider>
@ -21,17 +22,19 @@ export function MeteorProvider({ children }) {
<SessionProvider>
<SidebarProvider>
<ToastMessagesProvider>
<SettingsProvider>
<CustomSoundProvider>
<AvatarUrlProvider>
<UserProvider>
<AuthorizationProvider>
{children}
</AuthorizationProvider>
</UserProvider>
</AvatarUrlProvider>
</CustomSoundProvider>
</SettingsProvider>
<ModalProvider>
<SettingsProvider>
<CustomSoundProvider>
<AvatarUrlProvider>
<UserProvider>
<AuthorizationProvider>
{children}
</AuthorizationProvider>
</UserProvider>
</AvatarUrlProvider>
</CustomSoundProvider>
</SettingsProvider>
</ModalProvider>
</ToastMessagesProvider>
</SidebarProvider>
</SessionProvider>

@ -0,0 +1,10 @@
import React from 'react';
import { ModalContext } from '../contexts/ModalContext';
import { modal } from '../../app/ui-utils/client/lib/modal';
function ModalProvider({ children }) {
return <ModalContext.Provider children={children} value={modal} />;
}
export default ModalProvider;

@ -167,13 +167,13 @@ FlowRouter.route('/invite/:hash', {
FlowRouter.route('/setup-wizard/:step?', {
name: 'setup-wizard',
action: () => {
renderRouteComponent(() => import('./components/setupWizard/SetupWizardRoute'));
renderRouteComponent(() => import('./views/setupWizard/SetupWizardRoute'));
},
});
FlowRouter.notFound = {
action: () => {
renderRouteComponent(() => import('./components/pageNotFound/PageNotFound'));
renderRouteComponent(() => import('./views/notFound/NotFoundPage'));
},
};

@ -1,12 +1,12 @@
import { Box, Button, ButtonGroup, Flex, Margins } from '@rocket.chat/fuselage';
import React from 'react';
import ConnectionStatusAlert from '../../components/connectionStatus/ConnectionStatusAlert';
import { useRoute } from '../../contexts/RouterContext';
import { useWipeInitialPageLoading } from '../../hooks/useWipeInitialPageLoading';
import { ConnectionStatusAlert } from '../connectionStatus/ConnectionStatusAlert';
import { useTranslation } from '../../contexts/TranslationContext';
import { useWipeInitialPageLoading } from '../../hooks/useWipeInitialPageLoading';
function PageNotFound() {
function NotFoundPage() {
useWipeInitialPageLoading();
const t = useTranslation();
@ -54,4 +54,4 @@ function PageNotFound() {
</>;
}
export default PageNotFound;
export default NotFoundPage;

@ -0,0 +1,10 @@
import React from 'react';
import NotFoundPage from './NotFoundPage';
export default {
title: 'views/notFound/NotFoundPage',
component: NotFoundPage,
};
export const Default = () => <NotFoundPage />;

@ -5,7 +5,7 @@ import React from 'react';
import { Pager } from './Pager';
export default {
title: 'setupWizard/Pager',
title: 'views/setupWizard/Pager',
component: Pager,
};

@ -4,7 +4,7 @@ import React from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import { useWipeInitialPageLoading } from '../../hooks/useWipeInitialPageLoading';
import { ConnectionStatusAlert } from '../connectionStatus/ConnectionStatusAlert';
import ConnectionStatusAlert from '../../components/connectionStatus/ConnectionStatusAlert';
import { finalStep } from './SetupWizardState';
import FinalStep from './steps/FinalStep';
import SideBar from './SideBar';

@ -4,7 +4,7 @@ import SetupWizardPage from './SetupWizardPage';
import { finalStep } from './SetupWizardState';
export default {
title: 'setupWizard/SetupWizardPage',
title: 'views/setupWizard/SetupWizardPage',
component: SetupWizardPage,
};

@ -3,7 +3,7 @@ import { useMediaQuery } from '@rocket.chat/fuselage-hooks';
import React from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import Logo from './Logo';
import Logo from '../../components/basic/Logo';
import './SideBar.css';
function SideBar({

@ -4,7 +4,7 @@ import React from 'react';
import SideBar from './SideBar';
export default {
title: 'setupWizard/SideBar',
title: 'views/setupWizard/SideBar',
component: SideBar,
};

@ -4,7 +4,7 @@ import React from 'react';
import { StepHeader } from './StepHeader';
export default {
title: 'setupWizard/StepHeader',
title: 'views/setupWizard/StepHeader',
component: StepHeader,
};

@ -4,7 +4,7 @@ import React from 'react';
import AdminUserInformationStep from './AdminUserInformationStep';
export default {
title: 'setupWizard/steps/AdminUserInformationStep',
title: 'views/setupWizard/steps/AdminUserInformationStep',
component: AdminUserInformationStep,
};

@ -3,7 +3,7 @@ import React from 'react';
import FinalStep from './FinalStep';
export default {
title: 'setupWizard/steps/FinalStep',
title: 'views/setupWizard/steps/FinalStep',
component: FinalStep,
};

@ -4,7 +4,7 @@ import React from 'react';
import RegisterServerStep from './RegisterServerStep';
export default {
title: 'setupWizard/steps/RegisterServerStep',
title: 'views/setupWizard/steps/RegisterServerStep',
component: RegisterServerStep,
};

@ -4,7 +4,7 @@ import React from 'react';
import SettingsBasedStep from './SettingsBasedStep';
export default {
title: 'setupWizard/steps/SettingsBasedStep',
title: 'views/setupWizard/steps/SettingsBasedStep',
component: SettingsBasedStep,
};

@ -4,7 +4,7 @@ import React from 'react';
import { ChannelsTab } from '.';
export default {
title: 'admin/engagement/ChannelsTab',
title: 'admin/enterprise/engagement/ChannelsTab',
component: ChannelsTab,
decorators: [
(fn) => <Margins children={fn()} all='x24' />,

@ -3,7 +3,7 @@ import React from 'react';
import { EngagementDashboardPage } from './EngagementDashboardPage';
export default {
title: 'admin/engagement/EngagementDashboardPage',
title: 'admin/enterprise/engagement/EngagementDashboardPage',
component: EngagementDashboardPage,
decorators: [(fn) => <div children={fn()} style={{ height: '100vh' }} />],
};

@ -4,7 +4,7 @@ import React from 'react';
import { MessagesTab } from '.';
export default {
title: 'admin/engagement/MessagesTab',
title: 'admin/enterprise/engagement/MessagesTab',
component: MessagesTab,
decorators: [
(fn) => <Margins children={fn()} all='x24' />,

@ -4,7 +4,7 @@ import React from 'react';
import { UsersTab } from '.';
export default {
title: 'admin/engagement/UsersTab',
title: 'admin/enterprise/engagement/UsersTab',
component: UsersTab,
decorators: [
(fn) => <Margins children={fn()} all='x24' />,

@ -3,7 +3,7 @@ import React from 'react';
import { Counter } from './Counter';
export default {
title: 'admin/engagement/data/Counter',
title: 'admin/enterprise/engagement/data/Counter',
component: Counter,
};

@ -3,7 +3,7 @@ import React from 'react';
import { CounterSet } from './CounterSet';
export default {
title: 'admin/engagement/data/CounterSet',
title: 'admin/enterprise/engagement/data/CounterSet',
component: CounterSet,
};

@ -4,7 +4,7 @@ import React from 'react';
import { Growth } from './Growth';
export default {
title: 'admin/engagement/data/Growth',
title: 'admin/enterprise/engagement/data/Growth',
component: Growth,
decorators: [(fn) => <Margins children={fn()} all='x16' />],
};

@ -4,7 +4,7 @@ import React from 'react';
import { Histogram } from './Histogram';
export default {
title: 'admin/engagement/data/Histogram',
title: 'admin/enterprise/engagement/data/Histogram',
component: Histogram,
decorators: [(fn) => <Margins all='x16'>
<Flex.Container>

@ -5,7 +5,7 @@ import { LegendSymbol } from './LegendSymbol';
import { monochromaticColors, polychromaticColors } from './colors';
export default {
title: 'admin/engagement/data/LegendSymbol',
title: 'admin/enterprise/engagement/data/LegendSymbol',
component: LegendSymbol,
decorators: [(fn) => <Margins children={fn()} all='x16' />],
};

@ -4,7 +4,7 @@ import React from 'react';
import { NegativeGrowthSymbol } from './NegativeGrowthSymbol';
export default {
title: 'admin/engagement/data/NegativeGrowthSymbol',
title: 'admin/enterprise/engagement/data/NegativeGrowthSymbol',
component: NegativeGrowthSymbol,
decorators: [(fn) => <Margins children={fn()} all='x16' />],
};

@ -4,7 +4,7 @@ import React from 'react';
import { PositiveGrowthSymbol } from './PositiveGrowthSymbol';
export default {
title: 'admin/engagement/data/PositiveGrowthSymbol',
title: 'admin/enterprise/engagement/data/PositiveGrowthSymbol',
component: PositiveGrowthSymbol,
decorators: [(fn) => <Margins children={fn()} all='x16' />],
};

@ -1,4 +1,4 @@
import EventEmitter from 'events';
import { EventEmitter } from 'events';
import { Users } from '../../../../app/models/server';
import { resetEnterprisePermissions } from '../../authorization/server/resetEnterprisePermissions';

1554
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -20,6 +20,7 @@
"tslint": "eslint --ext .ts,.tsx .",
"eslint": "meteor npm run jslint && meteor npm run tslint",
"stylelint": "stylelint \"app/**/*.css\" \"client/**/*.css\" \"app/**/*.less\" \"client/**/*.less\" \"ee/**/*.less\"",
"typecheck": "tsc --noEmit --skipLibCheck",
"deploy": "npm run build && pm2 startOrRestart pm2.json",
"postinstall": "node .scripts/npm-postinstall.js",
"coverage": "nyc -r html mocha --opts ./mocha.opts",
@ -56,10 +57,11 @@
"@rocket.chat/eslint-config": "^0.3.0",
"@rocket.chat/livechat": "^1.5.0",
"@settlin/spacebars-loader": "^1.0.7",
"@storybook/addon-actions": "^5.3.18",
"@storybook/addon-knobs": "^5.3.18",
"@storybook/addons": "^5.3.18",
"@storybook/react": "^5.3.18",
"@storybook/addon-actions": "^5.3.19",
"@storybook/addon-knobs": "^5.3.19",
"@storybook/addon-viewport": "^5.3.19",
"@storybook/addons": "^5.3.19",
"@storybook/react": "^5.3.19",
"@types/bcrypt": "^3.0.0",
"@types/chai": "^4.2.11",
"@types/chai-spies": "^1.0.1",
@ -110,6 +112,7 @@
"stylelint": "^9.9.0",
"stylelint-order": "^2.0.0",
"supertest": "^3.3.0",
"ts-loader": "^7.0.5",
"ts-node": "^8.8.2",
"typescript": "^3.7.3",
"webpack": "^4.29.3"
@ -126,6 +129,7 @@
"@nivo/line": "^0.61.1",
"@nivo/pie": "^0.61.1",
"@rocket.chat/apps-engine": "1.15.0",
"@rocket.chat/css-in-js": "^0.9.0",
"@rocket.chat/fuselage": "^0.9.0",
"@rocket.chat/fuselage-hooks": "^0.9.0",
"@rocket.chat/fuselage-polyfills": "^0.9.0",
@ -133,6 +137,7 @@
"@rocket.chat/icons": "^0.6.3-dev.23",
"@rocket.chat/ui-kit": "^0.6.3-dev.23",
"@slack/client": "^4.8.0",
"@types/use-subscription": "^1.0.0",
"adm-zip": "RocketChat/adm-zip",
"apn": "2.2.0",
"archiver": "^3.0.0",

2
server/main.d.ts vendored

@ -24,7 +24,7 @@ declare module 'meteor/meteor' {
interface Error extends globalError {
error: string | number;
reason?: string;
details?: any;
details?: string | undefined;
}
const server: any;

Loading…
Cancel
Save