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 { createPublicLinkAction } from '@hcengineering/model-guest'
import { generateClassNotificationTypes } from '@hcengineering/model-notification' import { generateClassNotificationTypes } from '@hcengineering/model-notification'
import presentation from '@hcengineering/model-presentation' 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 workbench from '@hcengineering/model-workbench'
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
import type { Asset, IntlString, Resource } from '@hcengineering/platform' import type { Asset, IntlString, Resource } from '@hcengineering/platform'
@ -1142,4 +1142,6 @@ export function createModel (builder: Builder): void {
domain: DOMAIN_CONTACT, domain: DOMAIN_CONTACT,
disabled: [{ attachedToClass: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { createdOn: -1 }, { attachedTo: 1 }] 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 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, { builder.mixin(hr.class.Department, core.class.Class, view.mixin.ObjectEditor, {
editor: hr.component.EditDepartment editor: hr.component.EditDepartment
}) })

View File

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

View File

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

View File

@ -23,7 +23,8 @@ import {
type DocumentQuery, type DocumentQuery,
type Domain, type Domain,
type Ref, type Ref,
type Space type Space,
type AnyAttribute
} from '@hcengineering/core' } from '@hcengineering/core'
import { type Builder, Mixin, Model, UX } from '@hcengineering/model' import { type Builder, Mixin, Model, UX } from '@hcengineering/model'
import core, { TClass, TDoc } from '@hcengineering/model-core' import core, { TClass, TDoc } from '@hcengineering/model-core'
@ -87,10 +88,13 @@ import {
type ViewletPreference, type ViewletPreference,
type ObjectIdentifier, type ObjectIdentifier,
type ObjectIcon, type ObjectIcon,
type ObjectTooltip type ObjectTooltip,
type AttrPresenter,
type AttributeCategory
} from '@hcengineering/view' } from '@hcengineering/view'
import view from './plugin' import view from './plugin'
import { classPresenter, createAction } from './utils'
export { viewId } from '@hcengineering/view' export { viewId } from '@hcengineering/view'
export { viewOperation } from './migration' export { viewOperation } from './migration'
@ -98,38 +102,7 @@ export type { ViewAction, Viewlet }
export const DOMAIN_VIEW = 'view' as Domain export const DOMAIN_VIEW = 'view' as Domain
export function createAction<T extends Doc = Doc, P = Record<string, any>> ( export * from './utils'
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
})
}
}
@Model(view.class.FilteredView, core.class.Doc, DOMAIN_VIEW) @Model(view.class.FilteredView, core.class.Doc, DOMAIN_VIEW)
@UX(view.string.FilteredViews) @UX(view.string.FilteredViews)
@ -383,6 +356,14 @@ export class TObjectPanel extends TClass implements ObjectPanel {
component!: AnyComponent 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>> export type ActionTemplate = Partial<Data<Action>>
/** /**
@ -467,7 +448,8 @@ export function createModel (builder: Builder): void {
TGroupping, TGroupping,
TObjectIdentifier, TObjectIdentifier,
TObjectTooltip, TObjectTooltip,
TObjectIcon TObjectIcon,
TAttrPresenter
) )
classPresenter( 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, Domain,
Enum, Enum,
EnumOf, EnumOf,
Hierarchy,
Hyperlink, Hyperlink,
Mixin as IMixin, Mixin as IMixin,
IndexKind, IndexKind,
@ -290,6 +291,7 @@ function _generateTx (tx: ClassTxes): Tx[] {
*/ */
export class Builder { export class Builder {
private readonly txes: Tx[] = [] private readonly txes: Tx[] = []
readonly hierarchy = new Hierarchy()
onTx?: (tx: Tx) => void onTx?: (tx: Tx) => void
@ -322,7 +324,7 @@ export class Builder {
for (const tx of generated) { for (const tx of generated) {
this.txes.push(tx) this.txes.push(tx)
this.onTx?.(tx) this.onTx?.(tx)
// this.hierarchy.tx(tx) this.hierarchy.tx(tx)
} }
} }
@ -352,6 +354,7 @@ export class Builder {
} }
this.txes.push(tx) this.txes.push(tx)
this.onTx?.(tx) this.onTx?.(tx)
this.hierarchy.tx(tx)
return TxProcessor.createDoc2Doc(tx) return TxProcessor.createDoc2Doc(tx)
} }
@ -364,6 +367,7 @@ export class Builder {
const tx = txFactory.createTxMixin(objectId, objectClass, core.space.Model, mixin, attributes) const tx = txFactory.createTxMixin(objectId, objectClass, core.space.Model, mixin, attributes)
this.txes.push(tx) this.txes.push(tx)
this.onTx?.(tx) this.onTx?.(tx)
this.hierarchy.tx(tx)
} }
getTxes (): Tx[] { getTxes (): Tx[] {

View File

@ -61,6 +61,12 @@
return true 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 created: Doc[] = []
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -84,11 +90,7 @@
_id: { $nin: ignoreObjects, ..._idExtra } _id: { $nin: ignoreObjects, ..._idExtra }
}, },
(result) => { (result) => {
result.sort((a, b) => { result.sort(sort)
const aval: string = `${getObjectValue(groupBy, a as any)}`
const bval: string = `${getObjectValue(groupBy, b as any)}`
return aval.localeCompare(bval)
})
if (created.length > 0) { if (created.length > 0) {
const cmap = new Set(created.map((it) => it._id)) const cmap = new Set(created.map((it) => it._id))
objects = [...created, ...result.filter((d) => !cmap.has(d._id))].filter(filter) 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 { getMetadata, getResource } from '@hcengineering/platform'
import { LiveQuery as LQ } from '@hcengineering/query' import { LiveQuery as LQ } from '@hcengineering/query'
import { type AnyComponent, type AnySvelteComponent, type IconSize } from '@hcengineering/ui' 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 { deepEqual } from 'fast-equals'
import { onDestroy } from 'svelte' import { onDestroy } from 'svelte'
import { type KeyedAttribute } from '..' 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 * @public
*/ */

View File

@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Employee, PersonAccount } from '@hcengineering/contact' import { Contact, Employee, PersonAccount, getName } from '@hcengineering/contact'
import core, { Account, Ref } from '@hcengineering/core' import core, { Account, Ref, getCurrentAccount } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { ButtonKind, ButtonSize } from '@hcengineering/ui' import { ButtonKind, ButtonSize } from '@hcengineering/ui'
@ -33,7 +33,7 @@
export let includeItems: Ref<Account>[] | undefined = undefined export let includeItems: Ref<Account>[] | undefined = undefined
export let excludeItems: Ref<Account>[] | undefined = undefined export let excludeItems: Ref<Account>[] | undefined = undefined
export let emptyLabel: IntlString | undefined = undefined export let emptyLabel: IntlString | undefined = undefined
export let allowGuests: boolean = true export let allowGuests: boolean = false
let timer: any = null let timer: any = null
const client = getClient() 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> </script>
<UserBoxList <UserBoxList
@ -114,6 +137,7 @@
{docQuery} {docQuery}
on:update={onUpdate} on:update={onUpdate}
{size} {size}
{sort}
justify={'left'} justify={'left'}
width={width ?? 'min-content'} width={width ?? 'min-content'}
{kind} {kind}

View File

@ -4,14 +4,9 @@
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel' import { Panel } from '@hcengineering/panel'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import presentation, { import presentation, { createQuery, getClient, type KeyedAttribute } from '@hcengineering/presentation'
type AttributeCategory,
createQuery,
getClient,
type KeyedAttribute
} from '@hcengineering/presentation'
import { type AnyComponent, Button, Component, IconMixin, IconMoreH, Label } from '@hcengineering/ui' import { type AnyComponent, Button, Component, IconMixin, IconMoreH, Label } from '@hcengineering/ui'
import view from '@hcengineering/view' import view, { AttributeCategory } from '@hcengineering/view'
import { import {
DocAttributeBar, DocAttributeBar,
DocNavLink, 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. // limitations under the License.
--> -->
<script lang="ts"> <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 { Class, Doc, DocumentQuery, Ref } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform' import type { IntlString } from '@hcengineering/platform'
import { ObjectCreate, getClient } from '@hcengineering/presentation' import { ObjectCreate, getClient } from '@hcengineering/presentation'
@ -27,9 +27,9 @@
import UsersPopup from './UsersPopup.svelte' import UsersPopup from './UsersPopup.svelte'
import Members from './icons/Members.svelte' import Members from './icons/Members.svelte'
export let items: Ref<Employee>[] = [] export let items: Ref<Person>[] = []
export let _class: Ref<Class<Employee>> = contact.mixin.Employee export let _class: Ref<Class<Person>> = contact.mixin.Employee
export let docQuery: DocumentQuery<Employee> | undefined = {} export let docQuery: DocumentQuery<Person> | undefined = {}
export let label: IntlString | undefined = undefined export let label: IntlString | undefined = undefined
export let kind: ButtonKind = 'no-border' export let kind: ButtonKind = 'no-border'
@ -41,16 +41,18 @@
export let readonly: boolean = false export let readonly: boolean = false
export let create: ObjectCreate | undefined = undefined 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) return items.filter((it, idx, arr) => arr.indexOf(it) === idx)
} }
let persons: Person[] = filter(items) let persons: Person[] = filter(items)
.map((p) => $personByIdStore.get(p)) .map((p) => $personByIdStore.get(p))
.filter((p) => p !== undefined) as Employee[] .filter((p) => p !== undefined) as Person[]
$: persons = filter(items) $: persons = filter(items)
.map((p) => $personByIdStore.get(p)) .map((p) => $personByIdStore.get(p))
.filter((p) => p !== undefined) as Employee[] .filter((p) => p !== undefined) as Person[]
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const client = getClient() const client = getClient()
@ -62,36 +64,34 @@
.findAllSync(contact.class.PersonAccount, {}) .findAllSync(contact.class.PersonAccount, {})
.map((p) => p.person) .map((p) => p.person)
) )
showPopup( const popupProps: any = {
UsersPopup, _class,
{ label,
_class, docQuery,
label, multiSelect: true,
docQuery, allowDeselect: false,
multiSelect: true, selectedUsers: filter(items),
allowDeselect: false, filter: (it: Doc) => {
selectedUsers: filter(items), const h = client.getHierarchy()
filter: (it: Doc) => { if (h.hasMixin(it, contact.mixin.Employee)) {
const h = client.getHierarchy() const isActive = h.as(it, contact.mixin.Employee).active
if (h.hasMixin(it, contact.mixin.Employee)) { const isSelected = items.some((selectedItem) => selectedItem === it._id)
const isActive = h.as(it, contact.mixin.Employee).active return isActive || isSelected
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)
} }
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> </script>

View File

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

View File

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

View File

@ -188,7 +188,8 @@ export const contactPlugin = plugin(contactId, {
PersonIcon: '' as AnyComponent, PersonIcon: '' as AnyComponent,
EditOrganizationPanel: '' as AnyComponent, EditOrganizationPanel: '' as AnyComponent,
CollaborationUserAvatar: '' as AnyComponent, CollaborationUserAvatar: '' as AnyComponent,
CreateGuest: '' as AnyComponent CreateGuest: '' as AnyComponent,
SpaceMembersEditor: '' as AnyComponent
}, },
channelProvider: { channelProvider: {
Email: '' as Ref<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 { showPopup } from '@hcengineering/ui'
import { type Request } from '@hcengineering/hr' import { type Request } from '@hcengineering/hr'
import EditRequestType from './components/EditRequestType.svelte' import EditRequestType from './components/EditRequestType.svelte'
import DepartmentRefPresenter from './components/DepartmentRefPresenter.svelte'
async function editRequestType (object: Request): Promise<void> { async function editRequestType (object: Request): Promise<void> {
showPopup(EditRequestType, { object }) showPopup(EditRequestType, { object })
@ -46,7 +47,8 @@ export default async (): Promise<Resources> => ({
TzDateEditor, TzDateEditor,
RequestPresenter, RequestPresenter,
EditRequestType, EditRequestType,
DepartmentPresenter DepartmentPresenter,
DepartmentRefPresenter
}, },
actionImpl: { actionImpl: {
EditRequestType: editRequestType EditRequestType: editRequestType

View File

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

View File

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

View File

@ -51,7 +51,7 @@
const client = getClient() const client = getClient()
const key = { key: filter.key.key } 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 values = new Set<any>()
let selectedValues: Set<any> = new Set<any>(filter.value.map((p) => p[0])) let selectedValues: Set<any> = new Set<any>(filter.value.map((p) => p[0]))

View File

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

View File

@ -780,4 +780,21 @@ export interface IconProps {
color?: number 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 type ObjectPresenterType = 'link' | 'text'
export interface AttrPresenter extends Doc {
attribute: Ref<AnyAttribute>
category: AttributeCategory
objectClass: Ref<Class<Doc>>
component: AnyComponent
}