From b40427315b61b955502f517ce4fbbb4b22ce00f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Vieira?= <36481167+stephanevieira75@users.noreply.github.com> Date: Mon, 7 Feb 2022 18:22:35 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A0=20#1900=20Workspace=20members=20in?= =?UTF-8?q?vite=20fixes=20(#1911)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * #1900 Verify that members can invite other users to the company/workspace (and not only the workspace moderators) * #1901 Add a notice to warn users that the company has reached the maximum number of users it can invite * #1901 Update workspace-pending-users.spect.ts * #1901 Update users.spec.ts * #1901 Update should 200 when company exists in users.spec.ts * #1901 Update workspace-pending-users.spec.ts * #1901 Resolve threads --- .../src/services/console/clients/remote.ts | 32 ++++++---- .../src/services/console/web/controller.ts | 2 +- .../src/services/user/entities/company.ts | 4 +- .../node/src/services/user/services/index.ts | 32 +++++++++- .../node/src/services/user/web/schemas.ts | 26 +++++--- .../node/src/services/user/web/types.ts | 31 ++++++++- .../src/services/workspaces/web/routes.ts | 63 ++++++++++++++++--- .../backend/node/test/e2e/users/users.spec.ts | 11 ++-- .../workspace-pending-users.spec.ts | 26 +++++--- twake/frontend/public/locales/de.json | 5 +- twake/frontend/public/locales/en.json | 5 +- twake/frontend/public/locales/es.json | 5 +- twake/frontend/public/locales/fr.json | 5 +- twake/frontend/public/locales/ru.json | 5 +- .../app/components/auto-height/auto-height.js | 1 + .../locked-invite-alert/index.tsx | 51 +++++++++++++++ .../channels/api/channel-api-client.ts | 5 +- .../app/features/companies/types/company.ts | 12 ++-- .../services/feature-toggles-service.ts | 7 +++ .../Parts/CurrentUser/CurrentUser.js | 2 +- .../client/popup/AddUser/AddUserByEmail.tsx | 36 ++++++++--- .../Pages/WorkspacePartner.tsx | 18 ++++-- .../Pages/WorkspacePartnerTabs/Pending.tsx | 4 +- 23 files changed, 313 insertions(+), 75 deletions(-) create mode 100644 twake/frontend/src/app/components/locked-features-components/locked-invite-alert/index.tsx diff --git a/twake/backend/node/src/services/console/clients/remote.ts b/twake/backend/node/src/services/console/clients/remote.ts index 67d5adf389..0866c96456 100644 --- a/twake/backend/node/src/services/console/clients/remote.ts +++ b/twake/backend/node/src/services/console/clients/remote.ts @@ -25,6 +25,7 @@ import UserServiceAPI from "../../user/api"; import coalesce from "../../../utils/coalesce"; import { logger } from "../../../core/platform/framework/logger"; import _ from "lodash"; +import { CompanyFeaturesEnum, CompanyLimitsEnum } from "../../user/web/types"; export class ConsoleRemoteClient implements ConsoleServiceClient { version: "1"; @@ -181,32 +182,37 @@ export class ConsoleRemoteClient implements ConsoleServiceClient { } if (!company.plan) { - company.plan = { name: "", features: {} }; + company.plan = { name: "", limits: undefined, features: undefined }; } //FIXME this is a hack right now! let planFeatures: any = { - "chat:guests": true, - "chat:message_history": true, - "chat:multiple_workspaces": true, - "chat:edit_files": true, - "chat:unlimited_storage": true, + [CompanyFeaturesEnum.CHAT_GUESTS]: true, + [CompanyFeaturesEnum.CHAT_MESSAGE_HISTORY]: true, + [CompanyFeaturesEnum.CHAT_MULTIPLE_WORKSPACES]: true, + [CompanyFeaturesEnum.CHAT_EDIT_FILES]: true, + [CompanyFeaturesEnum.CHAT_UNLIMITED_STORAGE]: true, + [CompanyFeaturesEnum.COMPANY_INVITE_MEMBER]: true, }; + if (companyDTO.limits.members < 0 && this.infos.type === "remote") { //Hack to say this is free version planFeatures = { - "chat:guests": false, - "chat:message_history": false, - "chat:message_history_limit": 10000, - "chat:multiple_workspaces": false, - "chat:edit_files": false, - "chat:unlimited_storage": false, //Currently inactive + [CompanyFeaturesEnum.CHAT_GUESTS]: false, + [CompanyFeaturesEnum.CHAT_MESSAGE_HISTORY]: false, + [CompanyFeaturesEnum.CHAT_MULTIPLE_WORKSPACES]: false, + [CompanyFeaturesEnum.CHAT_EDIT_FILES]: false, + [CompanyFeaturesEnum.CHAT_UNLIMITED_STORAGE]: false, // Currently inactive }; company.plan.name = "free"; } else { company.plan.name = "standard"; } - company.plan.features = { ...planFeatures, ...companyDTO.limits }; + company.plan.features = { ...planFeatures }; + company.plan.limits = { + [CompanyLimitsEnum.CHAT_MESSAGE_HISTORY_LIMIT]: 10000, // To remove duplicata since we define this in formatCompany function + [CompanyLimitsEnum.COMPANY_MEMBERS_LIMIT]: companyDTO.limits["members"], + }; company.stats = coalesce(companyDTO.stats, company.stats); diff --git a/twake/backend/node/src/services/console/web/controller.ts b/twake/backend/node/src/services/console/web/controller.ts index 8ce8872e66..524f5dee26 100644 --- a/twake/backend/node/src/services/console/web/controller.ts +++ b/twake/backend/node/src/services/console/web/controller.ts @@ -83,7 +83,7 @@ export class ConsoleController { if (!company) { const newCompany = getCompanyInstance({ name: "Twake", - plan: { name: "Local", features: {} }, + plan: { name: "Local", limits: undefined, features: undefined }, }); company = await this.userService.companies.createCompany(newCompany); } diff --git a/twake/backend/node/src/services/user/entities/company.ts b/twake/backend/node/src/services/user/entities/company.ts index c60c83f640..8473a55bcc 100644 --- a/twake/backend/node/src/services/user/entities/company.ts +++ b/twake/backend/node/src/services/user/entities/company.ts @@ -1,5 +1,6 @@ import { merge } from "lodash"; import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators"; +import { CompanyFeaturesObject, CompanyLimitsObject } from "../web/types"; // backward compatibility with PHP where companies used to be `group_entity` export const TYPE = "group_entity"; @@ -21,7 +22,8 @@ export default class Company { @Column("plan", "encoded_json") plan?: { name: string; - features: any; + limits: CompanyLimitsObject; + features: CompanyFeaturesObject; }; @Column("stats", "encoded_json") diff --git a/twake/backend/node/src/services/user/services/index.ts b/twake/backend/node/src/services/user/services/index.ts index 6f6ca612f6..9cd7a83ec4 100644 --- a/twake/backend/node/src/services/user/services/index.ts +++ b/twake/backend/node/src/services/user/services/index.ts @@ -10,6 +10,8 @@ import { getService as getExternalService } from "./external_links"; import { getService as getWorkspaceService } from "../../workspaces/services/workspace"; import { WorkspaceServiceAPI } from "../../workspaces/api"; import { + CompanyFeaturesEnum, + CompanyLimitsEnum, CompanyObject, CompanyShort, CompanyStatsObject, @@ -143,7 +145,7 @@ class Service implements UserServiceAPI { }; if (companyUserObject) { - res.status = "active"; // FIXME: with real status + res.status = "active"; // FIXME: Deactivated console user are removed from company on twake side res.role = companyUserObject.role; } @@ -151,6 +153,34 @@ class Service implements UserServiceAPI { res.stats = companyStats; } + res.plan = { + name: res.plan?.name || "free", + limits: res.plan?.limits || {}, + features: res.plan?.features || {}, + }; + + res.plan.limits = Object.assign( + { + [CompanyLimitsEnum.CHAT_MESSAGE_HISTORY_LIMIT]: 10000, + [CompanyLimitsEnum.COMPANY_MEMBERS_LIMIT]: -1, + }, + res.plan?.limits || {}, + ); + + res.plan.features = Object.assign( + { + [CompanyFeaturesEnum.CHAT_GUESTS]: true, + [CompanyFeaturesEnum.CHAT_MESSAGE_HISTORY]: true, + [CompanyFeaturesEnum.CHAT_MULTIPLE_WORKSPACES]: true, + [CompanyFeaturesEnum.CHAT_EDIT_FILES]: true, + [CompanyFeaturesEnum.CHAT_UNLIMITED_STORAGE]: true, + [CompanyFeaturesEnum.COMPANY_INVITE_MEMBER]: + res.plan?.limits[CompanyLimitsEnum.COMPANY_MEMBERS_LIMIT] <= 0 || + res.stats.total_members < res.plan?.limits[CompanyLimitsEnum.COMPANY_MEMBERS_LIMIT], + }, + res.plan?.features || {}, + ); + return res; } } diff --git a/twake/backend/node/src/services/user/web/schemas.ts b/twake/backend/node/src/services/user/web/schemas.ts index c0a5bd89aa..c28774286a 100644 --- a/twake/backend/node/src/services/user/web/schemas.ts +++ b/twake/backend/node/src/services/user/web/schemas.ts @@ -1,4 +1,5 @@ import { webSocketSchema } from "../../../utils/types"; +import { CompanyFeaturesEnum, CompanyLimitsEnum } from "./types"; export const userObjectSchema = { type: "object", @@ -60,18 +61,27 @@ const companyObjectSchema = { type: ["object", "null"], properties: { name: { type: "string" }, + limits: { + type: ["object", "null"], + properties: { + [CompanyLimitsEnum.CHAT_MESSAGE_HISTORY_LIMIT]: { type: "number" }, + [CompanyLimitsEnum.COMPANY_MEMBERS_LIMIT]: { type: "number" }, + }, + }, features: { type: "object", properties: { - "chat:edit_files": { type: "boolean" }, - "chat:guests": { type: "boolean" }, - "chat:message_history": { type: "boolean" }, - "chat:multiple_workspaces": { type: "boolean" }, - "chat:unlimited_storage": { type: "boolean" }, - guests: { type: "number" }, - members: { type: "number" }, - storage: { type: "number" }, + [CompanyFeaturesEnum.CHAT_EDIT_FILES]: { type: ["boolean"] }, + [CompanyFeaturesEnum.CHAT_GUESTS]: { type: ["boolean"] }, + [CompanyFeaturesEnum.CHAT_MESSAGE_HISTORY]: { type: "boolean" }, + [CompanyFeaturesEnum.CHAT_MULTIPLE_WORKSPACES]: { type: "boolean" }, + [CompanyFeaturesEnum.CHAT_UNLIMITED_STORAGE]: { type: "boolean" }, + [CompanyFeaturesEnum.COMPANY_INVITE_MEMBER]: { type: "boolean" }, + guests: { type: "number" }, // to rename or delete + members: { type: "number" }, // to rename or delete + storage: { type: "number" }, // to rename or delete }, + required: [] as string[], }, }, }, diff --git a/twake/backend/node/src/services/user/web/types.ts b/twake/backend/node/src/services/user/web/types.ts index d58f2d899d..ad3f422bf8 100644 --- a/twake/backend/node/src/services/user/web/types.ts +++ b/twake/backend/node/src/services/user/web/types.ts @@ -65,9 +65,38 @@ export interface UserObject { companies?: CompanyUserObject[]; } +export enum CompanyLimitsEnum { + CHAT_MESSAGE_HISTORY_LIMIT = "chat:message_history_limit", + COMPANY_MEMBERS_LIMIT = "company:members_limit", // 100 +} + +export enum CompanyFeaturesEnum { + CHAT_GUESTS = "chat:guests", + CHAT_MESSAGE_HISTORY = "chat:message_history", + CHAT_MULTIPLE_WORKSPACES = "chat:multiple_workspaces", + CHAT_EDIT_FILES = "chat:edit_files", + CHAT_UNLIMITED_STORAGE = "chat:unlimited_storage", + COMPANY_INVITE_MEMBER = "company:invite_member", +} + +export type CompanyFeaturesObject = { + [CompanyFeaturesEnum.CHAT_GUESTS]?: boolean; + [CompanyFeaturesEnum.CHAT_MESSAGE_HISTORY]?: boolean; + [CompanyFeaturesEnum.CHAT_MULTIPLE_WORKSPACES]?: boolean; + [CompanyFeaturesEnum.CHAT_EDIT_FILES]?: boolean; + [CompanyFeaturesEnum.CHAT_UNLIMITED_STORAGE]?: boolean; + [CompanyFeaturesEnum.COMPANY_INVITE_MEMBER]?: boolean; +}; + +export type CompanyLimitsObject = { + [CompanyLimitsEnum.CHAT_MESSAGE_HISTORY_LIMIT]?: number; + [CompanyLimitsEnum.COMPANY_MEMBERS_LIMIT]?: number; +}; + export interface CompanyPlanObject { name: string; - features: any; + limits?: CompanyLimitsObject; + features?: CompanyFeaturesObject; } export interface CompanyStatsObject { diff --git a/twake/backend/node/src/services/workspaces/web/routes.ts b/twake/backend/node/src/services/workspaces/web/routes.ts index afbda21d01..f3f1be3afd 100644 --- a/twake/backend/node/src/services/workspaces/web/routes.ts +++ b/twake/backend/node/src/services/workspaces/web/routes.ts @@ -26,6 +26,7 @@ import { hasWorkspaceAdminLevel, hasWorkspaceMemberLevel } from "../../../utils/ import { WorkspaceInviteTokensCrudController } from "./controllers/workspace-invite-tokens"; import WorkspaceUser from "../entities/workspace_user"; import { RealtimeServiceAPI } from "../../../core/platform/services/realtime/api"; +import { hasCompanyMemberLevel } from "../../../utils/company"; const workspacesUrl = "/companies/:company_id/workspaces"; const workspacePendingUsersUrl = "/companies/:company_id/workspaces/:workspace_id/pending"; @@ -110,11 +111,27 @@ const routes: FastifyPluginCallback<{ return workspaceUser; }; + const checkUserHasCompanyMemberLevel = async ( + request: FastifyRequest<{ Params: WorkspaceUsersRequest }>, + ) => { + if (!request.currentUser.id) { + throw fastify.httpErrors.forbidden("You must be authenticated"); + } + const companyUser = await options.service.companies.getCompanyUser( + { id: request.params.company_id }, + { id: request.currentUser.id }, + ); + + if (!hasCompanyMemberLevel(companyUser?.role)) { + throw fastify.httpErrors.forbidden("Only company member can perform this action"); + } + }; + const checkUserIsWorkspaceAdmin = async ( request: FastifyRequest<{ Params: WorkspaceUsersRequest }>, ) => { if (!request.currentUser.id) { - throw fastify.httpErrors.forbidden("Only workspace moderator can perform this action"); + throw fastify.httpErrors.forbidden("You must be authenticated"); } const workspaceUser = await checkUserWorkspace(request); const companyUser = await options.service.companies.getCompanyUser( @@ -130,7 +147,7 @@ const routes: FastifyPluginCallback<{ request: FastifyRequest<{ Params: WorkspaceUsersRequest }>, ) => { if (!request.currentUser.id) { - throw fastify.httpErrors.forbidden("Only workspace moderator can perform this action"); + throw fastify.httpErrors.forbidden("You must be authenticated"); } const workspaceUser = await checkUserWorkspace(request); const companyUser = await options.service.companies.getCompanyUser( @@ -240,7 +257,12 @@ const routes: FastifyPluginCallback<{ fastify.route({ method: "POST", url: `${workspaceUsersUrl}/invite`, - preHandler: [accessControl, companyCheck, checkUserIsWorkspaceAdmin], + preHandler: [ + accessControl, + companyCheck, + checkUserHasCompanyMemberLevel, + checkUserIsWorkspaceMember, + ], preValidation: [fastify.authenticate], schema: inviteWorkspaceUserSchema, handler: workspaceUsersController.invite.bind(workspaceUsersController), @@ -249,7 +271,12 @@ const routes: FastifyPluginCallback<{ fastify.route({ method: "DELETE", url: `${workspacePendingUsersUrl}/:email`, - preHandler: [accessControl, companyCheck, checkUserIsWorkspaceAdmin], + preHandler: [ + accessControl, + companyCheck, + checkUserHasCompanyMemberLevel, + checkUserIsWorkspaceMember, + ], preValidation: [fastify.authenticate], schema: deleteWorkspacePendingUsersSchema, handler: workspaceUsersController.deletePending.bind(workspaceUsersController), @@ -258,7 +285,12 @@ const routes: FastifyPluginCallback<{ fastify.route({ method: "GET", url: `${workspacePendingUsersUrl}`, - preHandler: [accessControl, companyCheck, checkUserIsWorkspaceAdmin], + preHandler: [ + accessControl, + companyCheck, + checkUserHasCompanyMemberLevel, + checkUserIsWorkspaceMember, + ], preValidation: [fastify.authenticate], schema: getWorkspacePendingUsersSchema, handler: workspaceUsersController.listPending.bind(workspaceUsersController), @@ -267,7 +299,12 @@ const routes: FastifyPluginCallback<{ fastify.route({ method: "GET", url: `${workspaceInviteTokensUrl}`, - preHandler: [accessControl, companyCheck, checkUserIsWorkspaceMember], + preHandler: [ + accessControl, + companyCheck, + checkUserIsWorkspaceMember, + checkUserHasCompanyMemberLevel, + ], preValidation: [fastify.authenticate], schema: getWorkspaceInviteTokenSchema, handler: workspaceInviteTokensController.list.bind(workspaceInviteTokensController), @@ -276,7 +313,12 @@ const routes: FastifyPluginCallback<{ fastify.route({ method: "POST", url: `${workspaceInviteTokensUrl}`, - preHandler: [accessControl, companyCheck, checkUserIsWorkspaceMember], + preHandler: [ + accessControl, + companyCheck, + checkUserIsWorkspaceMember, + checkUserHasCompanyMemberLevel, + ], preValidation: [fastify.authenticate], schema: postWorkspaceInviteTokenSchema, handler: workspaceInviteTokensController.save.bind(workspaceInviteTokensController), @@ -285,7 +327,12 @@ const routes: FastifyPluginCallback<{ fastify.route({ method: "DELETE", url: `${workspaceInviteTokensUrl}/:token`, - preHandler: [accessControl, companyCheck, checkUserIsWorkspaceMember], + preHandler: [ + accessControl, + companyCheck, + checkUserIsWorkspaceMember, + checkUserHasCompanyMemberLevel, + ], preValidation: [fastify.authenticate], schema: deleteWorkspaceInviteTokenSchema, handler: workspaceInviteTokensController.delete.bind(workspaceInviteTokensController), diff --git a/twake/backend/node/test/e2e/users/users.spec.ts b/twake/backend/node/test/e2e/users/users.spec.ts index 92f849fb59..10ffd6cc74 100644 --- a/twake/backend/node/test/e2e/users/users.spec.ts +++ b/twake/backend/node/test/e2e/users/users.spec.ts @@ -2,6 +2,7 @@ import { beforeAll, afterAll, afterEach, beforeEach, describe, expect, it } from import { init, TestPlatform } from "../setup"; import { TestDbService } from "../utils.prepare.db"; import { v1 as uuidv1 } from "uuid"; +import { CompanyLimitsEnum } from "../../../src/services/user/web/types"; describe("The /users API", () => { const url = "/internal/services/users/v1"; @@ -285,9 +286,8 @@ describe("The /users API", () => { expect(resource.plan).toMatchObject({ name: expect.any(String), limits: expect.objectContaining({ - members: expect.any(Number), - guests: expect.any(Number), - storage: expect.any(Number), + [CompanyLimitsEnum.CHAT_MESSAGE_HISTORY_LIMIT]: expect.any(Number || undefined), + [CompanyLimitsEnum.COMPANY_MEMBERS_LIMIT]: expect.any(Number || undefined), }), }); } @@ -334,9 +334,8 @@ describe("The /users API", () => { expect(json.resource.plan).toMatchObject({ name: expect.any(String), limits: expect.objectContaining({ - members: expect.any(Number), - guests: expect.any(Number), - storage: expect.any(Number), + [CompanyLimitsEnum.CHAT_MESSAGE_HISTORY_LIMIT]: expect.any(Number || undefined), + [CompanyLimitsEnum.COMPANY_MEMBERS_LIMIT]: expect.any(Number || undefined), }), }); } diff --git a/twake/backend/node/test/e2e/workspaces/workspace-pending-users.spec.ts b/twake/backend/node/test/e2e/workspaces/workspace-pending-users.spec.ts index e57c94bcce..5390fe9ec1 100644 --- a/twake/backend/node/test/e2e/workspaces/workspace-pending-users.spec.ts +++ b/twake/backend/node/test/e2e/workspaces/workspace-pending-users.spec.ts @@ -34,6 +34,7 @@ describe("The /workspace/pending users API", () => { const firstEmail = "first@test-user.com"; const secondEmail = "second@test-user.com"; const thirdEmail = "third@test-user.com"; + const fourthUser = "fourth@test-user.com"; const emailForExistedUser = "exist@email.com"; async function doTheTest() { @@ -65,8 +66,10 @@ describe("The /workspace/pending users API", () => { await testDbService.createCompany(companyId); const ws0pk = { id: uuidv1(), company_id: companyId }; const ws1pk = { id: uuidv1(), company_id: companyId }; + const ws2pk = { id: uuidv1(), company_id: companyId }; await testDbService.createWorkspace(ws0pk); await testDbService.createWorkspace(ws1pk); + await testDbService.createWorkspace(ws2pk); await testDbService.createUser([ws0pk], { companyRole: "member", workspaceRole: "moderator" }); await testDbService.createUser([ws0pk], { companyRole: "member", workspaceRole: "member" }); await testDbService.createUser([ws1pk], { @@ -74,6 +77,11 @@ describe("The /workspace/pending users API", () => { workspaceRole: "member", email: emailForExistedUser, }); + await testDbService.createUser([ws2pk], { + companyRole: "guest", + workspaceRole: "member", + email: fourthUser, + }); const console = platform.platform.getProvider("console"); consoleType = console.consoleType; @@ -122,9 +130,9 @@ describe("The /workspace/pending users API", () => { done(); }); - it("should 403 when requester is not workspace moderator", async done => { - const workspace_id = testDbService.workspaces[0].workspace.id; - const userId = testDbService.workspaces[0].users[1].id; + it("should 403 when requester is not at least workspace member", async done => { + const workspace_id = testDbService.workspaces[2].workspace.id; + const userId = testDbService.workspaces[2].users[0].id; const jwtToken = await platform.auth.getJWTToken({ sub: userId }); @@ -268,10 +276,10 @@ describe("The /workspace/pending users API", () => { done(); }); - it("should 403 when requester is not workspace moderator", async done => { + it("should 403 when requester is not at least workspace member", async done => { const companyId = testDbService.company.id; - const workspaceId = testDbService.workspaces[0].workspace.id; - const userId = testDbService.workspaces[0].users[1].id; + const workspaceId = testDbService.workspaces[2].workspace.id; + const userId = testDbService.workspaces[2].users[0].id; const email = "first@test-user.com"; const jwtToken = await platform.auth.getJWTToken({ sub: userId }); @@ -359,10 +367,10 @@ describe("The /workspace/pending users API", () => { done(); }); - it("should 403 when requester is not workspace moderator", async done => { + it("should 403 when requester is not at least workspace member", async done => { const companyId = testDbService.company.id; - const workspaceId = testDbService.workspaces[0].workspace.id; - const userId = testDbService.workspaces[0].users[1].id; + const workspaceId = testDbService.workspaces[2].workspace.id; + const userId = testDbService.workspaces[2].users[0].id; const email = "first@test-user.com"; const jwtToken = await platform.auth.getJWTToken({ sub: userId }); diff --git a/twake/frontend/public/locales/de.json b/twake/frontend/public/locales/de.json index 7851d8031e..7014a2244f 100644 --- a/twake/frontend/public/locales/de.json +++ b/twake/frontend/public/locales/de.json @@ -866,5 +866,8 @@ "components.rich_text_editor.plugins.suggestions.default_message.no_user_found": "Kein Benutzer gefunden", "scenes.app.channelsbar.currentuser.reset": "Zurücksetzen", "components.rich_text_editor.plugins.suggestions.default_message.no_emoji_found": "Keine Emojis gefunden", - "components.rich_text_editor.plugins.suggestions.default_message.no_command_found": "Kein Befehl gefunden" + "components.rich_text_editor.plugins.suggestions.default_message.no_command_found": "Kein Befehl gefunden", + "components.locked_features_components.locked_invite_alert.message_part_1": "Ihr Unternehmen hat die maximale Anzahl von {{$1}} Mitgliedern erreicht. Bitte ", + "components.locked_features_components.locked_invite_alert.message_link": "aktualisieren Sie Ihr Unternehmen", + "components.locked_features_components.locked_invite_alert.message_part_2": ", um weitere Mitglieder einzuladen." } diff --git a/twake/frontend/public/locales/en.json b/twake/frontend/public/locales/en.json index e4d730dd28..59d633ae67 100644 --- a/twake/frontend/public/locales/en.json +++ b/twake/frontend/public/locales/en.json @@ -866,5 +866,8 @@ "components.rich_text_editor.plugins.suggestions.default_message.no_command_found": "No command found", "scenes.app.messages.input.parts.is_writing.message.user_is_writing": "{{$1}} is writing...", "scenes.app.messages.input.parts.is_writing.message.users_are_writing": "{{$1}} and {{$2}} are writing...", - "scenes.app.messages.input.parts.is_writing.message.users_and_more_are_writing": "{{$1}}, {{$2}} and {{$3}} more users are writing..." + "scenes.app.messages.input.parts.is_writing.message.users_and_more_are_writing": "{{$1}}, {{$2}} and {{$3}} more users are writing...", + "components.locked_features_components.locked_invite_alert.message_part_1": "Your company has reached it's maximum limit of {{$1}} members. Please ", + "components.locked_features_components.locked_invite_alert.message_link": "upgrade your company", + "components.locked_features_components.locked_invite_alert.message_part_2": " to invite more members." } diff --git a/twake/frontend/public/locales/es.json b/twake/frontend/public/locales/es.json index b1f8d89ccd..7bcbdd24f7 100644 --- a/twake/frontend/public/locales/es.json +++ b/twake/frontend/public/locales/es.json @@ -865,5 +865,8 @@ "scenes.app.popup.workspaceparameter.pages.workspace_identity.toaster.error.bad_format": "Formato incorrecto, solo se permiten png gif y jpg", "scenes.app.popup.workspaceparameter.pages.workspace_identity.toaster.error.unknown": "Error desconocido", "scenes.app.popup.workspaceparameter.pages.workspace_identity.toaster.error.prefix": "Error", - "scenes.app.channelsbar.currentuser.reset": "Reiniciar" + "scenes.app.channelsbar.currentuser.reset": "Reiniciar", + "components.locked_features_components.locked_invite_alert.message_part_1": "Su empresa ha alcanzado el número máximo de {{$1}} miembros. Por favor, ", + "components.locked_features_components.locked_invite_alert.message_link": "actualice su empresa", + "components.locked_features_components.locked_invite_alert.message_part_2": " para invitar a más miembros." } diff --git a/twake/frontend/public/locales/fr.json b/twake/frontend/public/locales/fr.json index b3c2231006..2c7e4f96b0 100644 --- a/twake/frontend/public/locales/fr.json +++ b/twake/frontend/public/locales/fr.json @@ -866,5 +866,8 @@ "login.login_error": "Erreur lors de la connexion", "scenes.app.messages.input.parts.is_writing.message.user_is_writing": "{{$1}} est en train d'écrire...", "scenes.app.messages.input.parts.is_writing.message.users_are_writing": "{{$1}} et {{$2}} sont en train d'écrire...", - "scenes.app.messages.input.parts.is_writing.message.users_and_more_are_writing": "{{$1}}, {{$2}} et {{$3}} autres utilisateurs sont en train d'écrire..." + "scenes.app.messages.input.parts.is_writing.message.users_and_more_are_writing": "{{$1}}, {{$2}} et {{$3}} autres utilisateurs sont en train d'écrire...", + "components.locked_features_components.locked_invite_alert.message_part_1": "Votre entreprise a atteint sa limite maximale de {{$1}} membres. Veuillez ", + "components.locked_features_components.locked_invite_alert.message_link": "mettre à niveau votre entreprise", + "components.locked_features_components.locked_invite_alert.message_part_2": " pour inviter davantage de membres." } diff --git a/twake/frontend/public/locales/ru.json b/twake/frontend/public/locales/ru.json index 779d22f177..4dd255e9fe 100644 --- a/twake/frontend/public/locales/ru.json +++ b/twake/frontend/public/locales/ru.json @@ -866,5 +866,8 @@ "scenes.app.popup.workspaceparameter.pages.workspace_identity.toaster.error.bad_format": "Неверный формат, разрешены только форматы png, gif и jpg", "scenes.app.popup.workspaceparameter.pages.workspace_identity.toaster.error.unknown": "Неизвестная ошибка", "scenes.app.popup.workspaceparameter.pages.workspace_identity.toaster.error.prefix": "Ошибка", - "scenes.app.channelsbar.currentuser.reset": "Перезагрузить" + "scenes.app.channelsbar.currentuser.reset": "Перезагрузить", + "components.locked_features_components.locked_invite_alert.message_part_1": "Ваша компания достигла максимального лимита в {{$1}} участников. Пожалуйста, ", + "components.locked_features_components.locked_invite_alert.message_link": "повысьте статус вашей компании", + "components.locked_features_components.locked_invite_alert.message_part_2": " чтобы пригласить больше участников." } diff --git a/twake/frontend/src/app/components/auto-height/auto-height.js b/twake/frontend/src/app/components/auto-height/auto-height.js index 6954e34994..3a4279e3bd 100755 --- a/twake/frontend/src/app/components/auto-height/auto-height.js +++ b/twake/frontend/src/app/components/auto-height/auto-height.js @@ -66,6 +66,7 @@ export default class AutoHeight extends Component { >