[IMPROVE] Detach React components from Meteor API (#15482)

pull/15460/head^2
Tasso Evangelista 6 years ago committed by Guilherme Gazzo
parent aa8c80644d
commit b5114edff1
  1. 13
      .storybook/.babelrc
  2. 2
      .storybook/addons.js
  3. 3
      .storybook/config.js
  4. 44
      .storybook/helpers.js
  5. 22
      .storybook/webpack.config.js
  6. 17
      app/theme/client/imports/general/base_old.css
  7. 5
      client/components/admin/info/BuildEnvironmentSection.js
  8. 2
      client/components/admin/info/CommitSection.js
  9. 4
      client/components/admin/info/InformationPage.js
  10. 5
      client/components/admin/info/InstancesSection.js
  11. 7
      client/components/admin/info/RocketChatSection.js
  12. 7
      client/components/admin/info/RuntimeEnvironmentSection.js
  13. 5
      client/components/admin/info/UsageSection.js
  14. 64
      client/components/admin/info/formatters.js
  15. 20
      client/components/basic/Button.stories.js
  16. 16
      client/components/basic/ErrorAlert.stories.js
  17. 30
      client/components/basic/Input.stories.js
  18. 16
      client/components/basic/Link.stories.js
  19. 15
      client/components/connectionStatus/ConnectionStatusAlert.js
  20. 34
      client/components/connectionStatus/ConnectionStatusAlert.stories.js
  21. 14
      client/components/contexts/ConnectionStatusContext.js
  22. 38
      client/components/contexts/RouterContext.js
  23. 11
      client/components/contexts/TranslationContext.js
  24. 33
      client/components/header/BurgerMenuButton.js
  25. 2
      client/components/header/Header.js
  26. 7
      client/components/pageNotFound/PageNotFound.js
  27. 18
      client/components/providers/ConnectionStatusProvider.js
  28. 24
      client/components/providers/ConnectionStatusProvider.mock.js
  29. 15
      client/components/providers/MeteorProvider.js
  30. 50
      client/components/providers/RouterProvider.js
  31. 44
      client/components/providers/TranslationProvider.js
  32. 55
      client/components/providers/TranslationProvider.mock.js
  33. 2
      client/components/setupWizard/Epilogue.js
  34. 2
      client/components/setupWizard/Pager.js
  35. 2
      client/components/setupWizard/SideBar.js
  36. 8
      client/components/setupWizard/StateChecker.js
  37. 11
      client/components/setupWizard/StepHeader.css
  38. 4
      client/components/setupWizard/StepHeader.js
  39. 2
      client/components/setupWizard/Steps.js
  40. 13
      client/components/setupWizard/StepsState.js
  41. 2
      client/components/setupWizard/steps/AdminUserInformationStep.js
  42. 2
      client/components/setupWizard/steps/RegisterServerStep.js
  43. 2
      client/components/setupWizard/steps/SettingsBasedStep.js
  44. 3
      client/hooks/useEmbeddedLayout.js
  45. 78
      client/hooks/useFormatters.js
  46. 21
      client/hooks/useTranslation.js
  47. 6
      client/routes.js
  48. 34531
      package-lock.json
  49. 14
      package.json
  50. BIN
      public/images/onepassword-button.png
  51. BIN
      public/images/setup-wizard-intro.png

@ -0,0 +1,13 @@
{
"presets": [
[
"@babel/preset-env",
{
"shippedProposals": true,
"useBuiltIns": "usage",
"corejs": "3"
}
],
"@babel/preset-react"
]
}

@ -0,0 +1,2 @@
import '@storybook/addon-actions/register';
import '@storybook/addon-links/register';

@ -0,0 +1,3 @@
import { configure } from '@storybook/react';
configure(require.context('../client', true, /\.stories\.js$/), module);

@ -0,0 +1,44 @@
import { action } from '@storybook/addon-actions';
import '@rocket.chat/icons/dist/font/RocketChat.minimal.css';
import React from 'react';
import '../app/theme/client/main.css';
import { ConnectionStatusProvider } from '../client/components/providers/ConnectionStatusProvider.mock';
import { TranslationProvider } from '../client/components/providers/TranslationProvider.mock';
export const rocketChatWrapper = (fn) =>
<ConnectionStatusProvider connected status='connected' reconnect={action('reconnect')}>
<TranslationProvider>
<style>{`
body {
background-color: white;
}
.global-font-family {
font-family:
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Helvetica Neue',
'Apple Color Emoji',
'Segoe UI Emoji',
'Segoe UI Symbol',
'Meiryo UI',
Arial,
sans-serif;
}
.color-primary-font-color {
color: #444;
}
`}</style>
<div dangerouslySetInnerHTML={{__html: require('!!raw-loader!../private/public/icons.svg').default}} />
<div className='global-font-family color-primary-font-color'>
{fn()}
</div>
</TranslationProvider>
</ConnectionStatusProvider>;

@ -0,0 +1,22 @@
'use strict';
module.exports = async ({ config, mode }) => {
const cssRule = config.module.rules.find(({ test }) => test.test('index.css'));
cssRule.use[1].options.url = (url, resourcePath) => {
if (/^(\.\/)?images\//.test(url)) {
return false;
}
return true;
};
cssRule.use[2].options.plugins = [
require('postcss-custom-properties')({ preserve: true }),
require('postcss-media-minmax')(),
require('postcss-selector-not')(),
require('postcss-nested')(),
require('autoprefixer')(),
];
return config;
};

@ -4914,23 +4914,6 @@ rc-old select,
top: 19px;
}
.rc-old .one-passsword {
position: absolute;
top: -5px;
right: -8px;
float: right;
width: 40px;
height: 40px;
opacity: 0.6;
background-image: url('/images/onepassword-button.png');
background-repeat: no-repeat;
background-position: center;
background-size: 23px;
}
.rc-old .collapse-switch {
cursor: pointer;
}

@ -1,13 +1,12 @@
import React from 'react';
import { useTranslation } from '../../../hooks/useTranslation';
import { useFormatters } from '../../../hooks/useFormatters';
import { useTranslation } from '../../contexts/TranslationContext';
import { InformationList } from './InformationList';
import { InformationEntry } from './InformationEntry';
import { formatDate } from './formatters';
export function BuildEnvironmentSection({ info }) {
const t = useTranslation();
const { formatDate } = useFormatters();
const build = info && (info.compile || info.build);
return <>

@ -1,6 +1,6 @@
import React from 'react';
import { useTranslation } from '../../../hooks/useTranslation';
import { useTranslation } from '../../contexts/TranslationContext';
import { InformationList } from './InformationList';
import { InformationEntry } from './InformationEntry';

@ -3,13 +3,13 @@ import React, { useEffect, useState } from 'react';
import { call } from '../../../../app/ui-utils/client/lib/callMethod';
import { useViewStatisticsPermission } from '../../../hooks/usePermissions';
import { useTranslation } from '../../../hooks/useTranslation';
import { useReactiveValue } from '../../../hooks/useReactiveValue';
import { Info } from '../../../../app/utils';
import { SideNav } from '../../../../app/ui-utils/client/lib/SideNav';
import { Header } from '../../header/Header';
import { Link } from '../../basic/Link';
import { ErrorAlert } from '../../basic/ErrorAlert';
import { useTranslation } from '../../contexts/TranslationContext';
import { RocketChatSection } from './RocketChatSection';
import { CommitSection } from './CommitSection';
import { RuntimeEnvironmentSection } from './RuntimeEnvironmentSection';
@ -103,7 +103,7 @@ export function InformationPage() {
{canViewStatistics
&& <div className='rc-header__block rc-header__block-action'>
<Button primary type='button' onClick={handleRefreshClick}>
<Icon name='reload' /> {t('Refresh')}
<Icon iconName='reload' /> {t('Refresh')}
</Button>
</div>}
</Header>

@ -1,13 +1,12 @@
import React from 'react';
import { useTranslation } from '../../../hooks/useTranslation';
import { useFormatters } from '../../../hooks/useFormatters';
import { formatDate } from './formatters';
import { useTranslation } from '../../contexts/TranslationContext';
import { InformationList } from './InformationList';
import { InformationEntry } from './InformationEntry';
export function InstancesSection({ instances }) {
const t = useTranslation();
const { formatDate } = useFormatters();
if (!instances || !instances.length) {
return null;

@ -1,15 +1,14 @@
import React from 'react';
import { useTranslation } from '../../../hooks/useTranslation';
import { useTranslation } from '../../contexts/TranslationContext';
import { SkeletonText } from './SkeletonText';
import { useFormatters } from '../../../hooks/useFormatters';
import { formatDate, formatHumanReadableTime } from './formatters';
import { InformationList } from './InformationList';
import { InformationEntry } from './InformationEntry';
export function RocketChatSection({ info, statistics, isLoading }) {
const s = (fn) => (isLoading ? <SkeletonText /> : fn());
const t = useTranslation();
const { formatDate, formatHumanReadableTime } = useFormatters();
const appsEngineVersion = info.marketplaceApiVersion;
@ -25,7 +24,7 @@ export function RocketChatSection({ info, statistics, isLoading }) {
<InformationEntry label={t('DB_Migration')}>{s(() => statistics.migration.version)}</InformationEntry>
<InformationEntry label={t('DB_Migration_Date')}>{s(() => formatDate(statistics.migration.lockedAt))}</InformationEntry>
<InformationEntry label={t('Installed_at')}>{s(() => formatDate(statistics.installedAt))}</InformationEntry>
<InformationEntry label={t('Uptime')}>{s(() => formatHumanReadableTime(statistics.process.uptime))}</InformationEntry>
<InformationEntry label={t('Uptime')}>{s(() => formatHumanReadableTime(statistics.process.uptime, t))}</InformationEntry>
<InformationEntry label={t('Deployment_ID')}>{s(() => statistics.uniqueId)}</InformationEntry>
<InformationEntry label={t('PID')}>{s(() => statistics.process.pid)}</InformationEntry>
<InformationEntry label={t('Running_Instances')}>{s(() => statistics.instanceCount)}</InformationEntry>

@ -1,15 +1,14 @@
import React from 'react';
import { useTranslation } from '../../../hooks/useTranslation';
import { useTranslation } from '../../contexts/TranslationContext';
import { SkeletonText } from './SkeletonText';
import { useFormatters } from '../../../hooks/useFormatters';
import { formatMemorySize, formatHumanReadableTime, formatCPULoad } from './formatters';
import { InformationList } from './InformationList';
import { InformationEntry } from './InformationEntry';
export function RuntimeEnvironmentSection({ statistics, isLoading }) {
const s = (fn) => (isLoading ? <SkeletonText /> : fn());
const t = useTranslation();
const { formatMemorySize, formatHumanReadableTime, formatCPULoad } = useFormatters();
if (!statistics) {
return null;
@ -25,7 +24,7 @@ export function RuntimeEnvironmentSection({ statistics, isLoading }) {
<InformationEntry label={t('Node_version')}>{s(() => statistics.process.nodeVersion)}</InformationEntry>
<InformationEntry label={t('Mongo_version')}>{s(() => statistics.mongoVersion)}</InformationEntry>
<InformationEntry label={t('Mongo_storageEngine')}>{s(() => statistics.mongoStorageEngine)}</InformationEntry>
<InformationEntry label={t('OS_Uptime')}>{s(() => formatHumanReadableTime(statistics.os.uptime))}</InformationEntry>
<InformationEntry label={t('OS_Uptime')}>{s(() => formatHumanReadableTime(statistics.os.uptime, t))}</InformationEntry>
<InformationEntry label={t('OS_Loadavg')}>{s(() => formatCPULoad(statistics.os.loadavg))}</InformationEntry>
<InformationEntry label={t('OS_Totalmem')}>{s(() => formatMemorySize(statistics.os.totalmem))}</InformationEntry>
<InformationEntry label={t('OS_Freemem')}>{s(() => formatMemorySize(statistics.os.freemem))}</InformationEntry>

@ -1,15 +1,14 @@
import React from 'react';
import { useTranslation } from '../../../hooks/useTranslation';
import { useTranslation } from '../../contexts/TranslationContext';
import { SkeletonText } from './SkeletonText';
import { useFormatters } from '../../../hooks/useFormatters';
import { formatMemorySize } from './formatters';
import { InformationList } from './InformationList';
import { InformationEntry } from './InformationEntry';
export function UsageSection({ statistics, isLoading }) {
const s = (fn) => (isLoading ? <SkeletonText /> : fn());
const t = useTranslation();
const { formatMemorySize } = useFormatters();
if (!statistics) {
return null;

@ -0,0 +1,64 @@
import moment from 'moment';
import s from 'underscore.string';
export const formatNumber = (number) => s.numberFormat(number, 2);
export const formatMemorySize = (memorySize) => {
if (typeof memorySize !== 'number') {
return null;
}
const units = ['bytes', 'kB', 'MB', 'GB'];
let order;
for (order = 0; order < units.length - 1; ++order) {
const upperLimit = Math.pow(1024, order + 1);
if (memorySize < upperLimit) {
break;
}
}
const divider = Math.pow(1024, order);
const decimalDigits = order === 0 ? 0 : 2;
return `${ s.numberFormat(memorySize / divider, decimalDigits) } ${ units[order] }`;
};
export const formatDate = (date) => {
if (!date) {
return null;
}
return moment(date).format('LLL');
};
export const formatHumanReadableTime = (time, t) => {
const days = Math.floor(time / 86400);
const hours = Math.floor((time % 86400) / 3600);
const minutes = Math.floor(((time % 86400) % 3600) / 60);
const seconds = Math.floor(((time % 86400) % 3600) % 60);
let out = '';
if (days > 0) {
out += `${ days } ${ t('days') }, `;
}
if (hours > 0) {
out += `${ hours } ${ t('hours') }, `;
}
if (minutes > 0) {
out += `${ minutes } ${ t('minutes') }, `;
}
if (seconds > 0) {
out += `${ seconds } ${ t('seconds') }`;
}
return out;
};
export const formatCPULoad = (load) => {
if (!load) {
return null;
}
const [oneMinute, fiveMinutes, fifteenMinutes] = load;
return `${ formatNumber(oneMinute) }, ${ formatNumber(fiveMinutes) }, ${ formatNumber(fifteenMinutes) }`;
};

@ -0,0 +1,20 @@
import React from 'react';
import { rocketChatWrapper } from '../../../.storybook/helpers';
import { Button } from './Button';
export default {
title: 'basic/Button',
component: Button,
decorators: [
rocketChatWrapper,
],
};
export const _default = () => <Button>Button</Button>;
export const invisible = () => <Button invisible>Button</Button>;
export const primary = () => <Button primary>Button</Button>;
export const secondary = () => <Button secondary>Button</Button>;

@ -0,0 +1,16 @@
import React from 'react';
import { rocketChatWrapper } from '../../../.storybook/helpers';
import { ErrorAlert } from './ErrorAlert';
export default {
title: 'basic/ErrorAlert',
component: ErrorAlert,
decorators: [
rocketChatWrapper,
],
};
export const _default = () => <ErrorAlert>Content</ErrorAlert>;
export const withTitle = () => <ErrorAlert title='Title'>Content</ErrorAlert>;

@ -0,0 +1,30 @@
import React from 'react';
import { rocketChatWrapper } from '../../../.storybook/helpers';
import { Input } from './Input';
export default {
title: 'basic/Input',
component: Input,
decorators: [
rocketChatWrapper,
],
};
export const _default = () => <Input />;
export const withTitle = () => <Input title='Title' />;
export const withError = () => <Input error='Error' />;
export const withIcon = () => <Input icon='key' />;
export const withPlaceholder = () => <Input placeholder='Placeholder' />;
export const focused = () => <Input focused />;
export const ofTypeSelect = () => <Input type='select' options={[
{ label: 'A', value: 'a' },
{ label: 'B', value: 'b' },
{ label: 'C', value: 'c' },
]} />;

@ -0,0 +1,16 @@
import React from 'react';
import { rocketChatWrapper } from '../../../.storybook/helpers';
import { Link } from './Link';
export default {
title: 'basic/Link',
component: Link,
decorators: [
rocketChatWrapper,
],
};
export const _default = () => <Link href='#'>Link</Link>;
export const withTitle = () => <Link external href='#'>Content</Link>;

@ -1,16 +1,17 @@
import { Meteor } from 'meteor/meteor';
import { Icon } from '@rocket.chat/fuselage';
import React, { useEffect, useRef, useState } from 'react';
import { useReactiveValue } from '../../hooks/useReactiveValue';
import { useTranslation } from '../../hooks/useTranslation';
import { Icon } from '../basic/Icon';
import { useConnectionStatus, useReconnect } from '../contexts/ConnectionStatusContext';
import { useTranslation } from '../contexts/TranslationContext';
import './ConnectionStatusAlert.css';
export function ConnectionStatusAlert() {
const {
connected,
retryTime,
status,
} = useReactiveValue(() => ({ ...Meteor.status() }));
} = useConnectionStatus();
const reconnect = useReconnect();
const reconnectionTimerRef = useRef();
const [reconnectCountdown, setReconnectCountdown] = useState(0);
const t = useTranslation();
@ -42,12 +43,12 @@ export function ConnectionStatusAlert() {
const handleRetryClick = (event) => {
event.preventDefault();
Meteor.reconnect();
reconnect();
};
return <div className='ConnectionStatusAlert' role='alert'>
<strong>
<Icon icon='warning' /> {t('meteor_status', { context: status })}
<Icon iconName='warning' /> {t('meteor_status', { context: status })}
</strong>
{status === 'waiting' && <>

@ -0,0 +1,34 @@
import { action } from '@storybook/addon-actions';
import React from 'react';
import { rocketChatWrapper } from '../../../.storybook/helpers';
import { ConnectionStatusAlert } from './ConnectionStatusAlert';
import { ConnectionStatusProvider } from '../providers/ConnectionStatusProvider.mock';
export default {
title: 'connectionStatus/ConnectionStatusAlert',
component: ConnectionStatusAlert,
decorators: [
rocketChatWrapper,
],
};
export const connected = () => <ConnectionStatusProvider connected status='connected' reconnect={action('reconnect')}>
<ConnectionStatusAlert />
</ConnectionStatusProvider>;
export const connecting = () => <ConnectionStatusProvider status='connecting' reconnect={action('reconnect')}>
<ConnectionStatusAlert />
</ConnectionStatusProvider>;
export const failed = () => <ConnectionStatusProvider status='failed' reconnect={action('reconnect')}>
<ConnectionStatusAlert />
</ConnectionStatusProvider>;
export const waiting = () => <ConnectionStatusProvider status='waiting' retryTime={Date.now() + 300000} reconnect={action('reconnect')}>
<ConnectionStatusAlert />
</ConnectionStatusProvider>;
export const offline = () => <ConnectionStatusProvider status='offline' reconnect={action('reconnect')}>
<ConnectionStatusAlert />
</ConnectionStatusProvider>;

@ -0,0 +1,14 @@
import { createContext, useContext } from 'react';
export const ConnectionStatusContext = createContext({
status: {
connected: true,
status: 'connected',
retryCount: 0,
},
reconnect: () => {},
});
export const useConnectionStatus = () => useContext(ConnectionStatusContext).status;
export const useReconnect = () => useContext(ConnectionStatusContext).reconnect;

@ -0,0 +1,38 @@
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
export const RouterContext = createContext({
navigateTo: () => {},
replaceWith: () => {},
getRouteParameter: () => {},
watchRouteParameter: () => {},
getQueryStringParameter: () => {},
watchQueryStringParameter: () => {},
});
export const useRoute = (pathDefinition) => {
const { navigateTo, replaceWith } = useContext(RouterContext);
return useMemo(() => {
const navigate = (...args) => navigateTo(pathDefinition, ...args);
navigate.replacingState = (...args) => replaceWith(pathDefinition, ...args);
return navigate;
}, [navigateTo, replaceWith]);
};
export const useRouteParameter = (name) => {
const { getRouteParameter, watchRouteParameter } = useContext(RouterContext);
const [parameter, setParameter] = useState(getRouteParameter(name));
useEffect(() => watchRouteParameter(name, setParameter), [watchRouteParameter, name]);
return parameter;
};
export const useQueryStringParameter = (name) => {
const { getQueryStringParameter, watchQueryStringParameter } = useContext(RouterContext);
const [parameter, setParameter] = useState(getQueryStringParameter(name));
useEffect(() => watchQueryStringParameter(name, setParameter), [watchQueryStringParameter, name]);
return parameter;
};

@ -0,0 +1,11 @@
import { createContext, useContext } from 'react';
const translate = function(key) {
return key;
};
translate.has = () => true;
export const TranslationContext = createContext(translate);
export const useTranslation = () => useContext(TranslationContext);

@ -1,18 +1,21 @@
import React, { useMemo } from 'react';
import { ChatSubscription } from '../../../app/models/client/models/ChatSubscription';
import { Layout } from '../../../app/ui-utils/client/lib/Layout';
import { useSession } from '../../hooks/useSession';
import { menu } from '../../../app/ui-utils/client/lib/menu';
import { useEmbeddedLayout } from '../../hooks/useEmbeddedLayout';
import { useReactiveValue } from '../../hooks/useReactiveValue';
import { useSession } from '../../hooks/useSession';
import { useUserPreference } from '../../hooks/useUserPreference';
import { menu } from '../../../app/ui-utils/client/lib/menu';
import './BurgerMenuButton.css';
const useBurgerMenuState = () => {
const isMenuOpen = useSession('isMenuOpen');
const isLayoutEmbedded = useReactiveValue(() => Layout.isEmbedded(), []);
const useSidebarState = () => {
const isOpen = useSession('isMenuOpen');
const toggle = () => menu.toggle();
return [isOpen, toggle];
};
const useUnreadMessagesBadge = () => {
const alertUnreadMessages = useUserPreference('unreadAlert') !== false;
const openedRoom = useSession('openedRoom');
const [unreadCount, unreadAlert] = useReactiveValue(() => ChatSubscription
@ -41,30 +44,30 @@ const useBurgerMenuState = () => {
return [unreadCount, unreadAlert];
}, [0, false]), [openedRoom, alertUnreadMessages]);
const unreadBadge = useMemo(() => {
return useMemo(() => {
if (unreadCount > 0) {
return unreadCount > 99 ? '99+' : unreadCount.toString(10);
}
return unreadAlert || '';
}, [unreadCount, unreadAlert]);
return { isMenuOpen, isLayoutEmbedded, unreadBadge };
};
export function BurgerMenuButton() {
const { isMenuOpen, isLayoutEmbedded, unreadBadge } = useBurgerMenuState();
const [isSidebarOpen, toggleSidebarOpen] = useSidebarState();
const isLayoutEmbedded = useEmbeddedLayout();
const unreadMessagesBadge = useUnreadMessagesBadge();
const handleClick = () => {
menu.toggle();
toggleSidebarOpen();
};
return <button
aria-label={isMenuOpen ? 'Close menu' : 'Open menu'}
aria-label={isSidebarOpen ? 'Close menu' : 'Open menu'}
className={[
'rc-old',
'burger',
!!isMenuOpen && 'menu-opened',
!!isSidebarOpen && 'menu-opened',
].filter(Boolean).join(' ')}
type='button'
onClick={handleClick}
@ -72,9 +75,9 @@ export function BurgerMenuButton() {
<i className='burger__line' aria-hidden='true' />
<i className='burger__line' aria-hidden='true' />
<i className='burger__line' aria-hidden='true' />
{!isLayoutEmbedded && unreadBadge
{!isLayoutEmbedded && unreadMessagesBadge
&& <div className='unread-burger-alert color-error-contrast background-error-color'>
{unreadBadge}
{unreadMessagesBadge}
</div>}
</button>;
}

@ -1,7 +1,7 @@
import React from 'react';
import { useTranslation } from '../contexts/TranslationContext';
import { BurgerMenuButton } from './BurgerMenuButton';
import { useTranslation } from '../../hooks/useTranslation';
export function Header({
children,

@ -1,22 +1,23 @@
import { Button, ButtonGroup } from '@rocket.chat/fuselage';
import { FlowRouter } from 'meteor/kadira:flow-router';
import React from 'react';
import { useTranslation } from '../../hooks/useTranslation';
import { useTranslation } from '../contexts/TranslationContext';
import { useWipeInitialPageLoading } from '../../hooks/useWipeInitialPageLoading';
import { ConnectionStatusAlert } from '../connectionStatus/ConnectionStatusAlert';
import { useRoute } from '../contexts/RouterContext';
export function PageNotFound() {
useWipeInitialPageLoading();
const t = useTranslation();
const goToHome = useRoute('home');
const handleGoToPreviousPageClick = () => {
window.history.back();
};
const handleGoHomeClick = () => {
FlowRouter.go('home');
goToHome();
};
return <>

@ -0,0 +1,18 @@
import { Meteor } from 'meteor/meteor';
import React, { useMemo } from 'react';
import { ConnectionStatusContext } from '../contexts/ConnectionStatusContext';
import { useReactiveValue } from '../../hooks/useReactiveValue';
export function ConnectionStatusProvider({ children }) {
const status = useReactiveValue(() => ({ ...Meteor.status() }));
const contextValue = useMemo(() => ({
status,
reconnect: Meteor.reconnect,
}), [status]);
return <ConnectionStatusContext.Provider value={contextValue}>
{children}
</ConnectionStatusContext.Provider>;
}

@ -0,0 +1,24 @@
import React from 'react';
import { ConnectionStatusContext } from '../contexts/ConnectionStatusContext';
export function ConnectionStatusProvider({
children,
connected = false,
status,
retryTime,
retryCount = 3,
reconnect = () => {},
}) {
return <ConnectionStatusContext.Provider value={{
status: {
connected,
retryCount,
retryTime,
status,
},
reconnect,
}}>
{children}
</ConnectionStatusContext.Provider>;
}

@ -0,0 +1,15 @@
import React from 'react';
import { RouterProvider } from './RouterProvider';
import { ConnectionStatusProvider } from './ConnectionStatusProvider';
import { TranslationProvider } from './TranslationProvider';
export function MeteorProvider({ children }) {
return <ConnectionStatusProvider>
<RouterProvider>
<TranslationProvider>
{children}
</TranslationProvider>
</RouterProvider>
</ConnectionStatusProvider>;
}

@ -0,0 +1,50 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Tracker } from 'meteor/tracker';
import React from 'react';
import { RouterContext } from '../contexts/RouterContext';
const navigateTo = (pathDefinition, parameters, queryStringParameters) => {
FlowRouter.go(pathDefinition, parameters, queryStringParameters);
};
const replaceWith = (pathDefinition, parameters, queryStringParameters) => {
FlowRouter.withReplaceState(() => {
FlowRouter.go(pathDefinition, parameters, queryStringParameters);
});
};
const getRouteParameter = (name) => Tracker.nonreactive(() => FlowRouter.getParam(name));
const watchRouteParameter = (name, subscriber) => {
const computation = Tracker.autorun(() => {
subscriber(FlowRouter.getParam(name));
});
return () => computation.stop();
};
const getQueryStringParameter = (name) => Tracker.nonreactive(() => FlowRouter.getQueryParam(name));
const watchQueryStringParameter = (name, subscriber) => {
const computation = Tracker.autorun(() => {
subscriber(FlowRouter.getQueryParam(name));
});
return () => computation.stop();
};
const router = {
navigateTo,
replaceWith,
getRouteParameter,
watchRouteParameter,
getQueryStringParameter,
watchQueryStringParameter,
};
export function RouterProvider({ children }) {
return <RouterContext.Provider value={router}>
{children}
</RouterContext.Provider>;
}

@ -0,0 +1,44 @@
import React, { useMemo } from 'react';
import { TAPi18n, TAPi18next } from 'meteor/rocketchat:tap-i18n';
import { TranslationContext } from '../contexts/TranslationContext';
import { useReactiveValue } from '../../hooks/useReactiveValue';
const createContextValue = (language) => {
const translate = (key, ...replaces) => {
if (typeof replaces[0] === 'object') {
const [options, lang_tag = language] = replaces;
return TAPi18next.t(key, {
ns: 'project',
lng: lang_tag,
...options,
});
}
if (replaces.length === 0) {
return TAPi18next.t(key, { ns: 'project', lng: language });
}
return TAPi18next.t(key, {
postProcess: 'sprintf',
sprintf: replaces,
ns: 'project',
lng: language,
});
};
const has = (key, { lng = language, ...options } = {}) => TAPi18next.exists(key, { ...options, lng });
translate.has = has;
return translate;
};
export function TranslationProvider({ children }) {
const language = useReactiveValue(() => TAPi18n.getLanguage());
const contextValue = useMemo(() => createContextValue(language), [language]);
return <TranslationContext.Provider value={contextValue}>
{children}
</TranslationContext.Provider>;
}

@ -0,0 +1,55 @@
import React, { useEffect, useState } from 'react';
import i18next from 'i18next';
import { TranslationContext } from '../contexts/TranslationContext';
export function TranslationProvider({ children }) {
const [contextValue, setContextValue] = useState();
useEffect(() => {
const translate = (key, ...replaces) => {
if (typeof replaces[0] === 'object') {
const [options] = replaces;
return i18next.t(key, options);
}
if (replaces.length === 0) {
return i18next.t(key);
}
return i18next.t(key, {
postProcess: 'sprintf',
sprintf: replaces,
});
};
const has = (key) => i18next.exists(key);
translate.has = has;
const initializeI18next = async () => {
await i18next.init({
fallbackLng: 'en',
defaultNS: 'project',
resources: {
en: {
project: require('../../../packages/rocketchat-i18n/i18n/en.i18n.json'),
},
},
interpolation: {
prefix: '__',
suffix: '__',
},
});
setContextValue(() => translate);
};
initializeI18next();
}, []);
if (!contextValue) {
return <>{children}</>;
}
return <TranslationContext.Provider value={contextValue}>
{children}
</TranslationContext.Provider>;
}

@ -1,7 +1,7 @@
import React from 'react';
import { Button } from '@rocket.chat/fuselage';
import { useTranslation } from '../../hooks/useTranslation';
import { useTranslation } from '../contexts/TranslationContext';
import { useSetting } from '../../hooks/useSetting';
import { setSetting } from './functions';
import './Epilogue.css';

@ -1,7 +1,7 @@
import { Button, ButtonGroup } from '@rocket.chat/fuselage';
import React from 'react';
import { useTranslation } from '../../hooks/useTranslation';
import { useTranslation } from '../contexts/TranslationContext';
export function Pager({ disabled, onBackClick, isContinueEnabled = true }) {
const t = useTranslation();

@ -1,6 +1,6 @@
import React from 'react';
import { useTranslation } from '../../hooks/useTranslation';
import { useTranslation } from '../contexts/TranslationContext';
import { useSetupWizardStepsState } from './StepsState';
import './SideBar.css';

@ -1,4 +1,3 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import React, { useEffect, useState } from 'react';
import { hasRole } from '../../../app/authorization';
@ -6,6 +5,7 @@ import { Users } from '../../../app/models';
import { useSetting } from '../../hooks/useSetting';
import { useUserId } from '../../hooks/useUserId';
import { useReactiveValue } from '../../hooks/useReactiveValue';
import { useRoute } from '../contexts/RouterContext';
export function StateChecker({ children }) {
const setupWizardState = useSetting('Show_Setup_Wizard');
@ -14,6 +14,8 @@ export function StateChecker({ children }) {
const [renderAllowed, allowRender] = useState(false);
const goToHome = useRoute('home');
useEffect(() => {
if (!setupWizardState) {
return;
@ -30,9 +32,7 @@ export function StateChecker({ children }) {
const mustRedirect = isComplete || noUserLoggedInAndIsNotPending || userIsLoggedInButIsNotAdmin;
if (mustRedirect) {
FlowRouter.withReplaceState(() => {
FlowRouter.go('home');
});
goToHome.replacingState();
return;
}

@ -15,14 +15,3 @@
line-height: 1.125rem;
}
.SetupWizard__StepHeader-title {
letter-spacing: 0.05rem;
color: var(--color-dark-90);
font-size: 1.25rem;
font-weight: 500;
line-height: 1.75rem;
}

@ -1,7 +1,7 @@
import { Headline } from '@rocket.chat/fuselage';
import React from 'react';
import { useTranslation } from '../../hooks/useTranslation';
import { useTranslation } from '../contexts/TranslationContext';
import './StepHeader.css';
export function StepHeader({ number, title }) {
@ -9,6 +9,6 @@ export function StepHeader({ number, title }) {
return <header className='SetupWizard__StepHeader'>
<p className='SetupWizard__StepHeader-runningHead'>{t('Step')} {number}</p>
<Headline as='h2' className='SetupWizard__StepHeader-title'>{title}</Headline>
<Headline as='h2'>{title}</Headline>
</header>;
}

@ -3,7 +3,7 @@ import React from 'react';
import { AdminUserInformationStep } from './steps/AdminUserInformationStep';
import { SettingsBasedStep } from './steps/SettingsBasedStep';
import { RegisterServerStep } from './steps/RegisterServerStep';
import { useTranslation } from '../../hooks/useTranslation';
import { useTranslation } from '../contexts/TranslationContext';
import { Epilogue } from './Epilogue';
import { SideBar } from './SideBar';
import { useSetupWizardStepsState, finalStep } from './StepsState';

@ -1,7 +1,7 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useUserId } from '../../hooks/useUserId';
import { useRouteParameter, useRoute } from '../contexts/RouterContext';
const Context = createContext();
@ -10,10 +10,11 @@ export const useSetupWizardStepsState = () => useContext(Context);
export const finalStep = 'final';
const useStepRouting = () => {
const param = useRouteParameter('step');
const goToSetupWizard = useRoute('setup-wizard');
const userId = useUserId();
const [currentStep, setCurrentStep] = useState(() => {
const param = FlowRouter.getParam('step');
if (param === finalStep) {
return finalStep;
}
@ -33,10 +34,8 @@ const useStepRouting = () => {
setCurrentStep(2);
}
FlowRouter.withReplaceState(() => {
FlowRouter.go('setup-wizard', { step: String(currentStep) });
});
}, [userId, currentStep]);
goToSetupWizard.replacingState({ step: String(currentStep) });
}, [goToSetupWizard, userId, currentStep]);
return [currentStep, setCurrentStep];
};

@ -8,7 +8,7 @@ import { handleError } from '../../../../app/utils/client';
import { callbacks } from '../../../../app/callbacks/client';
import { useFocus } from '../../../hooks/useFocus';
import { useSetting } from '../../../hooks/useSetting';
import { useTranslation } from '../../../hooks/useTranslation';
import { useTranslation } from '../../contexts/TranslationContext';
import { useSetupWizardStepsState } from '../StepsState';
import { Step } from '../Step';
import { StepHeader } from '../StepHeader';

@ -3,7 +3,7 @@ import React, { useRef, useState } from 'react';
import { call } from '../../../../app/ui-utils/client';
import { handleError } from '../../../../app/utils/client';
import { useTranslation } from '../../../hooks/useTranslation';
import { useTranslation } from '../../contexts/TranslationContext';
import { Icon } from '../../basic/Icon';
import { Pager } from '../Pager';
import { useSetupWizardParameters } from '../ParametersProvider';

@ -4,7 +4,7 @@ import React, { useEffect, useReducer, useState } from 'react';
import { handleError } from '../../../../app/utils/client';
import { useFocus } from '../../../hooks/useFocus';
import { useTranslation } from '../../../hooks/useTranslation';
import { useTranslation } from '../../contexts/TranslationContext';
import { useReactiveValue } from '../../../hooks/useReactiveValue';
import { Pager } from '../Pager';
import { useSetupWizardParameters } from '../ParametersProvider';

@ -0,0 +1,3 @@
import { useQueryStringParameter } from '../components/contexts/RouterContext';
export const useEmbeddedLayout = () => useQueryStringParameter('layout') === 'embedded';

@ -1,78 +0,0 @@
import moment from 'moment';
import s from 'underscore.string';
import { useTranslation } from './useTranslation';
export const useFormatters = () => {
const formatNumber = (number) => s.numberFormat(number, 2);
const formatMemorySize = (memorySize) => {
if (typeof memorySize !== 'number') {
return null;
}
const units = ['bytes', 'kB', 'MB', 'GB'];
let order;
for (order = 0; order < units.length - 1; ++order) {
const upperLimit = Math.pow(1024, order + 1);
if (memorySize < upperLimit) {
break;
}
}
const divider = Math.pow(1024, order);
const decimalDigits = order === 0 ? 0 : 2;
return `${ s.numberFormat(memorySize / divider, decimalDigits) } ${ units[order] }`;
};
const formatDate = (date) => {
if (!date) {
return null;
}
return moment(date).format('LLL');
};
const t = useTranslation();
const formatHumanReadableTime = (time) => {
const days = Math.floor(time / 86400);
const hours = Math.floor((time % 86400) / 3600);
const minutes = Math.floor(((time % 86400) % 3600) / 60);
const seconds = Math.floor(((time % 86400) % 3600) % 60);
let out = '';
if (days > 0) {
out += `${ days } ${ t('days') }, `;
}
if (hours > 0) {
out += `${ hours } ${ t('hours') }, `;
}
if (minutes > 0) {
out += `${ minutes } ${ t('minutes') }, `;
}
if (seconds > 0) {
out += `${ seconds } ${ t('seconds') }`;
}
return out;
};
const formatCPULoad = (load) => {
if (!load) {
return null;
}
const [oneMinute, fiveMinutes, fifteenMinutes] = load;
return `${ formatNumber(oneMinute) }, ${ formatNumber(fiveMinutes) }, ${ formatNumber(fifteenMinutes) }`;
};
return {
formatNumber,
formatMemorySize,
formatDate,
formatHumanReadableTime,
formatCPULoad,
};
};

@ -1,21 +0,0 @@
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { Tracker } from 'meteor/tracker';
import { useReactiveValue } from './useReactiveValue';
const translator = (key, ...replaces) => Tracker.nonreactive(() => {
if (typeof replaces[0] === 'object') {
return TAPi18n.__(key, ...replaces);
}
return TAPi18n.__(key, {
postProcess: 'sprintf',
sprintf: replaces,
});
});
export const useTranslation = () => {
useReactiveValue(() => TAPi18n.getLanguage());
return translator;
};

@ -42,6 +42,7 @@ const createTemplateForComponent = async (
) => {
const React = await import('react');
const ReactDOM = await import('react-dom');
const { MeteorProvider } = await import('./components/providers/MeteorProvider');
const name = component.displayName || component.name;
@ -57,7 +58,10 @@ const createTemplateForComponent = async (
Template.instance().container = Template.instance().firstNode;
}
ReactDOM.render(React.createElement(component, props), Template.instance().firstNode);
ReactDOM.render(
React.createElement(MeteorProvider, {
children: React.createElement(component, props),
}), Template.instance().firstNode);
});
});

34531
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -79,7 +79,9 @@
"translation-fix-order": "node .scripts/fix-i18n.js",
"version": "node .scripts/version.js",
"set-version": "node .scripts/set-version.js",
"release": "meteor npm run set-version --silent"
"release": "meteor npm run set-version --silent",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},
"license": "MIT",
"repository": {
@ -91,12 +93,20 @@
"email": "support@rocket.chat"
},
"devDependencies": {
"@babel/core": "^7.6.2",
"@babel/preset-env": "^7.6.2",
"@babel/preset-react": "^7.0.0",
"@octokit/rest": "^16.1.0",
"@rocket.chat/eslint-config": "^0.3.0",
"@rocket.chat/livechat": "^1.1.6",
"@storybook/addon-actions": "^5.2.1",
"@storybook/addon-links": "^5.2.1",
"@storybook/addons": "^5.2.1",
"@storybook/react": "^5.2.1",
"acorn": "^6.0.7",
"autoprefixer": "^9.6.1",
"babel-eslint": "^10.0.1",
"babel-loader": "^8.0.6",
"babel-mocha-es6-compiler": "^0.1.0",
"babel-plugin-array-includes": "^2.0.3",
"chai": "^3.5.0",
@ -107,6 +117,8 @@
"eslint-plugin-react": "^7.14.3",
"fast-glob": "^2.2.6",
"husky": "^1.2.0",
"i18next": "^17.0.17",
"less-loader": "^5.0.0",
"mocha": "^5.2.0",
"mock-require": "^3.0.2",
"mongo-unit": "^1.4.4",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Loading…
Cancel
Save