Security improvments (#5595)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2024-05-14 14:29:05 +05:00 committed by GitHub
parent b60067cc92
commit 2d92b9aa46
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 344 additions and 144 deletions

View File

@ -66,7 +66,7 @@ import core, { TAccount, TAttachedDoc, TDoc } from '@hcengineering/model-core'
import { createPublicLinkAction } from '@hcengineering/model-guest'
import { generateClassNotificationTypes } from '@hcengineering/model-notification'
import presentation from '@hcengineering/model-presentation'
import view, { createAction, type Viewlet } from '@hcengineering/model-view'
import view, { createAction, createAttributePresenter, type Viewlet } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import notification from '@hcengineering/notification'
import type { Asset, IntlString, Resource } from '@hcengineering/platform'
@ -1142,4 +1142,6 @@ export function createModel (builder: Builder): void {
domain: DOMAIN_CONTACT,
disabled: [{ attachedToClass: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { createdOn: -1 }, { attachedTo: 1 }]
})
createAttributePresenter(builder, contact.component.SpaceMembersEditor, core.class.Space, 'members', 'array')
}

View File

@ -211,6 +211,10 @@ export function createModel (builder: Builder): void {
inlineEditor: hr.component.DepartmentEditor
})
builder.mixin(hr.class.Department, core.class.Class, view.mixin.AttributePresenter, {
presenter: hr.component.DepartmentRefPresenter
})
builder.mixin(hr.class.Department, core.class.Class, view.mixin.ObjectEditor, {
editor: hr.component.EditDepartment
})

View File

@ -50,7 +50,8 @@ export default mergeIds(hrId, hr, {
TzDatePresenter: '' as AnyComponent,
TzDateEditor: '' as AnyComponent,
RequestPresenter: '' as AnyComponent,
DepartmentPresenter: '' as AnyComponent
DepartmentPresenter: '' as AnyComponent,
DepartmentRefPresenter: '' as AnyComponent
},
category: {
HR: '' as Ref<ActionCategory>

View File

@ -18,9 +18,7 @@ import activity, { type ActivityMessage } from '@hcengineering/activity'
import chunter from '@hcengineering/chunter'
import {
DOMAIN_MODEL,
Hierarchy,
IndexKind,
type Space,
type Account,
type AttachedDoc,
type Class,
@ -31,6 +29,7 @@ import {
type Domain,
type Markup,
type Ref,
type Space,
type Timestamp,
type Tx
} from '@hcengineering/core'
@ -700,11 +699,7 @@ export function generateClassNotificationTypes (
ignoreKeys: string[] = [],
defaultEnabled: string[] = []
): void {
const txes = builder.getTxes()
const hierarchy = new Hierarchy()
for (const tx of txes) {
hierarchy.tx(tx)
}
const hierarchy = builder.hierarchy
const attributes = hierarchy.getAllAttributes(
_class,
hierarchy.isDerived(_class, core.class.AttachedDoc) ? core.class.AttachedDoc : core.class.Doc

View File

@ -23,7 +23,8 @@ import {
type DocumentQuery,
type Domain,
type Ref,
type Space
type Space,
type AnyAttribute
} from '@hcengineering/core'
import { type Builder, Mixin, Model, UX } from '@hcengineering/model'
import core, { TClass, TDoc } from '@hcengineering/model-core'
@ -87,10 +88,13 @@ import {
type ViewletPreference,
type ObjectIdentifier,
type ObjectIcon,
type ObjectTooltip
type ObjectTooltip,
type AttrPresenter,
type AttributeCategory
} from '@hcengineering/view'
import view from './plugin'
import { classPresenter, createAction } from './utils'
export { viewId } from '@hcengineering/view'
export { viewOperation } from './migration'
@ -98,38 +102,7 @@ export type { ViewAction, Viewlet }
export const DOMAIN_VIEW = 'view' as Domain
export function createAction<T extends Doc = Doc, P = Record<string, any>> (
builder: Builder,
data: Data<Action<T, P>>,
id?: Ref<Action<T, P>>
): void {
const { label, ...adata } = data
builder.createDoc<Action<T, P>>(view.class.Action, core.space.Model, { label, ...adata }, id)
}
export function classPresenter (
builder: Builder,
_class: Ref<Class<Doc>>,
presenter: AnyComponent,
editor?: AnyComponent,
popup?: AnyComponent,
activity?: AnyComponent
): void {
builder.mixin(_class, core.class.Class, view.mixin.AttributePresenter, {
presenter
})
if (editor !== undefined) {
builder.mixin(_class, core.class.Class, view.mixin.AttributeEditor, {
inlineEditor: editor,
popup
})
}
if (activity !== undefined) {
builder.mixin(_class, core.class.Class, view.mixin.ActivityAttributePresenter, {
presenter: activity
})
}
}
export * from './utils'
@Model(view.class.FilteredView, core.class.Doc, DOMAIN_VIEW)
@UX(view.string.FilteredViews)
@ -383,6 +356,14 @@ export class TObjectPanel extends TClass implements ObjectPanel {
component!: AnyComponent
}
@Model(view.class.AttrPresenter, core.class.Doc, DOMAIN_MODEL)
export class TAttrPresenter extends TDoc implements AttrPresenter {
category!: AttributeCategory
objectClass!: Ref<Class<Doc<Space>>>
attribute!: Ref<AnyAttribute>
component!: AnyComponent
}
export type ActionTemplate = Partial<Data<Action>>
/**
@ -467,7 +448,8 @@ export function createModel (builder: Builder): void {
TGroupping,
TObjectIdentifier,
TObjectTooltip,
TObjectIcon
TObjectIcon,
TAttrPresenter
)
classPresenter(

70
models/view/src/utils.ts Normal file
View File

@ -0,0 +1,70 @@
//
// Copyright © 2024 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.
//
import { type Class, type Data, type Doc, type Ref } from '@hcengineering/core'
import { type Builder } from '@hcengineering/model'
import core from '@hcengineering/model-core'
import { type AnyComponent } from '@hcengineering/ui'
import { type Action, type AttributeCategory } from '@hcengineering/view'
import view from '.'
export function createAction<T extends Doc = Doc, P = Record<string, any>> (
builder: Builder,
data: Data<Action<T, P>>,
id?: Ref<Action<T, P>>
): void {
const { label, ...adata } = data
builder.createDoc<Action<T, P>>(view.class.Action, core.space.Model, { label, ...adata }, id)
}
export function classPresenter (
builder: Builder,
_class: Ref<Class<Doc>>,
presenter: AnyComponent,
editor?: AnyComponent,
popup?: AnyComponent,
activity?: AnyComponent
): void {
builder.mixin(_class, core.class.Class, view.mixin.AttributePresenter, {
presenter
})
if (editor !== undefined) {
builder.mixin(_class, core.class.Class, view.mixin.AttributeEditor, {
inlineEditor: editor,
popup
})
}
if (activity !== undefined) {
builder.mixin(_class, core.class.Class, view.mixin.ActivityAttributePresenter, {
presenter: activity
})
}
}
export function createAttributePresenter<T extends Doc> (
builder: Builder,
component: AnyComponent,
_class: Ref<Class<T>>,
key: keyof T,
category: AttributeCategory
): void {
const attr = builder.hierarchy.getAttribute(_class, key as string)
builder.createDoc(view.class.AttrPresenter, core.space.Model, {
component,
attribute: attr._id,
objectClass: _class,
category
})
}

View File

@ -27,6 +27,7 @@ import core, {
Domain,
Enum,
EnumOf,
Hierarchy,
Hyperlink,
Mixin as IMixin,
IndexKind,
@ -290,6 +291,7 @@ function _generateTx (tx: ClassTxes): Tx[] {
*/
export class Builder {
private readonly txes: Tx[] = []
readonly hierarchy = new Hierarchy()
onTx?: (tx: Tx) => void
@ -322,7 +324,7 @@ export class Builder {
for (const tx of generated) {
this.txes.push(tx)
this.onTx?.(tx)
// this.hierarchy.tx(tx)
this.hierarchy.tx(tx)
}
}
@ -352,6 +354,7 @@ export class Builder {
}
this.txes.push(tx)
this.onTx?.(tx)
this.hierarchy.tx(tx)
return TxProcessor.createDoc2Doc(tx)
}
@ -364,6 +367,7 @@ export class Builder {
const tx = txFactory.createTxMixin(objectId, objectClass, core.space.Model, mixin, attributes)
this.txes.push(tx)
this.onTx?.(tx)
this.hierarchy.tx(tx)
}
getTxes (): Tx[] {

View File

@ -61,6 +61,12 @@
return true
}
export let sort: <T extends Doc>(a: T, b: T) => number = (a, b) => {
const aval: string = `${getObjectValue(groupBy, a as any)}`
const bval: string = `${getObjectValue(groupBy, b as any)}`
return aval.localeCompare(bval)
}
const created: Doc[] = []
const dispatch = createEventDispatcher()
@ -84,11 +90,7 @@
_id: { $nin: ignoreObjects, ..._idExtra }
},
(result) => {
result.sort((a, b) => {
const aval: string = `${getObjectValue(groupBy, a as any)}`
const bval: string = `${getObjectValue(groupBy, b as any)}`
return aval.localeCompare(bval)
})
result.sort(sort)
if (created.length > 0) {
const cmap = new Set(created.map((it) => it._id))
objects = [...created, ...result.filter((d) => !cmap.has(d._id))].filter(filter)

View File

@ -48,7 +48,7 @@ import core, {
import { getMetadata, getResource } from '@hcengineering/platform'
import { LiveQuery as LQ } from '@hcengineering/query'
import { type AnyComponent, type AnySvelteComponent, type IconSize } from '@hcengineering/ui'
import view, { type AttributeEditor } from '@hcengineering/view'
import view, { type AttributeCategory, type AttributeEditor } from '@hcengineering/view'
import { deepEqual } from 'fast-equals'
import { onDestroy } from 'svelte'
import { type KeyedAttribute } from '..'
@ -407,16 +407,6 @@ export async function copyTextToClipboard (text: string): Promise<void> {
}
}
/**
* @public
*/
export type AttributeCategory = 'object' | 'attribute' | 'inplace' | 'collection' | 'array'
/**
* @public
*/
export const AttributeCategoryOrder = { attribute: 0, inplace: 1, collection: 2, array: 2, object: 3 }
/**
* @public
*/

View File

@ -13,8 +13,8 @@
// limitations under the License.
-->
<script lang="ts">
import { Employee, PersonAccount } from '@hcengineering/contact'
import core, { Account, Ref } from '@hcengineering/core'
import { Contact, Employee, PersonAccount, getName } from '@hcengineering/contact'
import core, { Account, Ref, getCurrentAccount } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { ButtonKind, ButtonSize } from '@hcengineering/ui'
@ -33,7 +33,7 @@
export let includeItems: Ref<Account>[] | undefined = undefined
export let excludeItems: Ref<Account>[] | undefined = undefined
export let emptyLabel: IntlString | undefined = undefined
export let allowGuests: boolean = true
export let allowGuests: boolean = false
let timer: any = null
const client = getClient()
@ -103,6 +103,29 @@
: {})
}
}
const hierarchy = client.getHierarchy()
const me = getCurrentAccount() as PersonAccount
function sort (a: Contact, b: Contact): number {
if (me.person === a._id) {
return -1
}
if (me.person === b._id) {
return 1
}
const aIncludes = employees.includes(a._id as Ref<Employee>)
const bIncludes = employees.includes(b._id as Ref<Employee>)
if (aIncludes && !bIncludes) {
return -1
}
if (!aIncludes && bIncludes) {
return 1
}
const aName = getName(hierarchy, a)
const bName = getName(hierarchy, b)
return aName.localeCompare(bName)
}
</script>
<UserBoxList
@ -114,6 +137,7 @@
{docQuery}
on:update={onUpdate}
{size}
{sort}
justify={'left'}
width={width ?? 'min-content'}
{kind}

View File

@ -4,14 +4,9 @@
import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel'
import { getResource } from '@hcengineering/platform'
import presentation, {
type AttributeCategory,
createQuery,
getClient,
type KeyedAttribute
} from '@hcengineering/presentation'
import presentation, { createQuery, getClient, type KeyedAttribute } from '@hcengineering/presentation'
import { type AnyComponent, Button, Component, IconMixin, IconMoreH, Label } from '@hcengineering/ui'
import view from '@hcengineering/view'
import view, { AttributeCategory } from '@hcengineering/view'
import {
DocAttributeBar,
DocNavLink,

View File

@ -0,0 +1,45 @@
<!--
// Copyright © 2024 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 { Account, Ref, getCurrentAccount } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { Button, ButtonKind, ButtonSize } from '@hcengineering/ui'
import view from '@hcengineering/view'
import AccountArrayEditor from './AccountArrayEditor.svelte'
export let label: IntlString
export let value: Ref<Account>[]
export let onChange: ((refs: Ref<Account>[]) => void) | undefined
export let readonly = false
export let kind: ButtonKind = 'link'
export let size: ButtonSize = 'large'
export let width: string | undefined = undefined
const me = getCurrentAccount()._id
$: joined = value.includes(me)
function join (): void {
if (value.includes(me)) return
if (onChange === undefined) return
onChange([...value, me])
}
</script>
{#if !joined && onChange !== undefined}
<Button label={view.string.Join} {size} {width} kind={'primary'} on:click={join} />
{:else}
<AccountArrayEditor {label} {value} {onChange} {readonly} {kind} {size} {width} allowGuests />
{/if}

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import contact, { Employee, Person } from '@hcengineering/contact'
import contact, { Contact, Employee, Person } from '@hcengineering/contact'
import type { Class, Doc, DocumentQuery, Ref } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform'
import { ObjectCreate, getClient } from '@hcengineering/presentation'
@ -27,9 +27,9 @@
import UsersPopup from './UsersPopup.svelte'
import Members from './icons/Members.svelte'
export let items: Ref<Employee>[] = []
export let _class: Ref<Class<Employee>> = contact.mixin.Employee
export let docQuery: DocumentQuery<Employee> | undefined = {}
export let items: Ref<Person>[] = []
export let _class: Ref<Class<Person>> = contact.mixin.Employee
export let docQuery: DocumentQuery<Person> | undefined = {}
export let label: IntlString | undefined = undefined
export let kind: ButtonKind = 'no-border'
@ -41,16 +41,18 @@
export let readonly: boolean = false
export let create: ObjectCreate | undefined = undefined
function filter (items: Ref<Employee>[]): Ref<Employee>[] {
export let sort: ((a: Person, b: Person) => number) | undefined = undefined
function filter (items: Ref<Person>[]): Ref<Person>[] {
return items.filter((it, idx, arr) => arr.indexOf(it) === idx)
}
let persons: Person[] = filter(items)
.map((p) => $personByIdStore.get(p))
.filter((p) => p !== undefined) as Employee[]
.filter((p) => p !== undefined) as Person[]
$: persons = filter(items)
.map((p) => $personByIdStore.get(p))
.filter((p) => p !== undefined) as Employee[]
.filter((p) => p !== undefined) as Person[]
const dispatch = createEventDispatcher()
const client = getClient()
@ -62,36 +64,34 @@
.findAllSync(contact.class.PersonAccount, {})
.map((p) => p.person)
)
showPopup(
UsersPopup,
{
_class,
label,
docQuery,
multiSelect: true,
allowDeselect: false,
selectedUsers: filter(items),
filter: (it: Doc) => {
const h = client.getHierarchy()
if (h.hasMixin(it, contact.mixin.Employee)) {
const isActive = h.as(it, contact.mixin.Employee).active
const isSelected = items.some((selectedItem) => selectedItem === it._id)
return isActive || isSelected
}
return accounts.has(it._id as Ref<Person>)
},
readonly,
create
},
evt.target as HTMLElement,
undefined,
(result) => {
if (result != null) {
items = filter(result)
dispatch('update', items)
const popupProps: any = {
_class,
label,
docQuery,
multiSelect: true,
allowDeselect: false,
selectedUsers: filter(items),
filter: (it: Doc) => {
const h = client.getHierarchy()
if (h.hasMixin(it, contact.mixin.Employee)) {
const isActive = h.as(it, contact.mixin.Employee).active
const isSelected = items.some((selectedItem) => selectedItem === it._id)
return isActive || isSelected
}
return accounts.has(it._id as Ref<Person>)
},
readonly,
create
}
if (sort !== undefined) {
popupProps.sort = sort
}
showPopup(UsersPopup, popupProps, evt.target as HTMLElement, undefined, (result) => {
if (result != null) {
items = filter(result)
dispatch('update', items)
}
)
})
}
</script>

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import contact, { Contact, getFirstName, getLastName, Person } from '@hcengineering/contact'
import contact, { Contact, getFirstName, getLastName, getName, Person } from '@hcengineering/contact'
import type { Class, Doc, DocumentQuery, FindOptions, Ref } from '@hcengineering/core'
import type { Asset, IntlString } from '@hcengineering/platform'
import presentation, { getClient, ObjectCreate, ObjectPopup } from '@hcengineering/presentation'
@ -35,6 +35,14 @@
return true
}
const hierarchy = getClient().getHierarchy()
export let sort: (a: Doc, b: Doc) => number = (a, b) => {
const aName = getName(hierarchy, a as Contact)
const bName = getName(hierarchy, b as Contact)
return aName.localeCompare(bName)
}
export let multiSelect: boolean = false
export let allowDeselect: boolean = false
export let titleDeselect: IntlString | undefined = undefined
@ -46,7 +54,6 @@
export let create: ObjectCreate | undefined = undefined
export let readonly = false
const hierarchy = getClient().getHierarchy()
const dispatch = createEventDispatcher()
$: _create =
@ -72,6 +79,7 @@
type={'object'}
docQuery={readonly ? { ...docQuery, _id: { $in: selectedUsers } } : docQuery}
{filter}
{sort}
groupBy={'_class'}
bind:selectedObjects={selectedUsers}
bind:ignoreObjects={ignoreUsers}

View File

@ -107,6 +107,7 @@ import UserDetails from './components/UserDetails.svelte'
import EditOrganizationPanel from './components/EditOrganizationPanel.svelte'
import ChannelIcon from './components/ChannelIcon.svelte'
import CreateGuest from './components/CreateGuest.svelte'
import SpaceMembersEditor from './components/SpaceMembersEditor.svelte'
import contact from './plugin'
import {
@ -342,7 +343,8 @@ export default async (): Promise<Resources> => ({
PersonAccountRefPresenter,
PersonIcon,
EditOrganizationPanel,
ChannelIcon
ChannelIcon,
SpaceMembersEditor
},
completion: {
EmployeeQuery: async (

View File

@ -188,7 +188,8 @@ export const contactPlugin = plugin(contactId, {
PersonIcon: '' as AnyComponent,
EditOrganizationPanel: '' as AnyComponent,
CollaborationUserAvatar: '' as AnyComponent,
CreateGuest: '' as AnyComponent
CreateGuest: '' as AnyComponent,
SpaceMembersEditor: '' as AnyComponent
},
channelProvider: {
Email: '' as Ref<ChannelProvider>,

View File

@ -0,0 +1,33 @@
<!--
// Copyright © 2024 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 { Ref } from '@hcengineering/core'
import hr, { Department } from '@hcengineering/hr'
import DepartmentPresenter from './DepartmentPresenter.svelte'
import { createQuery } from '@hcengineering/presentation'
export let value: Ref<Department>
let department: Department | undefined
const query = createQuery()
query.query(hr.class.Department, { _id: value }, (result) => {
department = result[0]
})
</script>
{#if department}
<DepartmentPresenter value={department} />
{/if}

View File

@ -28,6 +28,7 @@ import RequestPresenter from './components/RequestPresenter.svelte'
import { showPopup } from '@hcengineering/ui'
import { type Request } from '@hcengineering/hr'
import EditRequestType from './components/EditRequestType.svelte'
import DepartmentRefPresenter from './components/DepartmentRefPresenter.svelte'
async function editRequestType (object: Request): Promise<void> {
showPopup(EditRequestType, { object })
@ -46,7 +47,8 @@ export default async (): Promise<Resources> => ({
TzDateEditor,
RequestPresenter,
EditRequestType,
DepartmentPresenter
DepartmentPresenter,
DepartmentRefPresenter
},
actionImpl: {
EditRequestType: editRequestType

View File

@ -20,7 +20,6 @@
import { getResource } from '@hcengineering/platform'
import {
ActionContext,
AttributeCategory,
AttributesBar,
KeyedAttribute,
createQuery,
@ -29,7 +28,7 @@
reduceCalls
} from '@hcengineering/presentation'
import { AnyComponent, Button, Component, IconMixin, IconMoreH } from '@hcengineering/ui'
import view from '@hcengineering/view'
import view, { AttributeCategory } from '@hcengineering/view'
import { createEventDispatcher, onDestroy } from 'svelte'
import { DocNavLink, ParentsNavigator, getDocAttrsInfo, getDocLabel, getDocMixins, showMenu } from '..'

View File

@ -152,9 +152,9 @@
if (hierarchy.isDerived(type._class, core.class.RefTo)) {
return '$lookup.' + name
}
if (hierarchy.isDerived(type._class, core.class.ArrOf)) {
return getValue(name, (type as ArrOf<any>).of)
}
// if (hierarchy.isDerived(type._class, core.class.ArrOf)) {
// return getValue(name, (type as ArrOf<any>).of)
// }
return name
}

View File

@ -51,7 +51,7 @@
const client = getClient()
const key = { key: filter.key.key }
const promise = getPresenter(client, filter.key._class, key, key)
const promise = getPresenter(client, filter.key._class, key, key, undefined, false, 'attribute')
let values = new Set<any>()
let selectedValues: Set<any> = new Set<any>(filter.value.map((p) => p[0]))

View File

@ -36,6 +36,7 @@ import core, {
type Lookup,
type Mixin,
type Obj,
type Permission,
type Ref,
type RefTo,
type ReverseLookup,
@ -48,22 +49,19 @@ import core, {
type TxOperations,
type TxUpdateDoc,
type TypeAny,
type TypedSpace,
type Permission
type TypedSpace
} from '@hcengineering/core'
import { type Restrictions } from '@hcengineering/guest'
import type { Asset, IntlString } from '@hcengineering/platform'
import { getResource, translate } from '@hcengineering/platform'
import {
type AttributeCategory,
AttributeCategoryOrder,
createQuery,
getAttributePresenterClass,
getClient,
hasResource,
type KeyedAttribute,
getFiltredKeys,
hasResource,
isAdminUser,
createQuery
type KeyedAttribute
} from '@hcengineering/presentation'
import { type CollaborationUser } from '@hcengineering/text-editor'
import {
@ -81,8 +79,10 @@ import {
type Location
} from '@hcengineering/ui'
import view, {
type AttributePresenter,
AttributeCategoryOrder,
type AttributeCategory,
type AttributeModel,
type AttributePresenter,
type BuildModelKey,
type BuildModelOptions,
type CollectionPresenter,
@ -179,37 +179,58 @@ export async function getAttributePresenter (
_class: Ref<Class<Obj>>,
key: string,
preserveKey: BuildModelKey,
mixinClass?: Ref<Mixin<CollectionPresenter>>
mixinClass?: Ref<Mixin<CollectionPresenter>>,
_category?: AttributeCategory
): Promise<AttributeModel> {
const actualMixinClass = mixinClass ?? view.mixin.AttributePresenter
const hierarchy = client.getHierarchy()
const attribute = hierarchy.getAttribute(_class, key)
let { attrClass, category } = getAttributePresenterClass(hierarchy, attribute)
if (_category !== undefined) {
category = _category
}
const presenterClass = getAttributePresenterClass(hierarchy, attribute)
const isCollectionAttr = presenterClass.category === 'collection'
let overridedPresenter = await client
.getModel()
.findOne(view.class.AttrPresenter, { objectClass: _class, attribute: attribute._id, category })
if (overridedPresenter === undefined) {
overridedPresenter = await client
.getModel()
.findOne(view.class.AttrPresenter, { attribute: attribute._id, category })
}
const isCollectionAttr = category === 'collection'
const mixin = isCollectionAttr ? view.mixin.CollectionPresenter : actualMixinClass
let presenterMixin: AttributePresenter | CollectionPresenter | undefined = hierarchy.classHierarchyMixin(
presenterClass.attrClass,
attrClass,
mixin
)
if (presenterMixin?.presenter === undefined && mixinClass != null && mixin === mixinClass) {
presenterMixin = hierarchy.classHierarchyMixin(presenterClass.attrClass, view.mixin.AttributePresenter)
presenterMixin = hierarchy.classHierarchyMixin(attrClass, view.mixin.AttributePresenter)
}
let presenter: AnySvelteComponent
let presenter: AnySvelteComponent | undefined
const attributePresenter = presenterMixin as AttributePresenter
if (presenterClass.category === 'array' && attributePresenter.arrayPresenter !== undefined) {
presenter = await getResource(attributePresenter.arrayPresenter)
} else if (presenterMixin?.presenter !== undefined) {
presenter = await getResource(presenterMixin.presenter)
} else if (presenterClass.attrClass === core.class.TypeAny) {
const typeAny = attribute.type as TypeAny
presenter = await getResource(typeAny.presenter)
} else {
if (overridedPresenter !== undefined) {
presenter = await getResource(overridedPresenter.component)
}
if (presenter === undefined) {
const attributePresenter = presenterMixin as AttributePresenter
if (category === 'array' && attributePresenter.arrayPresenter !== undefined) {
presenter = await getResource(attributePresenter.arrayPresenter)
} else if (presenterMixin?.presenter !== undefined) {
presenter = await getResource(presenterMixin.presenter)
} else if (attrClass === core.class.TypeAny) {
const typeAny = attribute.type as TypeAny
presenter = await getResource(typeAny.presenter)
}
}
if (presenter === undefined) {
throw new Error('attribute presenter not found for ' + JSON.stringify(preserveKey))
}
@ -223,7 +244,7 @@ export async function getAttributePresenter (
return {
key: preserveKey.key,
sortingKey,
_class: presenterClass.attrClass,
_class: attrClass,
label: preserveKey.label ?? attribute.shortLabel ?? attribute.label,
presenter,
props: preserveKey.props,
@ -265,7 +286,8 @@ export async function getPresenter<T extends Doc> (
key: BuildModelKey,
preserveKey: BuildModelKey,
lookup?: Lookup<T>,
isCollectionAttr: boolean = false
isCollectionAttr: boolean = false,
_category?: AttributeCategory
): Promise<AttributeModel> {
if (key.presenter !== undefined) {
const { presenter, label, sortingKey } = key
@ -294,7 +316,7 @@ export async function getPresenter<T extends Doc> (
}
return await getLookupPresenter(client, _class, key, preserveKey, lookup)
}
return await getAttributePresenter(client, _class, key.key, preserveKey)
return await getAttributePresenter(client, _class, key.key, preserveKey, undefined, _category)
}
}

View File

@ -52,6 +52,7 @@ import {
ObjectTitle,
ObjectTooltip,
ObjectValidator,
AttrPresenter,
PreviewPresenter,
SpaceHeader,
SpaceName,
@ -116,7 +117,8 @@ const view = plugin(viewId, {
ActionCategory: '' as Ref<Class<ActionCategory>>,
LinkPresenter: '' as Ref<Class<LinkPresenter>>,
FilterMode: '' as Ref<Class<FilterMode>>,
FilteredView: '' as Ref<Class<FilteredView>>
FilteredView: '' as Ref<Class<FilteredView>>,
AttrPresenter: '' as Ref<Class<AttrPresenter>>
},
action: {
Delete: '' as Ref<Action>,

View File

@ -780,4 +780,21 @@ export interface IconProps {
color?: number
}
export type AttributeCategory = 'attribute' | 'inplace' | 'collection' | 'array' | 'object'
export const AttributeCategoryOrder: Record<AttributeCategory, number> = {
attribute: 0,
inplace: 1,
collection: 2,
array: 3,
object: 4
}
export type ObjectPresenterType = 'link' | 'text'
export interface AttrPresenter extends Doc {
attribute: Ref<AnyAttribute>
category: AttributeCategory
objectClass: Ref<Class<Doc>>
component: AnyComponent
}