fix: restructure workspace general settings (#10120)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov
2025-10-21 20:49:53 +07:00
committed by GitHub
parent 0ee7e4ed6d
commit 092f5965fc
17 changed files with 290 additions and 83 deletions
@@ -19,9 +19,10 @@
import type { Ref } from '@hcengineering/core'
import { Blob as PlatformBlob } from '@hcengineering/core'
import { Asset } from '@hcengineering/platform'
import presentation, { Card, getFileUrl } from '@hcengineering/presentation'
import {
import presentation, { Card, getFileUrl, uiContext } from '@hcengineering/presentation'
import ui, {
AnySvelteComponent,
Button,
ColorDefinition,
Label,
TabList,
@@ -171,6 +172,12 @@
}
}
function handleClearClick (): void {
selectedAvatar = undefined
selectedAvatarType = AvatarType.COLOR
selectedAvatarProps = { color: getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name }
}
const showColorPopup = (event: MouseEvent) => {
showPopup(
ColorsPopup,
@@ -238,6 +245,9 @@
on:select={handleDropdownSelection}
/>
</div>
<svelte:fragment slot="buttons">
<Button minWidth={'5rem'} label={ui.string.Clear} kind={'ghost'} size={'large'} on:click={handleClearClick} />
</svelte:fragment>
<svelte:fragment slot="footer">
{#if selectedAvatarType === AvatarType.GRAVATAR}
<div class="flex-col">
+6 -1
View File
@@ -197,6 +197,11 @@
"IntegrationIsUnstable": "Integrační služba má problémy. Některé funkce nemusí fungovat správně.",
"MinValue": "Minimální hodnota",
"MaxValue": "Maximální hodnota",
"IntegerOnly": "Pouze celá čísla"
"IntegerOnly": "Pouze celá čísla",
"AccessControl": "Řízení přístupu",
"DangerZone": "Nebezpečná zóna",
"ApiAccess": "Přístup k API",
"ApiToken": "API token",
"GenerateApiToken": "Vygenerovat API token"
}
}
+6 -1
View File
@@ -199,6 +199,11 @@
"IntegrationIsUnstable": "Der Integrationsdienst hat Probleme. Einige Funktionen funktionieren möglicherweise nicht richtig.",
"MinValue": "Minimalwert",
"MaxValue": "Maximalwert",
"IntegerOnly": "Nur ganze Zahlen"
"IntegerOnly": "Nur ganze Zahlen",
"AccessControl": "Zugriffskontrolle",
"DangerZone": "Gefahrenzone",
"ApiAccess": "API-Zugriff",
"ApiToken": "API-Token",
"GenerateApiToken": "API-Token generieren"
}
}
+6 -1
View File
@@ -199,6 +199,11 @@
"IntegrationIsUnstable": "Integration service is experiencing issues. Some features may not work properly.",
"MinValue": "Minimum value",
"MaxValue": "Maximum value",
"IntegerOnly": "Integer numbers only"
"IntegerOnly": "Integer numbers only",
"AccessControl": "Access control",
"DangerZone": "Danger zone",
"ApiAccess": "API access",
"ApiToken": "API token",
"GenerateApiToken": "Generate API token"
}
}
+6 -1
View File
@@ -190,6 +190,11 @@
"IntegrationIsUnstable": "El servicio de integración está experimentando problemas. Es posible que algunas funciones no funcionen correctamente.",
"MinValue": "Valor mínimo",
"MaxValue": "Valor máximo",
"IntegerOnly": "Solo números enteros"
"IntegerOnly": "Solo números enteros",
"AccessControl": "Control de acceso",
"DangerZone": "Zona de peligro",
"ApiAccess": "Acceso API",
"ApiToken": "Token API",
"GenerateApiToken": "Generar token API"
}
}
+6 -1
View File
@@ -199,6 +199,11 @@
"IntegrationIsUnstable": "Le service d'intégration rencontre des problèmes. Certaines fonctionnalités peuvent ne pas fonctionner correctement.",
"MinValue": "Valeur minimale",
"MaxValue": "Valeur maximale",
"IntegerOnly": "Nombres entiers uniquement"
"IntegerOnly": "Nombres entiers uniquement",
"AccessControl": "Contrôle d'accès",
"DangerZone": "Zone dangereuse",
"ApiAccess": "Accès API",
"ApiToken": "Token API",
"GenerateApiToken": "Générer un token API"
}
}
+6 -1
View File
@@ -199,6 +199,11 @@
"IntegrationIsUnstable": "L'integrazione è instabile. Alcune funzionalità potrebbero non funzionare correttamente.",
"MinValue": "Valore minimo",
"MaxValue": "Valore massimo",
"IntegerOnly": "Solo numeri interi"
"IntegerOnly": "Solo numeri interi",
"AccessControl": "Controllo accessi",
"DangerZone": "Zona pericolosa",
"ApiAccess": "Accesso API",
"ApiToken": "Token API",
"GenerateApiToken": "Genera token API"
}
}
+6 -1
View File
@@ -199,6 +199,11 @@
"IntegrationIsUnstable": "統合サービスに問題が発生しています。一部の機能が正しく動作しない可能性があります。",
"MinValue": "最小値",
"MaxValue": "最大値",
"IntegerOnly": "整数のみ"
"IntegerOnly": "整数のみ",
"AccessControl": "アクセス制御",
"DangerZone": "危険ゾーン",
"ApiAccess": "APIアクセス",
"ApiToken": "APIトークン",
"GenerateApiToken": "APIトークンを生成"
}
}
+9 -1
View File
@@ -187,6 +187,14 @@
"Disconnected": "Desconectado",
"Available": "Disponível",
"NotConnectedIntegration": "A conta {account} não está integrada com o espaço de trabalho",
"IntegrationIsUnstable": "O serviço de integração está enfrentando problemas. Algumas funcionalidades podem não funcionar corretamente."
"IntegrationIsUnstable": "O serviço de integração está enfrentando problemas. Algumas funcionalidades podem não funcionar corretamente.",
"MinValue": "Valor mínimo",
"MaxValue": "Valor máximo",
"IntegerOnly": "Apenas números inteiros",
"AccessControl": "Controle de acesso",
"DangerZone": "Zona de perigo",
"ApiAccess": "Acesso à API",
"ApiToken": "Token de API",
"GenerateApiToken": "Gerar token de API"
}
}
+6 -1
View File
@@ -199,6 +199,11 @@
"Disconnected": "Отключено",
"Available": "Доступно",
"NotConnectedIntegration": "Учетная запись {account} не интегрирована с рабочим пространством",
"IntegrationIsUnstable": "Сервис интеграции испытывает проблемы. Некоторые функции могут работать некорректно."
"IntegrationIsUnstable": "Сервис интеграции испытывает проблемы. Некоторые функции могут работать некорректно.",
"AccessControl": "Контроль доступа",
"DangerZone": "Опасная зона",
"ApiAccess": "Доступ к API",
"ApiToken": "API токен",
"GenerateApiToken": "Создать API токен"
}
}
+6 -1
View File
@@ -199,6 +199,11 @@
"IntegrationIsUnstable": "Entegrasyon servisi sorunlar yaşıyor. Bazı özellikler düzgün çalışmayabilir.",
"MinValue": "Minimum değer",
"MaxValue": "Maksimum değer",
"IntegerOnly": "Sadece tam sayılar"
"IntegerOnly": "Sadece tam sayılar",
"AccessControl": "Erişim kontrolü",
"DangerZone": "Tehlike bölgesi",
"ApiAccess": "API erişimi",
"ApiToken": "API token",
"GenerateApiToken": "API token oluştur"
}
}
+6 -1
View File
@@ -199,6 +199,11 @@
"IntegrationIsUnstable": "集成服务出现问题。某些功能可能无法正常工作。",
"MinValue": "最小值",
"MaxValue": "最大值",
"IntegerOnly": "仅整数"
"IntegerOnly": "仅整数",
"AccessControl": "访问控制",
"DangerZone": "危险区域",
"ApiAccess": "API访问",
"ApiToken": "API令牌",
"GenerateApiToken": "生成API令牌"
}
}
@@ -0,0 +1,104 @@
<!--
// Copyright © 2025 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
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an 'AS IS' BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Timestamp } from '@hcengineering/core'
import presentation, { copyTextToClipboard } from '@hcengineering/presentation'
import view from '@hcengineering/view'
import { Button, Label, ticker } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import settings from '../plugin'
export let token: string
const isSecureContext = window.isSecureContext
const dispatch = createEventDispatcher()
let copiedTime: Timestamp | undefined
let copied = false
$: if (copiedTime !== undefined && copied && $ticker - copiedTime > 1000) {
copied = false
}
async function copy (): Promise<void> {
if (!isSecureContext) return
if (token === undefined) return
await copyTextToClipboard(token)
copied = true
copiedTime = Date.now()
}
</script>
<div class="antiPopup popup" class:secure={isSecureContext}>
<div class="overflow-label fs-title mb-4">
<Label label={settings.string.ApiToken} />
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="token" class:notSecure={!isSecureContext} class:over-underline={isSecureContext} on:click={copy}>
{token}
</div>
<div class="buttons">
<Button
label={presentation.string.Close}
size={'medium'}
kind={'primary'}
on:click={() => {
dispatch('close')
}}
/>
{#if isSecureContext}
<Button label={copied ? view.string.Copied : view.string.CopyToClipboard} size={'medium'} on:click={copy} />
{/if}
</div>
</div>
<style lang="scss">
.popup {
display: flex;
flex-direction: column;
padding: 1.75rem;
width: 30rem;
max-width: 40rem;
background: var(--popup-bg-color);
border-radius: 1.25rem;
user-select: none;
box-shadow: var(--popup-shadow);
.token {
margin: 1.75rem 0 0;
overflow-wrap: break-word;
&.notSecure {
user-select: text;
}
}
.buttons {
margin-top: 1.75rem;
flex-shrink: 0;
display: grid;
grid-auto-flow: column;
direction: rtl;
justify-content: flex-start;
align-items: center;
column-gap: 0.5rem;
}
}
</style>
@@ -172,7 +172,7 @@
<Scroller padding={'1.5rem'} noStretch>
{#if loading}
<Loading size={'small'} />
<Loading />
{:else if backupInfo == null}
<div class="no-backups">
<Label label={setting.string.BackupNoBackup} />
@@ -37,13 +37,12 @@
Header,
IconCheckmark,
IconClose,
IconDelete,
IconEdit,
Label,
Loading,
navigate,
Scroller,
showPopup,
Spinner,
themeStore,
Toggle
} from '@hcengineering/ui'
@@ -57,11 +56,13 @@
import communication, { GuestCommunicationSettings } from '@hcengineering/communication'
import card, { Card } from '@hcengineering/card'
import chat from '@hcengineering/chat'
import ApiTokenPopup from './ApiTokenPopup.svelte'
let loading = true
let isEditingName = false
let oldName: string
let name: string = ''
let workspaceUrl = ''
let allowReadOnlyGuests: boolean
let allowGuestSignUp: boolean
@@ -80,6 +81,7 @@
async function loadWorkspaceName (): Promise<void> {
const res = await accountClient.getWorkspaceInfo()
workspaceUrl = res.url
oldName = res.name
name = oldName
allowReadOnlyGuests = res.allowReadOnlyGuest ?? false
@@ -126,25 +128,26 @@
})
async function handleAvatarDone (): Promise<void> {
if (workspaceSettings === undefined) {
const existing = await client.findOne(settingsRes.class.WorkspaceSetting, { _id: settingsRes.ids.WorkspaceSetting })
if (existing !== undefined) {
const avatar = await avatarEditor.createAvatar()
// Remove old avatar if changed
if (existing.icon != null && existing.icon !== avatar.avatar) {
await avatarEditor.removeAvatar(existing.icon)
}
const icon = avatar.avatarType === AvatarType.IMAGE ? avatar.avatar : null
await client.diffUpdate(existing, { icon })
} else {
const avatar = await avatarEditor.createAvatar()
await client.createDoc(
settingsRes.class.WorkspaceSetting,
core.space.Workspace,
{ icon: avatar.avatar },
settingsRes.ids.WorkspaceSetting
)
return
}
const avatar = await avatarEditor.createAvatar()
if (workspaceSettings.icon != null && workspaceSettings.icon !== avatar.avatar) {
// Different avatar
await avatarEditor.removeAvatar(workspaceSettings.icon)
}
await client.update(workspaceSettings, {
icon: avatar.avatar
})
}
const permissionConfigurationQuery = createQuery()
@@ -193,6 +196,11 @@
await accountClient.updateAllowGuestSignUp(e.detail)
}
async function handleGenerateApiToken (): Promise<void> {
const { token } = await accountClient.selectWorkspace(workspaceUrl)
showPopup(ApiTokenPopup, { token })
}
function handleTogglePermissions (): void {
const newState = !arePermissionsDisabled
showPopup(MessageBox, {
@@ -268,11 +276,13 @@
<div class="hulyComponent">
<Header adaptive={'disabled'}>
<Breadcrumb icon={settingsRes.icon.Setting} label={settingsRes.string.General} size={'large'} isCurrent />
<Breadcrumb icon={settingsRes.icon.Setting} label={settingsRes.string.WorkspaceSettings} size={'large'} isCurrent />
</Header>
<div class="hulyComponent-content__column content">
{#if loading}
<Spinner size={'small'} />
<div class="w-full h-full flex-col-center justify-center">
<Loading />
</div>
{:else}
<Scroller align={'center'} padding={'var(--spacing-3)'} bottomPadding={'var(--spacing-3)'}>
<div class="hulyComponent-content flex-col flex-gap-4">
@@ -280,10 +290,11 @@
<div class="ws">
<EditableAvatar
person={{
avatarType: AvatarType.IMAGE,
avatarType: workspaceSettings?.icon !== undefined ? AvatarType.IMAGE : AvatarType.COLOR,
avatar: workspaceSettings?.icon
}}
size="medium"
{name}
bind:this={avatarEditor}
on:done={handleAvatarDone}
imageOnly
@@ -307,13 +318,8 @@
{#if isEditingName}
<Button icon={IconClose} kind="ghost" size="small" on:click={handleCancelEditName} />
{/if}
<Button
icon={IconDelete}
kind="dangerous"
on:click={handleDelete}
showTooltip={{ label: settingsRes.string.DeleteWorkspace }}
/>
</div>
<div class="flex-col flex-gap-4 mt-6">
<div class="title"><Label label={settingsRes.string.Calendar} /></div>
<div class="flex-row-center flex-gap-4">
@@ -328,46 +334,80 @@
/>
</div>
</div>
<div class="title mt-6"><Label label={settingsRes.string.GuestAccess} /></div>
<div class="flex-row-center flex-gap-4">
<Label label={settingsRes.string.GuestAccessDescription} />
<Toggle
on={allowReadOnlyGuests}
on:change={(e) => {
void handleToggleReadonlyAccess(e)
}}
/>
<div class="flex-col flex-gap-4 mt-6">
<div class="title"><Label label={settingsRes.string.GuestAccess} /></div>
<div class="flex-row-center flex-gap-4">
<Label label={settingsRes.string.GuestAccessDescription} />
<Toggle
on={allowReadOnlyGuests}
on:change={(e) => {
void handleToggleReadonlyAccess(e)
}}
/>
</div>
<div class="flex-row-center flex-gap-4">
<Label label={settingsRes.string.GuestSignUpDescription} />
<Toggle
disabled={!allowReadOnlyGuests}
on={allowGuestSignUp}
on:change={(e) => {
void handleToggleGuestSignUp(e)
}}
/>
</div>
<div class="flex-row-center flex-gap-4">
<Label label={settingsRes.string.GuestChannelsDescription} />
<Component
is={card.component.CardArrayEditor}
props={{
_class: chat.masterTag.Thread,
value: existingGuestChatSettings !== undefined ? existingGuestChatSettings.allowedCards : [],
label: settingsRes.string.GuestChannelsArrayLabel,
onChange: onAllowedCardsChange
}}
/>
</div>
</div>
<div class="flex-row-center flex-gap-4">
<Label label={settingsRes.string.GuestSignUpDescription} />
<Toggle
disabled={!allowReadOnlyGuests}
on={allowGuestSignUp}
on:change={(e) => {
void handleToggleGuestSignUp(e)
}}
/>
<div class="flex-col flex-gap-4 mt-6">
<div class="title"><Label label={settingsRes.string.AccessControl} /></div>
<div class="w-32">
<Button
kind="regular"
label={arePermissionsDisabled
? settingsRes.string.EnablePermissions
: settingsRes.string.DisablePermissions}
on:click={handleTogglePermissions}
/>
</div>
</div>
<div class="flex-row-center flex-gap-4">
<Label label={settingsRes.string.GuestChannelsDescription} />
<Component
is={card.component.CardArrayEditor}
props={{
_class: chat.masterTag.Thread,
value: existingGuestChatSettings !== undefined ? existingGuestChatSettings.allowedCards : [],
label: settingsRes.string.GuestChannelsArrayLabel,
onChange: onAllowedCardsChange
}}
/>
<div class="flex-col flex-gap-4 mt-6">
<div class="title"><Label label={settingsRes.string.ApiAccess} /></div>
<div class="w-32">
<Button
label={settingsRes.string.GenerateApiToken}
kind="regular"
disabled={workspaceUrl === ''}
showTooltip={{ label: settingsRes.string.GenerateApiToken }}
on:click={handleGenerateApiToken}
/>
</div>
</div>
<div class="delete">
<Button
kind="regular"
label={arePermissionsDisabled
? settingsRes.string.EnablePermissions
: settingsRes.string.DisablePermissions}
on:click={handleTogglePermissions}
/>
<div class="flex-col flex-gap-4 mt-6">
<div class="title"><Label label={settingsRes.string.DangerZone} /></div>
<div class="w-32">
<Button
label={settingsRes.string.DeleteWorkspace}
kind="dangerous"
on:click={handleDelete}
showTooltip={{ label: settingsRes.string.DeleteWorkspace }}
/>
</div>
</div>
</div>
</Scroller>
@@ -389,8 +429,4 @@
.editBox {
width: 16rem;
}
.delete {
width: 6rem;
}
</style>
@@ -131,12 +131,6 @@
<Label label={setting.string.Primary} />
</div>
{/if}
{#if canRelease}
<div class="on-hover">
<Button label={setting.string.Release} kind="ghost" on:click={handleRelease} />
</div>
{/if}
</div>
<style lang="scss">
+5
View File
@@ -129,11 +129,16 @@ export default mergeIds(settingId, setting, {
DisablePermissionsConfirmation: '' as IntlString,
EnablePermissionsConfirmation: '' as IntlString,
BetaWarning: '' as IntlString,
DangerZone: '' as IntlString,
AccessControl: '' as IntlString,
GuestAccess: '' as IntlString,
GuestAccessDescription: '' as IntlString,
GuestSignUpDescription: '' as IntlString,
GuestChannelsDescription: '' as IntlString,
GuestChannelsArrayLabel: '' as IntlString,
ApiAccess: '' as IntlString,
ApiToken: '' as IntlString,
GenerateApiToken: '' as IntlString,
ManageIdentities: '' as IntlString,
AddNew: '' as IntlString,
Release: '' as IntlString,