mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
feat: Redesign workspace join (#10507)
* Redesign workspace join Signed-off-by: Artem Savchenko <armisav@gmail.com> * Login rename Signed-off-by: Artem Savchenko <armisav@gmail.com> * Fix UI tests Signed-off-by: Artem Savchenko <armisav@gmail.com> * Fix unstable tests Signed-off-by: Artem Savchenko <armisav@gmail.com> * Minor fixes Signed-off-by: Artem Savchenko <armisav@gmail.com> * Clean up Signed-off-by: Artem Savchenko <armisav@gmail.com> * Fix translations Signed-off-by: Artem Savchenko <armisav@gmail.com> * Redesign join Signed-off-by: Artem Savchenko <armisav@gmail.com> --------- Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
@@ -42,6 +42,7 @@ import type {
|
||||
LoginInfo,
|
||||
LoginInfoByToken,
|
||||
LoginInfoRequestData,
|
||||
InviteInfo,
|
||||
LoginInfoWithWorkspaces,
|
||||
MailboxInfo,
|
||||
MailboxOptions,
|
||||
@@ -124,7 +125,9 @@ export interface AccountClient {
|
||||
}
|
||||
) => Promise<string>
|
||||
checkJoin: (inviteId: string) => Promise<WorkspaceLoginInfo>
|
||||
joinByToken: (inviteId: string) => Promise<WorkspaceLoginInfo>
|
||||
checkAutoJoin: (inviteId: string, firstName?: string, lastName?: string) => Promise<WorkspaceLoginInfo>
|
||||
getInviteInfo: (inviteId: string) => Promise<InviteInfo>
|
||||
getWorkspaceInfo: (updateLastVisit?: boolean) => Promise<WorkspaceInfoWithStatus>
|
||||
getWorkspacesInfo: (workspaces: WorkspaceUuid[]) => Promise<WorkspaceInfoWithStatus[]>
|
||||
updateLastVisit: (workspaces: WorkspaceUuid[]) => Promise<void>
|
||||
@@ -577,6 +580,15 @@ class AccountClientImpl implements AccountClient {
|
||||
return await this.rpc(request)
|
||||
}
|
||||
|
||||
async joinByToken (inviteId: string): Promise<WorkspaceLoginInfo> {
|
||||
const request = {
|
||||
method: 'joinByToken' as const,
|
||||
params: { inviteId }
|
||||
}
|
||||
|
||||
return await this.rpc(request)
|
||||
}
|
||||
|
||||
async checkAutoJoin (inviteId: string, firstName?: string, lastName?: string): Promise<WorkspaceLoginInfo> {
|
||||
const request = {
|
||||
method: 'checkAutoJoin' as const,
|
||||
@@ -586,6 +598,15 @@ class AccountClientImpl implements AccountClient {
|
||||
return await this.rpc(request)
|
||||
}
|
||||
|
||||
async getInviteInfo (inviteId: string): Promise<InviteInfo> {
|
||||
const request = {
|
||||
method: 'getInviteInfo' as const,
|
||||
params: { inviteId }
|
||||
}
|
||||
|
||||
return await this.rpc(request)
|
||||
}
|
||||
|
||||
async getWorkspacesInfo (ids: WorkspaceUuid[]): Promise<WorkspaceInfoWithStatus[]> {
|
||||
const request = {
|
||||
method: 'getWorkspacesInfo' as const,
|
||||
|
||||
@@ -80,6 +80,11 @@ export interface WorkspaceInviteInfo {
|
||||
name?: string
|
||||
}
|
||||
|
||||
/** Public invite details from getInviteInfo (no auth required). */
|
||||
export interface InviteInfo {
|
||||
workspaceName: string | null
|
||||
}
|
||||
|
||||
export interface OtpInfo {
|
||||
sent: boolean
|
||||
retryOn: Timestamp
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"LastName": "Příjmení",
|
||||
"FirstName": "Jméno",
|
||||
"Join": "Připojit se",
|
||||
"JoinWorkspace": "Připojit se k {workspaceName}",
|
||||
"Email": "E-mail",
|
||||
"Password": "Heslo",
|
||||
"Workspace": "Název pracovního prostoru",
|
||||
@@ -84,6 +85,13 @@
|
||||
"SignUpToCreateWorkspace": "Neexistují žádné pracovní prostory s přístupem pro hosty. Prosím, zaregistrujte se a vytvořte si vlastní.",
|
||||
"WhatIsYourName": "Jak se jmenujete?",
|
||||
"AccessExpired": "Doba přístupu vypršela. Požádejte prosím o nový odkaz pro přístup.",
|
||||
"AccessNotActive": "Doba přístupu ještě nezačala. Začíná za: "
|
||||
"AccessNotActive": "Doba přístupu ještě nezačala. Začíná za: ",
|
||||
"JoinWithThisAccount": "Připojit se tímto účtem",
|
||||
"UseDifferentAccount": "Použít jiný účet",
|
||||
"UseCurrentAccountToJoin": "Už jste přihlášeni?",
|
||||
"LogInAndJoin": "Přihlásit se a připojit",
|
||||
"SignUpAndJoin": "Registrovat se a připojit",
|
||||
"CreateNewAccount": "Vytvořit nový účet",
|
||||
"SignedInAs": "Přihlášen jako {name}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"LastName": "Nachname",
|
||||
"FirstName": "Vorname",
|
||||
"Join": "Beitreten",
|
||||
"JoinWorkspace": "{workspaceName} beitreten",
|
||||
"Email": "E-Mail",
|
||||
"Password": "Passwort",
|
||||
"Workspace": "Name des Arbeitsbereichs",
|
||||
@@ -84,6 +85,13 @@
|
||||
"SignUpToCreateWorkspace": "Keine Arbeitsbereiche mit Gastzugang. Bitte registrieren Sie sich, um Ihren eigenen zu erstellen.",
|
||||
"WhatIsYourName": "Wie ist Ihr Name?",
|
||||
"AccessExpired": "Zugriffszeit abgelaufen. Bitte fragen Sie nach einem neuen Zugangslink.",
|
||||
"AccessNotActive": "Zugriffszeit hat noch nicht begonnen. Sie beginnt in: "
|
||||
"AccessNotActive": "Zugriffszeit hat noch nicht begonnen. Sie beginnt in: ",
|
||||
"JoinWithThisAccount": "Mit diesem Konto beitreten",
|
||||
"UseDifferentAccount": "Anderes Konto verwenden",
|
||||
"UseCurrentAccountToJoin": "Bereits angemeldet?",
|
||||
"LogInAndJoin": "Anmelden & beitreten",
|
||||
"SignUpAndJoin": "Registrieren & beitreten",
|
||||
"CreateNewAccount": "Neues Konto erstellen",
|
||||
"SignedInAs": "Angemeldet als {name}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"LastName": "Last name",
|
||||
"FirstName": "First name",
|
||||
"Join": "Join",
|
||||
"JoinWorkspace": "Join {workspaceName}",
|
||||
"Email": "Email",
|
||||
"Password": "Password",
|
||||
"Workspace": "Workspace name",
|
||||
@@ -83,6 +84,13 @@
|
||||
"SignUpToCreateWorkspace": "There are no workspaces with guest access. Please sign up to create your own.",
|
||||
"WhatIsYourName": "What is your name?",
|
||||
"AccessExpired": "Access time expired. Please ask for a new access link.",
|
||||
"AccessNotActive": "Access time hasn't started yet. It is starting in: "
|
||||
"AccessNotActive": "Access time hasn't started yet. It is starting in: ",
|
||||
"JoinWithThisAccount": "Join with this account",
|
||||
"UseDifferentAccount": "Use different account",
|
||||
"UseCurrentAccountToJoin": "Already signed in?",
|
||||
"LogInAndJoin": "Log In & Join",
|
||||
"SignUpAndJoin": "Sign Up & Join",
|
||||
"CreateNewAccount": "Create new account",
|
||||
"SignedInAs": "Signed in as {name}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"LastName": "Apellido",
|
||||
"FirstName": "Nombre",
|
||||
"Join": "Unirse",
|
||||
"JoinWorkspace": "Unirse a {workspaceName}",
|
||||
"Email": "Correo Electrónico",
|
||||
"Password": "Contraseña",
|
||||
"Workspace": "Nombre del espacio de trabajo",
|
||||
@@ -83,6 +84,13 @@
|
||||
"SignUpToCreateWorkspace": "No hay espacios de trabajo con acceso de invitado. Por favor, regístrese para crear el suyo.",
|
||||
"WhatIsYourName": "¿Cuál es su nombre?",
|
||||
"AccessExpired": "El tiempo de acceso ha expirado. Por favor, solicite un nuevo enlace de acceso.",
|
||||
"AccessNotActive": "El tiempo de acceso aún no ha comenzado. Comienza en: "
|
||||
"AccessNotActive": "El tiempo de acceso aún no ha comenzado. Comienza en: ",
|
||||
"JoinWithThisAccount": "Unirse con esta cuenta",
|
||||
"UseDifferentAccount": "Usar otra cuenta",
|
||||
"UseCurrentAccountToJoin": "¿Ya has iniciado sesión?",
|
||||
"LogInAndJoin": "Iniciar sesión y unirse",
|
||||
"SignUpAndJoin": "Registrarse y unirse",
|
||||
"CreateNewAccount": "Crear nueva cuenta",
|
||||
"SignedInAs": "Conectado como {name}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"LastName": "Nom de famille",
|
||||
"FirstName": "Prénom",
|
||||
"Join": "Rejoindre",
|
||||
"JoinWorkspace": "Rejoindre {workspaceName}",
|
||||
"Email": "E-mail",
|
||||
"Password": "Mot de passe",
|
||||
"Workspace": "Nom de l'espace de travail",
|
||||
@@ -83,6 +84,13 @@
|
||||
"SignUpToCreateWorkspace": "Aucun espace de travail avec accès invité. Veuillez vous inscrire pour créer le vôtre.",
|
||||
"WhatIsYourName": "Quel est votre nom ?",
|
||||
"AccessExpired": "Le temps d'accès a expiré. Veuillez demander un nouveau lien d'accès.",
|
||||
"AccessNotActive": "Le temps d'accès n'a pas encore commencé. Il commence dans : "
|
||||
"AccessNotActive": "Le temps d'accès n'a pas encore commencé. Il commence dans : ",
|
||||
"JoinWithThisAccount": "Rejoindre avec ce compte",
|
||||
"UseDifferentAccount": "Utiliser un autre compte",
|
||||
"UseCurrentAccountToJoin": "Déjà connecté ?",
|
||||
"LogInAndJoin": "Se connecter et rejoindre",
|
||||
"SignUpAndJoin": "S'inscrire et rejoindre",
|
||||
"CreateNewAccount": "Créer un nouveau compte",
|
||||
"SignedInAs": "Connecté en tant que {name}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"LastName": "Cognome",
|
||||
"FirstName": "Nome",
|
||||
"Join": "Unisciti",
|
||||
"JoinWorkspace": "Partecipa a {workspaceName}",
|
||||
"Email": "Email",
|
||||
"Password": "Password",
|
||||
"Workspace": "Nome dello spazio di lavoro",
|
||||
@@ -83,6 +84,13 @@
|
||||
"SignUpToCreateWorkspace": "Nessuna area di lavoro con accesso ospite. Per favore, registrati per crearne una tua.",
|
||||
"WhatIsYourName": "Come ti chiami?",
|
||||
"AccessExpired": "Il tempo di accesso è scaduto. Per favore, richiedi un nuovo link di accesso.",
|
||||
"AccessNotActive": "Il tempo di accesso non è ancora iniziato. Inizia tra: "
|
||||
"AccessNotActive": "Il tempo di accesso non è ancora iniziato. Inizia tra: ",
|
||||
"JoinWithThisAccount": "Partecipa con questo account",
|
||||
"UseDifferentAccount": "Usa un altro account",
|
||||
"UseCurrentAccountToJoin": "Già effettuato l'accesso?",
|
||||
"LogInAndJoin": "Accedi e partecipa",
|
||||
"SignUpAndJoin": "Iscriviti e partecipa",
|
||||
"CreateNewAccount": "Crea nuovo account",
|
||||
"SignedInAs": "Accesso effettuato come {name}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"LastName": "姓",
|
||||
"FirstName": "名",
|
||||
"Join": "参加",
|
||||
"JoinWorkspace": "{workspaceName} に参加",
|
||||
"Email": "メールアドレス",
|
||||
"Password": "パスワード",
|
||||
"Workspace": "ワークスペース名",
|
||||
@@ -83,6 +84,13 @@
|
||||
"EnterCurrentPassword": "現在のパスワードを入力",
|
||||
"EnterNewPassword": "新しいパスワードを入力",
|
||||
"RepeatNewPassword": "新しいパスワードを再入力",
|
||||
"PasswordExpiredDesc": "パスワードの有効期限が切れています。続行するには新しいパスワードを設定してください。"
|
||||
"PasswordExpiredDesc": "パスワードの有効期限が切れています。続行するには新しいパスワードを設定してください。",
|
||||
"JoinWithThisAccount": "このアカウントで参加",
|
||||
"UseDifferentAccount": "別のアカウントを使用",
|
||||
"UseCurrentAccountToJoin": "すでにサインインしていますか?",
|
||||
"LogInAndJoin": "ログインして参加",
|
||||
"SignUpAndJoin": "サインアップして参加",
|
||||
"CreateNewAccount": "新規アカウントを作成",
|
||||
"SignedInAs": "{name} としてサインイン中"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"LastName": "Apelido",
|
||||
"FirstName": "Nome",
|
||||
"Join": "Participar",
|
||||
"JoinWorkspace": "Participar de {workspaceName}",
|
||||
"Email": "Email",
|
||||
"Password": "Palavra-Passe",
|
||||
"Workspace": "Nome do espaço de trabalho",
|
||||
@@ -83,6 +84,13 @@
|
||||
"SignUpToCreateWorkspace": "Não há espaços de trabalho com acesso de convidado. Por favor, cadastre-se para criar o seu.",
|
||||
"WhatIsYourName": "Qual é o seu nome?",
|
||||
"AccessExpired": "O tempo de acesso expirou. Por favor, solicite um novo link de acesso.",
|
||||
"AccessNotActive": "O tempo de acesso ainda não começou. Começa em: "
|
||||
"AccessNotActive": "O tempo de acesso ainda não começou. Começa em: ",
|
||||
"JoinWithThisAccount": "Entrar com esta conta",
|
||||
"UseDifferentAccount": "Usar outra conta",
|
||||
"UseCurrentAccountToJoin": "Já está conectado?",
|
||||
"LogInAndJoin": "Entrar e participar",
|
||||
"SignUpAndJoin": "Cadastrar e participar",
|
||||
"CreateNewAccount": "Criar nova conta",
|
||||
"SignedInAs": "Conectado como {name}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"LastName": "Apelido",
|
||||
"FirstName": "Nome",
|
||||
"Join": "Participar",
|
||||
"JoinWorkspace": "Participar em {workspaceName}",
|
||||
"Email": "Email",
|
||||
"Password": "Palavra-Passe",
|
||||
"Workspace": "Nome do espaço de trabalho",
|
||||
@@ -83,6 +84,13 @@
|
||||
"SignUpToCreateWorkspace": "Não há espaços de trabalho com acesso de convidado. Por favor, cadastre-se para criar o seu.",
|
||||
"WhatIsYourName": "Qual é o seu nome?",
|
||||
"AccessExpired": "O tempo de acesso expirou. Por favor, solicite um novo link de acesso.",
|
||||
"AccessNotActive": "O tempo de acesso ainda não começou. Começa em: "
|
||||
"AccessNotActive": "O tempo de acesso ainda não começou. Começa em: ",
|
||||
"JoinWithThisAccount": "Participar com esta conta",
|
||||
"UseDifferentAccount": "Usar outra conta",
|
||||
"UseCurrentAccountToJoin": "Já tem sessão iniciada?",
|
||||
"LogInAndJoin": "Entrar e participar",
|
||||
"SignUpAndJoin": "Registar e participar",
|
||||
"CreateNewAccount": "Criar nova conta",
|
||||
"SignedInAs": "Sessão iniciada como {name}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"LastName": "Фамилия",
|
||||
"FirstName": "Имя",
|
||||
"Join": "Присоединиться",
|
||||
"JoinWorkspace": "Присоединиться к {workspaceName}",
|
||||
"Email": "Email",
|
||||
"Password": "Пароль",
|
||||
"Workspace": "Рабочее пространство",
|
||||
@@ -83,6 +84,13 @@
|
||||
"SignUpToCreateWorkspace": "Нет рабочих пространств с гостевым доступом. Пожалуйста, зарегистрируйтесь, чтобы создать своё.",
|
||||
"WhatIsYourName": "Как вас зовут?",
|
||||
"AccessExpired": "Время доступа истекло. Пожалуйста, запросите новую ссылку доступа.",
|
||||
"AccessNotActive": "Время доступа еще не началось. Начинается через: "
|
||||
"AccessNotActive": "Время доступа еще не началось. Начинается через: ",
|
||||
"JoinWithThisAccount": "Присоединиться с этим аккаунтом",
|
||||
"UseDifferentAccount": "Использовать другой аккаунт",
|
||||
"UseCurrentAccountToJoin": "Уже вошли в аккаунт?",
|
||||
"LogInAndJoin": "Войти и присоединиться",
|
||||
"SignUpAndJoin": "Зарегистрироваться и присоединиться",
|
||||
"CreateNewAccount": "Создать новый аккаунт",
|
||||
"SignedInAs": "Вы вошли как {name}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"LastName": "Soyad",
|
||||
"FirstName": "Ad",
|
||||
"Join": "Katıl",
|
||||
"JoinWorkspace": "{workspaceName} alanına katıl",
|
||||
"Email": "E-posta",
|
||||
"Password": "Şifre",
|
||||
"Workspace": "Çalışma alanı adı",
|
||||
@@ -83,6 +84,13 @@
|
||||
"SignUpToCreateWorkspace": "Misafir erişimi olan çalışma alanı yok. Kendi alanınızı oluşturmak için lütfen kayıt olun.",
|
||||
"WhatIsYourName": "Adınız nedir?",
|
||||
"AccessExpired": "Erişim süresi doldu. Lütfen yeni bir erişim bağlantısı isteyin.",
|
||||
"AccessNotActive": "Erişim süresi henüz başlamadı. Başlangıç zamanı: "
|
||||
"AccessNotActive": "Erişim süresi henüz başlamadı. Başlangıç zamanı: ",
|
||||
"JoinWithThisAccount": "Bu hesapla katıl",
|
||||
"UseDifferentAccount": "Farklı hesap kullan",
|
||||
"UseCurrentAccountToJoin": "Zaten giriş yaptınız mı?",
|
||||
"LogInAndJoin": "Giriş yap ve katıl",
|
||||
"SignUpAndJoin": "Kayıt ol ve katıl",
|
||||
"CreateNewAccount": "Yeni hesap oluştur",
|
||||
"SignedInAs": "{name} olarak giriş yapıldı"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"LastName": "姓",
|
||||
"FirstName": "名",
|
||||
"Join": "加入",
|
||||
"JoinWorkspace": "加入 {workspaceName}",
|
||||
"Email": "电子邮件",
|
||||
"Password": "密码",
|
||||
"Workspace": "工作区名称",
|
||||
@@ -83,6 +84,13 @@
|
||||
"SignUpToCreateWorkspace": "没有支持访客访问的工作区。请注册以创建您自己的工作区。",
|
||||
"WhatIsYourName": "您的姓名是什么?",
|
||||
"AccessExpired": "访问时间已过期。请申请新的访问链接。",
|
||||
"AccessNotActive": "访问时间尚未开始。开始时间:"
|
||||
"AccessNotActive": "访问时间尚未开始。开始时间:",
|
||||
"JoinWithThisAccount": "使用此账户加入",
|
||||
"UseDifferentAccount": "使用其他账户",
|
||||
"UseCurrentAccountToJoin": "已登录?",
|
||||
"LogInAndJoin": "登录并加入",
|
||||
"SignUpAndJoin": "注册并加入",
|
||||
"CreateNewAccount": "创建新账户",
|
||||
"SignedInAs": "已以 {name} 身份登录"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
}
|
||||
|
||||
export let caption: IntlString
|
||||
export let captionParams: Record<string, any> = {}
|
||||
export let status: Status
|
||||
export let fields: Field[]
|
||||
export let action: Action
|
||||
@@ -46,6 +47,8 @@
|
||||
export let subtitle: string | undefined = undefined
|
||||
export let signUpDisabled = false
|
||||
export let isLoading: boolean = false
|
||||
export let actionButtonDataId: string | undefined = undefined
|
||||
export let secondaryButtonDataId: string | undefined = undefined
|
||||
|
||||
const validate = makeSequential(async function validateAsync (language: string): Promise<boolean> {
|
||||
if (ignoreInitialValidation || isLoading) return true
|
||||
@@ -147,7 +150,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-row-center">
|
||||
<div class="title"><Label label={caption} /></div>
|
||||
<div class="title"><Label label={caption} params={captionParams} /></div>
|
||||
<slot name="region-selector" />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -174,6 +177,7 @@
|
||||
|
||||
<div class="form-row send">
|
||||
<Button
|
||||
dataId={actionButtonDataId}
|
||||
label={action.i18n}
|
||||
kind={'contrast'}
|
||||
shape={'round2'}
|
||||
@@ -196,6 +200,7 @@
|
||||
{#if secondaryButtonLabel !== undefined && secondaryButtonAction}
|
||||
<div class="form-row">
|
||||
<Button
|
||||
dataId={secondaryButtonDataId}
|
||||
label={secondaryButtonLabel}
|
||||
width="100%"
|
||||
on:click={(e) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
// Copyright © 2026 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
@@ -13,11 +13,29 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { OK, Severity, Status } from '@hcengineering/platform'
|
||||
import { Location, getCurrentLocation, navigate } from '@hcengineering/ui'
|
||||
import { OK, PlatformError, Severity, Status, getMetadata, setMetadata } from '@hcengineering/platform'
|
||||
import {
|
||||
Button,
|
||||
Label,
|
||||
Loading,
|
||||
Location,
|
||||
deviceOptionsStore as deviceInfo,
|
||||
getCurrentLocation,
|
||||
navigate
|
||||
} from '@hcengineering/ui'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
|
||||
import { checkJoined, join, setLoginInfo, signUpJoin } from '../utils'
|
||||
import {
|
||||
checkJoined,
|
||||
getInviteWorkspaceName,
|
||||
join,
|
||||
joinByToken,
|
||||
setLoginInfo,
|
||||
signUpJoin,
|
||||
getLoginInfo
|
||||
} from '../utils'
|
||||
import Form from './Form.svelte'
|
||||
import StatusControl from './StatusControl.svelte'
|
||||
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { signupStore } from '@hcengineering/analytics-providers'
|
||||
@@ -28,7 +46,14 @@
|
||||
|
||||
const location = getCurrentLocation()
|
||||
Analytics.handleEvent('invite_link_activated', { invite_id: location.query?.inviteId })
|
||||
let page = 'signUp'
|
||||
|
||||
const token = getMetadata(presentation.metadata.Token)
|
||||
let page = token != null ? 'login' : 'signUp'
|
||||
let checking = true
|
||||
let showJoinWithAccount = false
|
||||
let currentAccountName: string | undefined
|
||||
let joiningWithAccount = false
|
||||
let inviteWorkspaceName: string | undefined
|
||||
|
||||
$: signupStore.setSignUpFlow(page === 'signUp')
|
||||
|
||||
@@ -62,7 +87,7 @@
|
||||
let status = OK
|
||||
|
||||
$: action = {
|
||||
i18n: page === 'login' ? login.string.Join : login.string.SignUp,
|
||||
i18n: page === 'login' ? login.string.LogInAndJoin : login.string.SignUpAndJoin,
|
||||
func: async () => {
|
||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
||||
|
||||
@@ -105,7 +130,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: secondaryButtonLabel = page === 'login' ? login.string.SignUp : login.string.Join
|
||||
$: secondaryButtonLabel = page === 'login' ? login.string.CreateNewAccount : login.string.HaveAccount
|
||||
$: secondaryButtonAction =
|
||||
page === 'login'
|
||||
? () => {
|
||||
@@ -120,11 +145,18 @@
|
||||
})
|
||||
|
||||
async function check (): Promise<void> {
|
||||
if (location.query?.inviteId === undefined || location.query?.inviteId === null) return
|
||||
if (location.query?.inviteId === undefined || location.query?.inviteId === null) {
|
||||
checking = false
|
||||
return
|
||||
}
|
||||
checking = true
|
||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
||||
|
||||
const result = await checkJoined(location.query.inviteId)
|
||||
const inviteId = location.query.inviteId
|
||||
const [result, workspaceName] = await Promise.all([checkJoined(inviteId), getInviteWorkspaceName(inviteId)])
|
||||
inviteWorkspaceName = workspaceName
|
||||
status = OK
|
||||
|
||||
if (result != null) {
|
||||
setLoginInfo(result)
|
||||
|
||||
@@ -140,18 +172,199 @@
|
||||
}
|
||||
}
|
||||
navigate({ path: [workbenchId, result.workspaceUrl] })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const info = await getLoginInfo()
|
||||
if (info != null) {
|
||||
showJoinWithAccount = true
|
||||
currentAccountName = info.name
|
||||
if (info.token != null) {
|
||||
setMetadata(presentation.metadata.Token, info.token)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// No session (metadata token or cookie)
|
||||
}
|
||||
|
||||
checking = false
|
||||
}
|
||||
|
||||
async function handleJoinWithThisAccount (): Promise<void> {
|
||||
const inviteId = location.query?.inviteId
|
||||
if (inviteId == null) return
|
||||
joiningWithAccount = true
|
||||
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
|
||||
try {
|
||||
const result = await joinByToken(inviteId)
|
||||
await logIn(result)
|
||||
setLoginInfo(result)
|
||||
|
||||
if (location.query?.navigateUrl != null) {
|
||||
try {
|
||||
const loc = JSON.parse(decodeURIComponent(location.query.navigateUrl)) as Location
|
||||
if (loc.path[1] === result.workspaceUrl) {
|
||||
navigate(loc)
|
||||
return
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Json parse error could be ignored
|
||||
}
|
||||
}
|
||||
navigate({ path: [workbenchId, result.workspaceUrl] })
|
||||
} catch (err: any) {
|
||||
status =
|
||||
err instanceof PlatformError ? err.status : new Status(Severity.ERROR, login.status.ConnectingToServer, {})
|
||||
} finally {
|
||||
joiningWithAccount = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleUseDifferentAccount (): void {
|
||||
setMetadata(presentation.metadata.Token, null)
|
||||
showJoinWithAccount = false
|
||||
page = 'login'
|
||||
}
|
||||
|
||||
async function handleUseCurrentAccountToJoin (): Promise<void> {
|
||||
try {
|
||||
if (currentAccountName == null) {
|
||||
console.error('Current account is not found')
|
||||
return
|
||||
}
|
||||
const info = await getLoginInfo()
|
||||
if (info != null) {
|
||||
showJoinWithAccount = true
|
||||
}
|
||||
} catch {
|
||||
// No session
|
||||
}
|
||||
}
|
||||
|
||||
const useCurrentAccountToJoinAction = {
|
||||
caption: login.string.UseCurrentAccountToJoin,
|
||||
i18n: login.string.JoinWithThisAccount,
|
||||
func: () => {
|
||||
void handleUseCurrentAccountToJoin()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form
|
||||
caption={login.string.Join}
|
||||
{status}
|
||||
{fields}
|
||||
{object}
|
||||
{action}
|
||||
{secondaryButtonLabel}
|
||||
{secondaryButtonAction}
|
||||
bottomActions={[loginAction, recoveryAction]}
|
||||
withProviders
|
||||
/>
|
||||
{#if checking}
|
||||
<div class="join-checking">
|
||||
<Loading size="small" shrink={true} />
|
||||
<Label label={login.string.ProcessingInvite} />
|
||||
</div>
|
||||
{:else if showJoinWithAccount}
|
||||
<div
|
||||
class="join-with-account-container"
|
||||
style:padding={$deviceInfo.docWidth <= 480 ? '.25rem 1.25rem' : '4rem 5rem'}
|
||||
style:min-height={$deviceInfo.docHeight > 720 ? '42rem' : '0'}
|
||||
>
|
||||
<div class="join-with-account">
|
||||
<div class="join-title">
|
||||
<Label label={login.string.JoinWorkspace} params={{ workspaceName: inviteWorkspaceName ?? '' }} />
|
||||
</div>
|
||||
{#if currentAccountName}
|
||||
<div class="join-subtitle pb-4">
|
||||
<Label label={login.string.SignedInAs} params={{ name: currentAccountName }} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if status.severity !== Severity.OK}
|
||||
<div class="join-status">
|
||||
<StatusControl {status} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="join-with-account-buttons">
|
||||
<Button
|
||||
dataId="join-with-this-account"
|
||||
label={login.string.Join}
|
||||
kind={'contrast'}
|
||||
shape={'round2'}
|
||||
size={'large'}
|
||||
loading={joiningWithAccount}
|
||||
disabled={joiningWithAccount}
|
||||
on:click={() => handleJoinWithThisAccount()}
|
||||
/>
|
||||
<Button
|
||||
dataId="join-use-different-account"
|
||||
label={login.string.UseDifferentAccount}
|
||||
size={'large'}
|
||||
shape={'round2'}
|
||||
disabled={joiningWithAccount}
|
||||
on:click={handleUseDifferentAccount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Form
|
||||
caption={login.string.JoinWorkspace}
|
||||
captionParams={{ workspaceName: inviteWorkspaceName ?? '' }}
|
||||
actionButtonDataId="join-form-submit"
|
||||
secondaryButtonDataId="join-form-toggle"
|
||||
{status}
|
||||
{fields}
|
||||
{object}
|
||||
{action}
|
||||
{secondaryButtonLabel}
|
||||
{secondaryButtonAction}
|
||||
bottomActions={[
|
||||
loginAction,
|
||||
...(currentAccountName != null ? [useCurrentAccountToJoinAction] : []),
|
||||
recoveryAction
|
||||
]}
|
||||
withProviders
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.join-title {
|
||||
font-weight: 500;
|
||||
font-size: 1.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
|
||||
.join-subtitle {
|
||||
font-size: 0.95rem;
|
||||
color: var(--theme-content-color);
|
||||
}
|
||||
|
||||
.join-checking {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
gap: 1rem;
|
||||
padding: 2rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
|
||||
.join-with-account-container {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.join-with-account {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 0.25rem;
|
||||
|
||||
.join-status {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.join-with-account-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
:global(button:first-child) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -34,6 +34,7 @@ export default mergeIds(loginId, login, {
|
||||
HaveAccount: '' as IntlString,
|
||||
LoadingAccount: '' as IntlString,
|
||||
Join: '' as IntlString,
|
||||
JoinWorkspace: '' as IntlString<{ workspaceName: string }>,
|
||||
Workspace: '' as IntlString,
|
||||
SignUp: '' as IntlString,
|
||||
DoNotHaveAnAccount: '' as IntlString,
|
||||
@@ -75,6 +76,13 @@ export default mergeIds(loginId, login, {
|
||||
SignUpToCreateWorkspace: '' as IntlString,
|
||||
WhatIsYourName: '' as IntlString,
|
||||
AccessExpired: '' as IntlString,
|
||||
AccessNotActive: '' as IntlString
|
||||
AccessNotActive: '' as IntlString,
|
||||
JoinWithThisAccount: '' as IntlString,
|
||||
UseDifferentAccount: '' as IntlString,
|
||||
UseCurrentAccountToJoin: '' as IntlString,
|
||||
LogInAndJoin: '' as IntlString,
|
||||
SignUpAndJoin: '' as IntlString,
|
||||
CreateNewAccount: '' as IntlString,
|
||||
SignedInAs: '' as IntlString<{ name: string }>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -558,12 +558,9 @@ export function navigateToWorkspace (
|
||||
export async function checkJoined (inviteId: string): Promise<WorkspaceLoginInfo | undefined> {
|
||||
const token = getMetadata(presentation.metadata.Token)
|
||||
|
||||
if (token == null) return
|
||||
|
||||
try {
|
||||
const workspaceLoginInfo = await getAccountClient(token).checkJoin(inviteId)
|
||||
|
||||
return workspaceLoginInfo
|
||||
const workspaceLoginInfo = await getAccountClient(token ?? undefined).checkJoin(inviteId)
|
||||
return workspaceLoginInfo ?? undefined
|
||||
} catch (err: any) {
|
||||
if (!(err instanceof PlatformError)) {
|
||||
Analytics.handleError(err)
|
||||
@@ -571,6 +568,29 @@ export async function checkJoined (inviteId: string): Promise<WorkspaceLoginInfo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches workspace name for a valid invite (no auth required). Returns undefined for invalid or expired invites.
|
||||
*/
|
||||
export async function getInviteWorkspaceName (inviteId: string): Promise<string | undefined> {
|
||||
try {
|
||||
const client = getAccountClient(null)
|
||||
const info = await client.getInviteInfo(inviteId)
|
||||
return info.workspaceName ?? undefined
|
||||
} catch (err: any) {
|
||||
console.error('Failed to get invite workspace name', err)
|
||||
if (!(err instanceof PlatformError)) {
|
||||
Analytics.handleError(err)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export async function joinByToken (inviteId: string): Promise<WorkspaceLoginInfo> {
|
||||
const token = getMetadata(presentation.metadata.Token)
|
||||
|
||||
return await getAccountClient(token ?? undefined).joinByToken(inviteId)
|
||||
}
|
||||
|
||||
export async function checkAutoJoin (
|
||||
inviteId: string,
|
||||
firstName?: string,
|
||||
|
||||
@@ -64,7 +64,8 @@ import {
|
||||
type PersonWithProfile,
|
||||
type Subscription,
|
||||
SubscriptionStatus,
|
||||
type Query
|
||||
type Query,
|
||||
type InviteInfo
|
||||
} from './types'
|
||||
import {
|
||||
addSocialIdBase,
|
||||
@@ -993,6 +994,38 @@ export async function join (
|
||||
return await doJoinByInvite(ctx, db, branding, token, account, workspaceJoinInfo.workspace, workspaceJoinInfo.invite)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns public invite details (e.g. workspace name) for a valid invite. No auth required.
|
||||
* Returns { workspaceName: null } for invalid or expired invites.
|
||||
*/
|
||||
export async function getInviteInfo (
|
||||
ctx: MeasureContext,
|
||||
db: AccountDB,
|
||||
_branding: Branding | null,
|
||||
_token: string,
|
||||
params: { inviteId: string }
|
||||
): Promise<InviteInfo> {
|
||||
const { inviteId } = params
|
||||
|
||||
if (inviteId == null || inviteId === '') {
|
||||
ctx.error('Mandatory param inviteId is missing in getInviteInfo', { inviteId })
|
||||
return { workspaceName: null }
|
||||
}
|
||||
|
||||
const invite = await getWorkspaceInvite(db, inviteId)
|
||||
if (invite == null) {
|
||||
ctx.error('Invite not found in getInviteInfo', { inviteId })
|
||||
return { workspaceName: null }
|
||||
}
|
||||
|
||||
const workspace = await getWorkspaceById(db, invite.workspaceUuid)
|
||||
if (workspace === null) {
|
||||
return { workspaceName: null }
|
||||
}
|
||||
|
||||
return { workspaceName: workspace.name }
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an invite and a token, checks if the user has already joined the workspace and updates the role if necessary.
|
||||
* Returns the workspace login information if the user has already joined. Otherwise, throws an error.
|
||||
@@ -1042,6 +1075,46 @@ export async function checkJoin (
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins the workspace using the current session token (no password).
|
||||
* Called only when the user explicitly clicks "Join with this account".
|
||||
*/
|
||||
export async function joinByToken (
|
||||
ctx: MeasureContext,
|
||||
db: AccountDB,
|
||||
branding: Branding | null,
|
||||
token: string,
|
||||
params: { inviteId: string }
|
||||
): Promise<WorkspaceLoginInfo> {
|
||||
const { inviteId } = params
|
||||
|
||||
if (inviteId == null || inviteId === '') {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {}))
|
||||
}
|
||||
|
||||
const invite = await getWorkspaceInvite(db, inviteId)
|
||||
if (invite == null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
|
||||
const { account: accountUuid } = decodeTokenVerbose(ctx, token)
|
||||
const emailSocialId = await db.socialId.findOne({
|
||||
type: SocialIdType.EMAIL,
|
||||
personUuid: accountUuid,
|
||||
verifiedOn: { $gt: 0 }
|
||||
})
|
||||
const email = emailSocialId?.value ?? ''
|
||||
const workspaceUuid = await checkInvite(ctx, invite, email)
|
||||
const workspace = await getWorkspaceById(db, workspaceUuid)
|
||||
|
||||
if (workspace === null) {
|
||||
ctx.error('Workspace not found in joinByToken', { workspaceUuid, email, inviteId })
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspaceUuid }))
|
||||
}
|
||||
|
||||
return await doJoinByInvite(ctx, db, branding, token, accountUuid, workspace, invite)
|
||||
}
|
||||
|
||||
export async function checkAutoJoin (
|
||||
ctx: MeasureContext,
|
||||
db: AccountDB,
|
||||
@@ -2892,8 +2965,10 @@ export type AccountMethods =
|
||||
| 'resendInvite'
|
||||
| 'selectWorkspace'
|
||||
| 'join'
|
||||
| 'joinByToken'
|
||||
| 'checkJoin'
|
||||
| 'checkAutoJoin'
|
||||
| 'getInviteInfo'
|
||||
| 'signUpJoin'
|
||||
| 'confirm'
|
||||
| 'changePassword'
|
||||
@@ -2966,8 +3041,10 @@ export function getMethods (hasSignUp: boolean = true): Partial<Record<AccountMe
|
||||
resendInvite: wrap(resendInvite),
|
||||
selectWorkspace: wrap(selectWorkspace),
|
||||
join: wrap(join),
|
||||
joinByToken: wrap(joinByToken),
|
||||
checkJoin: wrap(checkJoin),
|
||||
checkAutoJoin: wrap(checkAutoJoin),
|
||||
getInviteInfo: wrap(getInviteInfo),
|
||||
signUpJoin: wrap(signUpJoin),
|
||||
confirm: wrap(confirm),
|
||||
changePassword: wrap(changePassword),
|
||||
|
||||
@@ -487,6 +487,10 @@ export interface WorkspaceInviteInfo {
|
||||
name?: string
|
||||
}
|
||||
|
||||
export interface InviteInfo {
|
||||
workspaceName: string | null
|
||||
}
|
||||
|
||||
export interface MailboxOptions {
|
||||
availableDomains: string[]
|
||||
minNameLength: number
|
||||
|
||||
@@ -12,13 +12,34 @@ export class SignInJoinPage extends CommonPage {
|
||||
|
||||
inputEmail = (): Locator => this.page.locator('input[name="email"]')
|
||||
inputPassword = (): Locator => this.page.locator('//div[text()="Password"]/../input')
|
||||
buttonJoin = (): Locator => this.page.locator('button', { hasText: 'Join' })
|
||||
buttonSubmit = (): Locator => this.page.locator('button[data-id="join-form-submit"]')
|
||||
buttonToggle = (): Locator => this.page.locator('button[data-id="join-form-toggle"]')
|
||||
buttonJoinWithThisAccount = (): Locator => this.page.locator('button[data-id="join-with-this-account"]')
|
||||
buttonUseDifferentAccount = (): Locator => this.page.locator('button[data-id="join-use-different-account"]')
|
||||
|
||||
async join (data: Pick<SignUpData, 'email' | 'password'>): Promise<void> {
|
||||
await this.buttonJoin().click()
|
||||
await Promise.race([
|
||||
this.buttonSubmit().waitFor({ state: 'visible', timeout: 10000 }),
|
||||
this.buttonJoinWithThisAccount().waitFor({ state: 'visible', timeout: 10000 })
|
||||
])
|
||||
|
||||
const submitBtn = this.buttonSubmit()
|
||||
const toggleBtn = this.buttonToggle()
|
||||
const joinWithAccountBtn = this.buttonJoinWithThisAccount()
|
||||
|
||||
if (await joinWithAccountBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
await this.buttonUseDifferentAccount().click()
|
||||
await this.inputEmail().waitFor({ state: 'visible', timeout: 5000 })
|
||||
}
|
||||
|
||||
const submitText = await submitBtn.textContent().catch(() => '')
|
||||
if (submitText != null && submitText.toLowerCase().includes('sign up')) {
|
||||
await toggleBtn.click()
|
||||
await this.inputEmail().waitFor({ state: 'visible', timeout: 5000 })
|
||||
}
|
||||
await this.inputEmail().fill(data.email)
|
||||
await this.inputPassword().fill(data.password)
|
||||
expect(await this.buttonJoin().isEnabled()).toBe(true)
|
||||
await this.buttonJoin().click()
|
||||
await expect(this.buttonSubmit()).toBeEnabled()
|
||||
await this.buttonSubmit().click()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user