mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-05 17:27:41 +02:00
feat: Add ability to configure guest permissions (#10708)
* Configure guest permissions Signed-off-by: Artem Savchenko <armisav@gmail.com> * Fix permission domain Signed-off-by: Artem Savchenko <armisav@gmail.com> * Fix permission declaration Signed-off-by: Artem Savchenko <armisav@gmail.com> * Fix lock file Signed-off-by: Artem Savchenko <armisav@gmail.com> * Guest permissions Signed-off-by: Artem Savchenko <armisav@gmail.com> * Allow guests to update their own documents Signed-off-by: Artem Savchenko <armisav@gmail.com> * Add modules order Signed-off-by: Artem Savchenko <armisav@gmail.com> * Fix translations, icons Signed-off-by: Artem Savchenko <armisav@gmail.com> * Fix disabled apps and update translations Signed-off-by: Artem Savchenko <armisav@gmail.com> --------- Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
@@ -582,6 +582,19 @@ export interface ClassPermission extends Permission {
|
||||
targetClass: Ref<Class<Doc>>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ModulePermissionGroup extends Doc {
|
||||
application: Ref<Doc>
|
||||
role: AccountRole
|
||||
permissions: Ref<Permission>[]
|
||||
disabledPermissions?: Ref<Permission>[]
|
||||
spaceClass: Ref<Class<Space>>
|
||||
enabled: boolean
|
||||
order?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
||||
@@ -43,6 +43,7 @@ import type {
|
||||
MarkupBlobRef,
|
||||
MigrationState,
|
||||
Mixin,
|
||||
ModulePermissionGroup,
|
||||
Obj,
|
||||
Permission,
|
||||
PersonId,
|
||||
@@ -180,7 +181,8 @@ export default plugin(coreId, {
|
||||
Sequence: '' as Ref<Class<Sequence>>,
|
||||
CustomSequence: '' as Ref<Class<CustomSequence>>,
|
||||
ClassCollaborators: '' as Ref<Class<ClassCollaborators<Doc>>>,
|
||||
Collaborator: '' as Ref<Class<Collaborator>>
|
||||
Collaborator: '' as Ref<Class<Collaborator>>,
|
||||
ModulePermissionGroup: '' as Ref<Class<ModulePermissionGroup>>
|
||||
},
|
||||
icon: {
|
||||
TypeString: '' as Asset,
|
||||
@@ -279,6 +281,7 @@ export default plugin(coreId, {
|
||||
Account: '' as IntlString,
|
||||
StatusCategory: '' as IntlString,
|
||||
Rank: '' as IntlString,
|
||||
Order: '' as IntlString,
|
||||
Members: '' as IntlString,
|
||||
Owners: '' as IntlString,
|
||||
Permission: '' as IntlString,
|
||||
|
||||
@@ -7,10 +7,14 @@ import {
|
||||
import core, {
|
||||
type Account,
|
||||
AccountRole,
|
||||
type Class,
|
||||
type Doc,
|
||||
type ClassPermission,
|
||||
type Permission,
|
||||
hasAccountRole,
|
||||
type MeasureContext,
|
||||
type PersonId,
|
||||
type Ref,
|
||||
type SessionData,
|
||||
type Space,
|
||||
type Tx,
|
||||
@@ -22,7 +26,15 @@ import core, {
|
||||
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
|
||||
import contact, { type Person } from '@hcengineering/contact'
|
||||
|
||||
/** Cached state loaded from GuestPermissionsSettings configuration document. */
|
||||
interface GuestPermissionsCache {
|
||||
roleAllowedClasses: Map<AccountRole, Set<Ref<Class<Doc>>>>
|
||||
}
|
||||
|
||||
export class GuestPermissionsMiddleware extends BaseMiddleware implements Middleware {
|
||||
private permissionsCache: GuestPermissionsCache | undefined = undefined
|
||||
private initPromise: Promise<void> | undefined = undefined
|
||||
|
||||
static async create (
|
||||
ctx: MeasureContext,
|
||||
context: PipelineContext,
|
||||
@@ -31,9 +43,86 @@ export class GuestPermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
return new GuestPermissionsMiddleware(context, next)
|
||||
}
|
||||
|
||||
private async getPermissionsCache (ctx: MeasureContext): Promise<GuestPermissionsCache> {
|
||||
if (this.permissionsCache !== undefined) return this.permissionsCache
|
||||
if (this.initPromise === undefined) {
|
||||
this.initPromise = this.loadPermissionsCache(ctx)
|
||||
}
|
||||
await this.initPromise
|
||||
this.initPromise = undefined
|
||||
return this.permissionsCache ?? { roleAllowedClasses: new Map() }
|
||||
}
|
||||
|
||||
private async loadPermissionsCache (ctx: MeasureContext): Promise<void> {
|
||||
try {
|
||||
const docs = await this.findAll(ctx, core.class.ModulePermissionGroup, {}, {})
|
||||
if (docs.length > 0) {
|
||||
const rolePermissions = new Map<AccountRole, Set<Ref<Permission>>>()
|
||||
const allPermissionIds = new Set<Ref<Permission>>()
|
||||
for (const group of docs as any[]) {
|
||||
if (group.enabled === false) continue
|
||||
const role = ((group.role as AccountRole | undefined) ??
|
||||
(Array.isArray(group.roles) && group.roles.length > 0 ? (group.roles[0] as AccountRole) : undefined) ??
|
||||
AccountRole.Guest) as AccountRole
|
||||
const permissions = (group.permissions ?? []) as Ref<Permission>[]
|
||||
const disabled = new Set<Ref<Permission>>((group.disabledPermissions ?? []) as Ref<Permission>[])
|
||||
const current = rolePermissions.get(role) ?? new Set<Ref<Permission>>()
|
||||
for (const permissionId of permissions) {
|
||||
if (disabled.has(permissionId)) continue
|
||||
current.add(permissionId)
|
||||
allPermissionIds.add(permissionId)
|
||||
}
|
||||
rolePermissions.set(role, current)
|
||||
}
|
||||
const classPermissions =
|
||||
allPermissionIds.size > 0
|
||||
? await this.findAll(
|
||||
ctx,
|
||||
core.class.ClassPermission as Ref<Class<Doc>>,
|
||||
{ _id: { $in: Array.from(allPermissionIds) } } as any
|
||||
)
|
||||
: []
|
||||
const permissionToClass = new Map<Ref<Permission>, Ref<Class<Doc>>>(
|
||||
classPermissions
|
||||
.map(
|
||||
(permission) => [permission._id as Ref<Permission>, (permission as ClassPermission).targetClass] as const
|
||||
)
|
||||
.filter((entry): entry is readonly [Ref<Permission>, Ref<Class<Doc>>] => entry[1] !== undefined)
|
||||
)
|
||||
const roleAllowedClasses = new Map<AccountRole, Set<Ref<Class<Doc>>>>()
|
||||
for (const [role, permissions] of rolePermissions.entries()) {
|
||||
const allowedClasses = new Set<Ref<Class<Doc>>>()
|
||||
for (const permissionId of permissions) {
|
||||
const targetClass = permissionToClass.get(permissionId)
|
||||
if (targetClass !== undefined) allowedClasses.add(targetClass)
|
||||
}
|
||||
roleAllowedClasses.set(role, allowedClasses)
|
||||
}
|
||||
this.permissionsCache = { roleAllowedClasses }
|
||||
} else {
|
||||
this.permissionsCache = { roleAllowedClasses: new Map() }
|
||||
}
|
||||
} catch {
|
||||
this.permissionsCache = { roleAllowedClasses: new Map() }
|
||||
}
|
||||
}
|
||||
|
||||
private invalidateCacheIfNeeded (txes: Tx[]): void {
|
||||
for (const tx of txes) {
|
||||
if (TxProcessor.isExtendsCUD(tx._class)) {
|
||||
const cudTx = tx as TxCUD<Doc>
|
||||
if (cudTx.objectClass === core.class.ModulePermissionGroup) {
|
||||
this.permissionsCache = undefined
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async tx (ctx: MeasureContext<SessionData>, txes: Tx[]): Promise<TxMiddlewareResult> {
|
||||
const account = ctx.contextData.account
|
||||
if (hasAccountRole(account, AccountRole.User)) {
|
||||
this.invalidateCacheIfNeeded(txes)
|
||||
return await this.provideTx(ctx, txes)
|
||||
}
|
||||
|
||||
@@ -71,9 +160,64 @@ export class GuestPermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the covered-class ancestor of the objectClass if one exists in the new permissions model,
|
||||
* or undefined if the class is not covered.
|
||||
*/
|
||||
private getCoveredClass (
|
||||
objectClass: Ref<Class<Doc>>,
|
||||
allowedClasses: Set<Ref<Class<Doc>>>
|
||||
): Ref<Class<Doc>> | undefined {
|
||||
if (allowedClasses.size === 0) return undefined
|
||||
const h = this.context.hierarchy
|
||||
for (const coveredClass of allowedClasses) {
|
||||
if (h.isDerived(objectClass, coveredClass)) {
|
||||
return coveredClass
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
private isCreatedByAccount (doc: Doc, account: Account): boolean {
|
||||
const creator = doc.createdBy
|
||||
if (creator === undefined) return false
|
||||
if (creator === account.primarySocialId) return true
|
||||
return account.socialIds.includes(creator)
|
||||
}
|
||||
|
||||
private async isGuestMutationOnOwnDoc (ctx: MeasureContext, tx: TxCUD<Doc>, account: Account): Promise<boolean> {
|
||||
if (tx._class !== core.class.TxUpdateDoc && tx._class !== core.class.TxRemoveDoc) return false
|
||||
const docs = await this.findAll(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 })
|
||||
const doc = docs[0] as Doc | undefined
|
||||
if (doc === undefined) return false
|
||||
return this.isCreatedByAccount(doc, account)
|
||||
}
|
||||
|
||||
private async isForbiddenTx (ctx: MeasureContext, tx: TxCUD<Doc>, account: Account): Promise<boolean> {
|
||||
if (tx._class === core.class.TxMixin) return false
|
||||
return !(await this.hasMixinAccessLevel(ctx, tx, account))
|
||||
|
||||
// For TxCreateDoc, check the new permission model first for covered types.
|
||||
if (tx._class === core.class.TxCreateDoc) {
|
||||
const cache = await this.getPermissionsCache(ctx)
|
||||
const roleAllowedClasses = cache.roleAllowedClasses.get(account.role) ?? new Set<Ref<Class<Doc>>>()
|
||||
const coveredClass = this.getCoveredClass(tx.objectClass, roleAllowedClasses)
|
||||
if (coveredClass !== undefined) {
|
||||
return false
|
||||
}
|
||||
// Uncovered class: fall through to TxAccessLevel check.
|
||||
}
|
||||
|
||||
if (await this.hasMixinAccessLevel(ctx, tx, account)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (tx._class === core.class.TxUpdateDoc || tx._class === core.class.TxRemoveDoc) {
|
||||
if (await this.isGuestMutationOnOwnDoc(ctx, tx, account)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private async isForbiddenSpaceTx (ctx: MeasureContext, tx: TxCUD<Space>, account: Account): Promise<boolean> {
|
||||
|
||||
@@ -0,0 +1,491 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
/**
|
||||
* Tests for GuestPermissionsMiddleware
|
||||
*
|
||||
* Verifies that:
|
||||
* - Non-guest users pass through without restriction.
|
||||
* - DocGuest / ReadOnlyGuest users are always forbidden.
|
||||
* - For covered classes (resolved from module allowedPermissions):
|
||||
* new permission model is authoritative; TxAccessLevel is ignored.
|
||||
* Create in any space → permitted.
|
||||
* - For uncovered classes: TxAccessLevel fallback is used.
|
||||
*/
|
||||
|
||||
import core, {
|
||||
AccountRole,
|
||||
generateId,
|
||||
Hierarchy,
|
||||
MeasureMetricsContext,
|
||||
type Account,
|
||||
type Class,
|
||||
type Doc,
|
||||
type MeasureContext,
|
||||
type PersonId,
|
||||
type Ref,
|
||||
type SessionData,
|
||||
type Space,
|
||||
type Tx,
|
||||
TxFactory
|
||||
} from '@hcengineering/core'
|
||||
import type { PipelineContext, TxMiddlewareResult } from '@hcengineering/server-core'
|
||||
import { GuestPermissionsMiddleware } from '../guestPermissions'
|
||||
|
||||
const COVERED_CLASS = 'test:class:CoveredClass' as Ref<Class<Doc>>
|
||||
const UNCOVERED_CLASS = 'test:class:UncoveredClass' as Ref<Class<Doc>>
|
||||
const COVERED_CLASS_PERMISSION = 'test:permission:CoveredClassPermission' as Ref<Doc>
|
||||
const MODULE_PERMISSION_GROUP_CLASS = core.class.ModulePermissionGroup
|
||||
const ALLOWED_SPACE = 'test:space:Allowed' as Ref<Space>
|
||||
const FORBIDDEN_SPACE = 'test:space:Forbidden' as Ref<Space>
|
||||
|
||||
function makeAccount (role: AccountRole): Account {
|
||||
return {
|
||||
uuid: generateId() as any,
|
||||
role,
|
||||
primarySocialId: 'test' as PersonId,
|
||||
socialIds: ['test' as PersonId],
|
||||
fullSocialIds: []
|
||||
}
|
||||
}
|
||||
|
||||
function makeCtx (account: Account): MeasureContext<SessionData> {
|
||||
const ctx = new MeasureMetricsContext('test', {}) as MeasureContext<SessionData>
|
||||
ctx.contextData = {
|
||||
account,
|
||||
broadcast: { txes: [], queue: [], sessions: {} }
|
||||
} as any
|
||||
return ctx
|
||||
}
|
||||
|
||||
type FindAllFn = (ctx: MeasureContext, _class: Ref<Class<Doc>>, query: object, options?: object) => Promise<Doc[]>
|
||||
|
||||
function makePipelineContext (findAll?: FindAllFn): PipelineContext {
|
||||
const hierarchy = new Hierarchy()
|
||||
const model = { findAllSync: (_class: any, _query: any) => [] } as any
|
||||
return {
|
||||
workspace: { uuid: 'test-workspace' as any, url: 'test', dataId: 'test' as any },
|
||||
hierarchy,
|
||||
modelDb: model,
|
||||
branding: null as any,
|
||||
adapterManager: {} as any,
|
||||
storageAdapter: {} as any,
|
||||
contextVars: {},
|
||||
lastTx: '',
|
||||
lastHash: '',
|
||||
broadcastEvent: async () => {}
|
||||
} as any
|
||||
}
|
||||
|
||||
function makeMiddleware (
|
||||
findAll: FindAllFn,
|
||||
nextFn?: (ctx: MeasureContext, txes: Tx[]) => Promise<TxMiddlewareResult>
|
||||
): GuestPermissionsMiddleware {
|
||||
const context = makePipelineContext(findAll)
|
||||
const next = nextFn !== undefined ? { tx: nextFn } : { tx: async (_ctx: MeasureContext, _txes: Tx[]) => ({}) }
|
||||
const mw = new (GuestPermissionsMiddleware as any)(context, next)
|
||||
// Override findAll to inject our test data
|
||||
mw.findAll = findAll
|
||||
return mw
|
||||
}
|
||||
|
||||
function makeCreateTx (objectClass: Ref<Class<Doc>>, objectSpace: Ref<Space>): Tx {
|
||||
const factory = new TxFactory('test:account:System' as PersonId)
|
||||
return factory.createTxCreateDoc(objectClass, objectSpace, {})
|
||||
}
|
||||
|
||||
// Helper: buildGuestSettings - simulate the document that loadPermissionsCache would find
|
||||
function makeGuestSettingsDoc (allowedPermissions: Ref<Doc>[], disabledPermissions?: Ref<Doc>[]): Doc {
|
||||
return {
|
||||
_id: generateId(),
|
||||
_class: MODULE_PERMISSION_GROUP_CLASS,
|
||||
space: 'core:space:Workspace' as Ref<Space>,
|
||||
modifiedOn: Date.now(),
|
||||
modifiedBy: 'test' as PersonId,
|
||||
application: 'test:app:tracker' as Ref<Doc>,
|
||||
role: AccountRole.Guest,
|
||||
permissions: allowedPermissions,
|
||||
...(disabledPermissions !== undefined && disabledPermissions.length > 0 ? { disabledPermissions } : {}),
|
||||
spaceClass: 'core:class:Space' as Ref<Class<Doc>>,
|
||||
enabled: true
|
||||
} as any
|
||||
}
|
||||
|
||||
describe('GuestPermissionsMiddleware', () => {
|
||||
// ─── Non-guest users pass through ───────────────────────────────────────────
|
||||
describe('non-guest users', () => {
|
||||
it('User role: passes through without restriction', async () => {
|
||||
let nextCalled = false
|
||||
const mw = makeMiddleware(
|
||||
async () => [],
|
||||
async (ctx, txes) => {
|
||||
nextCalled = true
|
||||
return {}
|
||||
}
|
||||
)
|
||||
const tx = makeCreateTx(COVERED_CLASS, FORBIDDEN_SPACE)
|
||||
const ctx = makeCtx(makeAccount(AccountRole.User))
|
||||
await mw.tx(ctx, [tx])
|
||||
expect(nextCalled).toBe(true)
|
||||
})
|
||||
|
||||
it('Owner role: passes through without restriction', async () => {
|
||||
let nextCalled = false
|
||||
const mw = makeMiddleware(
|
||||
async () => [],
|
||||
async () => {
|
||||
nextCalled = true
|
||||
return {}
|
||||
}
|
||||
)
|
||||
const tx = makeCreateTx(COVERED_CLASS, FORBIDDEN_SPACE)
|
||||
const ctx = makeCtx(makeAccount(AccountRole.Owner))
|
||||
await mw.tx(ctx, [tx])
|
||||
expect(nextCalled).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// ─── DocGuest / ReadOnlyGuest are always forbidden ──────────────────────────
|
||||
describe('DocGuest and ReadOnlyGuest', () => {
|
||||
it('DocGuest: throws Forbidden for any tx', async () => {
|
||||
const mw = makeMiddleware(async () => [])
|
||||
const tx = makeCreateTx(COVERED_CLASS, ALLOWED_SPACE)
|
||||
const ctx = makeCtx(makeAccount(AccountRole.DocGuest))
|
||||
await expect(mw.tx(ctx, [tx])).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('ReadOnlyGuest: throws Forbidden for any tx', async () => {
|
||||
const mw = makeMiddleware(async () => [])
|
||||
const tx = makeCreateTx(COVERED_CLASS, ALLOWED_SPACE)
|
||||
const ctx = makeCtx(makeAccount(AccountRole.ReadOnlyGuest))
|
||||
await expect(mw.tx(ctx, [tx])).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
// ─── New permission model (covered class) ───────────────────────────────────
|
||||
describe('covered class – new permission model', () => {
|
||||
const settingsDoc = makeGuestSettingsDoc([COVERED_CLASS_PERMISSION])
|
||||
|
||||
const findAllWithSettings: FindAllFn = async (_ctx, _class) => {
|
||||
if (_class === MODULE_PERMISSION_GROUP_CLASS) return [settingsDoc]
|
||||
if (_class === core.class.ClassPermission) {
|
||||
return [{ _id: COVERED_CLASS_PERMISSION, targetClass: COVERED_CLASS } as any]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function patchHierarchy (mw: GuestPermissionsMiddleware): void {
|
||||
;(mw as any).context.hierarchy.isDerived = (a: any, b: any) => {
|
||||
if (b === core.class.Space) return false
|
||||
return a === b
|
||||
}
|
||||
;(mw as any).context.hierarchy.classHierarchyMixin = () => undefined
|
||||
}
|
||||
|
||||
it('allows create for covered class in any space (TxAccessLevel is irrelevant)', async () => {
|
||||
let nextCalled = false
|
||||
const mw = makeMiddleware(findAllWithSettings, async () => {
|
||||
nextCalled = true
|
||||
return {}
|
||||
})
|
||||
patchHierarchy(mw)
|
||||
const tx = makeCreateTx(COVERED_CLASS, ALLOWED_SPACE)
|
||||
const ctx = makeCtx(makeAccount(AccountRole.Guest))
|
||||
await mw.tx(ctx, [tx])
|
||||
expect(nextCalled).toBe(true)
|
||||
})
|
||||
|
||||
it('also allows create in another space when class is covered', async () => {
|
||||
let nextCalled = false
|
||||
const mw = makeMiddleware(findAllWithSettings, async () => {
|
||||
nextCalled = true
|
||||
return {}
|
||||
})
|
||||
patchHierarchy(mw)
|
||||
const tx = makeCreateTx(COVERED_CLASS, FORBIDDEN_SPACE)
|
||||
const ctx = makeCtx(makeAccount(AccountRole.Guest))
|
||||
await mw.tx(ctx, [tx])
|
||||
expect(nextCalled).toBe(true)
|
||||
})
|
||||
|
||||
it('ignores permissions listed in disabledPermissions (falls back to TxAccessLevel)', async () => {
|
||||
const docWithDisabled = makeGuestSettingsDoc([COVERED_CLASS_PERMISSION], [COVERED_CLASS_PERMISSION])
|
||||
const findAll: FindAllFn = async (_ctx, _class) => {
|
||||
if (_class === MODULE_PERMISSION_GROUP_CLASS) return [docWithDisabled]
|
||||
if (_class === core.class.ClassPermission) {
|
||||
return [{ _id: COVERED_CLASS_PERMISSION, targetClass: COVERED_CLASS } as any]
|
||||
}
|
||||
return []
|
||||
}
|
||||
const mw = makeMiddleware(findAll)
|
||||
patchHierarchy(mw)
|
||||
const tx = makeCreateTx(COVERED_CLASS, ALLOWED_SPACE)
|
||||
const ctx = makeCtx(makeAccount(AccountRole.Guest))
|
||||
await expect(mw.tx(ctx, [tx])).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
// ─── Uncovered class falls back to TxAccessLevel ────────────────────────────
|
||||
describe('uncovered class – TxAccessLevel fallback', () => {
|
||||
it('forbids create when class has no TxAccessLevel mixin and no GuestPermissionsSettings', async () => {
|
||||
const mw = makeMiddleware(async () => [])
|
||||
const tx = makeCreateTx(UNCOVERED_CLASS, ALLOWED_SPACE)
|
||||
const ctx = makeCtx(makeAccount(AccountRole.Guest))
|
||||
await expect(mw.tx(ctx, [tx])).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('allows create when TxAccessLevel.createAccessLevel === Guest (uncovered type)', async () => {
|
||||
// Settings exist but UNCOVERED_CLASS is NOT in allowedPermissions-derived classes
|
||||
const settingsDoc = makeGuestSettingsDoc([COVERED_CLASS_PERMISSION])
|
||||
let nextCalled = false
|
||||
|
||||
const mw = makeMiddleware(
|
||||
async (_ctx, _class) => {
|
||||
if (_class === MODULE_PERMISSION_GROUP_CLASS) return [settingsDoc]
|
||||
if (_class === core.class.ClassPermission) {
|
||||
return [{ _id: COVERED_CLASS_PERMISSION, targetClass: COVERED_CLASS } as any]
|
||||
}
|
||||
return []
|
||||
},
|
||||
async () => {
|
||||
nextCalled = true
|
||||
return {}
|
||||
}
|
||||
)
|
||||
|
||||
// Simulate TxAccessLevel mixin via hierarchy mock on the middleware context
|
||||
;(mw as any).context.hierarchy.classHierarchyMixin = (_class: any, _mixin: any) => {
|
||||
if (_class === UNCOVERED_CLASS) {
|
||||
return { createAccessLevel: AccountRole.Guest }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
;(mw as any).context.hierarchy.isDerived = (a: any, b: any) => {
|
||||
if (b === core.class.Space) return false
|
||||
return a === b
|
||||
}
|
||||
|
||||
const tx = makeCreateTx(UNCOVERED_CLASS, ALLOWED_SPACE)
|
||||
const ctx = makeCtx(makeAccount(AccountRole.Guest))
|
||||
await mw.tx(ctx, [tx])
|
||||
expect(nextCalled).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// ─── Precedence: covered class ignores TxAccessLevel even if it would deny ──
|
||||
describe('precedence – new model overrides TxAccessLevel for covered types', () => {
|
||||
it('allows covered class create in allowed space regardless of missing TxAccessLevel', async () => {
|
||||
const settingsDoc = makeGuestSettingsDoc([COVERED_CLASS_PERMISSION])
|
||||
let nextCalled = false
|
||||
|
||||
const mw = makeMiddleware(
|
||||
async (_ctx, _class) => {
|
||||
if (_class === MODULE_PERMISSION_GROUP_CLASS) return [settingsDoc]
|
||||
if (_class === core.class.ClassPermission) {
|
||||
return [{ _id: COVERED_CLASS_PERMISSION, targetClass: COVERED_CLASS } as any]
|
||||
}
|
||||
return []
|
||||
},
|
||||
async () => {
|
||||
nextCalled = true
|
||||
return {}
|
||||
}
|
||||
)
|
||||
|
||||
// Ensure hierarchy says TxAccessLevel is absent for the covered class
|
||||
;(mw as any).context.hierarchy.classHierarchyMixin = (_class: any, _mixin: any) => undefined
|
||||
;(mw as any).context.hierarchy.isDerived = (a: any, b: any) => {
|
||||
if (b === core.class.Space) return false
|
||||
return a === b
|
||||
}
|
||||
|
||||
const tx = makeCreateTx(COVERED_CLASS, ALLOWED_SPACE)
|
||||
const ctx = makeCtx(makeAccount(AccountRole.Guest))
|
||||
await mw.tx(ctx, [tx])
|
||||
expect(nextCalled).toBe(true)
|
||||
})
|
||||
|
||||
it('allows covered class create in any space even if TxAccessLevel would deny', async () => {
|
||||
const settingsDoc = makeGuestSettingsDoc([COVERED_CLASS_PERMISSION])
|
||||
|
||||
const mw = makeMiddleware(async (_ctx, _class) => {
|
||||
if (_class === MODULE_PERMISSION_GROUP_CLASS) return [settingsDoc]
|
||||
if (_class === core.class.ClassPermission) {
|
||||
return [{ _id: COVERED_CLASS_PERMISSION, targetClass: COVERED_CLASS } as any]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
// TxAccessLevel would allow (createAccessLevel === Guest) – should be ignored
|
||||
;(mw as any).context.hierarchy.classHierarchyMixin = (_class: any, _mixin: any) => {
|
||||
if (_class === COVERED_CLASS) return { createAccessLevel: AccountRole.Guest }
|
||||
return undefined
|
||||
}
|
||||
;(mw as any).context.hierarchy.isDerived = (a: any, b: any) => {
|
||||
if (b === core.class.Space) return false
|
||||
return a === b
|
||||
}
|
||||
|
||||
const tx = makeCreateTx(COVERED_CLASS, FORBIDDEN_SPACE)
|
||||
const ctx = makeCtx(makeAccount(AccountRole.Guest))
|
||||
await mw.tx(ctx, [tx])
|
||||
})
|
||||
})
|
||||
|
||||
// ─── Own-document mutations for guests ───────────────────────────────────────
|
||||
describe('guest update/remove own documents', () => {
|
||||
const GUEST_SOCIAL = 'test:guest-social' as PersonId
|
||||
|
||||
function makeGuestAccountWithSocial (): Account {
|
||||
return {
|
||||
uuid: generateId() as any,
|
||||
role: AccountRole.Guest,
|
||||
primarySocialId: GUEST_SOCIAL,
|
||||
socialIds: [GUEST_SOCIAL],
|
||||
fullSocialIds: []
|
||||
}
|
||||
}
|
||||
|
||||
function patchHierarchyNoTxAccessLevel (mw: GuestPermissionsMiddleware): void {
|
||||
;(mw as any).context.hierarchy.classHierarchyMixin = () => undefined
|
||||
;(mw as any).context.hierarchy.isDerived = (a: any, b: any) => {
|
||||
if (b === core.class.Space) return false
|
||||
return a === b
|
||||
}
|
||||
}
|
||||
|
||||
it('allows guest to update document created by same account', async () => {
|
||||
const objectId = generateId()
|
||||
const findAll: FindAllFn = async (_ctx, _class, query: any) => {
|
||||
if (_class === UNCOVERED_CLASS && query?._id === objectId) {
|
||||
return [
|
||||
{
|
||||
_id: objectId,
|
||||
_class: UNCOVERED_CLASS,
|
||||
space: ALLOWED_SPACE,
|
||||
modifiedOn: Date.now(),
|
||||
modifiedBy: GUEST_SOCIAL,
|
||||
createdBy: GUEST_SOCIAL
|
||||
} as any
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
let nextCalled = false
|
||||
const mw = makeMiddleware(findAll, async () => {
|
||||
nextCalled = true
|
||||
return {}
|
||||
})
|
||||
patchHierarchyNoTxAccessLevel(mw)
|
||||
const factory = new TxFactory(GUEST_SOCIAL)
|
||||
const tx = factory.createTxUpdateDoc(UNCOVERED_CLASS, ALLOWED_SPACE, objectId, { name: 'x' } as any)
|
||||
await mw.tx(makeCtx(makeGuestAccountWithSocial()), [tx])
|
||||
expect(nextCalled).toBe(true)
|
||||
})
|
||||
|
||||
it('allows guest to remove document created by same account', async () => {
|
||||
const objectId = generateId()
|
||||
const findAll: FindAllFn = async (_ctx, _class, query: any) => {
|
||||
if (_class === UNCOVERED_CLASS && query?._id === objectId) {
|
||||
return [
|
||||
{
|
||||
_id: objectId,
|
||||
_class: UNCOVERED_CLASS,
|
||||
space: ALLOWED_SPACE,
|
||||
modifiedOn: Date.now(),
|
||||
modifiedBy: GUEST_SOCIAL,
|
||||
createdBy: GUEST_SOCIAL
|
||||
} as any
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
let nextCalled = false
|
||||
const mw = makeMiddleware(findAll, async () => {
|
||||
nextCalled = true
|
||||
return {}
|
||||
})
|
||||
patchHierarchyNoTxAccessLevel(mw)
|
||||
const factory = new TxFactory(GUEST_SOCIAL)
|
||||
const tx = factory.createTxRemoveDoc(UNCOVERED_CLASS, ALLOWED_SPACE, objectId)
|
||||
await mw.tx(makeCtx(makeGuestAccountWithSocial()), [tx])
|
||||
expect(nextCalled).toBe(true)
|
||||
})
|
||||
|
||||
it('forbids guest to update document created by another account', async () => {
|
||||
const objectId = generateId()
|
||||
const otherSocial = 'test:other-social' as PersonId
|
||||
const findAll: FindAllFn = async (_ctx, _class, query: any) => {
|
||||
if (_class === UNCOVERED_CLASS && query?._id === objectId) {
|
||||
return [
|
||||
{
|
||||
_id: objectId,
|
||||
_class: UNCOVERED_CLASS,
|
||||
space: ALLOWED_SPACE,
|
||||
modifiedOn: Date.now(),
|
||||
modifiedBy: otherSocial,
|
||||
createdBy: otherSocial
|
||||
} as any
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
const mw = makeMiddleware(findAll)
|
||||
patchHierarchyNoTxAccessLevel(mw)
|
||||
const factory = new TxFactory(GUEST_SOCIAL)
|
||||
const tx = factory.createTxUpdateDoc(UNCOVERED_CLASS, ALLOWED_SPACE, objectId, { name: 'x' } as any)
|
||||
await expect(mw.tx(makeCtx(makeGuestAccountWithSocial()), [tx])).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
// ─── Cache invalidation ──────────────────────────────────────────────────────
|
||||
describe('cache invalidation', () => {
|
||||
it('invalidates cache when GuestPermissionsSettings is updated', async () => {
|
||||
const findAll: FindAllFn = async (_ctx, _class) => {
|
||||
if (_class === MODULE_PERMISSION_GROUP_CLASS) {
|
||||
return [makeGuestSettingsDoc([COVERED_CLASS_PERMISSION])]
|
||||
}
|
||||
if (_class === core.class.ClassPermission) {
|
||||
return [{ _id: COVERED_CLASS_PERMISSION, targetClass: COVERED_CLASS } as any]
|
||||
}
|
||||
return []
|
||||
}
|
||||
const mw = makeMiddleware(findAll)
|
||||
;(mw as any).context.hierarchy.isDerived = (a: any, b: any) => {
|
||||
if (b === core.class.Space) return false
|
||||
return a === b
|
||||
}
|
||||
;(mw as any).context.hierarchy.classHierarchyMixin = () => undefined
|
||||
|
||||
// First tx as guest should load cache
|
||||
const userCtx = makeCtx(makeAccount(AccountRole.User))
|
||||
const settingsTx: Tx = {
|
||||
_id: generateId(),
|
||||
_class: core.class.TxCreateDoc,
|
||||
space: core.space.Tx,
|
||||
modifiedOn: Date.now(),
|
||||
modifiedBy: 'test' as PersonId,
|
||||
objectId: generateId(),
|
||||
objectClass: MODULE_PERMISSION_GROUP_CLASS,
|
||||
objectSpace: 'core:space:Workspace' as Ref<Space>
|
||||
} as any
|
||||
|
||||
// Owner updates settings – should invalidate cache
|
||||
await mw.tx(userCtx, [settingsTx])
|
||||
// Cache should be cleared after settings update
|
||||
expect((mw as any).permissionsCache).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -922,6 +922,31 @@ export function createModel (builder: Builder): void {
|
||||
card.ids.ManageMasterTags
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
core.class.ClassPermission,
|
||||
core.space.Model,
|
||||
{
|
||||
label: card.string.AllowCreatingCards,
|
||||
scope: 'space',
|
||||
targetClass: card.class.Card
|
||||
},
|
||||
card.ids.GuestCardClassPermission
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
core.class.ModulePermissionGroup,
|
||||
core.space.Model,
|
||||
{
|
||||
application: card.app.Card,
|
||||
role: AccountRole.Guest,
|
||||
permissions: [card.ids.GuestCardClassPermission],
|
||||
spaceClass: card.class.CardSpace,
|
||||
enabled: true,
|
||||
order: 20
|
||||
},
|
||||
card.ids.ModulePermissionGroup
|
||||
)
|
||||
|
||||
builder.mixin(card.class.Card, core.class.Class, view.mixin.ClassFilters, {
|
||||
filters: ['space'],
|
||||
ignoreKeys: ['parent']
|
||||
|
||||
@@ -69,6 +69,20 @@ export function createModel (builder: Builder): void {
|
||||
chunter.app.Chunter
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
core.class.ModulePermissionGroup,
|
||||
core.space.Model,
|
||||
{
|
||||
application: chunter.app.Chunter,
|
||||
role: AccountRole.Guest,
|
||||
permissions: [],
|
||||
spaceClass: chunter.class.ChunterSpace,
|
||||
enabled: true,
|
||||
order: 30
|
||||
},
|
||||
chunter.ids.ModulePermissionGroup
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
workbench.class.Widget,
|
||||
core.space.Model,
|
||||
|
||||
@@ -94,7 +94,8 @@ export default mergeIds(chunterId, chunter, {
|
||||
Channels: '' as Ref<Viewlet>
|
||||
},
|
||||
ids: {
|
||||
ChunterNotificationGroup: '' as Ref<NotificationGroup>
|
||||
ChunterNotificationGroup: '' as Ref<NotificationGroup>,
|
||||
ModulePermissionGroup: '' as Ref<Doc>
|
||||
},
|
||||
space: {
|
||||
General: '' as Ref<Channel>,
|
||||
|
||||
@@ -1111,6 +1111,19 @@ export function createModel (builder: Builder): void {
|
||||
createPrintAction(documents.class.Document, documents.action.Print)
|
||||
|
||||
defineSpaceType(builder)
|
||||
builder.createDoc(
|
||||
core.class.ModulePermissionGroup,
|
||||
core.space.Model,
|
||||
{
|
||||
application: documents.app.Documents,
|
||||
role: AccountRole.Guest,
|
||||
permissions: [],
|
||||
spaceClass: documents.class.OrgSpace,
|
||||
enabled: true,
|
||||
order: 42
|
||||
},
|
||||
documents.ids.ModulePermissionGroup
|
||||
)
|
||||
definePermissions(builder)
|
||||
defineNotifications(builder)
|
||||
defineSearch(builder)
|
||||
|
||||
@@ -91,5 +91,8 @@ export default mergeIds(documentsId, documents, {
|
||||
DocumentsNotificationGroup: '' as Ref<NotificationGroup>,
|
||||
ContentNotification: '' as Ref<NotificationType>,
|
||||
StateNotification: '' as Ref<NotificationType>
|
||||
},
|
||||
ids: {
|
||||
ModulePermissionGroup: '' as Ref<Doc>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -80,6 +80,7 @@ import {
|
||||
import { definePermissions } from './permissions'
|
||||
import {
|
||||
TAttributePermission,
|
||||
TModulePermissionGroup,
|
||||
TClassPermission,
|
||||
TPermission,
|
||||
TRole,
|
||||
@@ -136,6 +137,7 @@ export function createModel (builder: Builder): void {
|
||||
TSpaceTypeDescriptor,
|
||||
TRole,
|
||||
TPermission,
|
||||
TModulePermissionGroup,
|
||||
TAttributePermission,
|
||||
TClassPermission,
|
||||
TAttribute,
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
DOMAIN_MODEL,
|
||||
DOMAIN_SPACE,
|
||||
IndexKind,
|
||||
type ModulePermissionGroup,
|
||||
type AccountRole,
|
||||
type AccountUuid,
|
||||
type AnyAttribute,
|
||||
@@ -46,6 +47,7 @@ import {
|
||||
Prop,
|
||||
TypeAccountUuid,
|
||||
TypeBoolean,
|
||||
TypeNumber,
|
||||
TypeRef,
|
||||
TypeString,
|
||||
UX
|
||||
@@ -193,3 +195,27 @@ export class TTxAccessLevel extends TClass implements TxAccessLevel {
|
||||
updateAccessLevel?: AccountRole
|
||||
isIdentity?: boolean
|
||||
}
|
||||
|
||||
@Model(core.class.ModulePermissionGroup, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TModulePermissionGroup extends TDoc implements ModulePermissionGroup {
|
||||
@Prop(TypeRef(core.class.Doc), core.string.AttachedTo)
|
||||
application!: Ref<Doc>
|
||||
|
||||
@Prop(TypeString(), core.string.Roles)
|
||||
role!: AccountRole
|
||||
|
||||
@Prop(ArrOf(TypeRef(core.class.Permission)), core.string.Permission)
|
||||
permissions!: Ref<Permission>[]
|
||||
|
||||
@Prop(ArrOf(TypeRef(core.class.Permission)), core.string.Permission)
|
||||
disabledPermissions?: Ref<Permission>[]
|
||||
|
||||
@Prop(TypeRef(core.class.Class), core.string.Class)
|
||||
spaceClass!: Ref<Class<Space>>
|
||||
|
||||
@Prop(TypeBoolean(), core.string.Name)
|
||||
enabled!: boolean
|
||||
|
||||
@Prop(TypeNumber(), core.string.Order)
|
||||
order?: number
|
||||
}
|
||||
|
||||
@@ -540,6 +540,19 @@ export function createModel (builder: Builder): void {
|
||||
defineDocument(builder)
|
||||
|
||||
defineApplication(builder)
|
||||
builder.createDoc(
|
||||
core.class.ModulePermissionGroup,
|
||||
core.space.Model,
|
||||
{
|
||||
application: document.app.Documents,
|
||||
role: AccountRole.Guest,
|
||||
permissions: [],
|
||||
spaceClass: document.class.Teamspace,
|
||||
enabled: true,
|
||||
order: 40
|
||||
},
|
||||
document.ids.ModulePermissionGroup
|
||||
)
|
||||
definePermissions(builder)
|
||||
|
||||
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
|
||||
|
||||
@@ -61,6 +61,9 @@ export default mergeIds(documentId, document, {
|
||||
Document: '' as Ref<ActionCategory>,
|
||||
Other: '' as Ref<TagCategory>
|
||||
},
|
||||
ids: {
|
||||
ModulePermissionGroup: '' as Ref<Doc>
|
||||
},
|
||||
string: {
|
||||
ConfigDescription: '' as IntlString,
|
||||
ParentDocument: '' as IntlString,
|
||||
|
||||
@@ -842,5 +842,18 @@ export function createModel (builder: Builder): void {
|
||||
defineFile(builder)
|
||||
defineFileVersion(builder)
|
||||
defineApplication(builder)
|
||||
builder.createDoc(
|
||||
core.class.ModulePermissionGroup,
|
||||
core.space.Model,
|
||||
{
|
||||
application: drive.app.Drive,
|
||||
role: AccountRole.Guest,
|
||||
permissions: [],
|
||||
spaceClass: drive.class.Drive,
|
||||
enabled: false,
|
||||
order: 60
|
||||
},
|
||||
drive.ids.ModulePermissionGroup
|
||||
)
|
||||
definePermissions(builder)
|
||||
}
|
||||
|
||||
@@ -101,6 +101,9 @@ export default mergeIds(driveId, drive, {
|
||||
RenameFolder: '' as ViewAction,
|
||||
RestoreFileVersion: '' as ViewAction
|
||||
},
|
||||
ids: {
|
||||
ModulePermissionGroup: '' as Ref<Doc>
|
||||
},
|
||||
string: {
|
||||
Grid: '' as IntlString,
|
||||
Name: '' as IntlString,
|
||||
|
||||
@@ -266,6 +266,20 @@ export function createModel (builder: Builder): void {
|
||||
love.app.Love
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
core.class.ModulePermissionGroup,
|
||||
core.space.Model,
|
||||
{
|
||||
application: love.app.Love,
|
||||
role: AccountRole.Guest,
|
||||
permissions: [],
|
||||
spaceClass: love.class.Office,
|
||||
enabled: true,
|
||||
order: 50
|
||||
},
|
||||
love.ids.ModulePermissionGroup
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
workbench.class.Widget,
|
||||
core.space.Model,
|
||||
|
||||
@@ -50,7 +50,8 @@ export default mergeIds(loveId, love, {
|
||||
ids: {
|
||||
Settings: '' as Ref<Doc>,
|
||||
LoveNotificationGroup: '' as Ref<NotificationGroup>,
|
||||
MeetingMinutesChatNotification: '' as Ref<NotificationType>
|
||||
MeetingMinutesChatNotification: '' as Ref<NotificationType>,
|
||||
ModulePermissionGroup: '' as Ref<Doc>
|
||||
},
|
||||
function: {
|
||||
MeetingMinutesTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>
|
||||
|
||||
@@ -312,6 +312,19 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
setting.ids.Owners
|
||||
)
|
||||
builder.createDoc(
|
||||
setting.class.WorkspaceSettingCategory,
|
||||
core.space.Model,
|
||||
{
|
||||
name: 'guestPermissions',
|
||||
label: setting.string.GuestPermissionsSettings,
|
||||
icon: setting.icon.Members,
|
||||
component: setting.component.GuestPermissionsSettings,
|
||||
role: AccountRole.Owner,
|
||||
order: 1050
|
||||
},
|
||||
'setting:ids:AccountPermissionsSettings' as Ref<any>
|
||||
)
|
||||
builder.createDoc(
|
||||
setting.class.WorkspaceSettingCategory,
|
||||
core.space.Model,
|
||||
@@ -426,6 +439,7 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
setting.ids.OfficeSettings
|
||||
)
|
||||
|
||||
// Currently remove Support item from settings
|
||||
// builder.createDoc(
|
||||
// setting.class.SettingsCategory,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import activity from '@hcengineering/activity'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import core from '@hcengineering/model-core'
|
||||
import { SortingOrder, type FindOptions } from '@hcengineering/core'
|
||||
import { AccountRole, SortingOrder, type FindOptions } from '@hcengineering/core'
|
||||
|
||||
import { type Builder } from '@hcengineering/model'
|
||||
import view, { createAction } from '@hcengineering/model-view'
|
||||
@@ -202,6 +202,19 @@ export function createModel (builder: Builder): void {
|
||||
definePresenters(builder)
|
||||
|
||||
defineApplication(builder)
|
||||
builder.createDoc(
|
||||
core.class.ModulePermissionGroup,
|
||||
core.space.Model,
|
||||
{
|
||||
application: testManagement.app.TestManagement,
|
||||
role: AccountRole.Guest,
|
||||
permissions: [],
|
||||
spaceClass: testManagement.class.TestProject,
|
||||
enabled: false,
|
||||
order: 70
|
||||
},
|
||||
testManagement.ids.ModulePermissionGroup
|
||||
)
|
||||
|
||||
builder.mixin(testManagement.class.TestCase, core.class.Class, view.mixin.ObjectIcon, {
|
||||
component: testManagement.component.TestCaseStatusPresenter
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
import { testManagementId } from '@hcengineering/test-management'
|
||||
import testManganement from '@hcengineering/test-management-resources/src/plugin'
|
||||
import type { Ref } from '@hcengineering/core'
|
||||
import type { Doc, Ref } from '@hcengineering/core'
|
||||
import { mergeIds } from '@hcengineering/platform'
|
||||
import { type AnyComponent } from '@hcengineering/ui/src/types'
|
||||
import type { ActionCategory } from '@hcengineering/view'
|
||||
@@ -48,5 +48,8 @@ export default mergeIds(testManagementId, testManganement, {
|
||||
TestPlanItemPresenter: '' as AnyComponent,
|
||||
CreateTestRunButton: '' as AnyComponent,
|
||||
RunTestPlanButton: '' as AnyComponent
|
||||
},
|
||||
ids: {
|
||||
ModulePermissionGroup: '' as Ref<Doc>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -657,6 +657,31 @@ export function createModel (builder: Builder): void {
|
||||
order: 4000
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
core.class.ClassPermission,
|
||||
core.space.Model,
|
||||
{
|
||||
label: tracker.string.AllowCreatingIssues,
|
||||
scope: 'space',
|
||||
targetClass: tracker.class.Issue
|
||||
},
|
||||
tracker.ids.GuestIssueClassPermission
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
core.class.ModulePermissionGroup,
|
||||
core.space.Model,
|
||||
{
|
||||
application: tracker.app.Tracker,
|
||||
role: AccountRole.Guest,
|
||||
permissions: [tracker.ids.GuestIssueClassPermission],
|
||||
spaceClass: tracker.class.Project,
|
||||
enabled: true,
|
||||
order: 10
|
||||
},
|
||||
tracker.ids.ModulePermissionGroup
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
chunter.class.ChatMessageViewlet,
|
||||
core.space.Model,
|
||||
|
||||
@@ -41,7 +41,8 @@ export default mergeIds(trackerId, tracker, {
|
||||
ConfigDescription: '' as IntlString,
|
||||
AllProjects: '' as IntlString,
|
||||
MapRelatedIssues: '' as IntlString,
|
||||
Extensions: '' as IntlString
|
||||
Extensions: '' as IntlString,
|
||||
AllowCreatingIssues: '' as IntlString
|
||||
},
|
||||
activity: {
|
||||
StatusIcon: '' as AnyComponent,
|
||||
@@ -79,6 +80,8 @@ export default mergeIds(trackerId, tracker, {
|
||||
TrackerNotificationGroup: '' as Ref<NotificationGroup>,
|
||||
AssigneeNotification: '' as Ref<NotificationType>,
|
||||
BaseProjectType: '' as Ref<ProjectType>,
|
||||
GuestIssueClassPermission: '' as Ref<Doc>,
|
||||
ModulePermissionGroup: '' as Ref<Doc>,
|
||||
IssueUpdatedActivityViewlet: '' as Ref<DocUpdateMessageViewlet>,
|
||||
IssueCreatedActivityViewlet: '' as Ref<DocUpdateMessageViewlet>,
|
||||
IssueRemovedActivityViewlet: '' as Ref<DocUpdateMessageViewlet>,
|
||||
|
||||
@@ -898,6 +898,31 @@ function defineSettings (builder: Builder): void {
|
||||
},
|
||||
training.setting.Trainings
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
core.class.ClassPermission,
|
||||
core.space.Model,
|
||||
{
|
||||
label: training.string.AllowToTakeTraining,
|
||||
scope: 'space',
|
||||
targetClass: training.class.TrainingAttempt
|
||||
},
|
||||
training.ids.GuestTrainingAttemptClassPermission
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
core.class.ModulePermissionGroup,
|
||||
core.space.Model,
|
||||
{
|
||||
application: training.app.Training,
|
||||
role: AccountRole.Guest,
|
||||
permissions: [training.ids.GuestTrainingAttemptClassPermission],
|
||||
spaceClass: core.class.TypedSpace,
|
||||
enabled: true,
|
||||
order: 25
|
||||
},
|
||||
training.ids.ModulePermissionGroup
|
||||
)
|
||||
}
|
||||
|
||||
const columns = {
|
||||
|
||||
@@ -33,6 +33,10 @@ export default mergeIds(trainingId, training, {
|
||||
TrainingGroup: '' as Ref<NotificationGroup>,
|
||||
TrainingRequest: '' as Ref<NotificationType>
|
||||
},
|
||||
ids: {
|
||||
GuestTrainingAttemptClassPermission: '' as Ref<Doc>,
|
||||
ModulePermissionGroup: '' as Ref<Doc>
|
||||
},
|
||||
|
||||
// TODO: Move function resources declarations to plugins/*-resources
|
||||
// Currently, dependencies look like this:
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"Cards": "Karty",
|
||||
"Content": "Obsah",
|
||||
"CreateCard": "Vytvořit kartu",
|
||||
"AllowCreatingCards": "Povolit vytváření karet",
|
||||
"CreateMasterTag": "Vytvořit typ",
|
||||
"CreateTag": "Vytvořit štítek",
|
||||
"MasterTag": "Typ",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"Cards": "Karten",
|
||||
"Content": "Inhalt",
|
||||
"CreateCard": "Karte erstellen",
|
||||
"AllowCreatingCards": "Erstellen von Karten erlauben",
|
||||
"CreateMasterTag": "Typ erstellen",
|
||||
"CreateTag": "Tag erstellen",
|
||||
"MasterTag": "Typ",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"Cards": "Cards",
|
||||
"Content": "Content",
|
||||
"CreateCard": "Create Card",
|
||||
"AllowCreatingCards": "Allow creating cards",
|
||||
"CreateMasterTag": "Create Type",
|
||||
"CreateTag": "Create Tag",
|
||||
"MasterTag": "Type",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"Cards": "Tarjetas",
|
||||
"Content": "Contenido",
|
||||
"CreateCard": "Crear Tarjeta",
|
||||
"AllowCreatingCards": "Permitir crear tarjetas",
|
||||
"CreateMasterTag": "Crear Tipo",
|
||||
"CreateTag": "Crear Etiqueta",
|
||||
"MasterTag": "Tipo",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"Cards": "Cartes",
|
||||
"Content": "Contenu",
|
||||
"CreateCard": "Créer une carte",
|
||||
"AllowCreatingCards": "Autoriser la création de cartes",
|
||||
"CreateMasterTag": "Créer un type",
|
||||
"CreateTag": "Créer une étiquette",
|
||||
"MasterTag": "Type",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"Cards": "Carte",
|
||||
"Content": "Contenuto",
|
||||
"CreateCard": "Crea Carta",
|
||||
"AllowCreatingCards": "Consenti la creazione di carte",
|
||||
"CreateMasterTag": "Crea Tipo",
|
||||
"CreateTag": "Crea Tag",
|
||||
"MasterTag": "Tipo",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"Cards": "カード",
|
||||
"Content": "コンテンツ",
|
||||
"CreateCard": "カードを作成",
|
||||
"AllowCreatingCards": "カードの作成を許可",
|
||||
"CreateMasterTag": "タイプを作成",
|
||||
"CreateTag": "タグを作成",
|
||||
"MasterTag": "タイプ",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"Cards": "Cartões",
|
||||
"Content": "Conteúdo",
|
||||
"CreateCard": "Criar Cartão",
|
||||
"AllowCreatingCards": "Permitir criar cartões",
|
||||
"CreateMasterTag": "Criar Tipo",
|
||||
"CreateTag": "Criar Tag",
|
||||
"MasterTag": "Tipo",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"Cards": "Cartões",
|
||||
"Content": "Conteúdo",
|
||||
"CreateCard": "Criar Cartão",
|
||||
"AllowCreatingCards": "Permitir criar cartões",
|
||||
"CreateMasterTag": "Criar Tipo",
|
||||
"CreateTag": "Criar Tag",
|
||||
"MasterTag": "Tipo",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"Cards": "Карты",
|
||||
"Content": "Содержание",
|
||||
"CreateCard": "Создать карту",
|
||||
"AllowCreatingCards": "Разрешить создание карт",
|
||||
"CreateMasterTag": "Создать тип",
|
||||
"CreateTag": "Создать тег",
|
||||
"MasterTag": "Тип",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"Cards": "Kartlar",
|
||||
"Content": "İçerik",
|
||||
"CreateCard": "Kart Oluştur",
|
||||
"AllowCreatingCards": "Kart oluşturmaya izin ver",
|
||||
"CreateMasterTag": "Tip Oluştur",
|
||||
"CreateTag": "Etiket Oluştur",
|
||||
"MasterTag": "Tip",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"Cards": "卡片",
|
||||
"Content": "内容",
|
||||
"CreateCard": "创建卡片",
|
||||
"AllowCreatingCards": "允许创建卡片",
|
||||
"CreateMasterTag": "创建类型",
|
||||
"CreateTag": "创建标签",
|
||||
"MasterTag": "类型",
|
||||
|
||||
@@ -206,6 +206,7 @@ const cardPlugin = plugin(cardId, {
|
||||
AllCards: '' as IntlString,
|
||||
Favorites: '' as IntlString,
|
||||
CreateCard: '' as IntlString,
|
||||
AllowCreatingCards: '' as IntlString,
|
||||
Version: '' as IntlString,
|
||||
Versions: '' as IntlString,
|
||||
LockSection: '' as IntlString,
|
||||
@@ -223,7 +224,9 @@ const cardPlugin = plugin(cardId, {
|
||||
CommunicationMessages: '' as Ref<CardSection>
|
||||
},
|
||||
ids: {
|
||||
CardWidget: '' as Ref<Doc>
|
||||
CardWidget: '' as Ref<Doc>,
|
||||
GuestCardClassPermission: '' as Ref<Doc>,
|
||||
ModulePermissionGroup: '' as Ref<Doc>
|
||||
},
|
||||
component: {
|
||||
LabelsPresenter: '' as AnyComponent,
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
<path d="M4.5 11C3.11929 11 2 12.1193 2 13.5C2 13.7761 1.77614 14 1.5 14C1.22386 14 1 13.7761 1 13.5C1 11.567 2.567 10 4.5 10H6.5C8.433 10 10 11.567 10 13.5C10 13.7761 9.77614 14 9.5 14C9.22386 14 9 13.7761 9 13.5C9 12.1193 7.88071 11 6.5 11H4.5Z" />
|
||||
<path d="M10.5 2.99988C10.2238 2.99988 9.99995 3.22374 9.99995 3.49988C9.99995 3.77602 10.2238 3.99988 10.5 3.99988C10.7846 3.99988 11.0661 4.06066 11.3254 4.17815C11.5847 4.29564 11.8159 4.46714 12.0036 4.68119C12.1913 4.89523 12.3312 5.14688 12.4138 5.41931C12.4965 5.69174 12.52 5.97868 12.4828 6.26093C12.4457 6.54318 12.3487 6.81425 12.1984 7.05601C12.048 7.29777 11.8478 7.50465 11.6111 7.66282C11.3744 7.82098 11.1066 7.92679 10.8257 7.97316C10.5449 8.01954 10.2573 8.00541 9.98232 7.93173C9.71558 7.86026 9.44141 8.01855 9.36994 8.28528C9.29847 8.55202 9.45677 8.82618 9.7235 8.89766C10.136 9.00818 10.5673 9.02937 10.9886 8.95981C11.41 8.89025 11.8116 8.73153 12.1667 8.49429C12.5217 8.25704 12.8221 7.94672 13.0476 7.58408C13.2731 7.22144 13.4186 6.81484 13.4743 6.39146C13.53 5.96807 13.4947 5.53767 13.3708 5.12902C13.2468 4.72038 13.037 4.3429 12.7555 4.02184C12.4739 3.70078 12.127 3.44353 11.7381 3.26729C11.3491 3.09105 10.927 2.99988 10.5 2.99988Z" />
|
||||
</symbol>
|
||||
<symbol id="members" viewBox="0 0 32 32">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.7,5.7c-1.4,0-2.7,0.5-3.5,1.5c-0.9,0.9-1.3,2.3-1.2,3.7c0.2,2.7,2.3,5.1,4.8,5.1c2.5,0,4.6-2.3,4.8-5.1C25.7,8,23.5,5.7,20.7,5.7z M18.5,8.5C18,9,17.7,9.8,17.7,10.8c0.1,2,1.6,3.3,2.9,3.3c1.3,0,2.8-1.3,2.9-3.3c0.1-1.9-1.1-3.2-2.9-3.2C19.8,7.6,19,7.9,18.5,8.5z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.4,24.1c1.1-4.2,5.2-6.3,9.3-6.3c4,0,8.2,2,9.3,6.3c0.2,0.9-0.4,2.1-1.6,2.1H13C11.7,26.3,11.2,25.1,11.4,24.1z M13.3,24.4h14.8c-0.9-3-3.9-4.7-7.4-4.7C17.2,19.7,14.1,21.4,13.3,24.4z" />
|
||||
<path d="M9.6,16.2c-2.1,0-3.9-1.9-4-4.3c-0.1-1.2,0.3-2.3,1-3.1c0.8-0.8,1.8-1.2,3-1.2c1.2,0,2.2,0.4,3,1.3c0.8,0.8,1.1,1.9,1.1,3.1C13.5,14.3,11.7,16.2,9.6,16.2z M9.6,9.5C9,9.5,8.4,9.7,8,10.1c-0.4,0.4-0.6,1-0.5,1.7c0.1,1.4,1.1,2.5,2.2,2.5s2.1-1.2,2.2-2.5c0-0.7-0.2-1.3-0.6-1.7C10.9,9.7,10.3,9.5,9.6,9.5z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2,22.5c0.9-3.5,4.3-5.2,7.6-5.2c1.3,0,2.6,0.2,3.8,0.8c0.5,0.2,0.7,0.8,0.5,1.2c-0.2,0.5-0.8,0.7-1.2,0.5c-0.9-0.4-1.9-0.6-3.1-0.6c-2.6,0-4.9,1.2-5.7,3.4H10c0.5,0,0.9,0.4,0.9,0.9s-0.4,0.9-0.9,0.9H3.5C2.4,24.4,1.8,23.3,2,22.5L2,22.5z" />
|
||||
</symbol>
|
||||
<symbol id="password" viewBox="0 0 16 16">
|
||||
<path d="M11 6C11.5523 6 12 5.55228 12 5C12 4.44772 11.5523 4 11 4C10.4477 4 10 4.44772 10 5C10 5.55228 10.4477 6 11 6Z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5 1C8.01472 1 6 3.01472 6 5.5C6 5.89536 6.05111 6.27942 6.14726 6.64563L1.29289 11.5C1.10536 11.6875 1 11.9419 1 12.2071V14.5C1 14.7761 1.22386 15 1.5 15H3.79289C4.05811 15 4.31246 14.8946 4.5 14.7071L9.35437 9.85274C9.72058 9.94889 10.1046 10 10.5 10C12.9853 10 15 7.98528 15 5.5C15 3.01472 12.9853 1 10.5 1ZM7 5.5C7 3.567 8.567 2 10.5 2C12.433 2 14 3.567 14 5.5C14 7.433 12.433 9 10.5 9C10.1048 9 9.72588 8.93469 9.37284 8.81468C9.19252 8.75339 8.99304 8.79986 8.85837 8.93453L8 9.79289L7.85355 9.64645C7.65829 9.45118 7.34171 9.45118 7.14645 9.64645C6.95118 9.84171 6.95118 10.1583 7.14645 10.3536L7.29289 10.5L6.5 11.2929L5.85355 10.6464C5.65829 10.4512 5.34171 10.4512 5.14645 10.6464C4.95118 10.8417 4.95118 11.1583 5.14645 11.3536L5.79289 12L5 12.7929L4.35355 12.1464C4.15829 11.9512 3.84171 11.9512 3.64645 12.1464C3.45118 12.3417 3.45118 12.6583 3.64645 12.8536L4.29289 13.5L3.79289 14H2V12.2071L7.06547 7.14163C7.20014 7.00696 7.24661 6.80748 7.18532 6.62716C7.06531 6.27412 7 5.8952 7 5.5Z" />
|
||||
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
@@ -212,6 +212,9 @@
|
||||
"OfficeDefaultSettings": "Výchozí nastavení pro jednací místnosti",
|
||||
"DefaultStartWithTranscription": "Povolit přepis v nových kancelářích",
|
||||
"DefaultStartWithRecording": "Povolit záznam v nových kancelářích",
|
||||
"GuestPermissionsSettings": "Oprávnění hostů",
|
||||
"GuestPermissionsModulePermissions": "Oprávnění modulů",
|
||||
"GuestPermissionsModulePermissionsHint": "Vyberte, které moduly mohou hosté používat, a níže upravte oprávnění pro jednotlivé aplikace.",
|
||||
"ImportDocumentPermission": "Importovat dokumenty",
|
||||
"ImportDocumentDescription": "Umožňuje uživatelům importovat dokumenty do pracovního prostoru",
|
||||
"SelectUsers": "Vybrat uživatele",
|
||||
|
||||
@@ -214,6 +214,9 @@
|
||||
"OfficeDefaultSettings": "Standardeinstellungen für Besprechungsräume",
|
||||
"DefaultStartWithTranscription": "Transkription in neuen Büroräumen aktivieren",
|
||||
"DefaultStartWithRecording": "Aufnahme in neuen Büroräumen aktivieren",
|
||||
"GuestPermissionsSettings": "Gastberechtigungen",
|
||||
"GuestPermissionsModulePermissions": "Modulberechtigungen",
|
||||
"GuestPermissionsModulePermissionsHint": "Wählen Sie, welche Module Gäste nutzen dürfen, und passen Sie unten die Berechtigungen für jede App an.",
|
||||
"ImportDocumentPermission": "Dokumente importieren",
|
||||
"ImportDocumentDescription": "Gewährt Benutzern die Möglichkeit, Dokumente in den Arbeitsbereich zu importieren",
|
||||
"SelectUsers": "Benutzer auswählen",
|
||||
|
||||
@@ -215,6 +215,9 @@
|
||||
"OfficeDefaultSettings": "Default settings for meeting rooms",
|
||||
"DefaultStartWithTranscription": "Enable transcription in new office rooms",
|
||||
"DefaultStartWithRecording": "Enable recording in new office rooms",
|
||||
"GuestPermissionsSettings": "Guest permissions",
|
||||
"GuestPermissionsModulePermissions": "Module permissions",
|
||||
"GuestPermissionsModulePermissionsHint": "Choose which modules guests can use, then adjust permissions for each app below.",
|
||||
"ImportDocumentPermission": "Import documents",
|
||||
"ImportDocumentDescription": "Grants users ability to import documents into the workspace",
|
||||
"SelectUsers": "Select users",
|
||||
|
||||
@@ -205,6 +205,9 @@
|
||||
"OfficeDefaultSettings": "Configuración predeterminada para salas de reuniones",
|
||||
"DefaultStartWithTranscription": "Habilitar transcripción en nuevas oficinas",
|
||||
"DefaultStartWithRecording": "Habilitar grabación en nuevas oficinas",
|
||||
"GuestPermissionsSettings": "Permisos de invitados",
|
||||
"GuestPermissionsModulePermissions": "Permisos de módulos",
|
||||
"GuestPermissionsModulePermissionsHint": "Elija qué módulos pueden usar los invitados y luego ajuste los permisos de cada aplicación a continuación.",
|
||||
"ImportDocumentPermission": "Importar documentos",
|
||||
"ImportDocumentDescription": "Otorga a los usuarios la capacidad de importar documentos al espacio de trabajo",
|
||||
"SelectUsers": "Seleccionar usuarios",
|
||||
|
||||
@@ -214,6 +214,9 @@
|
||||
"OfficeDefaultSettings": "Paramètres par défaut pour les salles de réunion",
|
||||
"DefaultStartWithTranscription": "Activer la transcription dans les nouveaux bureaux",
|
||||
"DefaultStartWithRecording": "Activer l'enregistrement dans les nouveaux bureaux",
|
||||
"GuestPermissionsSettings": "Permissions des invités",
|
||||
"GuestPermissionsModulePermissions": "Permissions des modules",
|
||||
"GuestPermissionsModulePermissionsHint": "Choisissez les modules accessibles aux invités, puis ajustez les permissions pour chaque application ci-dessous.",
|
||||
"ImportDocumentPermission": "Importer des documents",
|
||||
"ImportDocumentDescription": "Accorde aux utilisateurs la possibilité d'importer des documents dans l'espace de travail",
|
||||
"SelectUsers": "Sélectionner des utilisateurs",
|
||||
|
||||
@@ -214,6 +214,9 @@
|
||||
"OfficeDefaultSettings": "Impostazioni predefinite per le sale riunioni",
|
||||
"DefaultStartWithTranscription": "Abilita trascrizione nelle nuove stanze dell'ufficio",
|
||||
"DefaultStartWithRecording": "Abilita registrazione nelle nuove stanze dell'ufficio",
|
||||
"GuestPermissionsSettings": "Permessi ospite",
|
||||
"GuestPermissionsModulePermissions": "Permessi dei moduli",
|
||||
"GuestPermissionsModulePermissionsHint": "Scegli quali moduli possono usare gli ospiti, poi regola i permessi per ogni app qui sotto.",
|
||||
"ImportDocumentPermission": "Importa documenti",
|
||||
"ImportDocumentDescription": "Concede agli utenti la possibilità di importare documenti nell'area di lavoro",
|
||||
"SelectUsers": "Seleziona utenti",
|
||||
|
||||
@@ -214,6 +214,9 @@
|
||||
"OfficeDefaultSettings": "会議室のデフォルト設定",
|
||||
"DefaultStartWithTranscription": "新しいオフィスルームで文字起こしを有効にする",
|
||||
"DefaultStartWithRecording": "新しいオフィスルームで録画を有効にする",
|
||||
"GuestPermissionsSettings": "ゲストの権限",
|
||||
"GuestPermissionsModulePermissions": "モジュールの権限",
|
||||
"GuestPermissionsModulePermissionsHint": "ゲストが利用できるモジュールを選び、その下で各アプリの権限を調整します。",
|
||||
"ImportDocumentPermission": "ドキュメントをインポート",
|
||||
"ImportDocumentDescription": "ユーザーにワークスペースにドキュメントをインポートする機能を付与します",
|
||||
"SelectUsers": "ユーザーを選択",
|
||||
|
||||
@@ -205,6 +205,9 @@
|
||||
"OfficeDefaultSettings": "Configurações padrão para salas de reunião",
|
||||
"DefaultStartWithTranscription": "Habilitar transcrição em novos escritórios",
|
||||
"DefaultStartWithRecording": "Habilitar gravação em novos escritórios",
|
||||
"GuestPermissionsSettings": "Permissões de convidados",
|
||||
"GuestPermissionsModulePermissions": "Permissões de módulos",
|
||||
"GuestPermissionsModulePermissionsHint": "Escolha quais módulos os convidados podem usar e, em seguida, ajuste as permissões de cada aplicativo abaixo.",
|
||||
"ImportDocumentPermission": "Importar documentos",
|
||||
"ImportDocumentDescription": "Concede aos usuários a capacidade de importar documentos para o espaço de trabalho",
|
||||
"SelectUsers": "Selecionar usuários",
|
||||
|
||||
@@ -205,6 +205,9 @@
|
||||
"OfficeDefaultSettings": "Configurações padrão para salas de reunião",
|
||||
"DefaultStartWithTranscription": "Habilitar transcrição em novos escritórios",
|
||||
"DefaultStartWithRecording": "Habilitar gravação em novos escritórios",
|
||||
"GuestPermissionsSettings": "Permissões de convidados",
|
||||
"GuestPermissionsModulePermissions": "Permissões de módulos",
|
||||
"GuestPermissionsModulePermissionsHint": "Escolha quais módulos os convidados podem usar e, em seguida, ajuste as permissões de cada aplicação abaixo.",
|
||||
"ImportDocumentPermission": "Importar documentos",
|
||||
"ImportDocumentDescription": "Concede aos usuários a capacidade de importar documentos para o espaço de trabalho",
|
||||
"SelectUsers": "Selecionar usuários",
|
||||
|
||||
@@ -215,6 +215,9 @@
|
||||
"OfficeDefaultSettings": "Настройки по умолчанию для переговорных",
|
||||
"DefaultStartWithTranscription": "Включить транскрипцию в новых комнатах",
|
||||
"DefaultStartWithRecording": "Включить запись в новых комнатах",
|
||||
"GuestPermissionsSettings": "Права гостей",
|
||||
"GuestPermissionsModulePermissions": "Права модулей",
|
||||
"GuestPermissionsModulePermissionsHint": "Выберите, какие модули доступны гостям, затем настройте права для каждого приложения ниже.",
|
||||
"ImportDocumentPermission": "Импорт документов",
|
||||
"ImportDocumentDescription": "Предоставляет пользователям возможность импортировать документы в рабочее пространство",
|
||||
"SelectUsers": "Выбрать пользователей",
|
||||
|
||||
@@ -214,6 +214,9 @@
|
||||
"OfficeDefaultSettings": "Toplantı odaları için varsayılan ayarlar",
|
||||
"DefaultStartWithTranscription": "Yeni ofis odalarında transkripsiyonu etkinleştir",
|
||||
"DefaultStartWithRecording": "Yeni ofis odalarında kaydı etkinleştir",
|
||||
"GuestPermissionsSettings": "Misafir izinleri",
|
||||
"GuestPermissionsModulePermissions": "Modül izinleri",
|
||||
"GuestPermissionsModulePermissionsHint": "Misafirlerin hangi modülleri kullanabileceğini seçin, ardından her uygulama için izinleri aşağıdan ayarlayın.",
|
||||
"ImportDocumentPermission": "Belgeleri içe aktar",
|
||||
"ImportDocumentDescription": "Kullanıcılara çalışma alanına belge içe aktarma yeteneği verir",
|
||||
"SelectUsers": "Kullanıcıları seç",
|
||||
|
||||
@@ -214,6 +214,9 @@
|
||||
"OfficeDefaultSettings": "会议室的默认设置",
|
||||
"DefaultStartWithTranscription": "在新办公室启用转录",
|
||||
"DefaultStartWithRecording": "在新办公室启用录制",
|
||||
"GuestPermissionsSettings": "访客权限",
|
||||
"GuestPermissionsModulePermissions": "模块权限",
|
||||
"GuestPermissionsModulePermissionsHint": "选择访客可以使用哪些模块,然后在下方调整每个应用的权限。",
|
||||
"ImportDocumentPermission": "导入文档",
|
||||
"ImportDocumentDescription": "授予用户将文档导入工作区的权限",
|
||||
"SelectUsers": "选择用户",
|
||||
|
||||
@@ -20,6 +20,7 @@ const icons = require('../assets/icons.svg') as string // eslint-disable-line
|
||||
loadMetadata(setting.icon, {
|
||||
AccountSettings: `${icons}#accountSettings`,
|
||||
Owners: `${icons}#owners`,
|
||||
Members: `${icons}#members`,
|
||||
Password: `${icons}#password`,
|
||||
Setting: `${icons}#settings`,
|
||||
Integrations: `${icons}#integration`,
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
<!--
|
||||
// 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
|
||||
// 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 core, { ModulePermissionGroup, type Doc, type Permission, type Ref } from '@hcengineering/core'
|
||||
import { getEmbeddedLabel, getMetadata, type IntlString } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import workbench, { type Application } from '@hcengineering/workbench'
|
||||
import { Breadcrumb, Header, Icon, Label, Loading, Scroller, Toggle } from '@hcengineering/ui'
|
||||
import setting from '@hcengineering/setting'
|
||||
|
||||
let loadingSettings = true
|
||||
let loadingPermissions = true
|
||||
let workspaceAppsReady = false
|
||||
|
||||
let moduleGroups: ModulePermissionGroup[] = []
|
||||
let permissionsMap: Map<Ref<Permission>, Permission> = new Map<Ref<Permission>, Permission>()
|
||||
let hiddenApplicationIds: Array<Ref<Application>> = []
|
||||
|
||||
const excludedApplicationIds = getMetadata(workbench.metadata.ExcludedApplications) ?? []
|
||||
|
||||
const client = getClient()
|
||||
const moduleGroupsQuery = createQuery()
|
||||
const permissionsQuery = createQuery()
|
||||
const hiddenAppsQuery = createQuery()
|
||||
|
||||
$: moduleGroupsQuery.query(core.class.ModulePermissionGroup, {}, (res) => {
|
||||
moduleGroups = res as unknown as ModulePermissionGroup[]
|
||||
loadingSettings = false
|
||||
})
|
||||
|
||||
$: permissionsQuery.query(core.class.Permission, {}, (res) => {
|
||||
permissionsMap = new Map((res as Permission[]).map((permission) => [permission._id, permission]))
|
||||
loadingPermissions = false
|
||||
})
|
||||
|
||||
$: hiddenAppsQuery.query(workbench.class.HiddenApplication, { space: core.space.Workspace }, (res) => {
|
||||
hiddenApplicationIds = res.map((r) => r.attachedTo)
|
||||
workspaceAppsReady = true
|
||||
})
|
||||
|
||||
/** Same notion of “available in this workspace” as the app switcher: model apps minus hidden/excluded. */
|
||||
$: workspaceApplications = client
|
||||
.getModel()
|
||||
.findAllSync<Application>(workbench.class.Application, {
|
||||
hidden: false,
|
||||
_id: { $nin: excludedApplicationIds }
|
||||
})
|
||||
.filter((app) => !hiddenApplicationIds.includes(app._id))
|
||||
|
||||
$: applicationsMap = new Map<Ref<Doc>, Application>(
|
||||
workspaceApplications.map((application) => [application._id as Ref<Doc>, application])
|
||||
)
|
||||
|
||||
$: loading = loadingSettings || loadingPermissions || !workspaceAppsReady
|
||||
|
||||
/** Ignore permission groups for applications not enabled in this workspace. */
|
||||
$: visibleModuleGroups = moduleGroups.filter((group) => applicationsMap.has(group.application))
|
||||
|
||||
$: sortedVisibleModuleGroups = [...visibleModuleGroups].sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
|
||||
|
||||
function getApplicationLabel (applicationId: Ref<Doc>): IntlString {
|
||||
return applicationsMap.get(applicationId)?.label ?? getEmbeddedLabel(applicationId)
|
||||
}
|
||||
|
||||
function getApplication (applicationId: Ref<Doc>): Application | undefined {
|
||||
return applicationsMap.get(applicationId)
|
||||
}
|
||||
|
||||
function getDisabledPermissions (group: ModulePermissionGroup): Set<Ref<Permission>> {
|
||||
return new Set(group.disabledPermissions ?? [])
|
||||
}
|
||||
|
||||
function isPermissionActive (group: ModulePermissionGroup, permissionId: Ref<Permission>): boolean {
|
||||
return !getDisabledPermissions(group).has(permissionId)
|
||||
}
|
||||
|
||||
async function togglePermission (
|
||||
group: ModulePermissionGroup,
|
||||
permissionId: Ref<Permission>,
|
||||
enabled: boolean
|
||||
): Promise<void> {
|
||||
if (!isModuleEnabled(group)) return
|
||||
const disabled = getDisabledPermissions(group)
|
||||
if (enabled) {
|
||||
disabled.delete(permissionId)
|
||||
} else {
|
||||
disabled.add(permissionId)
|
||||
}
|
||||
await client.updateDoc(core.class.ModulePermissionGroup, core.space.Model, group._id, {
|
||||
disabledPermissions: Array.from(disabled)
|
||||
} as any)
|
||||
}
|
||||
|
||||
async function toggleModule (group: ModulePermissionGroup, enabled: boolean): Promise<void> {
|
||||
await client.updateDoc(core.class.ModulePermissionGroup, core.space.Model, group._id, {
|
||||
enabled
|
||||
} as any)
|
||||
}
|
||||
|
||||
function isModuleEnabled (group: ModulePermissionGroup): boolean {
|
||||
return group.enabled ?? true
|
||||
}
|
||||
|
||||
function getPermissionLabel (permissionId: Ref<Permission>): IntlString {
|
||||
return permissionsMap.get(permissionId)?.label ?? getEmbeddedLabel(permissionId)
|
||||
}
|
||||
|
||||
function onAccessToggle (group: ModulePermissionGroup, ev: Event): void {
|
||||
const e = ev as CustomEvent<boolean>
|
||||
void toggleModule(group, e.detail)
|
||||
}
|
||||
|
||||
function onPermissionToggle (group: ModulePermissionGroup, permissionId: Ref<Permission>, ev: Event): void {
|
||||
const e = ev as CustomEvent<boolean>
|
||||
void togglePermission(group, permissionId, e.detail)
|
||||
}
|
||||
|
||||
function handleAccessToggle (group: ModulePermissionGroup): (ev: Event) => void {
|
||||
return (ev: Event) => {
|
||||
onAccessToggle(group, ev)
|
||||
}
|
||||
}
|
||||
|
||||
function handlePermissionToggle (group: ModulePermissionGroup, permissionId: Ref<Permission>): (ev: Event) => void {
|
||||
return (ev: Event) => {
|
||||
onPermissionToggle(group, permissionId, ev)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="hulyComponent">
|
||||
<Header adaptive={'disabled'}>
|
||||
<Breadcrumb label={setting.string.GuestPermissionsSettings} size={'large'} isCurrent />
|
||||
</Header>
|
||||
<div class="hulyComponent-content__column content">
|
||||
{#if loading}
|
||||
<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 guestPermissionsRoot flex-col">
|
||||
<section class="section">
|
||||
<div class="sectionHeader">
|
||||
<div class="sectionTitle">
|
||||
<Label label={setting.string.GuestPermissionsModulePermissions} />
|
||||
</div>
|
||||
<div class="sectionHint">
|
||||
<Label label={setting.string.GuestPermissionsModulePermissionsHint} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cardStack">
|
||||
{#each sortedVisibleModuleGroups as group}
|
||||
{@const app = getApplication(group.application)}
|
||||
{@const moduleOn = isModuleEnabled(group)}
|
||||
{@const permissionCount = (group.permissions ?? []).length}
|
||||
<div class="permissionModuleCard" class:permissionModuleCard-off={!moduleOn}>
|
||||
<div
|
||||
class="permissionModuleCard-header"
|
||||
class:permissionModuleCard-headerOnly={permissionCount === 0}
|
||||
>
|
||||
<div class="permissionModuleCard-headerMain">
|
||||
{#if app}
|
||||
<div class="appIcon appIcon-sm">
|
||||
<Icon icon={app.icon} size={'small'} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="appIcon appIcon-sm appIcon-placeholder" />
|
||||
{/if}
|
||||
<div class="permissionModuleCard-titles">
|
||||
<div class="permissionModuleCard-name">
|
||||
<Label label={getApplicationLabel(group.application)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="permissionModuleCard-toggleCell">
|
||||
<Toggle on={moduleOn} on:change={handleAccessToggle(group)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if permissionCount > 0}
|
||||
<div class="permissionRows">
|
||||
{#each group.permissions ?? [] as permissionId}
|
||||
<div class="permissionRow">
|
||||
<div class="permissionRow-label">
|
||||
<Label label={getPermissionLabel(permissionId)} />
|
||||
</div>
|
||||
<div class="permissionRow-toggleCell">
|
||||
<Toggle
|
||||
disabled={!moduleOn}
|
||||
on={isPermissionActive(group, permissionId)}
|
||||
on:change={handlePermissionToggle(group, permissionId)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if visibleModuleGroups.length === 0}
|
||||
<div class="emptyState emptyState-block">—</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Scroller>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.guestPermissionsRoot {
|
||||
max-width: 40rem;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
color: var(--theme-content-color);
|
||||
}
|
||||
|
||||
.sectionHint {
|
||||
font-size: 0.8rem;
|
||||
color: var(--theme-halfcontent-color);
|
||||
}
|
||||
|
||||
/* Matches packages/ui Toggle width for a single aligned column */
|
||||
$toggleTrackWidth: 2.25rem;
|
||||
|
||||
.cardStack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.appIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: var(--small-focus-BorderRadius);
|
||||
background-color: var(--theme-button-default);
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
|
||||
.appIcon-sm {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.appIcon-placeholder {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.permissionModuleCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--small-focus-BorderRadius);
|
||||
border: 1px solid var(--theme-navpanel-divider);
|
||||
overflow: hidden;
|
||||
background-color: var(--theme-panel-color);
|
||||
box-shadow: var(--theme-popup-shadow);
|
||||
}
|
||||
|
||||
.permissionModuleCard-off .permissionRows {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.permissionModuleCard-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) #{$toggleTrackWidth};
|
||||
align-items: center;
|
||||
column-gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background-color: var(--theme-comp-header-color);
|
||||
border-bottom: 1px solid var(--theme-divider-color);
|
||||
|
||||
&.permissionModuleCard-headerOnly {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.permissionModuleCard-headerMain {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.permissionModuleCard-toggleCell,
|
||||
.permissionRow-toggleCell {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: $toggleTrackWidth;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.permissionModuleCard-name {
|
||||
font-weight: 500;
|
||||
font-size: 0.9375rem;
|
||||
color: var(--theme-content-color);
|
||||
}
|
||||
|
||||
.permissionModuleCard-meta {
|
||||
font-size: 0.75rem;
|
||||
color: var(--theme-halfcontent-color);
|
||||
}
|
||||
|
||||
.permissionRows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.25rem 1rem 0.75rem;
|
||||
background-color: var(--theme-panel-color);
|
||||
}
|
||||
|
||||
.permissionRow {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) #{$toggleTrackWidth};
|
||||
align-items: center;
|
||||
column-gap: 0.75rem;
|
||||
padding: 0.625rem 0 0.625rem 0.25rem;
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
.permissionRow:not(:first-child) {
|
||||
border-top: 1px solid var(--theme-navpanel-divider);
|
||||
}
|
||||
|
||||
.permissionRow-label {
|
||||
min-width: 0;
|
||||
color: var(--theme-content-color);
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
font-size: 0.875rem;
|
||||
color: var(--theme-halfcontent-color);
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.emptyState-block {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
</style>
|
||||
@@ -72,6 +72,7 @@ import EditRelation from './components/EditRelation.svelte'
|
||||
import AddSocialId from './components/socialIds/AddSocialId.svelte'
|
||||
import AddEmailSocialId from './components/socialIds/AddEmailSocialId.svelte'
|
||||
import Mailboxes from './components/Mailboxes.svelte'
|
||||
import GuestPermissionsSettings from './components/GuestPermissionsSettings.svelte'
|
||||
import OfficeSettings from './components/OfficeSettings.svelte'
|
||||
import BaseIntegrationState from './components/integrations/BaseIntegrationState.svelte'
|
||||
import IntegrationStateRow from './components/integrations/IntegrationStateRow.svelte'
|
||||
@@ -164,6 +165,7 @@ export default async (): Promise<Resources> => ({
|
||||
CreateRelation,
|
||||
EditRelation,
|
||||
Mailboxes,
|
||||
GuestPermissionsSettings,
|
||||
OfficeSettings,
|
||||
AddSocialId,
|
||||
AddEmailSocialId,
|
||||
|
||||
@@ -30,7 +30,8 @@ export default mergeIds(settingId, setting, {
|
||||
ManageSpaceTypesTools: '' as AnyComponent,
|
||||
ManageSpaceTypeContent: '' as AnyComponent,
|
||||
Spaces: '' as AnyComponent,
|
||||
AddSocialId: '' as AnyComponent
|
||||
AddSocialId: '' as AnyComponent,
|
||||
GuestPermissionsSettings: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
IntegrationDisabled: '' as IntlString,
|
||||
|
||||
@@ -311,6 +311,9 @@ export default plugin(settingId, {
|
||||
OfficeDefaultSettings: '' as IntlString,
|
||||
DefaultStartWithTranscription: '' as IntlString,
|
||||
DefaultStartWithRecording: '' as IntlString,
|
||||
GuestPermissionsSettings: '' as IntlString,
|
||||
GuestPermissionsModulePermissions: '' as IntlString,
|
||||
GuestPermissionsModulePermissionsHint: '' as IntlString,
|
||||
MailboxErrorInvalidName: '' as IntlString,
|
||||
MailboxErrorDomainNotFound: '' as IntlString,
|
||||
MailboxErrorNameRulesViolated: '' as IntlString,
|
||||
@@ -352,6 +355,7 @@ export default plugin(settingId, {
|
||||
icon: {
|
||||
AccountSettings: '' as Asset,
|
||||
Owners: '' as Asset,
|
||||
Members: '' as Asset,
|
||||
Password: '' as Asset,
|
||||
Setting: '' as Asset,
|
||||
Integrations: '' as Asset,
|
||||
|
||||
@@ -279,7 +279,8 @@
|
||||
"Extensions": "Rozšíření",
|
||||
"UnsetParentIssue": "Odebrat nadřazený úkol",
|
||||
"ForbidCreateProjectPermission": "Zakázat vytvoření projektu",
|
||||
"ForbidCreateProjectPermissionDescription": "Zakazuje uživatelům vytvářet nové projekty"
|
||||
"ForbidCreateProjectPermissionDescription": "Zakazuje uživatelům vytvářet nové projekty",
|
||||
"AllowCreatingIssues": "Povolit vytváření úkolů"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
||||
@@ -289,7 +289,8 @@
|
||||
"Extensions": "Erweiterungen",
|
||||
"UnsetParentIssue": "Übergeordnete Aufgabe entfernen",
|
||||
"ForbidCreateProjectPermission": "Projekterstellung verbieten",
|
||||
"ForbidCreateProjectPermissionDescription": "Verbietet Benutzern das Erstellen neuer Projekte"
|
||||
"ForbidCreateProjectPermissionDescription": "Verbietet Benutzern das Erstellen neuer Projekte",
|
||||
"AllowCreatingIssues": "Erstellen von Aufgaben erlauben"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
||||
@@ -289,7 +289,8 @@
|
||||
"Extensions": "Extensions",
|
||||
"UnsetParentIssue": "Unset parent issue",
|
||||
"ForbidCreateProjectPermission": "Forbid create project",
|
||||
"ForbidCreateProjectPermissionDescription": "Forbid users creating new projects"
|
||||
"ForbidCreateProjectPermissionDescription": "Forbid users creating new projects",
|
||||
"AllowCreatingIssues": "Allow creating issues"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
||||
@@ -272,7 +272,8 @@
|
||||
"Extensions": "Extensions",
|
||||
"UnsetParentIssue": "Unset parent issue",
|
||||
"ForbidCreateProjectPermission": "Prohibir crear proyecto",
|
||||
"ForbidCreateProjectPermissionDescription": "Prohíbe a los usuarios crear nuevos proyectos"
|
||||
"ForbidCreateProjectPermissionDescription": "Prohíbe a los usuarios crear nuevos proyectos",
|
||||
"AllowCreatingIssues": "Permitir crear incidencias"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
||||
@@ -272,7 +272,8 @@
|
||||
"Extensions": "Extensions",
|
||||
"UnsetParentIssue": "Désélectionner l'issue parent",
|
||||
"ForbidCreateProjectPermission": "Interdire la création de projet",
|
||||
"ForbidCreateProjectPermissionDescription": "Interdit aux utilisateurs de créer de nouveaux projets"
|
||||
"ForbidCreateProjectPermissionDescription": "Interdit aux utilisateurs de créer de nouveaux projets",
|
||||
"AllowCreatingIssues": "Autoriser la création d'issues"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
||||
@@ -272,7 +272,8 @@
|
||||
"Extensions": "Estensioni",
|
||||
"UnsetParentIssue": "Annulla l'issue genitore",
|
||||
"ForbidCreateProjectPermission": "Vieta creazione progetto",
|
||||
"ForbidCreateProjectPermissionDescription": "Vieta agli utenti di creare nuovi progetti"
|
||||
"ForbidCreateProjectPermissionDescription": "Vieta agli utenti di creare nuovi progetti",
|
||||
"AllowCreatingIssues": "Consenti la creazione di issue"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
||||
@@ -272,7 +272,8 @@
|
||||
"Extensions": "拡張機能",
|
||||
"UnsetParentIssue": "親イシューの設定を解除",
|
||||
"ForbidCreateProjectPermission": "プロジェクト作成禁止",
|
||||
"ForbidCreateProjectPermissionDescription": "ユーザーが新しいプロジェクトを作成することを禁止します"
|
||||
"ForbidCreateProjectPermissionDescription": "ユーザーが新しいプロジェクトを作成することを禁止します",
|
||||
"AllowCreatingIssues": "イシューの作成を許可"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
||||
@@ -272,7 +272,8 @@
|
||||
"Extensions": "Extensions",
|
||||
"UnsetParentIssue": "Desmarcar problema pai",
|
||||
"ForbidCreateProjectPermission": "Proibir criação de projeto",
|
||||
"ForbidCreateProjectPermissionDescription": "Proíbe os usuários de criar novos projetos"
|
||||
"ForbidCreateProjectPermissionDescription": "Proíbe os usuários de criar novos projetos",
|
||||
"AllowCreatingIssues": "Permitir criar problemas"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
||||
@@ -272,7 +272,8 @@
|
||||
"Extensions": "Extensions",
|
||||
"UnsetParentIssue": "Desmarcar problema pai",
|
||||
"ForbidCreateProjectPermission": "Proibir criação de projeto",
|
||||
"ForbidCreateProjectPermissionDescription": "Proíbe os usuários de criar novos projetos"
|
||||
"ForbidCreateProjectPermissionDescription": "Proíbe os usuários de criar novos projetos",
|
||||
"AllowCreatingIssues": "Permitir criar problemas"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
||||
@@ -289,7 +289,8 @@
|
||||
"Extensions": "Дополнительно",
|
||||
"UnsetParentIssue": "Снять родительскую задачу",
|
||||
"ForbidCreateProjectPermission": "Запретить создание проекта",
|
||||
"ForbidCreateProjectPermissionDescription": "Запрещает пользователям создавать новые проекты"
|
||||
"ForbidCreateProjectPermissionDescription": "Запрещает пользователям создавать новые проекты",
|
||||
"AllowCreatingIssues": "Разрешить создание задач"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
||||
@@ -270,7 +270,8 @@
|
||||
"DefaultIssueStatus": "Varsayılan sorun durumu",
|
||||
"IssueStatus": "Durum",
|
||||
"Extensions": "Uzantılar",
|
||||
"UnsetParentIssue": "Üst sorunu kaldır"
|
||||
"UnsetParentIssue": "Üst sorunu kaldır",
|
||||
"AllowCreatingIssues": "Sorun oluşturmaya izin ver"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
||||
@@ -289,7 +289,8 @@
|
||||
"Extensions": "扩展",
|
||||
"UnsetParentIssue": "取消父问题",
|
||||
"ForbidCreateProjectPermission": "禁止创建项目",
|
||||
"ForbidCreateProjectPermissionDescription": "禁止用户创建新项目"
|
||||
"ForbidCreateProjectPermissionDescription": "禁止用户创建新项目",
|
||||
"AllowCreatingIssues": "允许创建问题"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"TrainingStateDraft": "Koncept",
|
||||
"TrainingStateReleased": "Publikováno",
|
||||
"TrainingTake": "Zúčastnit se školení",
|
||||
"AllowToTakeTraining": "Povolit účast na školení",
|
||||
"TrainingTitle": "Název",
|
||||
"Trainings": "Školení",
|
||||
"ViewAllTrainings": "Všechna školení",
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"TrainingStateDraft": "Entwurf",
|
||||
"TrainingStateReleased": "Veröffentlicht",
|
||||
"TrainingTake": "Schulung absolvieren",
|
||||
"AllowToTakeTraining": "Gästen erlauben, Schulung zu absolvieren",
|
||||
"TrainingTitle": "Titel",
|
||||
"Trainings": "Schulungen",
|
||||
"ViewAllTrainings": "Alle Schulungen",
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"TrainingStateDraft": "Draft",
|
||||
"TrainingStateReleased": "Released",
|
||||
"TrainingTake": "Take training",
|
||||
"AllowToTakeTraining": "Allow to take training",
|
||||
"TrainingTitle": "Title",
|
||||
"Trainings": "Trainings",
|
||||
"ViewAllTrainings": "All Trainings",
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"TrainingStateDraft": "Borrador",
|
||||
"TrainingStateReleased": "Publicado",
|
||||
"TrainingTake": "Tomar capacitación",
|
||||
"AllowToTakeTraining": "Permitir tomar la capacitación",
|
||||
"TrainingTitle": "Título",
|
||||
"Trainings": "Capacitaciones",
|
||||
"ViewAllTrainings": "Todas las Capacitaciones",
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"TrainingStateDraft": "Brouillon",
|
||||
"TrainingStateReleased": "Sortie",
|
||||
"TrainingTake": "Suivre la formation",
|
||||
"AllowToTakeTraining": "Autoriser à suivre la formation",
|
||||
"TrainingTitle": "Titre",
|
||||
"Trainings": "Formations",
|
||||
"ViewAllTrainings": "Toutes les formations",
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"TrainingStateDraft": "Bozza",
|
||||
"TrainingStateReleased": "Rilasciato",
|
||||
"TrainingTake": "Partecipa alla formazione",
|
||||
"AllowToTakeTraining": "Consentire di seguire la formazione",
|
||||
"TrainingTitle": "Titolo",
|
||||
"Trainings": "Formazioni",
|
||||
"ViewAllTrainings": "Tutte le formazioni",
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"TrainingStateDraft": "下書き",
|
||||
"TrainingStateReleased": "リリース済み",
|
||||
"TrainingTake": "トレーニングを受ける",
|
||||
"AllowToTakeTraining": "トレーニングを受講できるようにする",
|
||||
"TrainingTitle": "タイトル",
|
||||
"Trainings": "トレーニング",
|
||||
"ViewAllTrainings": "すべてのトレーニング",
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"TrainingStateDraft": "Rascunho",
|
||||
"TrainingStateReleased": "Publicado",
|
||||
"TrainingTake": "Realizar treinamento",
|
||||
"AllowToTakeTraining": "Permitir realizar o treinamento",
|
||||
"TrainingTitle": "Título",
|
||||
"Trainings": "Treinamentos",
|
||||
"ViewAllTrainings": "Todos os treinamentos",
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"TrainingStateDraft": "Rascunho",
|
||||
"TrainingStateReleased": "Publicado",
|
||||
"TrainingTake": "Realizar treinamento",
|
||||
"AllowToTakeTraining": "Permitir realizar o treinamento",
|
||||
"TrainingTitle": "Título",
|
||||
"Trainings": "Treinamentos",
|
||||
"ViewAllTrainings": "Todos os treinamentos",
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"TrainingStateDraft": "Рабочая копия",
|
||||
"TrainingStateReleased": "Актуальный",
|
||||
"TrainingTake": "Пройти тренинг ещё раз",
|
||||
"AllowToTakeTraining": "Разрешить проходить тренинг",
|
||||
"TrainingTitle": "Заголовок",
|
||||
"Trainings": "Тренинги",
|
||||
"ViewAllTrainings": "Все тренинги",
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"TrainingStateDraft": "Taslak",
|
||||
"TrainingStateReleased": "Yayınlandı",
|
||||
"TrainingTake": "Eğitimi al",
|
||||
"AllowToTakeTraining": "Eğitimi almaya izin ver",
|
||||
"TrainingTitle": "Başlık",
|
||||
"Trainings": "Eğitimler",
|
||||
"ViewAllTrainings": "Tüm Eğitimler",
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"TrainingStateDraft": "草稿",
|
||||
"TrainingStateReleased": "已发布",
|
||||
"TrainingTake": "参加培训",
|
||||
"AllowToTakeTraining": "允许参加培训",
|
||||
"TrainingTitle": "标题",
|
||||
"Trainings": "培训",
|
||||
"ViewAllTrainings": "所有培训",
|
||||
|
||||
@@ -116,6 +116,7 @@ export default plugin(trainingId, {
|
||||
TrainingTake: '' as IntlString,
|
||||
TrainingTitle: '' as IntlString,
|
||||
Trainings: '' as IntlString,
|
||||
AllowToTakeTraining: '' as IntlString,
|
||||
ViewAllTrainings: '' as IntlString,
|
||||
ViewAllTrainingsAll: '' as IntlString,
|
||||
ViewAllTrainingsArchived: '' as IntlString,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import core, { AccountRole, getCurrentAccount, type Ref } from '@hcengineering/core'
|
||||
import core, { AccountRole, getCurrentAccount, type ModulePermissionGroup, type Ref } from '@hcengineering/core'
|
||||
import { createNotificationsQuery, createQuery } from '@hcengineering/presentation'
|
||||
import { Scroller, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||
import { NavLink } from '@hcengineering/view-resources'
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function getClickHandler (app: Application, customProps: any) {
|
||||
function getClickHandler (app: Application, customProps: any): () => void {
|
||||
return (
|
||||
customProps.onClick ??
|
||||
(() => {
|
||||
@@ -46,10 +46,34 @@
|
||||
}
|
||||
|
||||
let loaded: boolean = false
|
||||
let permissionsLoaded: boolean = false
|
||||
let hiddenAppsIds: Array<Ref<Application>> = []
|
||||
let excludedApps: string[] = []
|
||||
let disabledApplications: Set<Ref<Application>> = new Set<Ref<Application>>()
|
||||
|
||||
const hiddenAppsIdsQuery = createQuery()
|
||||
const modulePermissionGroupsQuery = createQuery()
|
||||
modulePermissionGroupsQuery.query(core.class.ModulePermissionGroup, {}, (res) => {
|
||||
try {
|
||||
const modulePermissionGroups = res as ModulePermissionGroup[]
|
||||
disabledApplications = new Set<Ref<Application>>(
|
||||
modulePermissionGroups
|
||||
.filter((g) => {
|
||||
if (g.enabled ?? true) return false
|
||||
const role = getCurrentAccount().role
|
||||
if (role === g.role) return true
|
||||
// DocGuest / ReadOnlyGuest should also respect Guest module disables.
|
||||
return (role === AccountRole.DocGuest || role === AccountRole.ReadOnlyGuest) && g.role === AccountRole.Guest
|
||||
})
|
||||
.map((g) => g.application as Ref<Application>)
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error loading module permission groups:', error)
|
||||
} finally {
|
||||
permissionsLoaded = true
|
||||
}
|
||||
})
|
||||
|
||||
hiddenAppsIdsQuery.query(
|
||||
workbench.class.HiddenApplication,
|
||||
{
|
||||
@@ -86,22 +110,20 @@
|
||||
|
||||
updateExcludedApps()
|
||||
|
||||
function isAppVisibleInSwitcher (app: Application, disabledModules: Set<Ref<Application>>): boolean {
|
||||
return !hiddenAppsIds.includes(app._id) && !excludedApps.includes(app.alias) && !disabledModules.has(app._id)
|
||||
}
|
||||
|
||||
$: topApps = apps
|
||||
.filter((it) => it.position === 'top' && !hiddenAppsIds.includes(it._id) && !excludedApps.includes(it.alias))
|
||||
.filter((it) => it.position === 'top' && isAppVisibleInSwitcher(it, disabledApplications))
|
||||
.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
|
||||
$: midApps = apps
|
||||
.filter(
|
||||
(it) =>
|
||||
!hiddenAppsIds.includes(it._id) &&
|
||||
!excludedApps.includes(it.alias) &&
|
||||
it.position !== 'top' &&
|
||||
it.position !== 'bottom'
|
||||
(it) => it.position !== 'top' && it.position !== 'bottom' && isAppVisibleInSwitcher(it, disabledApplications)
|
||||
)
|
||||
.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
|
||||
|
||||
$: bottomApps = apps.filter(
|
||||
(it) => it.position === 'bottom' && !hiddenAppsIds.includes(it._id) && !excludedApps.includes(it.alias)
|
||||
)
|
||||
$: bottomApps = apps.filter((it) => it.position === 'bottom' && isAppVisibleInSwitcher(it, disabledApplications))
|
||||
|
||||
const inboxClient = InboxNotificationsClientImpl.getClient()
|
||||
const inboxNotificationsByContextStore = inboxClient.inboxNotificationsByContext
|
||||
@@ -135,7 +157,7 @@
|
||||
</script>
|
||||
|
||||
<div class="flex-{direction === 'horizontal' ? 'row-center' : 'col-center'} clear-mins apps-{direction} relative">
|
||||
{#if loaded}
|
||||
{#if loaded && permissionsLoaded}
|
||||
<Scroller
|
||||
invertScroll
|
||||
padding={direction === 'horizontal' ? '.75rem .5rem' : '.5rem .75rem'}
|
||||
|
||||
Reference in New Issue
Block a user