Improve cards permissions (#10260)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2025-11-29 13:19:46 +07:00
committed by GitHub
parent 207cb4c72a
commit 2c2a217b1e
18 changed files with 171 additions and 37 deletions
+1 -1
View File
@@ -1 +1 @@
"0.7.301"
"0.7.312"
@@ -551,6 +551,10 @@ export interface Permission extends Doc {
icon?: Asset
}
export interface AttributePermission extends Permission {
attribute: Ref<AnyAttribute>
}
/**
* @public
*/
@@ -63,7 +63,8 @@ import type {
Version,
AccountUuid,
ClassCollaborators,
Collaborator
Collaborator,
AttributePermission
} from './classes'
import { type Status, type StatusCategory } from './status'
import type {
@@ -129,6 +130,7 @@ export default plugin(coreId, {
SpaceType: '' as Ref<Class<SpaceType>>,
Role: '' as Ref<Class<Role>>,
Permission: '' as Ref<Class<Permission>>,
AttributePermission: '' as Ref<Class<AttributePermission>>,
Type: '' as Ref<Class<Type<any>>>,
TypeRelation: '' as Ref<Class<Type<string>>>,
TypeString: '' as Ref<Class<Type<string>>>,
+5 -1
View File
@@ -177,7 +177,7 @@ export class TCardViewDefaults extends TMasterTag implements CardViewDefaults {
@Model(card.class.Role, core.class.Role, DOMAIN_MODEL)
export class TRole extends TBaseRole implements Role {
type!: Ref<MasterTag | Tag>
types!: Ref<MasterTag | Tag>[]
}
@Model(card.class.FavoriteCard, preference.class.Preference)
@@ -901,6 +901,10 @@ function defineTabs (builder: Builder): void {
}
})
builder.mixin(card.class.Role, core.class.Class, view.mixin.ObjectPresenter, {
presenter: setting.component.PermissionPresenter
})
builder.createDoc<Viewlet>(view.class.Viewlet, core.space.Model, {
attachTo: card.class.FavoriteCard,
descriptor: view.viewlet.List,
+23 -7
View File
@@ -13,27 +13,27 @@
// limitations under the License.
//
import cardPlugin, { type Card, cardId, DOMAIN_CARD, type Role } from '@hcengineering/card'
import cardPlugin, { cardId, DOMAIN_CARD, type Card, type Role } from '@hcengineering/card'
import core, {
DOMAIN_MODEL,
type Ref,
TxOperations,
type Client,
type Data,
type Doc,
type DocumentUpdate
type DocumentUpdate,
type Ref
} from '@hcengineering/core'
import {
createOrUpdate,
tryMigrate,
tryUpgrade,
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient,
createOrUpdate
type MigrationUpgradeClient
} from '@hcengineering/model'
import tags from '@hcengineering/tags'
import view, { type Viewlet } from '@hcengineering/view'
import card from '.'
import tags from '@hcengineering/tags'
export const cardOperation: MigrateOperation = {
async migrate (client: MigrationClient, mode): Promise<void> {
@@ -100,6 +100,11 @@ export const cardOperation: MigrateOperation = {
state: 'add-space-type',
mode: 'upgrade',
func: addSpaceType
},
{
state: 'migrate-role-types',
mode: 'upgrade',
func: migrateRoleTypes
}
])
}
@@ -118,7 +123,7 @@ async function migrateRolesToBaseRole (client: MigrationUpgradeClient): Promise<
const roles = await client.findAll(card.class.Role, { attachedTo: { $ne: cardPlugin.spaceType.SpaceType } })
for (const role of roles) {
const baseRoleData: DocumentUpdate<Role> = {
type: role.attachedTo as any,
types: [role.attachedTo as any],
attachedTo: cardPlugin.spaceType.SpaceType,
attachedToClass: core.class.SpaceType
}
@@ -383,3 +388,14 @@ async function makeConfigSortable (client: Client): Promise<void> {
await txOp.update(currentViewlet, { configOptions })
}
}
async function migrateRoleTypes (client: Client): Promise<void> {
const txOp = new TxOperations(client, core.account.System)
const roles = await client.findAll(card.class.Role, { types: { $exists: false } })
for (const role of roles) {
const baseRoleData: DocumentUpdate<Role> = {
types: [(role as any).type]
}
await txOp.update(role, baseRoleData)
}
}
+11 -1
View File
@@ -76,7 +76,16 @@ import {
TCollaborator
} from './core'
import { definePermissions } from './permissions'
import { TPermission, TRole, TSpace, TSpaceType, TSpaceTypeDescriptor, TSystemSpace, TTypedSpace } from './security'
import {
TAttributePermission,
TPermission,
TRole,
TSpace,
TSpaceType,
TSpaceTypeDescriptor,
TSystemSpace,
TTypedSpace
} from './security'
import { defineSpaceType } from './spaceType'
import { TDomainStatusPlaceholder, TStatus, TStatusCategory } from './status'
import { TUserStatus } from './transient'
@@ -124,6 +133,7 @@ export function createModel (builder: Builder): void {
TSpaceTypeDescriptor,
TRole,
TPermission,
TAttributePermission,
TAttribute,
TType,
TEnumOf,
+9 -1
View File
@@ -31,7 +31,9 @@ import {
type AccountUuid,
type TxAccessLevel,
type Tx,
type Doc
type Doc,
type AnyAttribute,
type AttributePermission
} from '@hcengineering/core'
import {
ArrOf,
@@ -165,6 +167,12 @@ export class TPermission extends TDoc implements Permission {
icon?: Asset
}
@Model(core.class.AttributePermission, core.class.Permission)
@UX(core.string.Permission)
export class TAttributePermission extends TPermission implements AttributePermission {
attribute!: Ref<AnyAttribute>
}
@Mixin(core.mixin.SpacesTypeData, core.class.Space)
@UX(getEmbeddedLabel("All spaces' type")) // TODO: add icon?
export class TSpacesTypeData extends TSpace implements RolesAssignment {
@@ -32,7 +32,8 @@
const hierarchy = client.getHierarchy()
const topLevelTypes = client.getModel().findAllSync(card.class.MasterTag, {
extends: card.class.Card
extends: card.class.Card,
removed: { $ne: true }
})
let types: Ref<MasterTag>[] =
@@ -22,12 +22,13 @@
import { CardSpace, MasterTag } from '@hcengineering/card'
import card from '../../plugin'
import { onDestroy } from 'svelte'
import { location } from '@hcengineering/ui'
import { Action, location } from '@hcengineering/ui'
export let space: CardSpace
export let model: SpacesNavModel
export let currentSpace: Ref<Space> | undefined
export let forciblyСollapsed: boolean = false
export let getActions: (space: Space) => Promise<Action[]> = async () => []
let classes: MasterTag[] = []
let allClasses: MasterTag[] = []
@@ -61,6 +62,7 @@
highlighted={currentSpace === space._id}
visible={currentSpace === space._id || forciblyСollapsed}
{forciblyСollapsed}
actions={() => getActions(space)}
on:dragstart={(evt) => {
evt.preventDefault()
}}
@@ -22,7 +22,7 @@
export let hidden: boolean = false
</script>
{#if hidden}
{#if !hidden}
<div class="section-relations">
<RelationsEditor object={doc} {readonly} on:loaded emptyKind="placeholder" />
</div>
@@ -36,7 +36,7 @@
'roles',
{
name: value,
type: masterTag._id,
types: [masterTag._id],
permissions: []
}
)
@@ -13,6 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import contact from '@hcengineering/contact'
import core, { Permission, Ref } from '@hcengineering/core'
import { AttributeEditor, MessageBox, createQuery, getClient } from '@hcengineering/presentation'
import {
@@ -23,39 +24,50 @@
IconSettings,
Label,
Scroller,
getCurrentLocation,
getCurrentResolvedLocation,
navigate,
showPopup
} from '@hcengineering/ui'
import contact from '@hcengineering/contact'
import { ObjectBoxPopup } from '@hcengineering/view-resources'
import cardPlugin from '../../plugin'
import { MasterTag, Role } from '@hcengineering/card'
import settingRes from '@hcengineering/setting-resources/src/plugin'
import { clearSettingsStore } from '@hcengineering/setting-resources'
import settingRes from '@hcengineering/setting-resources/src/plugin'
import { createEventDispatcher } from 'svelte'
import cardPlugin from '../../plugin'
export let _id: Ref<Role>
export let masterTag: MasterTag
export let readonly: boolean = false
const client = getClient()
const h = client.getHierarchy()
let role: Role | undefined = client.getModel().findAllSync(cardPlugin.class.Role, { _id })[0]
const cardPermissionsObjectClasses = client
.getModel()
.findAllSync(cardPlugin.class.PermissionObjectClass, {})
.map((poc) => poc.objectClass)
const tagDesc = h.getAncestors(masterTag._id)
const tagDesc = Array.from(new Set(role?.types?.map((p) => h.getAncestors(p)).flat()))
const allPermissions = client.getModel().findAllSync(core.class.Permission, {
scope: 'space',
objectClass: { $in: [...cardPermissionsObjectClasses, ...tagDesc] }
})
let role: Role | undefined
const dispatch = createEventDispatcher()
const roleQuery = createQuery()
$: roleQuery.query(cardPlugin.class.Role, { _id }, (res) => {
;[role] = res
if (role !== undefined) {
dispatch('change', [
{
id: res[0]?._id,
title: res[0]?.name
}
])
}
})
let permissions: Permission[] = []
@@ -112,11 +124,24 @@
)
}
const masterTag = getCurrentLocation().path[4] as Ref<MasterTag>
async function performDeleteRole (): Promise<void> {
if (role === undefined) {
return
}
const types = role.types.filter((p) => p !== masterTag)
if (types.length > 0) {
await client.update(role, { types })
const loc = getCurrentResolvedLocation()
loc.path.length = 5
clearSettingsStore()
navigate(loc)
return
}
const attribute = await client.findOne(core.class.Attribute, { name: _id, attributeOf: cardPlugin.class.CardSpace })
const ops = client.apply()
@@ -161,7 +186,7 @@
icon={IconDelete}
size="large"
kind="secondary"
disabled={readonly}
disabled={readonly || !role.types.includes(masterTag)}
on:click={handleDeleteRole}
/>
<ButtonIcon
@@ -0,0 +1,40 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { MasterTag, Role, Tag } from '@hcengineering/card'
import core, { Ref } from '@hcengineering/core'
import { ObjectPopup } from '@hcengineering/presentation'
import card from '../../plugin'
export let masterTag: MasterTag | Tag
export let roles: Ref<Role>[]
</script>
<ObjectPopup
_class={card.class.Role}
ignoreObjects={roles}
create={{
label: core.string.Role,
component: card.component.CreateRolePopup,
props: { masterTag }
}}
on:close
>
<svelte:fragment slot="item" let:item={doc}>
<div class="overflow-label">
{doc.name}
</div>
</svelte:fragment>
</ObjectPopup>
@@ -20,7 +20,7 @@
import { clearSettingsStore } from '@hcengineering/setting-resources'
import { ButtonIcon, getCurrentResolvedLocation, Icon, IconAdd, Label, navigate, showPopup } from '@hcengineering/ui'
import card from '../../plugin'
import CreateRolePopup from './CreateRolePopup.svelte'
import RolesPopup from './RolesPopup.svelte'
export let masterTag: MasterTag | Tag
@@ -28,14 +28,31 @@
const ancestors = client.getHierarchy().getAncestors(masterTag._id)
let roles = client.getModel().findAllSync(card.class.Role, { type: { $in: ancestors } })
let roles = client.getModel().findAllSync(card.class.Role, { types: { $in: ancestors } })
const query = createQuery()
query.query(card.class.Role, { type: { $in: ancestors } }, (res) => {
query.query(card.class.Role, { types: { $in: ancestors } }, (res) => {
roles = res
})
function addRole (): void {
showPopup(CreateRolePopup, { masterTag })
showPopup(
RolesPopup,
{
masterTag,
roles: roles.map((r) => r._id)
},
'top',
async (res) => {
if (res !== undefined) {
const role = res as Role
if (!role.types.some((type) => ancestors.includes(type))) {
await client.update(role, {
$push: { types: masterTag._id }
})
}
}
}
)
}
const handleSelect = (role: Role): void => {
+3 -1
View File
@@ -58,6 +58,7 @@ import TypesNavigator from './components/navigator/TypesNavigator.svelte'
import LabelsPresenter from './components/LabelsPresenter.svelte'
import RolesSection from './components/settings/RolesSection.svelte'
import EditRole from './components/settings/EditRole.svelte'
import CreateRolePopup from './components/settings/CreateRolePopup.svelte'
import CardWidget from './components/CardWidget.svelte'
import CreateSpace from './components/navigator/CreateSpace.svelte'
import CardHeaderButton from './components/navigator/CardHeaderButton.svelte'
@@ -127,7 +128,8 @@ export default async (): Promise<Resources> => ({
CardIcon,
CardFeedView,
CreateSpace,
CardHeaderButton
CardHeaderButton,
CreateRolePopup
},
sectionComponent: {
AttachmentsSection: AttachmentsCardSection,
+2 -1
View File
@@ -53,7 +53,8 @@ export default mergeIds(cardId, card, {
CardWidget: '' as AnyComponent,
CardWidgetTab: '' as AnyComponent,
CreateCard: '' as AnyComponent,
CardHeaderButton: '' as AnyComponent
CardHeaderButton: '' as AnyComponent,
CreateRolePopup: '' as AnyComponent
},
function: {
CardFactory: '' as Resource<(props?: Record<string, any>) => Promise<Ref<Doc> | undefined>>
+1 -1
View File
@@ -44,7 +44,7 @@ export interface MasterTag extends Class<Card> {
export interface Tag extends MasterTag, Mixin<Card> {}
export interface Role extends BaseRole {
type: Ref<MasterTag | Tag>
types: Ref<MasterTag | Tag>[]
}
export interface Card extends Doc, IconProps {
@@ -15,10 +15,10 @@
<script lang="ts">
import core, {
AnyAttribute,
AttributePermission,
Class,
DocumentUpdate,
IndexKind,
Permission,
PropertyType,
Ref,
Type
@@ -161,8 +161,8 @@
client.getModel().findObject(getAttributePermissionRef(attribute, false)) !== undefined ||
client.getModel().findObject(getAttributePermissionRef(attribute, true)) !== undefined
function getAttributePermissionRef (attr: AnyAttribute, forbidden: boolean): Ref<Permission> {
return `${attr._id}_${forbidden ? 'forbidden' : 'allowed'}` as Ref<Permission>
function getAttributePermissionRef (attr: AnyAttribute, forbidden: boolean): Ref<AttributePermission> {
return `${attr._id}_${forbidden ? 'forbidden' : 'allowed'}` as Ref<AttributePermission>
}
function changeRestricted (e: CustomEvent<boolean>): void {
@@ -177,7 +177,7 @@
const txClass = isMixin ? core.class.TxMixin : core.class.TxUpdateDoc
const txMatchQ = isMixin ? `attributes.${attribute.name}` : `operations.${attribute.name}`
await client.createDoc(
core.class.Permission,
core.class.AttributePermission,
core.space.Model,
{
objectClass: attribute.attributeOf,
@@ -188,12 +188,13 @@
scope: 'space',
forbid: false,
label: view.string.AllowAttributeChanges,
description: attribute.label
description: attribute.label,
attribute: attribute._id
},
getAttributePermissionRef(attribute, false)
)
await client.createDoc(
core.class.Permission,
core.class.AttributePermission,
core.space.Model,
{
objectClass: attribute.attributeOf,
@@ -204,7 +205,8 @@
scope: 'space',
forbid: true,
label: view.string.ForbidAttributeChanges,
description: attribute.label
description: attribute.label,
attribute: attribute._id
},
getAttributePermissionRef(attribute, true)
)