UBERF-9511: Allow to unarchive workspace by user request (#8084)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2025-02-25 03:15:45 +07:00
committed by GitHub
parent c6e752c7aa
commit dcbeb1e043
16 changed files with 109 additions and 45 deletions
+2 -1
View File
@@ -63,6 +63,7 @@
"PasswordMinDigits": "{count, plural, =1 {Heslo musí obsahovat alespoň # číslo} other {Heslo musí obsahovat alespoň # čísel}}",
"PasswordMinUpperChars": "{count, plural, =1 {Heslo musí obsahovat alespoň # velké písmeno} other {Heslo musí obsahovat alespoň # velkých písmen}}",
"PasswordMinLowerChars": "{count, plural, =1 {Heslo musí obsahovat alespoň # malé písmeno} other {Heslo musí obsahovat alespoň # malých písmen}}",
"WorkspaceIsArchived": "Pracovní prostor je archivován kvůli nečinnosti. Kontaktujte nás prosím pro obnovení..."
"WorkspaceIsArchived": "Pracovní prostor je archivován kvůli nečinnosti.",
"RestoreArchivedWorkspace": "Obnovit pracovní prostor."
}
}
+2 -1
View File
@@ -63,6 +63,7 @@
"PasswordMinDigits": "{count, plural, =1 {Das Passwort muss mindestens # Zahl enthalten} other {Das Passwort muss mindestens # Zahlen enthalten}}",
"PasswordMinUpperChars": "{count, plural, =1 {Das Passwort muss mindestens # Großbuchstaben enthalten} other {Das Passwort muss mindestens # Großbuchstaben enthalten}}",
"PasswordMinLowerChars": "{count, plural, =1 {Das Passwort muss mindestens # Kleinbuchstaben enthalten} other {Das Passwort muss mindestens # Kleinbuchstaben enthalten}}",
"WorkspaceArchivedDesc": "Workspace wurde wegen Inaktivität archiviert. Bitte kontaktieren Sie uns zur Wiederherstellung..."
"WorkspaceArchivedDesc": "Workspace wurde wegen Inaktivität archiviert.",
"RestoreArchivedWorkspace": "Workspace wiederherstellen."
}
}
+2 -1
View File
@@ -63,6 +63,7 @@
"PasswordMinDigits": "{count, plural, =1 {Password must contain at least # number} other {Password must contain at least # numbers}}",
"PasswordMinUpperChars": "{count, plural, =1 {Password must contain at least # uppercase letter} other {Password must contain at least # uppercase letters}}",
"PasswordMinLowerChars": "{count, plural, =1 {Password must contain at least # lowercase letter} other {Password must contain at least # lowercase letters}}",
"WorkspaceArchivedDesc": "Workspace is archived because of being unused, Please contact us to restore..."
"WorkspaceArchivedDesc": "Workspace is archived because of being unused.",
"RestoreArchivedWorkspace": "Unarchive"
}
}
+2 -1
View File
@@ -63,6 +63,7 @@
"PasswordMinDigits": "{count, plural, =1 {La contraseña debe contener al menos # número} other {La contraseña debe contener al menos # números}}",
"PasswordMinUpperChars": "{count, plural, =1 {La contraseña debe contener al menos # letra mayúscula} other {La contraseña debe contener al menos # letras mayúsculas}}",
"PasswordMinLowerChars": "{count, plural, =1 {La contraseña debe contener al menos # letra minúscula} other {La contraseña debe contener al menos # letras minúsculas}}",
"WorkspaceArchivedDesc": "El espacio de trabajo está archivado por no estar en uso, por favor contáctenos para restaurarlo..."
"WorkspaceArchivedDesc": "El espacio de trabajo está archivado por no estar en uso.",
"RestoreArchivedWorkspace": "Restaurar"
}
}
+2 -1
View File
@@ -63,6 +63,7 @@
"PasswordMinDigits": "{count, plural, =1 {Le mot de passe doit contenir au moins # chiffre} other {Le mot de passe doit contenir au moins # chiffres}}",
"PasswordMinUpperChars": "{count, plural, =1 {Le mot de passe doit contenir au moins # lettre majuscule} other {Le mot de passe doit contenir au moins # lettres majuscules}}",
"PasswordMinLowerChars": "{count, plural, =1 {Le mot de passe doit contenir au moins # lettre minuscule} other {Le mot de passe doit contenir au moins # lettres minuscules}}",
"WorkspaceArchivedDesc": "L'espace de travail est archivé en raison de son inactivité, veuillez nous contacter pour le restaurer..."
"WorkspaceArchivedDesc": "L'espace de travail est archivé en raison de son inactivité.",
"RestoreArchivedWorkspace": "Restaurer"
}
}
+2 -1
View File
@@ -63,6 +63,7 @@
"PasswordMinDigits": "{count, plural, =1 {La password deve contenere almeno # numero} other {La password deve contenere almeno # numeri}}",
"PasswordMinUpperChars": "{count, plural, =1 {La password deve contenere almeno # lettera maiuscola} other {La password deve contenere almeno # lettere maiuscole}}",
"PasswordMinLowerChars": "{count, plural, =1 {La password deve contenere almeno # lettera minuscola} other {La password deve contenere almeno # lettere minuscole}}",
"WorkspaceArchivedDesc": "Il workspace è stato archiviato perché inutilizzato. Si prega di contattarci per ripristinarlo..."
"WorkspaceArchivedDesc": "Il workspace è stato archiviato perché inutilizzato.",
"RestoreArchivedWorkspace": "Un archiviato workspace"
}
}
+2 -1
View File
@@ -63,6 +63,7 @@
"PasswordMinDigits": "{count, plural, =1 {A senha deve conter pelo menos # número} other {A senha deve conter pelo menos # números}}",
"PasswordMinUpperChars": "{count, plural, =1 {A senha deve conter pelo menos # letra maiúscula} other {A senha deve conter pelo menos # letras maiúsculas}}",
"PasswordMinLowerChars": "{count, plural, =1 {A senha deve conter pelo menos # letra minúscula} other {A senha deve conter pelo menos # letras minúsculas}}",
"WorkspaceArchivedDesc": "O espaço de trabalho está arquivado por estar inativo, por favor, entre em contato conosco para restaurá-lo..."
"WorkspaceArchivedDesc": "O espaço de trabalho está arquivado por estar inativo.",
"RestoreArchivedWorkspace": "Restaurar"
}
}
+2 -1
View File
@@ -63,6 +63,7 @@
"PasswordMinDigits": "{count, plural, =1 {Пароль должен содержать минимум # цифру} other {Пароль должен содержать минимум # цифр}}",
"PasswordMinUpperChars": "{count, plural, =1 {Пароль должен содержать минимум # заглавную букву} other {Пароль должен содержать минимум # заглавных букв}}",
"PasswordMinLowerChars": "{count, plural, =1 {Пароль должен содержать минимум # строчную букву} other {Пароль должен содержать минимум # строчных букв}}",
"WorkspaceArchivedDesc": "Рабочее пространство архивировано из-за неиспользования, пожалуйста, свяжитесь с нами для восстановления..."
"WorkspaceArchivedDesc": "Рабочее пространство архивировано из-за неиспользования.",
"RestoreArchivedWorkspace": "Восстановить"
}
}
+2 -1
View File
@@ -63,6 +63,7 @@
"PasswordMinDigits": "{count, plural, =1 {密码至少需要 # 个数字} other {密码至少需要 # 个数字}}",
"PasswordMinUpperChars": "{count, plural, =1 {密码至少需要 # 个大写字母} other {密码至少需要 # 个大写字母}}",
"PasswordMinLowerChars": "{count, plural, =1 {密码至少需要 # 个小写字母} other {密码至少需要 # 个小写字母}}",
"WorkspaceArchivedDesc": "工作区因未使用而归档,请与我们联系以恢复..."
"WorkspaceArchivedDesc": "工作区已被归档,因为未使用。",
"RestoreArchivedWorkspace": "解包"
}
}
@@ -14,7 +14,7 @@
// limitations under the License.
-->
<script lang="ts">
import { isArchivingMode } from '@hcengineering/core'
import { isActiveMode, isArchivingMode, isRestoringMode, isUpgradingMode } from '@hcengineering/core'
import { LoginInfo, Workspace } from '@hcengineering/login'
import { OK, Severity, Status } from '@hcengineering/platform'
import presentation, { NavLink, isAdminUser, reduceCalls } from '@hcengineering/presentation'
@@ -32,7 +32,7 @@
} from '@hcengineering/ui'
import { onMount } from 'svelte'
import login from '../plugin'
import { getAccount, getHref, getWorkspaces, goTo, navigateToWorkspace, selectWorkspace } from '../utils'
import { getAccount, getHref, getWorkspaces, goTo, navigateToWorkspace, selectWorkspace, unArchive } from '../utils'
import StatusControl from './StatusControl.svelte'
export let navigateUrl: string | undefined = undefined
@@ -69,13 +69,25 @@
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
const [loginStatus, result] = await selectWorkspace(workspace)
if (isArchivingMode(result?.mode)) {
if (isArchivingMode(result?.mode) && result?.workspaceId !== undefined) {
const workspaceId = result?.workspaceId
showPopup(MessageBox, {
label: login.string.SelectWorkspace,
message: login.string.WorkspaceArchivedDesc,
canSubmit: false,
canSubmit: true,
params: {},
action: async () => {}
okLabel: login.string.RestoreArchivedWorkspace,
action: async () => {
if (await unArchive(workspaceId, result.token)) {
workspaces = await getWorkspaces()
let info = workspaces.filter((it) => it.workspaceId === workspaceId).shift()
while (isRestoringMode(info?.mode) || isUpgradingMode(info?.mode)) {
await new Promise<void>((resolve) => setTimeout(resolve, 5000))
workspaces = await getWorkspaces()
info = workspaces.filter((it) => it.workspaceId === workspaceId).shift()
}
}
}
})
status = loginStatus
return
@@ -152,7 +164,7 @@
{#if isArchivingMode(workspace.mode)}
- <Label label={presentation.string.Archived} />
{/if}
{#if workspace.mode !== 'active' && workspace.mode !== 'archived'}
{#if !isActiveMode(workspace.mode) && !isArchivingMode(workspace.mode)}
({workspace.progress}%)
{/if}
</span>
+8 -21
View File
@@ -19,15 +19,18 @@ import InviteLink from './components/InviteLink.svelte'
import LoginApp from './components/LoginApp.svelte'
import {
changePassword,
createMissingEmployee,
fetchWorkspace,
getInviteLink,
getWorkspaces,
leaveWorkspace,
selectWorkspace,
sendInvite,
resendInvite,
fetchWorkspace,
createMissingEmployee,
getInviteLink
selectWorkspace,
sendInvite
} from './utils'
import { type Pages, pages } from '@hcengineering/login'
export { type Pages, pages }
/*!
* Anticrm Platform™ Login Plugin
* © 2020, 2021 Anticrm Platform Contributors.
@@ -53,21 +56,6 @@ export default async () => ({
}
})
export const pages = [
'login',
'signup',
'createWorkspace',
'password',
'recovery',
'selectWorkspace',
'admin',
'join',
'confirm',
'confirmationSend',
'auth',
'login-password'
] as const
export enum OtpLoginSteps {
Email = 'email',
Otp = 'otp'
@@ -77,7 +65,6 @@ export enum LoginMethods {
Password = 'password',
Otp = 'otp'
}
export type Pages = (typeof pages)[number]
export interface BottomAction {
i18n: IntlString
+32
View File
@@ -549,6 +549,38 @@ export async function fetchWorkspace (workspace: string): Promise<[Status, Works
return [unknownError(err), undefined]
}
}
export async function unArchive (workspaceId: string, token: string): Promise<boolean> {
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
if (accountsUrl === undefined) {
throw new Error('accounts url not specified')
}
if (token === undefined) {
return false
}
const request = {
method: 'performWorkspaceOperation',
params: [workspaceId, 'unarchive']
}
try {
const response = await fetch(accountsUrl, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify(request)
})
return (await response.json()).result
} catch (err: any) {
Analytics.handleError(err)
return false
}
}
export async function createMissingEmployee (workspace: string): Promise<[Status]> {
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
+19 -1
View File
@@ -64,6 +64,23 @@ export interface OtpInfo {
retryOn: Timestamp
}
export const pages = [
'login',
'signup',
'createWorkspace',
'password',
'recovery',
'selectWorkspace',
'admin',
'join',
'confirm',
'confirmationSend',
'auth',
'login-password'
] as const
export type Pages = (typeof pages)[number]
export default plugin(loginId, {
metadata: {
AccountsUrl: '' as Asset,
@@ -99,7 +116,8 @@ export default plugin(loginId, {
PasswordMinUpperChars: '' as IntlString<{ count: number }>,
PasswordMinLowerChars: '' as IntlString<{ count: number }>,
WorkspaceArchived: '' as IntlString,
WorkspaceArchivedDesc: '' as IntlString
WorkspaceArchivedDesc: '' as IntlString,
RestoreArchivedWorkspace: '' as IntlString
},
function: {
SendInvite: '' as Resource<(email: string, personId?: Ref<Doc>, role?: AccountRole) => Promise<void>>,
+7 -5
View File
@@ -14,7 +14,7 @@ import core, {
type MeasureMetricsContext,
type Version
} from '@hcengineering/core'
import login, { loginId } from '@hcengineering/login'
import login, { loginId, type Pages } from '@hcengineering/login'
import { broadcastEvent, getMetadata, getResource, setMetadata, translateCB } from '@hcengineering/platform'
import presentation, {
closeClient,
@@ -208,13 +208,15 @@ export async function connect (title: string): Promise<Client | undefined> {
},
onArchived: () => {
translateCB(plugin.string.WorkspaceIsArchived, {}, get(themeStore).language, (r) => {
versionError.set(r)
setTimeout(() => {
location.reload()
}, 5000)
const selectWorkspace: Pages = 'selectWorkspace'
navigate({
path: [loginId, selectWorkspace],
query: {}
})
})
},
onMigration: () => {
// TODO: Rework maitenance mode as well
translateCB(plugin.string.WorkspaceIsMigrating, {}, get(themeStore).language, (r) => {
versionError.set(r)
setTimeout(() => {
+3 -1
View File
@@ -1305,7 +1305,9 @@ export async function performWorkspaceOperation (
}
if (account.admin !== true) {
return false
if (event !== 'unarchive' || workspaceId !== decodedToken.workspace.name) {
return false
}
}
const workspaceInfos = await getWorkspacesById(db, workspaceId)
if (workspaceInfos.length === 0) {
@@ -82,8 +82,8 @@ test.describe('Workspace Archive tests', () => {
await page2.locator(`[id="${workspaceInfo.workspaceId}"]`).getByText('archived').waitFor()
})
await test.step('Check workspace is archived', async () => {
await page.reload()
await page.getByText('Workspace is archived').waitFor()
await page.reload() // Will redirect to select workspace page
await page.getByText('archived').waitFor()
})
await test.step('Restore workspace', async () => {
await page2.locator(`[id="${workspaceInfo.workspaceId}"]`).getByRole('button', { name: 'Unarchive' }).click()
@@ -94,6 +94,8 @@ test.describe('Workspace Archive tests', () => {
await test.step('Check workspace is active again', async () => {
await page.reload()
await selectWorkspacePage.selectWorkspace(workspaceInfo.workspaceName)
const issuesDetailsPage = new IssuesDetailsPage(page)
await issuesDetailsPage.checkIssue(newIssue)
})