Readonly fixes (#9407)

Signed-off-by: Anton Alexeyev <alexeyev.anton@gmail.com>
This commit is contained in:
Anton Alexeyev
2025-06-30 16:23:34 +07:00
committed by GitHub
parent 8d12b2c133
commit 5eccbe6464
34 changed files with 127 additions and 70 deletions
+1 -1
View File
@@ -21,7 +21,7 @@
"InvalidOtp": "Neplatný kód",
"InviteNotFound": "Pozvánka s email:{email} nebyla nalezena.",
"WorkspaceLimitReached": "Dosáhli jste limitu pracovních prostorů. Kontaktujte nás...",
"ReadOnlyAccount": "Režim pouze pro čtení",
"ReadOnlyAccount": "Ukázka pro hosty",
"SystemAccount": "Systémový účet"
}
}
+1 -1
View File
@@ -21,7 +21,7 @@
"InvalidOtp": "Ungültiger Code",
"InviteNotFound": "Einladung mit E-Mail: {email} nicht gefunden.",
"WorkspaceLimitReached": "Sie haben das Arbeitsbereichslimit erreicht. Bitte kontaktieren Sie uns...",
"ReadOnlyAccount": "Nur-Lese-Modus",
"ReadOnlyAccount": "Gast-Demo",
"SystemAccount": "Systemkonto"
}
}
+1 -1
View File
@@ -20,7 +20,7 @@
"InvalidOtp": "Invalid code",
"InviteNotFound": "Invitation with email:{email} not found.",
"WorkspaceLimitReached": "You have reached the workspace limit. Please contact us...",
"ReadOnlyAccount": "Read only mode",
"ReadOnlyAccount": "Guest Demo",
"SystemAccount": "System account"
}
}
+1 -1
View File
@@ -21,7 +21,7 @@
"InvalidOtp": "Código no válido",
"InviteNotFound": "No se encontró la invitación con email:{email}.",
"WorkspaceLimitReached": "Ha alcanzado el límite de espacios de trabajo. Póngase en contacto con nosotros...",
"ReadOnlyAccount": "Modo de solo lectura",
"ReadOnlyAccount": "Demo de Invitado",
"SystemAccount": "Cuenta del sistema"
}
}
+1 -1
View File
@@ -21,7 +21,7 @@
"InvalidOtp": "Code invalide",
"InviteNotFound": "Invitation avec l'email:{email} introuvable.",
"WorkspaceLimitReached": "Vous avez atteint la limite d'espace de travail. Veuillez contacter nous...",
"ReadOnlyAccount": "Mode lecture seule",
"ReadOnlyAccount": "Démo Invité",
"SystemAccount": "Compte système"
}
}
+1 -1
View File
@@ -21,7 +21,7 @@
"InvalidOtp": "Codice non valido",
"InviteNotFound": "Invito con email:{email} non trovato.",
"WorkspaceLimitReached": "Hai raggiunto il limite di spazi di lavoro. Contattaci...",
"ReadOnlyAccount": "Modalità sola lettura",
"ReadOnlyAccount": "Demo Ospite",
"SystemAccount": "Account di sistema"
}
}
+1 -1
View File
@@ -20,7 +20,7 @@
"InvalidOtp": "無効なコードです",
"InviteNotFound": "メールアドレス {email} に対する招待が見つかりませんでした",
"WorkspaceLimitReached": "作成可能なワークスペースの上限に達しました。お問い合わせください",
"ReadOnlyAccount": "読み取り専用モード",
"ReadOnlyAccount": "ゲストデモ",
"SystemAccount": "システムアカウント"
}
}
+1 -1
View File
@@ -21,7 +21,7 @@
"InvalidOtp": "Código inválido",
"InviteNotFound": "Convite com email:{email} não encontrado.",
"WorkspaceLimitReached": "Você atingiu o limite de espaço de trabalho. Entre em contato conosco...",
"ReadOnlyAccount": "Modo somente leitura",
"ReadOnlyAccount": "Demonstração para Convidados",
"SystemAccount": "Conta do sistema"
}
}
+1 -1
View File
@@ -21,7 +21,7 @@
"InvalidOtp": "Неверный код",
"InviteNotFound": "Приглашение с email:{email} не найдено.",
"WorkspaceLimitReached": "Вы достигли лимита рабочих пространств. Свяжитесь с нами...",
"ReadOnlyAccount": "Режим только для чтения",
"ReadOnlyAccount": "Гостевое демо",
"SystemAccount": "Системный аккаунт"
}
}
+1 -1
View File
@@ -21,6 +21,6 @@
"InvalidOtp": "无效的代码",
"InviteNotFound": "未找到 id 为 {email} 的邀请。",
"WorkspaceLimitReached": "您已达到工作区限制。请联系我们...",
"ReadOnlyAccount": "只读模式"
"ReadOnlyAccount": "访客演示"
}
}
@@ -33,7 +33,7 @@
let doc: Doc | undefined = undefined
let broken: boolean = false
const withoutDoc = [contact.mention.Here, contact.mention.Everyone]
const withoutDoc: Ref<Doc>[] = [contact.mention.Here, contact.mention.Everyone]
$: icon =
_class !== undefined && hierarchy.hasClass(_class) && !withoutDoc.includes(_id as any)
@@ -48,7 +48,7 @@
}
function onBrokenLinkClick (event: MouseEvent): void {
if (withoutDoc) return
if (_id !== undefined && withoutDoc.includes(_id)) return
showPopup(MessageBox, {
label: presentation.string.UnableToFollowMention,
message: presentation.string.AccessDenied,
+4
View File
@@ -547,6 +547,10 @@
color: var(--theme-broken-mention-color);
background-color: var(--theme-broken-mention-bg-color);
--theme-mention-focused-bg-color: var(--theme-broken-mention-focused-bg-color);
span {
color: var(--theme-broken-mention-color);
}
}
> svg {
-1
View File
@@ -38,7 +38,6 @@ export default mergeIds(loginId, login, {
Password: '' as IntlString,
PasswordRepeat: '' as IntlString,
Workspace: '' as IntlString,
LogIn: '' as IntlString,
SignUp: '' as IntlString,
SelectWorkspace: '' as IntlString,
DoNotHaveAnAccount: '' as IntlString,
+1
View File
@@ -68,6 +68,7 @@ export default plugin(loginId, {
InviteWorkspace: '' as Asset
},
string: {
LogIn: '' as IntlString,
LinkValidHours: '' as IntlString,
EmailMask: '' as IntlString,
NoLimit: '' as IntlString,
@@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, getCurrentAccount, Ref } from '@hcengineering/core'
import { AccountRole, Class, Doc, getCurrentAccount, Ref } from '@hcengineering/core'
import notification, { BrowserNotification } from '@hcengineering/notification'
import { createQuery, getClient } from '@hcengineering/presentation'
import { addNotification, getCurrentResolvedLocation, Location, NotificationSeverity } from '@hcengineering/ui'
@@ -54,6 +54,7 @@
)
}
const account = getCurrentAccount()
const client = getClient()
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
@@ -85,14 +86,14 @@
const sidebarObjectId = getSidebarObject()?._id
if (_id && _id === sidebarObjectId) {
await client.remove(value)
await removeNotification(value)
return
}
const locObjectId = await getObjectIdFromLocation(getCurrentResolvedLocation())
if (_id && _id === locObjectId) {
await client.remove(value)
await removeNotification(value)
return
}
addNotification(
@@ -103,7 +104,13 @@
NotificationSeverity.Info,
`notification-${value.objectId}`
)
await client.remove(value)
await removeNotification(value)
}
async function removeNotification (value: BrowserNotification): Promise<void> {
if (account.role !== AccountRole.ReadOnlyGuest) {
await client.remove(value)
}
}
const query = createQuery()
@@ -23,7 +23,7 @@
import { createQuery, getClient } from '@hcengineering/presentation'
import { getDocTitle, getDocIdentifier, Menu } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { Class, Doc, Ref, WithLookup } from '@hcengineering/core'
import { AccountRole, Class, Doc, getCurrentAccount, Ref, WithLookup } from '@hcengineering/core'
import chunter from '@hcengineering/chunter'
import { getPersonRefsByPersonIds } from '@hcengineering/contact-resources'
import { Person } from '@hcengineering/contact'
@@ -40,6 +40,7 @@
const maxNotifications = 3
const account = getCurrentAccount()
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
@@ -226,7 +227,7 @@
<div class="flex-center min-w-6">
{#if isArchiving}
<Spinner size="small" />
{:else}
{:else if account.role !== AccountRole.ReadOnlyGuest}
<CheckBox checked={archived} kind="todo" size="medium" on:value={checkContext} />
{/if}
</div>
@@ -14,14 +14,15 @@
//
import activity from '@hcengineering/activity'
import core, {
SortingOrder,
getCurrentAccount,
toIdMap,
type Account,
AccountRole,
type Client,
type Doc,
getCurrentAccount,
type IdMap,
type Ref,
SortingOrder,
toIdMap,
type TxOperations,
type WithLookup
} from '@hcengineering/core'
@@ -152,7 +153,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
async readDoc (_id: Ref<Doc>): Promise<void> {
const docNotifyContext = this._contextByDoc.get(_id)
if (docNotifyContext === undefined) {
if (docNotifyContext === undefined || getCurrentAccount().role === AccountRole.ReadOnlyGuest) {
return
}
+3 -2
View File
@@ -135,8 +135,9 @@
"Document": "Dokument",
"Loading": "Načítání...",
"NoRelations": "Žádné vztahy",
"ReadOnlyWarningTitle": "Režim pouze pro čtení",
"ReadOnlyWarningMessage": "Pro úplný přístup k tomuto pracovnímu prostoru kontaktujte administrátora.",
"ReadOnlyWarningTitle": "✨ Ukázka pro hosty",
"ReadOnlyWarningMessage": "Jste v režimu prohlížení. Chcete spravovat jakékoli projekty v Huly? Úkoly, termíny, tým — zaregistrujte se pro plný přístup!",
"ReadOnlySignUp": "Zaregistrovat se \uD83D\uDE80",
"Icon": "Ikona",
"Color": "Barva"
}
+3 -2
View File
@@ -135,8 +135,9 @@
"Document": "Dokument",
"Loading": "Laden...",
"NoRelations": "Keine Beziehungen",
"ReadOnlyWarningTitle": "Nur-Lese-Modus",
"ReadOnlyWarningMessage": "Wenden Sie sich an den Administrator, um vollen Zugriff auf diesen Arbeitsbereich zu erhalten.",
"ReadOnlyWarningTitle": "✨ Gast-Demo",
"ReadOnlyWarningMessage": "Sie sind im Ansichtsmodus. Möchten Sie beliebige Projekte in Huly verwalten? Aufgaben, Fristen, Team — registrieren Sie sich für vollen Zugriff!",
"ReadOnlySignUp": "Registrieren \uD83D\uDE80",
"Icon": "Symbol",
"Color": "Farbe"
}
+3 -2
View File
@@ -135,8 +135,9 @@
"Document": "Document",
"Loading": "Loading...",
"NoRelations": "No relations",
"ReadOnlyWarningTitle": "Read only mode",
"ReadOnlyWarningMessage": "Contact the administrator to gain full access to this workspace.",
"ReadOnlyWarningTitle": "✨ Guest Demo",
"ReadOnlyWarningMessage": "You're in view-only mode. Want to manage any project in Huly? Tasks, deadlines, team — sign up for full access!",
"ReadOnlySignUp": "Sign up \uD83D\uDE80",
"Icon": "Icon",
"Color": "Color"
}
+3 -2
View File
@@ -130,8 +130,9 @@
"Document": "Documento",
"Loading": "Cargando...",
"NoRelations": "No hay relaciones",
"ReadOnlyWarningTitle": "Modo de solo lectura",
"ReadOnlyWarningMessage": "Contacte al administrador para obtener acceso completo a este espacio de trabajo.",
"ReadOnlyWarningTitle": "✨ Demo de Invitado",
"ReadOnlyWarningMessage": "Estás en modo de solo lectura. ¿Quieres gestionar cualquier proyecto en Huly? Tareas, plazos, equipo — ¡regístrate para acceso completo!",
"ReadOnlySignUp": "Registrarse \uD83D\uDE80",
"Icon": "Icono",
"Color": "Color"
}
+3 -2
View File
@@ -130,8 +130,9 @@
"Document": "Document",
"Loading": "Chargement...",
"NoRelations": "Aucune relation",
"ReadOnlyWarningTitle": "Mode lecture seule",
"ReadOnlyWarningMessage": "Contactez l'administrateur pour obtenir un accès complet à cet espace de travail.",
"ReadOnlyWarningTitle": "✨ Démo Invité",
"ReadOnlyWarningMessage": "Vous êtes en mode consultation. Voulez-vous gérer tous types de projets dans Huly ? Tâches, délais, équipe — inscrivez-vous pour un accès complet !",
"ReadOnlySignUp": "Inscription \uD83D\uDE80",
"Icon": "Icône",
"Color": "Couleur"
}
+3 -2
View File
@@ -130,8 +130,9 @@
"Document": "Documento",
"Loading": "Caricamento...",
"NoRelations": "Nessuna relazione",
"ReadOnlyWarningTitle": "Modalità sola lettura",
"ReadOnlyWarningMessage": "Contatta l'amministratore per ottenere l'accesso completo a quest'area di lavoro.",
"ReadOnlyWarningTitle": "✨ Demo Ospite",
"ReadOnlyWarningMessage": "Sei in modalità di sola lettura. Vuoi gestire qualsiasi progetto su Huly? Task, scadenze, team — registrati per l'accesso completo!",
"ReadOnlySignUp": "Registrati \uD83D\uDE80",
"Icon": "Icona",
"Color": "Colore"
}
+3 -2
View File
@@ -130,8 +130,9 @@
"Document": "ドキュメント",
"Loading": "読み込み中...",
"NoRelations": "関係なし",
"ReadOnlyWarningTitle": "読み取り専用モード",
"ReadOnlyWarningMessage": "ワークスペースへのフルアクセス権を取得するには管理者に連絡してください",
"ReadOnlyWarningTitle": "✨ ゲストデモ",
"ReadOnlyWarningMessage": "閲覧モードです。Hulyでどんなプロジェクトも管理しませんか? タスク、期限、チーム — 完全アクセスには登録してください",
"ReadOnlySignUp": "サインアップ \uD83D\uDE80",
"Icon": "アイコン",
"Color": "色"
}
+3 -2
View File
@@ -130,8 +130,9 @@
"Document": "Documento",
"Loading": "Carregando...",
"NoRelations": "Sem relações",
"ReadOnlyWarningTitle": "Modo somente leitura",
"ReadOnlyWarningMessage": "Contate o administrador para obter acesso completo a este espaço de trabalho.",
"ReadOnlyWarningTitle": "✨ Demonstração para Convidados",
"ReadOnlyWarningMessage": "Você está no modo de visualização. Quer gerenciar qualquer projeto no Huly? Tarefas, prazos, equipe — cadastre-se para acesso total!",
"ReadOnlySignUp": "Registar \uD83D\uDE80",
"Icon": "Ícone",
"Color": "Cor"
}
+3 -2
View File
@@ -132,8 +132,9 @@
"Document": "Документ",
"Loading": "Загрузка...",
"NoRelations": "Нет связей",
"ReadOnlyWarningTitle": "Режим только для чтения",
"ReadOnlyWarningMessage": "Обратитесь к администратору для получения полного доступа к рабочему пространству.",
"ReadOnlyWarningTitle": "✨ Гостевое демо",
"ReadOnlyWarningMessage": "Вы в режиме просмотра. Хотите управлять любыми проектами в Huly? Задачи, сроки, команда — регистрируйтесь для полного доступа!",
"ReadOnlySignUp": "Регистрация \uD83D\uDE80",
"Icon": "Иконка",
"Color": "Цвет"
}
+3 -2
View File
@@ -135,8 +135,9 @@
"Document": "文档",
"Loading": "加载中...",
"NoRelations": "无关系",
"ReadOnlyWarningTitle": "只读模式",
"ReadOnlyWarningMessage": "请联系管理员获取此工作区的完全访问权限",
"ReadOnlyWarningTitle": "✨ 访客演示",
"ReadOnlyWarningMessage": "您处于只读模式。想在Huly管理任何项目吗?任务、截止日期、团队 — 立即注册获取完整权限",
"ReadOnlySignUp": "注册 \uD83D\uDE80",
"Icon": "图标",
"Color": "颜色"
}
@@ -14,8 +14,8 @@
-->
<script lang="ts">
import { concatLink, Doc, Hierarchy } from '@hcengineering/core'
import presentation, { NavLink, getClient } from '@hcengineering/presentation'
import { AnyComponent, getPanelURI, locationToUrl } from '@hcengineering/ui'
import presentation, { NavLink, getClient, createQuery, MessageBox } from '@hcengineering/presentation'
import { AnyComponent, getPanelURI, locationToUrl, showPopup } from '@hcengineering/ui'
import view from '../plugin'
import { getObjectLinkFragment, restrictionStore } from '../utils'
import { getMetadata } from '@hcengineering/platform'
@@ -37,11 +37,20 @@
export let noSelect: boolean = true
export let title: string | undefined = undefined
const docQuery = createQuery()
const client = getClient()
const hierarchy = client.getHierarchy()
let broken: boolean = false
let _disabled = disabled || $restrictionStore.disableNavigation
$: _disabled = disabled || $restrictionStore.disableNavigation
const client = getClient()
const hierarchy = client.getHierarchy()
$: if (object?._class != null && object?._id != null && hierarchy.hasClass(object?._class)) {
docQuery.query(object?._class, { _id: object?._id }, (r) => {
broken = r.shift() === undefined
})
}
let href: string | undefined =
object !== undefined
@@ -61,23 +70,39 @@
}
$: if (object !== undefined) getHref(object)
function onBrokenLinkClick (event: MouseEvent): void {
showPopup(MessageBox, {
label: presentation.string.UnableToFollowMention,
message: presentation.string.AccessDenied,
canSubmit: false
})
}
</script>
<NavLink
disabled={_disabled}
{onClick}
{noUnderline}
{inline}
{shrink}
{href}
{colorInherit}
{accent}
{noOverflow}
{inlineReference}
{transparent}
{inlineBlock}
{noSelect}
{title}
>
<slot />
</NavLink>
{#if broken}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span class="antiMention" class:transparent class:broken on:click={onBrokenLinkClick}>
<slot />
</span>
{:else}
<NavLink
disabled={_disabled}
{onClick}
{noUnderline}
{inline}
{shrink}
{href}
{colorInherit}
{accent}
{noOverflow}
{inlineReference}
{transparent}
{inlineBlock}
{noSelect}
{title}
>
<slot />
</NavLink>
{/if}
@@ -1,5 +1,6 @@
<script lang="ts">
import { Notification, NotificationToast } from '@hcengineering/ui'
import { Button, Notification, NotificationToast } from '@hcengineering/ui'
import view from '@hcengineering/view'
export let onRemove: () => void
export let notification: Notification
@@ -9,4 +10,10 @@
<svelte:fragment slot="content">
{notification.subTitle}
</svelte:fragment>
<svelte:fragment slot="buttons">
<div style="width: auto" />
<a href="https://huly.io/signup" target="_blank">
<Button label={view.string.ReadOnlySignUp} stopPropagation={false} />
</a>
</svelte:fragment>
</NotificationToast>
+1
View File
@@ -236,6 +236,7 @@ const view = plugin(viewId, {
Loading: '' as IntlString,
ReadOnlyWarningTitle: '' as IntlString,
ReadOnlyWarningMessage: '' as IntlString,
ReadOnlySignUp: '' as IntlString,
Icon: '' as IntlString,
Color: '' as IntlString
},
@@ -162,7 +162,7 @@
},
{
icon: setting.icon.Signout,
label: setting.string.Signout,
label: hasAccountRole(account, AccountRole.DocGuest) ? setting.string.Signout : login.string.LogIn,
action: async () => {
await logOut()
navigate({ path: [loginId] })
+3 -1
View File
@@ -218,7 +218,9 @@ export async function logIn (loginInfo: { account: string, token?: string }): Pr
export async function logOut (): Promise<void> {
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
await getAccountClient(accountsUrl).deleteCookie()
try {
await getAccountClient(accountsUrl).deleteCookie()
} catch (error) {}
const currentWorkspace = getMetadata(presentation.metadata.WorkspaceUuid)
if (currentWorkspace !== undefined) {
@@ -358,7 +358,7 @@ export async function pushInboxNotifications (
const notificationData = {
user: receiver.account,
isViewed: false,
isViewed: receiver.role === 'GUEST',
docNotifyContext: docNotifyContextId,
archived: false,
objectId,
@@ -221,7 +221,6 @@ export async function isShouldNotifyTx (
notificationControl: NotificationProviderControl,
docUpdateMessage?: DocUpdateMessage
): Promise<NotifyResult> {
if (receiver.role === 'GUEST') return new Map<Ref<NotificationProvider>, NotificationType[]>()
const types = getMatchedTypes(control, tx, isOwn, isSpace, docUpdateMessage?.attributeUpdates?.attrKey)
const modifiedByPersonId = tx.modifiedBy
const result = new Map<Ref<NotificationProvider>, NotificationType[]>()