fix: Login page language switcher (#30106)

Co-authored-by: Tasso Evangelista <2263066+tassoevan@users.noreply.github.com>
pull/30212/head^2
Henrique Guimarães Ribeiro 2 years ago committed by GitHub
parent e7fbc00965
commit b8f3d5014f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      .changeset/wise-onions-trade.md
  2. 130
      .yarn/patches/@storybook-react-docgen-typescript-plugin-npm-1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0-b31cc57c40.patch
  3. 4
      apps/meteor/app/theme/client/imports/general/base_old.css
  4. 4
      apps/meteor/app/utils/lib/i18n.ts
  5. 7
      apps/meteor/client/providers/TranslationProvider.tsx
  6. 17
      apps/meteor/client/providers/UserProvider/UserProvider.tsx
  7. 355
      apps/meteor/client/sidebar/header/actions/hooks/mockAppRoot.tsx
  8. 1
      apps/meteor/client/stories/contexts/TranslationContextMock.tsx
  9. 6
      apps/meteor/package.json
  10. 2
      apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
  11. 4
      apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json
  12. 4
      apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json
  13. 4
      apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json
  14. 1
      apps/meteor/server/lib/i18n.ts
  15. 3
      package.json
  16. 2
      packages/i18n/babel.config.cjs
  17. 11
      packages/i18n/package.json
  18. 9
      packages/i18n/tsconfig.json
  19. 6
      packages/livechat/package.json
  20. 5
      packages/livechat/src/components/Calls/CallNotification.tsx
  21. 5
      packages/livechat/src/components/Calls/JoinCallButton.tsx
  22. 6
      packages/mock-providers/package.json
  23. 193
      packages/mock-providers/src/MockedAppRootBuilder.tsx
  24. 2
      packages/ui-client/package.json
  25. 9
      packages/ui-client/src/components/PasswordVerifier/PasswordVerifiers.spec.tsx
  26. 2
      packages/ui-contexts/src/TranslationContext.ts
  27. 7
      packages/web-ui-registration/.babelrc.json
  28. 47
      packages/web-ui-registration/.storybook/logo.svg
  29. 3
      packages/web-ui-registration/.storybook/logo.svg.d.ts
  30. 12
      packages/web-ui-registration/.storybook/main.ts
  31. 36
      packages/web-ui-registration/.storybook/preview.tsx
  32. 48
      packages/web-ui-registration/package.json
  33. 6
      packages/web-ui-registration/src/ResetPassword/ResetPassword.stories.tsx
  34. 42
      packages/web-ui-registration/src/components/LoginSwitchLanguageFooter.stories.tsx
  35. 94
      packages/web-ui-registration/src/components/LoginSwitchLanguageFooter.tsx
  36. 8
      packages/web-ui-registration/tsconfig.build.json
  37. 7
      packages/web-ui-registration/tsconfig.json
  38. 2080
      yarn.lock

@ -0,0 +1,11 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/i18n": patch
"@rocket.chat/livechat": patch
"@rocket.chat/mock-providers": patch
"@rocket.chat/ui-client": patch
"@rocket.chat/ui-contexts": patch
"@rocket.chat/web-ui-registration": patch
---
Fixed the login page language switcher, now the component has a new look, is reactive and the language selection becomes concrete upon login in. Also changed the default language of the login page to be the browser language.

@ -0,0 +1,130 @@
diff --git a/dist/generateDocgenCodeBlock.js b/dist/generateDocgenCodeBlock.js
index 0993ac13e4b2aae6d24cf408d6a585b4ddeb7337..1405896291288eb1322d6c42144afd3b4fbd1abf 100644
--- a/dist/generateDocgenCodeBlock.js
+++ b/dist/generateDocgenCodeBlock.js
@@ -34,7 +34,7 @@ function insertTsIgnoreBeforeStatement(statement) {
* ```
*/
function setDisplayName(d) {
- return insertTsIgnoreBeforeStatement(typescript_1.default.createExpressionStatement(typescript_1.default.createBinary(typescript_1.default.createPropertyAccess(typescript_1.default.createIdentifier(d.displayName), typescript_1.default.createIdentifier("displayName")), typescript_1.default.SyntaxKind.EqualsToken, typescript_1.default.createLiteral(d.displayName))));
+ return insertTsIgnoreBeforeStatement(typescript_1.default.factory.createExpressionStatement(typescript_1.default.factory.createBinaryExpression(typescript_1.default.factory.createPropertyAccessExpression(typescript_1.default.factory.createIdentifier(d.displayName), typescript_1.default.factory.createIdentifier("displayName")), typescript_1.default.SyntaxKind.EqualsToken, typescript_1.default.factory.createStringLiteral(d.displayName))));
}
/**
* Set a component prop description.
@@ -65,7 +65,7 @@ function createPropDefinition(propName, prop, options) {
*
* @param defaultValue Default prop value or null if not set.
*/
- const setDefaultValue = (defaultValue) => typescript_1.default.createPropertyAssignment(typescript_1.default.createLiteral("defaultValue"),
+ const setDefaultValue = (defaultValue) => typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createStringLiteral("defaultValue"),
// Use a more extensive check on defaultValue. Sometimes the parser
// returns an empty object.
defaultValue !== null &&
@@ -75,12 +75,19 @@ function createPropDefinition(propName, prop, options) {
(typeof defaultValue.value === "string" ||
typeof defaultValue.value === "number" ||
typeof defaultValue.value === "boolean")
- ? typescript_1.default.createObjectLiteral([
- typescript_1.default.createPropertyAssignment(typescript_1.default.createIdentifier("value"), typescript_1.default.createLiteral(defaultValue.value)),
+ ? typescript_1.default.factory.createObjectLiteralExpression([
+ typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createIdentifier("value"), typeof defaultValue.value === "string"
+ ? typescript_1.default.factory.createStringLiteral(defaultValue.value)
+ : // eslint-disable-next-line no-nested-ternary
+ typeof defaultValue.value === "number"
+ ? typescript_1.default.factory.createNumericLiteral(defaultValue.value)
+ : defaultValue.value
+ ? typescript_1.default.factory.createTrue()
+ : typescript_1.default.factory.createFalse()),
])
- : typescript_1.default.createNull());
+ : typescript_1.default.factory.createNull());
/** Set a property with a string value */
- const setStringLiteralField = (fieldName, fieldValue) => typescript_1.default.createPropertyAssignment(typescript_1.default.createLiteral(fieldName), typescript_1.default.createLiteral(fieldValue));
+ const setStringLiteralField = (fieldName, fieldValue) => typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createStringLiteral(fieldName), typescript_1.default.factory.createStringLiteral(fieldValue));
/**
* ```
* SimpleComponent.__docgenInfo.props.someProp.description = "Prop description.";
@@ -101,7 +108,7 @@ function createPropDefinition(propName, prop, options) {
* ```
* @param required Whether prop is required or not.
*/
- const setRequired = (required) => typescript_1.default.createPropertyAssignment(typescript_1.default.createLiteral("required"), required ? typescript_1.default.createTrue() : typescript_1.default.createFalse());
+ const setRequired = (required) => typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createStringLiteral("required"), required ? typescript_1.default.factory.createTrue() : typescript_1.default.factory.createFalse());
/**
* ```
* SimpleComponent.__docgenInfo.props.someProp.type = {
@@ -113,7 +120,7 @@ function createPropDefinition(propName, prop, options) {
*/
const setValue = (typeValue) => Array.isArray(typeValue) &&
typeValue.every((value) => typeof value.value === "string")
- ? typescript_1.default.createPropertyAssignment(typescript_1.default.createLiteral("value"), typescript_1.default.createArrayLiteral(typeValue.map((value) => typescript_1.default.createObjectLiteral([
+ ? typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createStringLiteral("value"), typescript_1.default.factory.createArrayLiteralExpression(typeValue.map((value) => typescript_1.default.factory.createObjectLiteralExpression([
setStringLiteralField("value", value.value),
]))))
: undefined;
@@ -130,9 +137,9 @@ function createPropDefinition(propName, prop, options) {
if (valueField) {
objectFields.push(valueField);
}
- return typescript_1.default.createPropertyAssignment(typescript_1.default.createLiteral(options.typePropName), typescript_1.default.createObjectLiteral(objectFields));
+ return typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createStringLiteral(options.typePropName), typescript_1.default.factory.createObjectLiteralExpression(objectFields));
};
- return typescript_1.default.createPropertyAssignment(typescript_1.default.createLiteral(propName), typescript_1.default.createObjectLiteral([
+ return typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createStringLiteral(propName), typescript_1.default.factory.createObjectLiteralExpression([
setDefaultValue(prop.defaultValue),
setDescription(prop.description),
setName(prop.name),
@@ -158,10 +165,10 @@ function createPropDefinition(propName, prop, options) {
* @param relativeFilename Relative file path of the component source file.
*/
function insertDocgenIntoGlobalCollection(d, docgenCollectionName, relativeFilename) {
- return insertTsIgnoreBeforeStatement(typescript_1.default.createIf(typescript_1.default.createBinary(typescript_1.default.createTypeOf(typescript_1.default.createIdentifier(docgenCollectionName)), typescript_1.default.SyntaxKind.ExclamationEqualsEqualsToken, typescript_1.default.createLiteral("undefined")), insertTsIgnoreBeforeStatement(typescript_1.default.createStatement(typescript_1.default.createBinary(typescript_1.default.createElementAccess(typescript_1.default.createIdentifier(docgenCollectionName), typescript_1.default.createLiteral(`${relativeFilename}#${d.displayName}`)), typescript_1.default.SyntaxKind.EqualsToken, typescript_1.default.createObjectLiteral([
- typescript_1.default.createPropertyAssignment(typescript_1.default.createIdentifier("docgenInfo"), typescript_1.default.createPropertyAccess(typescript_1.default.createIdentifier(d.displayName), typescript_1.default.createIdentifier("__docgenInfo"))),
- typescript_1.default.createPropertyAssignment(typescript_1.default.createIdentifier("name"), typescript_1.default.createLiteral(d.displayName)),
- typescript_1.default.createPropertyAssignment(typescript_1.default.createIdentifier("path"), typescript_1.default.createLiteral(`${relativeFilename}#${d.displayName}`)),
+ return insertTsIgnoreBeforeStatement(typescript_1.default.factory.createIfStatement(typescript_1.default.factory.createBinaryExpression(typescript_1.default.factory.createTypeOfExpression(typescript_1.default.factory.createIdentifier(docgenCollectionName)), typescript_1.default.SyntaxKind.ExclamationEqualsEqualsToken, typescript_1.default.factory.createStringLiteral("undefined")), insertTsIgnoreBeforeStatement(typescript_1.default.factory.createExpressionStatement(typescript_1.default.factory.createBinaryExpression(typescript_1.default.factory.createElementAccessExpression(typescript_1.default.factory.createIdentifier(docgenCollectionName), typescript_1.default.factory.createStringLiteral(`${relativeFilename}#${d.displayName}`)), typescript_1.default.SyntaxKind.EqualsToken, typescript_1.default.factory.createObjectLiteralExpression([
+ typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createIdentifier("docgenInfo"), typescript_1.default.factory.createPropertyAccessExpression(typescript_1.default.factory.createIdentifier(d.displayName), typescript_1.default.factory.createIdentifier("__docgenInfo"))),
+ typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createIdentifier("name"), typescript_1.default.factory.createStringLiteral(d.displayName)),
+ typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createIdentifier("path"), typescript_1.default.factory.createStringLiteral(`${relativeFilename}#${d.displayName}`)),
]))))));
}
/**
@@ -180,15 +187,15 @@ function insertDocgenIntoGlobalCollection(d, docgenCollectionName, relativeFilen
* @param options Generator options.
*/
function setComponentDocGen(d, options) {
- return insertTsIgnoreBeforeStatement(typescript_1.default.createStatement(typescript_1.default.createBinary(
+ return insertTsIgnoreBeforeStatement(typescript_1.default.factory.createExpressionStatement(typescript_1.default.factory.createBinaryExpression(
// SimpleComponent.__docgenInfo
- typescript_1.default.createPropertyAccess(typescript_1.default.createIdentifier(d.displayName), typescript_1.default.createIdentifier("__docgenInfo")), typescript_1.default.SyntaxKind.EqualsToken, typescript_1.default.createObjectLiteral([
+ typescript_1.default.factory.createPropertyAccessExpression(typescript_1.default.factory.createIdentifier(d.displayName), typescript_1.default.factory.createIdentifier("__docgenInfo")), typescript_1.default.SyntaxKind.EqualsToken, typescript_1.default.factory.createObjectLiteralExpression([
// SimpleComponent.__docgenInfo.description
- typescript_1.default.createPropertyAssignment(typescript_1.default.createLiteral("description"), typescript_1.default.createLiteral(d.description)),
+ typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createStringLiteral("description"), typescript_1.default.factory.createStringLiteral(d.description)),
// SimpleComponent.__docgenInfo.displayName
- typescript_1.default.createPropertyAssignment(typescript_1.default.createLiteral("displayName"), typescript_1.default.createLiteral(d.displayName)),
+ typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createStringLiteral("displayName"), typescript_1.default.factory.createStringLiteral(d.displayName)),
// SimpleComponent.__docgenInfo.props
- typescript_1.default.createPropertyAssignment(typescript_1.default.createLiteral("props"), typescript_1.default.createObjectLiteral(Object.entries(d.props).map(([propName, prop]) => createPropDefinition(propName, prop, options)))),
+ typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createStringLiteral("props"), typescript_1.default.factory.createObjectLiteralExpression(Object.entries(d.props).map(([propName, prop]) => createPropDefinition(propName, prop, options)))),
]))));
}
function generateDocgenCodeBlock(options) {
@@ -196,7 +203,7 @@ function generateDocgenCodeBlock(options) {
const relativeFilename = path_1.default
.relative("./", path_1.default.resolve("./", options.filename))
.replace(/\\/g, "/");
- const wrapInTryStatement = (statements) => typescript_1.default.createTry(typescript_1.default.createBlock(statements, true), typescript_1.default.createCatchClause(typescript_1.default.createVariableDeclaration(typescript_1.default.createIdentifier("__react_docgen_typescript_loader_error")), typescript_1.default.createBlock([])), undefined);
+ const wrapInTryStatement = (statements) => typescript_1.default.factory.createTryStatement(typescript_1.default.factory.createBlock(statements, true), typescript_1.default.factory.createCatchClause(typescript_1.default.factory.createVariableDeclaration(typescript_1.default.factory.createIdentifier("__react_docgen_typescript_loader_error")), typescript_1.default.factory.createBlock([])), undefined);
const codeBlocks = options.componentDocs.map((d) => wrapInTryStatement([
options.setDisplayName ? setDisplayName(d) : null,
setComponentDocGen(d, options),
@@ -208,7 +215,7 @@ function generateDocgenCodeBlock(options) {
const printer = typescript_1.default.createPrinter({ newLine: typescript_1.default.NewLineKind.LineFeed });
const printNode = (sourceNode) => printer.printNode(typescript_1.default.EmitHint.Unspecified, sourceNode, sourceFile);
// Concat original source code with code from generated code blocks.
- const result = codeBlocks.reduce((acc, node) => `${acc}\n${printNode(node)}`,
+ const result = codeBlocks.reduce((acc, node) => `${acc}\n${printNode(node)}`,
// Use original source text rather than using printNode on the parsed form
// to prevent issue where literals are stripped within components.
// Ref: https://github.com/strothj/react-docgen-typescript-loader/issues/7

@ -988,10 +988,6 @@
font-size: 12px;
font-weight: 300;
}
& div.switch-language {
margin-top: 20px;
}
}
& .share {

@ -5,9 +5,7 @@ import { isObject } from '../../../lib/utils/isObject';
export const i18n = i18next.use(sprintf);
export const addSprinfToI18n = function (t: (typeof i18n)['t']): typeof t & {
(key: string, ...replaces: any): string;
} {
export const addSprinfToI18n = function (t: (typeof i18n)['t']) {
return function (key: string, ...replaces: any): string {
if (replaces[0] === undefined || isObject(replaces[0])) {
return t(key, ...replaces);

@ -122,6 +122,8 @@ const useI18next = (lng: string): typeof i18next => {
},
react: {
useSuspense: true,
bindI18n: 'languageChanged loaded',
bindI18nStore: 'added removed',
},
interpolation: {
escapeValue: false,
@ -174,7 +176,7 @@ type TranslationProviderProps = {
const useAutoLanguage = () => {
const serverLanguage = useSetting<string>('Language');
const browserLanguage = filterLanguage(window.navigator.userLanguage ?? window.navigator.language);
const defaultUserLanguage = serverLanguage || browserLanguage || 'en';
const defaultUserLanguage = browserLanguage || serverLanguage || 'en';
// if the language is supported, if not remove the region
const suggestedLanguage = languages.includes(defaultUserLanguage) ? defaultUserLanguage : defaultUserLanguage.split('-').shift() ?? 'en';
@ -209,11 +211,13 @@ const TranslationProvider = ({ children }: TranslationProviderProps): ReactEleme
{
en: 'Default',
name: i18nextInstance.t('Default'),
ogName: i18nextInstance.t('Default'),
key: '',
},
...[...new Set([...i18nextInstance.languages, ...languages])].map((key) => ({
en: key,
name: getLanguageName(key, language),
ogName: getLanguageName(key, key),
key,
})),
],
@ -270,6 +274,7 @@ const TranslationProviderInner = ({
availableLanguages: {
en: string;
name: string;
ogName: string;
key: string;
}[];
}): ReactElement => {

@ -1,7 +1,7 @@
import type { IRoom, ISubscription, IUser } from '@rocket.chat/core-typings';
import { useLocalStorage } from '@rocket.chat/fuselage-hooks';
import { UserContext, useEndpoint, useSetting } from '@rocket.chat/ui-contexts';
import type { LoginService, SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import { UserContext, useEndpoint, useSetting } from '@rocket.chat/ui-contexts';
import { Meteor } from 'meteor/meteor';
import type { ContextType, ReactElement, ReactNode } from 'react';
import React, { useEffect, useMemo } from 'react';
@ -66,7 +66,8 @@ const UserProvider = ({ children }: UserProviderProps): ReactElement => {
const userId = useReactiveValue(getUserId);
const user = useReactiveValue(getUser);
const [language, setLanguage] = useLocalStorage('userLanguage', user?.language ?? 'en');
const [userLanguage, setUserLanguage] = useLocalStorage('userLanguage', '');
const [preferedLanguage, setPreferedLanguage] = useLocalStorage('preferedLanguage', '');
const setUserPreferences = useEndpoint('POST', '/v1/users.setPreferences');
@ -167,10 +168,16 @@ const UserProvider = ({ children }: UserProviderProps): ReactElement => {
);
useEffect(() => {
if (user?.language !== undefined && user.language !== language) {
setLanguage(user.language);
if (!!userId && preferedLanguage !== userLanguage) {
setUserPreferences({ data: { language: preferedLanguage } });
setUserLanguage(preferedLanguage);
}
if (user?.language !== undefined && user.language !== userLanguage) {
setUserLanguage(user.language);
setPreferedLanguage(user.language);
}
}, [user?.language, language, setLanguage]);
}, [preferedLanguage, setPreferedLanguage, setUserLanguage, user?.language, userLanguage, userId, setUserPreferences]);
const { data: license } = useIsEnterprise({ enabled: !!userId });

@ -1,355 +0,0 @@
import type { Serialized } from '@rocket.chat/core-typings';
import type { Method, OperationParams, OperationResult, PathPattern, UrlParams } from '@rocket.chat/rest-typings';
import type { ServerMethodName, ServerMethodParameters, ServerMethodReturn, TranslationKey } from '@rocket.chat/ui-contexts';
import {
AuthorizationContext,
ConnectionStatusContext,
RouterContext,
ServerContext,
SettingsContext,
TranslationContext,
UserContext,
ActionManagerContext,
ModalContext,
} from '@rocket.chat/ui-contexts';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { WrapperComponent } from '@testing-library/react-hooks';
import type { ObjectId } from 'mongodb';
import type { ContextType, ReactNode } from 'react';
import React from 'react';
class MockedAppRootBuilder {
private wrappers: Array<(children: ReactNode) => ReactNode> = [];
private connectionStatus: ContextType<typeof ConnectionStatusContext> = {
connected: true,
status: 'connected',
retryTime: undefined,
reconnect: () => undefined,
};
private server: ContextType<typeof ServerContext> = {
absoluteUrl: (path: string) => `http://localhost:3000/${path}`,
callEndpoint: <TMethod extends Method, TPathPattern extends PathPattern>(_args: {
method: TMethod;
pathPattern: TPathPattern;
keys: UrlParams<TPathPattern>;
params: OperationParams<TMethod, TPathPattern>;
}): Promise<Serialized<OperationResult<TMethod, TPathPattern>>> => {
throw new Error('not implemented');
},
getSingleStream: () => () => () => undefined,
getStream: () => () => () => undefined,
uploadToEndpoint: () => Promise.reject(new Error('not implemented')),
callMethod: () => Promise.reject(new Error('not implemented')),
info: undefined,
};
private router: ContextType<typeof RouterContext> = {
buildRoutePath: () => '/',
defineRoutes: () => () => undefined,
getLocationPathname: () => '/',
getLocationSearch: () => '',
getRouteName: () => undefined,
getRouteParameters: () => ({}),
getRoutes: () => [],
getSearchParameters: () => ({}),
navigate: () => undefined,
subscribeToRouteChange: () => () => undefined,
subscribeToRoutesChange: () => () => undefined,
};
private settings: ContextType<typeof SettingsContext> = {
hasPrivateAccess: true,
isLoading: false,
querySetting: (_id: string) => [() => () => undefined, () => undefined],
querySettings: () => [() => () => undefined, () => []],
dispatch: async () => undefined,
};
private translation: ContextType<typeof TranslationContext> = {
language: 'en',
languages: [
{
en: 'English',
key: 'en',
name: 'English',
},
],
loadLanguage: () => Promise.resolve(),
translate: Object.assign((key: string) => key, {
has: (_key: string): _key is TranslationKey => true,
}),
};
private user: ContextType<typeof UserContext> = {
loginWithPassword: () => Promise.reject(new Error('not implemented')),
logout: () => Promise.reject(new Error('not implemented')),
loginWithService: () => () => Promise.reject(new Error('not implemented')),
loginWithToken: () => Promise.reject(new Error('not implemented')),
queryAllServices: () => [() => () => undefined, () => []],
queryPreference: () => [() => () => undefined, () => undefined],
queryRoom: () => [() => () => undefined, () => undefined],
querySubscription: () => [() => () => undefined, () => undefined],
querySubscriptions: () => [() => () => undefined, () => []],
user: null,
userId: null,
};
private modal: ContextType<typeof ModalContext> = {
currentModal: null,
modal: {
setModal: () => undefined,
},
};
private authorization: ContextType<typeof AuthorizationContext> = {
queryPermission: () => [() => () => undefined, () => false],
queryAtLeastOnePermission: () => [() => () => undefined, () => false],
queryAllPermissions: () => [() => () => undefined, () => false],
queryRole: () => [() => () => undefined, () => false],
roleStore: {
roles: {},
emit: () => undefined,
on: () => () => undefined,
off: () => undefined,
events: (): Array<'change'> => ['change'],
has: () => false,
once: () => () => undefined,
},
};
wrap(wrapper: (children: ReactNode) => ReactNode): this {
this.wrappers.push(wrapper);
return this;
}
withEndpoint<TMethod extends Method, TPathPattern extends PathPattern>(
method: TMethod,
pathPattern: TPathPattern,
response: (
params: OperationParams<TMethod, TPathPattern>,
) => Serialized<OperationResult<TMethod, TPathPattern>> | Promise<Serialized<OperationResult<TMethod, TPathPattern>>>,
): this {
const innerFn = this.server.callEndpoint;
const outerFn = <TMethod extends Method, TPathPattern extends PathPattern>(args: {
method: TMethod;
pathPattern: TPathPattern;
keys: UrlParams<TPathPattern>;
params: OperationParams<TMethod, TPathPattern>;
}): Promise<Serialized<OperationResult<TMethod, TPathPattern>>> => {
if (args.method === String(method) && args.pathPattern === String(pathPattern)) {
return Promise.resolve(response(args.params)) as Promise<Serialized<OperationResult<TMethod, TPathPattern>>>;
}
return innerFn(args);
};
this.server.callEndpoint = outerFn;
return this;
}
withMethod<TMethodName extends ServerMethodName>(methodName: TMethodName, response: () => ServerMethodReturn<TMethodName>): this {
const innerFn = this.server.callMethod;
const outerFn = <TMethodName extends ServerMethodName>(
innerMethodName: TMethodName,
...innerArgs: ServerMethodParameters<TMethodName>
): Promise<ServerMethodReturn<TMethodName>> => {
if (innerMethodName === String(methodName)) {
return Promise.resolve(response()) as Promise<ServerMethodReturn<TMethodName>>;
}
if (!innerFn) {
throw new Error('not implemented');
}
return innerFn(innerMethodName, ...innerArgs);
};
this.server.callMethod = outerFn;
return this;
}
withPermission(permission: string): this {
const innerFn = this.authorization.queryPermission;
const outerFn = (
innerPermission: string | ObjectId,
innerScope?: string | ObjectId | undefined,
innerScopedRoles?: string[] | undefined,
): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => boolean] => {
if (innerPermission === permission) {
return [() => () => undefined, () => true];
}
return innerFn(innerPermission, innerScope, innerScopedRoles);
};
this.authorization.queryPermission = outerFn;
const innerFn2 = this.authorization.queryAtLeastOnePermission;
const outerFn2 = (
innerPermissions: Array<string | ObjectId>,
innerScope?: string | ObjectId | undefined,
innerScopedRoles?: string[] | undefined,
): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => boolean] => {
if (innerPermissions.includes(permission)) {
return [() => () => undefined, () => true];
}
return innerFn2(innerPermissions, innerScope, innerScopedRoles);
};
this.authorization.queryAtLeastOnePermission = outerFn2;
const innerFn3 = this.authorization.queryAllPermissions;
const outerFn3 = (
innerPermissions: Array<string | ObjectId>,
innerScope?: string | ObjectId | undefined,
innerScopedRoles?: string[] | undefined,
): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => boolean] => {
if (innerPermissions.includes(permission)) {
return [() => () => undefined, () => true];
}
return innerFn3(innerPermissions, innerScope, innerScopedRoles);
};
this.authorization.queryAllPermissions = outerFn3;
return this;
}
withJohnDoe(): this {
this.user.userId = 'john.doe';
this.user.user = {
_id: 'john.doe',
username: 'john.doe',
name: 'John Doe',
createdAt: new Date(),
active: true,
_updatedAt: new Date(),
roles: ['admin'],
type: 'user',
};
return this;
}
withAnonymous(): this {
this.user.userId = null;
this.user.user = null;
return this;
}
withRole(role: string): this {
if (!this.user.user) {
throw new Error('user is not defined');
}
this.user.user.roles.push(role);
const innerFn = this.authorization.queryRole;
const outerFn = (
innerRole: string | ObjectId,
innerScope?: string | undefined,
innerIgnoreSubscriptions?: boolean | undefined,
): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => boolean] => {
if (innerRole === role) {
return [() => () => undefined, () => true];
}
return innerFn(innerRole, innerScope, innerIgnoreSubscriptions);
};
this.authorization.queryRole = outerFn;
return this;
}
build(): WrapperComponent<{ children: ReactNode }> {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const { connectionStatus, server, router, settings, translation, user, modal, authorization, wrappers } = this;
return function MockedAppRoot({ children }) {
return (
<QueryClientProvider client={queryClient}>
<ConnectionStatusContext.Provider value={connectionStatus}>
<ServerContext.Provider value={server}>
<RouterContext.Provider value={router}>
<SettingsContext.Provider value={settings}>
<TranslationContext.Provider value={translation}>
{/* <SessionProvider>
<TooltipProvider>
<ToastMessagesProvider>
<LayoutProvider>
<AvatarUrlProvider>
<CustomSoundProvider> */}
<UserContext.Provider value={user}>
{/* <DeviceProvider>*/}
<ModalContext.Provider value={modal}>
<AuthorizationContext.Provider value={authorization}>
{/* <EmojiPickerProvider>
<OmnichannelRoomIconProvider>
<UserPresenceProvider>*/}
<ActionManagerContext.Provider
value={{
triggerAction: () => Promise.reject(new Error('not implemented')),
generateTriggerId: () => '',
getUserInteractionPayloadByViewId: () => undefined,
handlePayloadUserInteraction: () => undefined,
off: () => undefined,
on: () => undefined,
triggerActionButtonAction: () => Promise.reject(new Error('not implemented')),
triggerBlockAction: () => Promise.reject(new Error('not implemented')),
triggerCancel: () => Promise.reject(new Error('not implemented')),
triggerSubmitView: () => Promise.reject(new Error('not implemented')),
}}
>
{/* <VideoConfProvider>
<CallProvider>
<OmnichannelProvider> */}
{wrappers.reduce((children, wrapper) => wrapper(children), children)}
{/* </OmnichannelProvider>
</CallProvider>
</VideoConfProvider>*/}
</ActionManagerContext.Provider>
{/* </UserPresenceProvider>
</OmnichannelRoomIconProvider>
</EmojiPickerProvider>*/}
</AuthorizationContext.Provider>
</ModalContext.Provider>
{/* </DeviceProvider>*/}
</UserContext.Provider>
{/* </CustomSoundProvider>
</AvatarUrlProvider>
</LayoutProvider>
</ToastMessagesProvider>
</TooltipProvider>
</SessionProvider> */}
</TranslationContext.Provider>
</SettingsContext.Provider>
</RouterContext.Provider>
</ServerContext.Provider>
</ConnectionStatusContext.Provider>
</QueryClientProvider>
);
};
}
}
export const mockAppRoot = () => new MockedAppRootBuilder();

@ -51,6 +51,7 @@ const TranslationContextMock = ({ children }: TranslationContextMockProps): Reac
{
name: 'English',
en: 'English',
ogName: 'English',
key: 'en',
},
],

@ -180,7 +180,7 @@
"eslint-plugin-testing-library": "~5.11.0",
"eslint-plugin-you-dont-need-lodash-underscore": "~6.12.0",
"fast-glob": "^3.2.12",
"i18next": "^20.6.1",
"i18next": "~23.4.5",
"jest": "~29.6.1",
"jest-axe": "^8.0.0",
"jsdom-global": "^3.0.2",
@ -398,8 +398,8 @@
"react-aria": "~3.23.1",
"react-dom": "~17.0.2",
"react-error-boundary": "^3.1.4",
"react-hook-form": "^7.30.0",
"react-i18next": "^11.16.7",
"react-hook-form": "~7.45.4",
"react-i18next": "~13.2.1",
"react-keyed-flatten-children": "^1.3.0",
"react-virtuoso": "^1.11.1",
"redis": "^4.0.6",

@ -5799,7 +5799,7 @@
"registration.component.login": "Login",
"registration.component.login.userNotFound": "User not found",
"registration.component.login.incorrectPassword": "Incorrect password",
"registration.component.switchLanguage": "Switch to <1>en</1>",
"registration.component.switchLanguage": "Change to <1>{{name}}</1>",
"registration.component.resetPassword": "Reset password",
"registration.component.form.emailOrUsername": "Email or username",
"registration.component.form.username": "Username",

@ -5545,7 +5545,7 @@
"registration.component.login": "Kirjaudu",
"registration.component.login.userNotFound": "Käyttäjää ei löydy",
"registration.component.login.incorrectPassword": "Väärä salasana",
"registration.component.switchLanguage": "Vaihda kieleksi <1>en</1>",
"registration.component.switchLanguage": "Vaihda kieleksi <1>{{name}}</1>",
"registration.component.resetPassword": "Nollaa salasana",
"registration.component.form.emailOrUsername": "Sähköpostiosoite tai käyttäjätunnus",
"registration.component.form.username": "Käyttäjätunnus",
@ -5760,4 +5760,4 @@
"Uninstall_grandfathered_app": "Poistetaanko {{appName}}?",
"App_will_lose_grandfathered_status": "**Tämä {{context}}sovellus menettää aikaisemmin käytetössä olleen sovelluksen tilansa.** \n \nYhteisöversion työtiloissa voi olla käytössä enintään {{limit}} {{context}} sovellusta. aikaisemmin Aikaisemmin käytössä olleet sovellukset lasketaan mukaan rajoitukseen, mutta rajoitusta ei sovelleta niihin.",
"Theme_Appearence": "Teeman ulkoasu"
}
}

@ -5339,7 +5339,7 @@
"registration.component.login": "Bejelentkezés",
"registration.component.login.userNotFound": "A felhasználó nem található",
"registration.component.login.incorrectPassword": "Hibás jelszó",
"registration.component.switchLanguage": "Átváltás <1>angolra</1>",
"registration.component.switchLanguage": "Átváltás <1>{{name}}</1>",
"registration.component.resetPassword": "Jelszó visszaállítása",
"registration.component.form.emailOrUsername": "E-mail-cím vagy felhasználónév",
"registration.component.form.username": "Felhasználónév",
@ -5438,4 +5438,4 @@
"Join_your_team": "Csatlakozás csapathoz",
"Create_an_account": "Fiók létrehozása",
"RegisterWorkspace_Features_Marketplace_Title": "Piactér"
}
}

@ -5550,7 +5550,7 @@
"registration.component.login": "Logga in",
"registration.component.login.userNotFound": "Användare inte hittad",
"registration.component.login.incorrectPassword": "Felaktigt lösenord",
"registration.component.switchLanguage": "Växla till <1>en</1>",
"registration.component.switchLanguage": "Växla till <1>{{name}}</1>",
"registration.component.resetPassword": "Återställ lösenord",
"registration.component.form.emailOrUsername": "E-postadress eller lösenord",
"registration.component.form.username": "Användarnamn",
@ -5765,4 +5765,4 @@
"Uninstall_grandfathered_app": "Avinstallera {{appName}}?",
"App_will_lose_grandfathered_status": "**Denna {{context}}-app kommer att förlora sin status som gammal app.** \n \nArbetsytorna i Community Edition kan ha upp till {{limit}} __kontext__-appar aktiverade. Gamla appar inkluderas i gränsen, men gränsen tillämpas inte på dem.",
"Theme_Appearence": "Utseende för tema"
}
}

@ -1,5 +1,6 @@
import type { RocketchatI18nKeys } from '@rocket.chat/i18n';
import i18nDict from '@rocket.chat/i18n';
import type { TOptions } from 'i18next';
import { i18n } from '../../app/utils/lib/i18n';

@ -62,6 +62,7 @@
"resolutions": {
"minimist": "1.2.6",
"adm-zip": "0.5.9",
"preact@10.15.1": "patch:preact@npm:10.15.1#.yarn/patches/preact-npm-10.15.1-bd458de913.patch"
"preact@10.15.1": "patch:preact@npm:10.15.1#.yarn/patches/preact-npm-10.15.1-bd458de913.patch",
"@storybook/react-docgen-typescript-plugin@1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0": "patch:@storybook/react-docgen-typescript-plugin@npm%3A1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0#./.yarn/patches/@storybook-react-docgen-typescript-plugin-npm-1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0-b31cc57c40.patch"
}
}

@ -1,3 +1,3 @@
module.exports = {
presets: ['@babel/preset-modules'],
presets: [['@babel/preset-env', { bugfixes: true }]],
};

@ -3,12 +3,8 @@
"version": "0.0.1",
"private": true,
"devDependencies": {
"@babel/core": "~7.22.9",
"@babel/preset-env": "~7.22.9",
"@babel/preset-typescript": "~7.22.5",
"@types/babel__core": "~7.20.1",
"@types/babel__preset-env": "~7.9.2",
"@types/jest": "~29.5.3",
"@babel/core": "~7.22.10",
"@babel/preset-env": "~7.22.10",
"babel-jest": "^29.5.0",
"eslint": "~8.45.0",
"jest": "~29.6.1",
@ -20,8 +16,7 @@
"build": "node ./src/index.mjs",
"lint": "eslint --ext .mjs,.js,.jsx,.ts,.tsx .",
"lint:fix": "eslint --ext .mjs,.js,.jsx,.ts,.tsx . --fix",
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput"
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
},
"main": "./dist/index.js",
"module": "./dist/index.mjs",

@ -1,9 +0,0 @@
{
"extends": "../../tsconfig.base.client.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["./src/**/*"],
"exclude": ["./dist/**/*"]
}

@ -103,15 +103,15 @@
"date-fns": "^2.15.0",
"emoji-mart": "^3.0.1",
"history": "~5.3.0",
"i18next": "^21.8.10",
"i18next": "~23.4.5",
"markdown-it": "^11.0.1",
"mem": "^6.1.1",
"mitt": "^2.1.0",
"preact": "10.15.1",
"preact-router": "^3.2.1",
"query-string": "^7.1.3",
"react-hook-form": "^7.45.0",
"react-i18next": "^11.16.9",
"react-hook-form": "~7.45.4",
"react-i18next": "~13.2.1",
"whatwg-fetch": "^3.6.2"
},
"browserslist": [

@ -1,5 +1,5 @@
import { type TFunction } from 'i18next';
import { useState } from 'preact/compat';
import type { TFunction } from 'react-i18next';
import { withTranslation } from 'react-i18next';
import { Livechat } from '../../api';
@ -9,8 +9,7 @@ import { isMobileDevice } from '../../helpers/isMobileDevice';
import PhoneAccept from '../../icons/phone.svg';
import PhoneDecline from '../../icons/phoneOff.svg';
import constants from '../../lib/constants';
import type { Dispatch } from '../../store';
import store from '../../store';
import store, { type Dispatch } from '../../store';
import { Avatar } from '../Avatar';
import { Button } from '../Button';
import { CallStatus } from './CallStatus';

@ -1,4 +1,4 @@
import type { TFunction } from 'react-i18next';
import { type TFunction } from 'i18next';
import { withTranslation } from 'react-i18next';
import { getConnectionBaseUrl } from '../../helpers/baseUrl';
@ -7,8 +7,7 @@ import VideoIcon from '../../icons/video.svg';
import constants from '../../lib/constants';
import store from '../../store';
import { Button } from '../Button';
import type { CallStatus } from './CallStatus';
import { isCallOngoing } from './CallStatus';
import { type CallStatus, isCallOngoing } from './CallStatus';
import styles from './styles.scss';
type JoinCallButtonProps = {

@ -2,8 +2,14 @@
"name": "@rocket.chat/mock-providers",
"version": "0.0.1",
"private": true,
"dependencies": {
"@rocket.chat/i18n": "workspace:~",
"i18next": "~23.4.5",
"react-i18next": "~13.2.1"
},
"devDependencies": {
"@rocket.chat/ui-contexts": "workspace:*",
"@storybook/react": "~6.5.16",
"@tanstack/react-query": "^4.16.1",
"@types/jest": "~29.5.3",
"eslint": "~8.45.0",

@ -1,7 +1,11 @@
import type { ISetting, Serialized, SettingValue } from '@rocket.chat/core-typings';
import type { Method, OperationParams, OperationResult, PathPattern, UrlParams } from '@rocket.chat/rest-typings';
import type { ServerMethodName, ServerMethodParameters, ServerMethodReturn, TranslationKey } from '@rocket.chat/ui-contexts';
import { type ISetting, type Serialized, type SettingValue } from '@rocket.chat/core-typings';
import languages from '@rocket.chat/i18n/dist/languages';
import { type Method, type OperationParams, type OperationResult, type PathPattern, type UrlParams } from '@rocket.chat/rest-typings';
import {
type ServerMethodName,
type ServerMethodParameters,
type ServerMethodReturn,
type TranslationKey,
AuthorizationContext,
ConnectionStatusContext,
RouterContext,
@ -12,11 +16,13 @@ import {
ActionManagerContext,
ModalContext,
} from '@rocket.chat/ui-contexts';
import { type DecoratorFn } from '@storybook/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { WrapperComponent } from '@testing-library/react-hooks';
import type { ObjectId } from 'mongodb';
import type { ContextType, ReactNode } from 'react';
import React from 'react';
import { type WrapperComponent } from '@testing-library/react-hooks';
import { createInstance } from 'i18next';
import { type ObjectId } from 'mongodb';
import React, { type ContextType, type ReactNode, useEffect, useReducer } from 'react';
import { I18nextProvider, initReactI18next } from 'react-i18next';
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
@ -71,21 +77,6 @@ export class MockedAppRootBuilder {
dispatch: async () => undefined,
};
private translation: ContextType<typeof TranslationContext> = {
language: 'en',
languages: [
{
en: 'English',
key: 'en',
name: 'English',
},
],
loadLanguage: () => Promise.resolve(),
translate: Object.assign((key: string) => key, {
has: (_key: string | number | symbol): _key is TranslationKey => true,
}),
};
private user: ContextType<typeof UserContext> = {
loginWithPassword: () => Promise.reject(new Error('not implemented')),
logout: () => Promise.reject(new Error('not implemented')),
@ -322,6 +313,40 @@ export class MockedAppRootBuilder {
return this;
}
private i18n = createInstance(
{
// debug: true,
lng: 'en',
fallbackLng: 'en',
ns: ['core'],
nsSeparator: '.',
partialBundledLanguages: true,
defaultNS: 'core',
interpolation: {
escapeValue: false,
},
initImmediate: false,
},
() => undefined,
).use(initReactI18next);
withTranslations(lng: string, ns: string, resources: Record<string, string>): this {
const addResources = () => {
this.i18n.addResources(lng, ns, resources);
for (const [key, value] of Object.entries(resources)) {
this.i18n.addResource(lng, ns, key, value);
}
};
if (this.i18n.isInitialized) {
addResources();
return this;
}
this.i18n.on('initialized', addResources);
return this;
}
build(): WrapperComponent<{ children: ReactNode }> {
const queryClient = new QueryClient({
defaultOptions: {
@ -330,65 +355,116 @@ export class MockedAppRootBuilder {
},
});
const { connectionStatus, server, router, settings, translation, user, modal, authorization, wrappers } = this;
const { connectionStatus, server, router, settings, user, modal, i18n, authorization, wrappers } = this;
const reduceTranslation = (translation?: ContextType<typeof TranslationContext>): ContextType<typeof TranslationContext> => {
return {
...translation,
language: i18n.isInitialized ? i18n.language : 'en',
languages: [
{
en: 'Default',
name: i18n.isInitialized ? i18n.t('Default') : 'Default',
ogName: i18n.isInitialized ? i18n.t('Default') : 'Default',
key: '',
},
...(i18n.isInitialized
? [...new Set([...i18n.languages, ...languages])].map((key) => ({
en: key,
name: new Intl.DisplayNames([key], { type: 'language' }).of(key) ?? key,
ogName: new Intl.DisplayNames([key], { type: 'language' }).of(key) ?? key,
key,
}))
: []),
],
loadLanguage: async (language) => {
if (!i18n.isInitialized) {
return;
}
await i18n.changeLanguage(language);
},
translate: Object.assign(
(key: TranslationKey, options?: unknown) => (i18n.isInitialized ? i18n.t(key, options as { lng?: string }) : ''),
{
has: (key: string, options?: { lng?: string }): key is TranslationKey =>
!!key && i18n.isInitialized && i18n.exists(key, options),
},
),
};
};
return function MockedAppRoot({ children }) {
const [translation, updateTranslation] = useReducer(reduceTranslation, undefined, () => reduceTranslation());
useEffect(() => {
i18n.on('initialized', updateTranslation);
i18n.on('languageChanged', updateTranslation);
return () => {
i18n.off('initialized', updateTranslation);
i18n.off('languageChanged', updateTranslation);
};
}, []);
return (
<QueryClientProvider client={queryClient}>
<ConnectionStatusContext.Provider value={connectionStatus}>
<ServerContext.Provider value={server}>
<RouterContext.Provider value={router}>
<SettingsContext.Provider value={settings}>
<TranslationContext.Provider value={translation}>
{/* <SessionProvider>
<I18nextProvider i18n={i18n}>
<TranslationContext.Provider value={translation}>
{/* <SessionProvider>
<TooltipProvider>
<ToastMessagesProvider>
<LayoutProvider>
<AvatarUrlProvider>
<CustomSoundProvider> */}
<UserContext.Provider value={user}>
{/* <DeviceProvider>*/}
<ModalContext.Provider value={modal}>
<AuthorizationContext.Provider value={authorization}>
{/* <EmojiPickerProvider>
<UserContext.Provider value={user}>
{/* <DeviceProvider>*/}
<ModalContext.Provider value={modal}>
<AuthorizationContext.Provider value={authorization}>
{/* <EmojiPickerProvider>
<OmnichannelRoomIconProvider>
<UserPresenceProvider>*/}
<ActionManagerContext.Provider
value={{
triggerAction: () => Promise.reject(new Error('not implemented')),
generateTriggerId: () => '',
getUserInteractionPayloadByViewId: () => undefined,
handlePayloadUserInteraction: () => undefined,
off: () => undefined,
on: () => undefined,
triggerActionButtonAction: () => Promise.reject(new Error('not implemented')),
triggerBlockAction: () => Promise.reject(new Error('not implemented')),
triggerCancel: () => Promise.reject(new Error('not implemented')),
triggerSubmitView: () => Promise.reject(new Error('not implemented')),
}}
>
{/* <VideoConfProvider>
<ActionManagerContext.Provider
value={{
triggerAction: () => Promise.reject(new Error('not implemented')),
generateTriggerId: () => '',
getUserInteractionPayloadByViewId: () => undefined,
handlePayloadUserInteraction: () => undefined,
off: () => undefined,
on: () => undefined,
triggerActionButtonAction: () => Promise.reject(new Error('not implemented')),
triggerBlockAction: () => Promise.reject(new Error('not implemented')),
triggerCancel: () => Promise.reject(new Error('not implemented')),
triggerSubmitView: () => Promise.reject(new Error('not implemented')),
}}
>
{/* <VideoConfProvider>
<CallProvider>
<OmnichannelProvider> */}
{wrappers.reduce((children, wrapper) => wrapper(children), children)}
{/* </OmnichannelProvider>
{wrappers.reduce((children, wrapper) => wrapper(children), children)}
{/* </OmnichannelProvider>
</CallProvider>
</VideoConfProvider>*/}
</ActionManagerContext.Provider>
{/* </UserPresenceProvider>
</ActionManagerContext.Provider>
{/* </UserPresenceProvider>
</OmnichannelRoomIconProvider>
</EmojiPickerProvider>*/}
</AuthorizationContext.Provider>
</ModalContext.Provider>
{/* </DeviceProvider>*/}
</UserContext.Provider>
{/* </CustomSoundProvider>
</AuthorizationContext.Provider>
</ModalContext.Provider>
{/* </DeviceProvider>*/}
</UserContext.Provider>
{/* </CustomSoundProvider>
</AvatarUrlProvider>
</LayoutProvider>
</ToastMessagesProvider>
</TooltipProvider>
</SessionProvider> */}
</TranslationContext.Provider>
</TranslationContext.Provider>
</I18nextProvider>
</SettingsContext.Provider>
</RouterContext.Provider>
</ServerContext.Provider>
@ -397,4 +473,11 @@ export class MockedAppRootBuilder {
);
};
}
buildStoryDecorator(): DecoratorFn {
const WrapperComponent = this.build();
// eslint-disable-next-line react/display-name, react/no-multi-comp
return (fn) => <WrapperComponent>{fn()}</WrapperComponent>;
}
}

@ -37,7 +37,7 @@
"jest": "~29.6.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hook-form": "^7.30.0",
"react-hook-form": "~7.45.4",
"ts-jest": "~29.0.5",
"typescript": "~5.2.2"
},

@ -16,15 +16,6 @@ type Response = {
][];
};
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (str: string) => str,
i18n: {
changeLanguage: () => new Promise(() => undefined),
},
}),
}));
afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();

@ -7,6 +7,7 @@ export { keys };
export type TranslationLanguage = {
en: string;
name: string;
ogName: string;
key: string;
};
@ -33,6 +34,7 @@ export const TranslationContext = createContext<TranslationContextValue>({
{
name: 'Default',
en: 'Default',
ogName: 'Default',
key: '',
},
],

@ -0,0 +1,7 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript"
]
}

@ -0,0 +1,47 @@
<svg width="768" height="221" viewBox="0 0 768 221" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M687.178 68.3765H699.452V85.2808H710.808V96.4023H699.452V126.607H710.193V137.528C708.453 137.935 706.305 138.141 703.849 138.141C692.7 138.141 687.174 132.326 687.174 120.692V68.3765H687.178Z"
fill="#F5455C" />
<path
d="M663.995 92.7303V85.2809H676.268V136.708H663.995V129.259C662.255 134.259 656.017 137.83 647.526 137.83C640.265 137.83 634.123 135.28 629.111 130.179C624.2 124.973 621.744 118.65 621.744 110.994C621.744 103.339 624.2 97.0159 629.111 91.9107C634.123 86.7048 640.26 84.1545 647.526 84.1545C656.013 84.1591 662.255 87.7305 663.995 92.7303ZM660.309 122.221C663.38 119.263 664.913 115.486 664.913 110.999C664.913 106.512 663.38 102.735 660.309 99.7722C657.344 96.8145 653.557 95.2806 649.16 95.2806C644.763 95.2806 641.183 96.8099 638.213 99.7722C635.349 102.73 633.917 106.507 633.917 110.999C633.917 115.491 635.349 119.263 638.213 122.221C641.178 125.179 644.758 126.713 649.16 126.713C653.562 126.713 657.344 125.179 660.309 122.221Z"
fill="#F5455C" />
<path
d="M209.724 136.708V85.2808H221.896V92.9317C224.352 87.5243 229.979 84.2598 237.24 84.2598C238.672 84.2598 239.903 84.3605 240.922 84.5665V96.4023C239.388 96.0955 237.649 95.8941 235.808 95.8941C227.317 95.8941 221.896 101.201 221.896 109.671V136.713H209.724V136.708Z"
fill="#F5455C" />
<path
d="M243.992 110.995C243.992 103.339 246.755 96.8099 252.176 91.604C257.597 86.3981 264.248 83.7471 272.124 83.7471C280 83.7471 286.651 86.3981 292.072 91.604C297.493 96.8099 300.256 103.339 300.256 110.995C300.256 118.645 297.493 125.179 292.072 130.38C286.651 135.586 280 138.237 272.124 138.237C264.248 138.237 257.597 135.586 252.176 130.38C246.755 125.179 243.992 118.645 243.992 110.995ZM283.479 122.524C286.55 119.36 288.083 115.582 288.083 110.995C288.083 106.402 286.55 102.625 283.479 99.5662C280.409 96.4024 276.627 94.8731 272.124 94.8731C267.52 94.8731 263.738 96.4024 260.667 99.5662C257.702 102.629 256.165 106.402 256.165 110.995C256.165 115.587 257.698 119.36 260.667 122.524C263.738 125.587 267.52 127.116 272.124 127.116C276.627 127.116 280.409 125.587 283.479 122.524Z"
fill="#F5455C" />
<path
d="M351.3 88.852V101.095C346.903 96.9104 341.583 94.7676 335.446 94.7676C330.641 94.7676 326.647 96.2969 323.48 99.36C320.309 102.423 318.776 106.301 318.776 110.889C318.776 115.481 320.309 119.359 323.48 122.418C326.652 125.481 330.641 127.01 335.446 127.01C341.689 127.01 347.004 124.868 351.3 120.683V132.926C347.004 136.397 341.482 138.132 334.73 138.132C326.753 138.132 320.102 135.582 314.682 130.376C309.261 125.17 306.599 118.741 306.599 110.884C306.599 103.027 309.256 96.5991 314.682 91.3932C320.102 86.1873 326.753 83.637 334.73 83.637C341.377 83.6461 346.903 85.3814 351.3 88.852Z"
fill="#F5455C" />
<path
d="M360.407 136.708V68.3765H372.58V107.217L389.971 85.2762H403.883L384.344 109.868L405.517 136.703H391.197L372.58 112.927V136.703H360.407V136.708Z"
fill="#F5455C" />
<path
d="M405.824 110.994C405.824 102.931 408.28 96.4023 413.292 91.5031C418.304 86.5033 424.749 84.0537 432.524 84.0537C439.992 84.0537 446.028 86.5033 450.631 91.5031C455.336 96.4023 457.691 102.524 457.691 109.767C457.691 111.297 457.59 112.83 457.484 114.154H417.997C418.405 122.317 424.441 127.523 433.548 127.523C441.63 127.523 447.767 125.586 452.063 121.603V133.031C447.051 136.502 440.708 138.237 433.038 138.237C424.955 138.237 418.309 135.788 413.297 130.994C408.284 126.095 405.829 119.566 405.829 111.503V110.994H405.824ZM445.206 105.486C445.206 102.528 443.98 99.8729 441.525 97.7301C439.069 95.5873 436.104 94.4655 432.524 94.4655C428.737 94.4655 425.465 95.5873 422.706 97.7301C419.943 99.8729 418.511 102.423 418.309 105.486H445.206Z"
fill="#F5455C" />
<path
d="M466.944 68.3765H479.218V85.2808H490.573V96.4023H479.218V126.607H489.958V137.528C488.218 137.935 486.07 138.141 483.615 138.141C472.466 138.141 466.939 132.326 466.939 120.692V68.3765H466.944Z"
fill="#F5455C" />
<path
d="M502.713 123.215C498.642 123.215 495.342 126.493 495.342 130.536C495.342 134.579 498.642 137.857 502.713 137.857C506.785 137.857 510.085 134.579 510.085 130.536C510.085 126.493 506.785 123.215 502.713 123.215Z"
fill="#F5455C" />
<path
d="M558.22 88.852V101.095C553.823 96.9104 548.503 94.7676 542.366 94.7676C537.561 94.7676 533.567 96.2969 530.4 99.36C527.229 102.423 525.696 106.301 525.696 110.889C525.696 115.481 527.229 119.359 530.4 122.418C533.572 125.481 537.561 127.01 542.366 127.01C548.604 127.01 553.924 124.868 558.22 120.683V132.926C553.924 136.397 548.402 138.132 541.65 138.132C533.673 138.132 527.022 135.582 521.601 130.376C516.181 125.17 513.519 118.741 513.519 110.884C513.519 103.027 516.176 96.5991 521.601 91.3932C527.022 86.1873 533.673 83.637 541.65 83.637C548.297 83.6461 553.823 85.3814 558.22 88.852Z"
fill="#F5455C" />
<path
d="M567.326 136.708V68.3765H579.499V92.4234C581.853 87.5243 587.476 84.2597 594.535 84.2597C606.4 84.2597 613.359 92.1167 613.359 104.873V136.708H601.186V106.603C601.186 99.5615 597.399 95.1752 590.854 95.1752C584.203 95.1752 579.499 100.074 579.499 107.112V136.703H567.326V136.708Z"
fill="#F5455C" />
<path
d="M174.643 91.8055C171.307 86.6179 166.629 82.0255 160.75 78.152C149.39 70.6796 134.463 66.5635 118.72 66.5635C113.46 66.5635 108.277 67.0213 103.247 67.9279C100.126 64.9197 96.4767 62.2138 92.612 60.0755C78.273 52.9191 65.6323 55.5885 59.2477 57.8824C57.1501 58.6378 56.5029 61.298 58.0543 62.896C62.557 67.5479 70.0065 76.7418 68.1751 85.1023C61.0515 92.3824 57.1914 101.155 57.1914 110.289C57.1914 119.598 61.0515 128.37 68.1705 135.646C70.0019 144.006 62.5525 153.205 58.0497 157.857C56.5029 159.455 57.1455 162.11 59.2431 162.866C65.6277 165.164 78.2638 167.834 92.6074 160.677C96.4721 158.539 100.121 155.833 103.242 152.825C108.273 153.731 113.455 154.189 118.715 154.189C134.463 154.189 149.39 150.078 160.745 142.605C166.625 138.732 171.302 134.144 174.639 128.952C178.357 123.174 180.239 116.951 180.239 110.468C180.243 103.801 178.357 97.5883 174.643 91.8055ZM118.077 143.397C111.27 143.397 104.78 142.518 98.8635 140.93L94.5398 145.092C92.1897 147.353 89.4358 149.4 86.5625 151.012C82.7574 152.875 78.9982 153.896 75.2804 154.203C75.4915 153.823 75.6843 153.438 75.8908 153.054C80.2237 145.087 81.3942 137.926 79.3975 131.575C72.3107 126.003 68.0604 118.87 68.0604 111.1C68.0604 93.266 90.4547 78.8067 118.077 78.8067C145.699 78.8067 168.098 93.266 168.098 111.1C168.094 128.943 145.699 143.397 118.077 143.397Z"
fill="#F5455C" />
<path
d="M94.1449 103.678C90.0736 103.678 86.7734 106.956 86.7734 110.999C86.7734 115.042 90.0736 118.32 94.1449 118.32C98.2162 118.32 101.516 115.042 101.516 110.999C101.516 106.956 98.2162 103.678 94.1449 103.678Z"
fill="#F5455C" />
<path
d="M117.875 103.678C113.804 103.678 110.504 106.956 110.504 110.999C110.504 115.042 113.804 118.32 117.875 118.32C121.947 118.32 125.247 115.042 125.247 110.999C125.247 106.956 121.947 103.678 117.875 103.678Z"
fill="#F5455C" />
<path
d="M141.605 103.678C137.534 103.678 134.233 106.956 134.233 110.999C134.233 115.042 137.534 118.32 141.605 118.32C145.676 118.32 148.976 115.042 148.976 110.999C148.976 106.956 145.672 103.678 141.605 103.678Z"
fill="#F5455C" />
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

@ -0,0 +1,3 @@
declare const path: string;
export = path;

@ -0,0 +1,12 @@
import { type StorybookConfig } from '@storybook/core-common';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-essentials', 'storybook-dark-mode'],
features: {
postcss: false,
},
framework: '@storybook/react',
};
module.exports = config;

@ -0,0 +1,36 @@
import { themes } from '@storybook/theming';
import { type Parameters } from '@storybook/addons';
import manifest from '../package.json';
import logo from './logo.svg';
import '@rocket.chat/fuselage/dist/fuselage.css';
import '@rocket.chat/icons/dist/rocketchat.css';
export const parameters: Parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
backgrounds: {
grid: {
cellSize: 4,
cellAmount: 4,
opacity: 0.5,
},
},
options: {
storySort: ([, a], [, b]) => a.kind.localeCompare(b.kind),
},
layout: 'fullscreen',
darkMode: {
dark: {
...themes.dark,
brandTitle: manifest.name,
brandImage: logo,
brandUrl: manifest.homepage,
},
light: {
...themes.normal,
brandTitle: manifest.name,
brandImage: logo,
brandUrl: manifest.homepage,
},
},
};

@ -2,16 +2,48 @@
"name": "@rocket.chat/web-ui-registration",
"version": "1.0.3",
"private": true,
"homepage": "https://rocket.chat",
"main": "./dist/index.js",
"typings": "./dist/index.d.ts",
"files": [
"/dist"
],
"scripts": {
"lint": "eslint --ext .js,.jsx,.ts,.tsx .",
"lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix",
"test": "jest",
"storybook": "start-storybook -p 6006 --no-version-updates",
"build": "tsc -p tsconfig.build.json",
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput"
},
"devDependencies": {
"@babel/core": "~7.22.10",
"@babel/preset-env": "~7.22.10",
"@babel/preset-react": "~7.22.5",
"@babel/preset-typescript": "~7.22.5",
"@rocket.chat/i18n": "workspace:~",
"@rocket.chat/layout": "next",
"@rocket.chat/mock-providers": "workspace:~",
"@rocket.chat/ui-client": "workspace:^",
"@rocket.chat/ui-contexts": "workspace:^",
"@storybook/addon-actions": "~6.5.16",
"@storybook/addon-docs": "~6.5.16",
"@storybook/addon-essentials": "~6.5.16",
"@storybook/builder-webpack4": "~6.5.16",
"@storybook/manager-webpack4": "~6.5.16",
"@storybook/react": "~6.5.16",
"@storybook/testing-library": "^0.2.0",
"@tanstack/react-query": "^4.16.1",
"@testing-library/react": "^13.3.0",
"@types/jest": "~29.5.3",
"@types/react": "~17.0.62",
"babel-loader": "~8.3.0",
"eslint": "~8.45.0",
"jest": "~29.6.1",
"react-hook-form": "^7.34.2",
"react": "~17.0.2",
"react-hook-form": "~7.45.4",
"react-i18next": "~13.2.1",
"storybook-dark-mode": "~3.0.1",
"ts-jest": "~29.0.5",
"typescript": "~5.2.2"
},
@ -22,17 +54,5 @@
"react": "*",
"react-hook-form": "*",
"react-i18next": "*"
},
"scripts": {
"lint": "eslint --ext .js,.jsx,.ts,.tsx .",
"lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix",
"test": "jest",
"build": "tsc -p tsconfig.json",
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput"
},
"main": "./dist/index.js",
"typings": "./dist/index.d.ts",
"files": [
"/dist"
]
}
}

@ -1,8 +1,12 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { type ComponentMeta } from '@storybook/react';
import ResetPasswordPage from './ResetPasswordPage';
export default {
title: 'Login/ResetPassword',
component: ResetPasswordPage,
};
decorators: [mockAppRoot().buildStoryDecorator()],
} satisfies ComponentMeta<typeof ResetPasswordPage>;
export const Basic = () => <ResetPasswordPage />;

@ -0,0 +1,42 @@
import { Box, Tile } from '@rocket.chat/fuselage';
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { type ComponentStory, type ComponentMeta } from '@storybook/react';
import { createElement } from 'react';
import { useTranslation } from 'react-i18next';
import LoginSwitchLanguageFooter from './LoginSwitchLanguageFooter';
export default {
title: 'components/LoginSwitchLanguageFooter',
component: LoginSwitchLanguageFooter,
decorators: [
(fn) =>
createElement(function ExampleTranslationDecorator() {
const { t } = useTranslation();
return (
<Box>
<Tile>{t('example.text')}</Tile>
{fn()}
</Box>
);
}),
mockAppRoot()
.withSetting('Language', 'fi')
.withTranslations('en', 'registration', { 'component.switchLanguage': 'Change to <1>{{ name }}</1>' })
.withTranslations('en', 'example', { text: 'Hello!' })
.withTranslations('fi', 'registration', { 'component.switchLanguage': 'Vaihda kieleksi <1>{{ name }}</1>' })
.withTranslations('fi', 'example', { text: 'Hei!' })
.withTranslations('pt-BR', 'registration', { 'component.switchLanguage': 'Mudar para <1>{{ name }}</1>' })
.withTranslations('pt', 'example', { text: 'Olá!' })
.buildStoryDecorator(),
],
args: {
browserLanguage: 'pt-BR',
},
parameters: {
layout: 'centered',
},
} satisfies ComponentMeta<typeof LoginSwitchLanguageFooter>;
export const Default: ComponentStory<typeof LoginSwitchLanguageFooter> = (args) => <LoginSwitchLanguageFooter {...args} />;

@ -1,9 +1,11 @@
import { useSetting, useLoadLanguage, useLanguage, useLanguages } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import { Fragment, useMemo } from 'react';
import { Trans } from 'react-i18next';
import { Button } from '@rocket.chat/fuselage';
import { useLocalStorage } from '@rocket.chat/fuselage-hooks';
import { HorizontalWizardLayoutCaption } from '@rocket.chat/layout';
import { type TranslationLanguage, useSetting, useLoadLanguage, useLanguage, useLanguages } from '@rocket.chat/ui-contexts';
import { type ReactElement, type UIEvent, useMemo, useEffect } from 'react';
import { Trans, useTranslation } from 'react-i18next';
export const normalizeLanguage = (language: string): string => {
const normalizeLanguage = (language: string): string => {
// Fix browsers having all-lowercase language settings eg. pt-br, en-us
const regex = /([a-z]{2,3})-([a-z]{2,4})/;
const matches = regex.exec(language);
@ -14,49 +16,67 @@ export const normalizeLanguage = (language: string): string => {
return language;
};
const browserLanguage = normalizeLanguage(window.navigator.language ?? 'en');
const LoginSwitchLanguageFooter = (): ReactElement | null => {
const useSuggestedLanguages = ({
browserLanguage = normalizeLanguage(window.navigator.language ?? 'en'),
}: {
browserLanguage?: string;
}) => {
const availableLanguages = useLanguages();
const currentLanguage = useLanguage();
const languages = useLanguages();
const loadLanguage = useLoadLanguage();
const serverLanguage = normalizeLanguage((useSetting('Language') as string | undefined) || 'en');
const serverLanguage = normalizeLanguage(useSetting<string>('Language') || 'en');
const suggestions = useMemo(() => {
const potentialSuggestions = new Set([serverLanguage, browserLanguage, 'en'].map(normalizeLanguage));
return Array.from(potentialSuggestions).filter(
(language) => language && language !== currentLanguage && Boolean(languages.find(({ key }) => key === language)),
const potentialLanguages = new Set([serverLanguage, browserLanguage, 'en'].map(normalizeLanguage));
const potentialSuggestions = Array.from(potentialLanguages).map((potentialLanguageKey) =>
availableLanguages.find((language) => language.key === potentialLanguageKey),
);
}, [serverLanguage, currentLanguage, languages]);
return potentialSuggestions.filter((language): language is TranslationLanguage => {
return !!language && language.key !== currentLanguage;
});
}, [serverLanguage, browserLanguage, availableLanguages, currentLanguage]);
const { i18n } = useTranslation();
useEffect(() => {
i18n.loadLanguages(suggestions.map((suggestion) => suggestion.key));
}, [i18n, suggestions]);
return { suggestions };
};
type LoginSwitchLanguageFooterProps = {
browserLanguage?: string;
};
const LoginSwitchLanguageFooter = ({
browserLanguage = normalizeLanguage(window.navigator.language ?? 'en'),
}: LoginSwitchLanguageFooterProps): ReactElement | null => {
const loadLanguage = useLoadLanguage();
const { suggestions } = useSuggestedLanguages({ browserLanguage });
const handleSwitchLanguageClick = (language: string) => (): void => {
loadLanguage(language);
};
const [, setPreferedLanguage] = useLocalStorage('preferedLanguage', '');
const handleSwitchLanguageClick =
(language: TranslationLanguage) =>
async (event: UIEvent): Promise<void> => {
event.preventDefault();
await loadLanguage(language.key);
setPreferedLanguage(language.key);
};
if (!suggestions.length) {
return null;
}
return (
<p className='switch-language' role='group'>
<Trans i18nKey='registration.component.switchLanguage'>
Switch to
<>
{suggestions.map((language, index) => {
return (
<Fragment key={language}>
{index > 0 ? <span aria-hidden='true'> | </span> : <></>}
<button onClick={handleSwitchLanguageClick(language)} className='js-switch-language'>
{language}
</button>
</Fragment>
);
})}
</>
</Trans>
</p>
<HorizontalWizardLayoutCaption>
{suggestions.map((suggestion) => (
<Button secondary small mie={8} key={suggestion.key} onClick={handleSwitchLanguageClick(suggestion)}>
<Trans i18nKey='registration.component.switchLanguage' tOptions={{ lng: suggestion.key }}>
Change to <strong>{{ name: suggestion.ogName }}</strong>
</Trans>
</Button>
))}
</HorizontalWizardLayoutCaption>
);
};

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ESNext"
},
"include": ["./src/**/*"],
"exclude": ["./src/**/*.spec.ts", "./src/**/*.stories.tsx"]
}

@ -1,8 +1,9 @@
{
"extends": "../../tsconfig.base.client.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
"rootDirs": ["./src","./.storybook"],
"outDir": "./dist",
"module": "CommonJS"
},
"include": ["./src/**/*"]
"include": ["./src", "./.storybook"],
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save