mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
Improve cards permissions (#10260)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
@@ -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>>>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>>
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user