Show doc attrs and collaborators for cards (#10390)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2026-01-11 20:23:53 +05:00
committed by GitHub
parent dcb25b2a07
commit ab9daa4991
9 changed files with 73 additions and 26 deletions
+1 -1
View File
@@ -97,7 +97,7 @@
"MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json",
// "SERVER_PROVIDER":"uweb"
"SERVER_PROVIDER": "ws",
"MODEL_VERSION": "0.7.331",
"MODEL_VERSION": "0.7.343",
// "version": "0.7.0",
"COMMUNICATION_API_ENABLED": "true",
"ELASTIC_INDEX_NAME": "local_storage_index",
@@ -944,6 +944,7 @@ export type SocialKey = Pick<SocialId, 'type' | 'value'>
export interface ClassCollaborators<T extends Doc> extends Doc {
attachedTo: Ref<Class<T>>
allFields?: boolean // for all (PersonId | Ref<Employee> | PersonId[] | Ref<Employee>[]) attributes
fields: (keyof T)[] // PersonId | Ref<Employee> | PersonId[] | Ref<Employee>[]
provideSecurity?: boolean // If true, will provide security for collaborators
}
+7
View File
@@ -38,6 +38,7 @@ import core, {
AccountRole,
type Blobs,
type Class,
type ClassCollaborators,
ClassifierKind,
type CollectionSize,
type Doc,
@@ -860,6 +861,12 @@ export function createModel (builder: Builder): void {
})
createPublicLinkAction(builder, card.class.Card, card.action.PublicLink)
builder.createDoc<ClassCollaborators<Card>>(core.class.ClassCollaborators, core.space.Model, {
attachedTo: card.class.Card,
fields: ['modifiedBy'],
allFields: true
})
}
function defineTabs (builder: Builder): void {
+1
View File
@@ -413,6 +413,7 @@ export class TCustomSequence extends TSequence implements CustomSequence {
@Model(core.class.ClassCollaborators, core.class.Doc, DOMAIN_MODEL)
export class TClassCollaborators extends TDoc implements ClassCollaborators<Doc> {
attachedTo!: Ref<Class<Doc>>
allFields?: boolean
fields!: (keyof Doc)[]
provideSecurity?: boolean
}
+4
View File
@@ -1367,6 +1367,10 @@ export function createModel (builder: Builder): void {
searchDisabled: true
})
builder.mixin(core.class.TypePersonId, core.class.Class, view.mixin.AttributeEditor, {
inlineEditor: view.component.PersonIdPresenter
})
builder.mixin(core.class.TypePersonId, core.class.Class, view.mixin.AttributePresenter, {
presenter: view.component.PersonIdPresenter,
arrayPresenter: view.component.PersonArrayEditor
@@ -17,11 +17,11 @@
import { PermissionsStore } from '@hcengineering/contact'
import { permissionsStore } from '@hcengineering/contact-resources'
import core, { AnyAttribute, Class, Doc, Ref, toRank, TypedSpace } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import {
AttributeBarEditor,
createQuery,
getClient,
getFiltredKeys,
isCollectionAttr,
KeyedAttribute
} from '@hcengineering/presentation'
@@ -29,11 +29,12 @@
export let object: Card
export let _class: Ref<Class<Doc>>
export let to: Ref<Class<Doc>> | undefined = core.class.Doc
export let to: Ref<Class<Doc>> | undefined = core.class.Obj
export let ignoreKeys: string[] = []
export let readonly = false
export let showHeader: boolean = true
export let fourRows: boolean = false
export let showCollaborators: boolean = false
const client = getClient()
const hierarchy = client.getHierarchy()
@@ -41,14 +42,18 @@
let keys: KeyedAttribute[] = []
function updateKeys (_class: Ref<Class<Doc>>, ignoreKeys: string[], to: Ref<Class<Doc>> | undefined): void {
const filtredKeys = getFiltredKeys(hierarchy, _class, ignoreKeys, to)
keys = filtredKeys
.filter((key) => !isCollectionAttr(hierarchy, key))
.sort((a, b) => {
const rankA = a.attr.rank ?? toRank(a.attr._id) ?? ''
const rankB = b.attr.rank ?? toRank(b.attr._id) ?? ''
return rankA.localeCompare(rankB)
})
const filtredKeys = [...hierarchy.getAllAttributes(_class, to).entries()]
.filter(
([key, value]) =>
value.hidden !== true && !ignoreKeys.includes(key) && !isCollectionAttr(hierarchy, { key, attr: value })
)
.map(([key, attr]) => ({ key, attr }))
keys = filtredKeys.sort((a, b) => {
const rankA = a.attr.rank ?? toRank(a.attr._id) ?? ''
const rankB = b.attr.rank ?? toRank(b.attr._id) ?? ''
return rankA.localeCompare(rankB)
})
}
$: updateKeys(_class, ignoreKeys, to)
@@ -75,6 +80,17 @@
on:update
/>
{/each}
{#if showCollaborators}
<AttributeBarEditor
key={'collaborators'}
_class={notification.mixin.Collaborators}
{object}
{showHeader}
{readonly}
withIcon
on:update
/>
{/if}
</div>
<style lang="scss">
@@ -88,7 +88,7 @@
{/if}
</div>
<ExpandCollapse isExpanded={!isCollapsed}>
<CardAttributes object={value} _class={value._class} {readonly} {ignoreKeys} {fourRows} />
<CardAttributes object={value} _class={value._class} {readonly} {ignoreKeys} {fourRows} showCollaborators />
</ExpandCollapse>
<style lang="scss">
@@ -38,8 +38,8 @@
export let ignoreKeys: string[]
const client = getClient()
const hierarchy = client.getHierarchy()
const label = hierarchy.getClass(tag._id).label
const h = client.getHierarchy()
const label = h.getClass(tag._id).label
let isCollapsed = false
@@ -52,6 +52,8 @@
}
$: color = getPlatformColorDef(tag.background ?? 0, $themeStore.dark).color
$: isEditable = h.hasMixin(tag, setting.mixin.Editable) && h.as(tag, setting.mixin.Editable).value
</script>
<div class="header flex flex-gap-2">
@@ -66,7 +68,7 @@
</span>
<Chevron expanded={!isCollapsed} outline fill={'var(--content-color'} />
</div>
{#if hasAccountRole(getCurrentAccount(), AccountRole.Maintainer)}
{#if hasAccountRole(getCurrentAccount(), AccountRole.Maintainer) && isEditable}
<div class="btns">
<Button
icon={IconAdd}
@@ -311,14 +311,30 @@ export async function getDocCollaborators (
control: TriggerControl
): Promise<AccountUuid[]> {
const collaborators = new Set<AccountUuid>()
for (const field of mixin.fields) {
const value = (doc as any)[field]
const newCollaborators = await ctx.with('getKeyCollaborators', {}, (ctx) =>
getKeyCollaborators(doc._class, value, field, control)
)
if (newCollaborators !== undefined) {
for (const newCollaborator of newCollaborators) {
collaborators.add(newCollaborator)
if (mixin.allFields !== true) {
for (const field of mixin.fields) {
const value = (doc as any)[field]
const newCollaborators = await ctx.with('getKeyCollaborators', {}, (ctx) =>
getKeyCollaborators(doc._class, value, field, control)
)
if (newCollaborators !== undefined) {
for (const newCollaborator of newCollaborators) {
collaborators.add(newCollaborator)
}
}
}
} else {
const attrs = control.hierarchy.getAllAttributes(doc._class)
// attr type will be checked into getKeyCollaborators
for (const [field] of attrs) {
const value = (doc as any)[field]
const newCollaborators = await ctx.with('getKeyCollaborators', {}, (ctx) =>
getKeyCollaborators(doc._class, value, field, control)
)
if (newCollaborators !== undefined) {
for (const newCollaborator of newCollaborators) {
collaborators.add(newCollaborator)
}
}
}
}
@@ -1137,7 +1153,7 @@ async function getNewCollaborators (
const newCollaborators = new Set<AccountUuid>()
if (ops.$push !== undefined) {
for (const key in ops.$push) {
if (mixin.fields.includes(key as any)) {
if (mixin.fields.includes(key as any) || mixin.allFields === true) {
let value = (ops.$push as any)[key]
if (typeof value !== 'string') {
value = value.$each
@@ -1153,7 +1169,7 @@ async function getNewCollaborators (
}
for (const key in ops) {
if (key.startsWith('$')) continue
if (mixin.fields.includes(key as any)) {
if (mixin.fields.includes(key as any) || mixin.allFields === true) {
const value = (ops as any)[key]
const newCollabs = await getKeyCollaborators(docClass, value, key, control)
if (newCollabs !== undefined) {
@@ -1176,7 +1192,7 @@ async function getRemovedMembers (
const removedCollaborators: AccountUuid[] = []
if (ops.$pull !== undefined && 'members' in ops.$pull) {
const key = 'members'
if (mixin.fields.includes(key as any)) {
if (mixin.fields.includes(key as any) || mixin.allFields === true) {
let value = (ops.$pull as any)[key]
if (typeof value !== 'string') {
value = value.$in