Process roles (#8654)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2025-04-22 14:39:21 +05:00 committed by GitHub
parent c07db83d1b
commit 470c629f9f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 772 additions and 84 deletions

View File

@ -19,7 +19,8 @@ import {
type Card, type Card,
type MasterTag, type MasterTag,
type ParentInfo, type ParentInfo,
type Tag type Tag,
type Role
} from '@hcengineering/card' } from '@hcengineering/card'
import chunter from '@hcengineering/chunter' import chunter from '@hcengineering/chunter'
import core, { import core, {
@ -47,7 +48,7 @@ import {
type Builder type Builder
} from '@hcengineering/model' } from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment' import attachment from '@hcengineering/model-attachment'
import { TClass, TDoc, TMixin, TSpace } from '@hcengineering/model-core' import { TAttachedDoc, TClass, TDoc, TMixin, TSpace } from '@hcengineering/model-core'
import presentation from '@hcengineering/model-presentation' import presentation from '@hcengineering/model-presentation'
import setting from '@hcengineering/model-setting' import setting from '@hcengineering/model-setting'
import view, { createAction } from '@hcengineering/model-view' import view, { createAction } from '@hcengineering/model-view'
@ -115,6 +116,13 @@ export class MasterTagEditorSection extends TDoc implements MasterTagEditorSecti
component!: AnyComponent component!: AnyComponent
} }
@Model(card.class.Role, core.class.AttachedDoc, DOMAIN_MODEL)
export class TRole extends TAttachedDoc implements Role {
name!: string
declare attachedTo: Ref<MasterTag | Tag>
declare collection: 'roles'
}
export * from './migration' export * from './migration'
const listConfig: (BuildModelKey | string)[] = [ const listConfig: (BuildModelKey | string)[] = [
@ -204,7 +212,7 @@ export function createSystemType (
} }
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
builder.createModel(TMasterTag, TTag, TCard, MasterTagEditorSection, TCardSpace) builder.createModel(TMasterTag, TTag, TCard, MasterTagEditorSection, TCardSpace, TRole)
createSystemType(builder, card.types.File, card.icon.File, attachment.string.File, attachment.string.Files) createSystemType(builder, card.types.File, card.icon.File, attachment.string.File, attachment.string.Files)
createSystemType(builder, card.types.Document, card.icon.Document, card.string.Document, card.string.Documents) createSystemType(builder, card.types.Document, card.icon.Document, card.string.Document, card.string.Documents)
@ -681,6 +689,12 @@ export function createModel (builder: Builder): void {
component: card.component.RelationsSection component: card.component.RelationsSection
}) })
builder.createDoc(card.class.MasterTagEditorSection, core.space.Model, {
id: 'roles',
label: core.string.Roles,
component: card.component.RolesSection
})
builder.createDoc(card.class.MasterTagEditorSection, core.space.Model, { builder.createDoc(card.class.MasterTagEditorSection, core.space.Model, {
id: 'views', id: 'views',
label: card.string.Views, label: card.string.Views,

View File

@ -15,11 +15,12 @@
// //
import activity from '@hcengineering/activity' import activity from '@hcengineering/activity'
import card, { type Role, type Card } from '@hcengineering/card'
import { import {
AvatarType, AvatarType,
type UserRole,
contactId, contactId,
type AvatarProvider, type AvatarProvider,
type SocialIdentity,
type Channel, type Channel,
type ChannelProvider, type ChannelProvider,
type Contact, type Contact,
@ -29,26 +30,27 @@ import {
type Member, type Member,
type Organization, type Organization,
type Person, type Person,
type Status, type PersonSpace,
type PersonSpace type SocialIdentity,
type Status
} from '@hcengineering/contact' } from '@hcengineering/contact'
import { import {
AccountRole, AccountRole,
ClassifierKind,
DOMAIN_MODEL, DOMAIN_MODEL,
DateRangeMode, DateRangeMode,
IndexKind, IndexKind,
type Collection, type AccountUuid,
type Blob, type Blob,
type Class, type Class,
type MarkupBlobRef, type Collection,
type Domain, type Domain,
type Ref, type MarkupBlobRef,
type Timestamp,
type SocialIdType,
type PersonUuid,
type PersonId, type PersonId,
type AccountUuid, type PersonUuid,
ClassifierKind type Ref,
type SocialIdType,
type Timestamp
} from '@hcengineering/core' } from '@hcengineering/core'
import { import {
Collection as CollectionType, Collection as CollectionType,
@ -84,7 +86,6 @@ import setting from '@hcengineering/setting'
import templates from '@hcengineering/templates' import templates from '@hcengineering/templates'
import { type AnyComponent } from '@hcengineering/ui/src/types' import { type AnyComponent } from '@hcengineering/ui/src/types'
import { type Action } from '@hcengineering/view' import { type Action } from '@hcengineering/view'
import card, { type Card } from '@hcengineering/card'
import contact from './plugin' import contact from './plugin'
export { contactId } from '@hcengineering/contact' export { contactId } from '@hcengineering/contact'
@ -92,6 +93,7 @@ export { contactOperation } from './migration'
export { contact as default } export { contact as default }
export const DOMAIN_CONTACT = 'contact' as Domain export const DOMAIN_CONTACT = 'contact' as Domain
export const DOMAIN_ROLE = 'role' as Domain
export const DOMAIN_CHANNEL = 'channel' as Domain export const DOMAIN_CHANNEL = 'channel' as Domain
@Model(contact.class.AvatarProvider, core.class.Doc, DOMAIN_MODEL) @Model(contact.class.AvatarProvider, core.class.Doc, DOMAIN_MODEL)
@ -272,6 +274,12 @@ export class TPersonSpace extends TSpace implements PersonSpace {
person!: Ref<Person> person!: Ref<Person>
} }
@Model(contact.class.UserRole, core.class.Doc, DOMAIN_ROLE)
export class TUserRole extends TDoc implements UserRole {
user!: Ref<Employee>
role!: Ref<Role>
}
function createUserProfileTag (builder: Builder): void { function createUserProfileTag (builder: Builder): void {
builder.createDoc( builder.createDoc(
card.class.MasterTag, card.class.MasterTag,
@ -334,7 +342,8 @@ export function createModel (builder: Builder): void {
TStatus, TStatus,
TMember, TMember,
TContactsTab, TContactsTab,
TPersonSpace TPersonSpace,
TUserRole
) )
builder.mixin(contact.class.Contact, core.class.Class, activity.mixin.ActivityDoc, {}) builder.mixin(contact.class.Contact, core.class.Class, activity.mixin.ActivityDoc, {})

View File

@ -164,6 +164,7 @@ export class TProcessFunction extends TDoc implements ProcessFunction {
label!: IntlString label!: IntlString
editor?: AnyComponent editor?: AnyComponent
allowMany?: boolean allowMany?: boolean
type!: 'transform' | 'reduce' | 'context'
} }
export * from './migration' export * from './migration'
@ -239,7 +240,8 @@ export function createModel (builder: Builder): void {
{ {
of: core.class.TypeString, of: core.class.TypeString,
category: 'attribute', category: 'attribute',
label: process.string.UpperCase label: process.string.UpperCase,
type: 'transform'
}, },
process.function.UpperCase process.function.UpperCase
) )
@ -250,7 +252,8 @@ export function createModel (builder: Builder): void {
{ {
of: core.class.TypeString, of: core.class.TypeString,
category: 'attribute', category: 'attribute',
label: process.string.LowerCase label: process.string.LowerCase,
type: 'transform'
}, },
process.function.LowerCase process.function.LowerCase
) )
@ -261,7 +264,8 @@ export function createModel (builder: Builder): void {
{ {
of: core.class.TypeString, of: core.class.TypeString,
category: 'attribute', category: 'attribute',
label: process.string.Trim label: process.string.Trim,
type: 'transform'
}, },
process.function.Trim process.function.Trim
) )
@ -272,7 +276,8 @@ export function createModel (builder: Builder): void {
{ {
of: core.class.ArrOf, of: core.class.ArrOf,
category: undefined, category: undefined,
label: process.string.FirstValue label: process.string.FirstValue,
type: 'reduce'
}, },
process.function.FirstValue process.function.FirstValue
) )
@ -282,6 +287,7 @@ export function createModel (builder: Builder): void {
core.space.Model, core.space.Model,
{ {
of: core.class.ArrOf, of: core.class.ArrOf,
type: 'reduce',
category: undefined, category: undefined,
label: process.string.LastValue label: process.string.LastValue
}, },
@ -293,6 +299,7 @@ export function createModel (builder: Builder): void {
core.space.Model, core.space.Model,
{ {
of: core.class.ArrOf, of: core.class.ArrOf,
type: 'reduce',
category: undefined, category: undefined,
label: process.string.Random label: process.string.Random
}, },
@ -304,6 +311,7 @@ export function createModel (builder: Builder): void {
core.space.Model, core.space.Model,
{ {
of: core.class.TypeNumber, of: core.class.TypeNumber,
type: 'transform',
category: 'attribute', category: 'attribute',
label: process.string.Add, label: process.string.Add,
allowMany: true, allowMany: true,
@ -320,7 +328,8 @@ export function createModel (builder: Builder): void {
category: 'attribute', category: 'attribute',
label: process.string.Subtract, label: process.string.Subtract,
allowMany: true, allowMany: true,
editor: process.component.NumberOffsetEditor editor: process.component.NumberOffsetEditor,
type: 'transform'
}, },
process.function.Subtract process.function.Subtract
) )
@ -332,7 +341,8 @@ export function createModel (builder: Builder): void {
of: core.class.TypeDate, of: core.class.TypeDate,
category: 'attribute', category: 'attribute',
label: process.string.Offset, label: process.string.Offset,
editor: process.component.DateOffsetEditor editor: process.component.DateOffsetEditor,
type: 'transform'
}, },
process.function.Offset process.function.Offset
) )
@ -343,11 +353,25 @@ export function createModel (builder: Builder): void {
{ {
of: core.class.TypeDate, of: core.class.TypeDate,
category: 'attribute', category: 'attribute',
label: process.string.FirstWorkingDayAfter label: process.string.FirstWorkingDayAfter,
type: 'transform'
}, },
process.function.FirstWorkingDayAfter process.function.FirstWorkingDayAfter
) )
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: contact.mixin.Employee,
editor: process.component.RoleEditor,
category: 'array',
label: core.string.Role,
type: 'context'
},
process.function.RoleContext
)
builder.mixin(process.class.Process, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(process.class.Process, core.class.Class, view.mixin.AttributePresenter, {
presenter: process.component.ProcessPresenter presenter: process.component.ProcessPresenter
}) })

View File

@ -95,6 +95,10 @@ export function createModel (builder: Builder): void {
func: serverProcess.transform.FirstWorkingDayAfter func: serverProcess.transform.FirstWorkingDayAfter
}) })
builder.mixin(process.function.RoleContext, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.RoleContext
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverProcess.trigger.OnExecutionContinue, trigger: serverProcess.trigger.OnExecutionContinue,
txMatch: { txMatch: {

View File

@ -0,0 +1,47 @@
<!--
// 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, Tag } from '@hcengineering/card'
import core from '@hcengineering/core'
import { Card, getClient } from '@hcengineering/presentation'
import { EditBox } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import card from '../../plugin'
export let masterTag: MasterTag | Tag
let value: string = ''
const dispatch = createEventDispatcher()
const client = getClient()
async function handleSave (): Promise<void> {
const _id = await client.addCollection(
card.class.Role,
core.space.Model,
masterTag._id,
masterTag._class,
'roles',
{
name: value
}
)
dispatch('close', _id)
}
</script>
<Card okAction={handleSave} label={core.string.Role} on:close width={'menu'} canSave={value.trim().length > 0}>
<EditBox bind:value placeholder={core.string.Role} />
</Card>

View File

@ -0,0 +1,186 @@
<!--
// 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 card, { Role } from '@hcengineering/card'
import contact, { Employee, UserRole } from '@hcengineering/contact'
import { SelectUsersPopup, UserDetails } from '@hcengineering/contact-resources'
import core, { Ref, WithLookup } from '@hcengineering/core'
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { clearSettingsStore } from '@hcengineering/setting-resources'
import { ButtonIcon, Icon, IconAdd, IconDelete, Modal, ModernButton, Scroller, showPopup } from '@hcengineering/ui'
export let _id: Ref<Role>
const client = getClient()
const query = createQuery()
let role: Role | undefined
query.query(card.class.Role, { _id }, (res) => {
role = res[0]
})
$: name = role?.name
const usersQ = createQuery()
let roles: WithLookup<UserRole>[] = []
usersQ.query(
contact.class.UserRole,
{ role: _id },
(res) => {
roles = res
},
{
lookup: {
user: contact.class.Person
}
}
)
let users: Employee[] = []
$: users = roles.map((role) => role.$lookup?.user).filter((p) => p !== undefined)
async function remove (): Promise<void> {
if (role === undefined) {
return
}
await client.remove(role)
clearSettingsStore()
}
async function removeAssignment (val: Ref<Employee>): Promise<void> {
const toRemove = roles.filter((role) => role.user === val)
for (const obj of toRemove) {
await client.remove(obj)
}
}
function openSelectUsersPopup (): void {
const selected = users.map((user) => user._id)
showPopup(
SelectUsersPopup,
{
okLabel: presentation.string.Add,
disableDeselectFor: selected,
skipInactive: true,
selected,
showStatus: true
},
'top',
(result?: Ref<Employee>[]) => {
if (result != null) {
void changeUsers(result)
}
}
)
}
async function changeUsers (persons: Ref<Employee>[]): Promise<void> {
const current = new Set(users.map((user) => user._id))
const newPersons = persons.filter((person) => !current.has(person))
if (newPersons.length === 0) {
return
}
const apply = client.apply()
for (const person of newPersons) {
await apply.createDoc(contact.class.UserRole, contact.space.Contacts, { role: _id, user: person })
}
await apply.commit()
}
</script>
<Modal
label={core.string.Role}
type={'type-aside'}
showCancelButton={false}
canSave={true}
okLabel={presentation.string.Close}
okAction={clearSettingsStore}
>
<svelte:fragment slot="actions">
<ButtonIcon icon={IconDelete} size={'small'} kind={'tertiary'} on:click={remove} />
</svelte:fragment>
<div class="hulyModal-content__titleGroup">
<div class="flex fs-title items-center flex-gap-2">
<Icon icon={contact.icon.Person} size={'medium'} />
<div>
{name}
</div>
</div>
</div>
<div class="hulyModal-content__settingsSet">
<div class="item" style:padding="var(--spacing-1_5)" class:withoutBorder={users.length === 0}>
<ModernButton
label={presentation.string.Add}
icon={IconAdd}
iconSize="small"
kind="secondary"
size="small"
on:click={openSelectUsersPopup}
/>
</div>
{#each users as user, index (user._id)}
<div class="item" class:withoutBorder={index === users.length - 1}>
<div class="item__content">
<UserDetails person={user} showStatus />
<div class="item__action">
<ButtonIcon
icon={IconDelete}
size="small"
on:click={async () => {
await removeAssignment(user._id)
}}
/>
</div>
</div>
</div>
{/each}
</div>
</Modal>
<style lang="scss">
.item {
padding: var(--spacing-0_75);
&.withoutBorder {
border: 0;
}
.item__content {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-0_75);
border-radius: var(--small-BorderRadius);
cursor: pointer;
&:hover {
background: var(--global-ui-highlight-BackgroundColor);
.item__action {
visibility: visible;
}
}
}
.item__action {
visibility: hidden;
&:hover {
visibility: visible;
}
}
}
</style>

View File

@ -0,0 +1,72 @@
<!--
// 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 contact from '@hcengineering/contact'
import core from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { clearSettingsStore, settingsStore } from '@hcengineering/setting-resources'
import { ButtonIcon, Icon, IconAdd, Label, showPopup } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import card from '../../plugin'
import CreateRolePopup from './CreateRolePopup.svelte'
export let masterTag: MasterTag | Tag
const client = getClient()
const ancestors = client.getHierarchy().getAncestors(masterTag._id)
let roles = client.getModel().findAllSync(card.class.Role, { attachedTo: { $in: ancestors } })
const query = createQuery()
query.query(card.class.Role, { attachedTo: { $in: ancestors } }, (res) => {
roles = res
})
function addRole (): void {
showPopup(CreateRolePopup, { masterTag }, undefined, (res) => {
if (res != null) {
$settingsStore = { id: res, component: card.component.EditRole, props: { _id: res } }
}
})
}
const handleSelect = (role: Role): void => {
$settingsStore = { id: role._id, component: card.component.EditRole, props: { _id: role._id } }
}
onDestroy(() => {
clearSettingsStore()
})
</script>
<div class="hulyTableAttr-header font-medium-12">
<Icon icon={contact.icon.User} size="small" />
<span><Label label={core.string.Roles} /></span>
<ButtonIcon kind="primary" icon={IconAdd} size="small" dataId={'btnAdd'} on:click={addRole} />
</div>
<div class="hulyTableAttr-content task">
{#each roles as role}
<button
class="hulyTableAttr-content__row justify-start"
on:click|stopPropagation={() => {
handleSelect(role)
}}
>
<div class="hulyTableAttr-content__row-label font-medium-14 cursor-pointer">
{role.name}
</div>
</button>
{/each}
</div>

View File

@ -47,6 +47,8 @@ import CardArrayEditor from './components/CardArrayEditor.svelte'
import NewCardHeader from './components/navigator/NewCardHeader.svelte' import NewCardHeader from './components/navigator/NewCardHeader.svelte'
import SpacePresenter from './components/navigator/SpacePresenter.svelte' import SpacePresenter from './components/navigator/SpacePresenter.svelte'
import LabelsPresenter from './components/LabelsPresenter.svelte' import LabelsPresenter from './components/LabelsPresenter.svelte'
import RolesSection from './components/settings/RolesSection.svelte'
import EditRole from './components/settings/EditRole.svelte'
export { default as CardSelector } from './components/CardSelector.svelte' export { default as CardSelector } from './components/CardSelector.svelte'
@ -76,7 +78,9 @@ export default async (): Promise<Resources> => ({
CardArrayEditor, CardArrayEditor,
NewCardHeader, NewCardHeader,
SpacePresenter, SpacePresenter,
LabelsPresenter LabelsPresenter,
RolesSection,
EditRole
}, },
completion: { completion: {
CardQuery: queryCard CardQuery: queryCard

View File

@ -46,7 +46,9 @@ export default mergeIds(cardId, card, {
CardArrayEditor: '' as AnyComponent, CardArrayEditor: '' as AnyComponent,
NewCardHeader: '' as AnyComponent, NewCardHeader: '' as AnyComponent,
SpacePresenter: '' as AnyComponent, SpacePresenter: '' as AnyComponent,
LabelsPresenter: '' as AnyComponent LabelsPresenter: '' as AnyComponent,
RolesSection: '' as AnyComponent,
EditRole: '' as AnyComponent
}, },
completion: { completion: {
CardQuery: '' as Resource<ObjectSearchFactory>, CardQuery: '' as Resource<ObjectSearchFactory>,

View File

@ -11,7 +11,19 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Blobs, Class, Doc, Domain, MarkupBlobRef, Mixin, Rank, Ref, Space } from '@hcengineering/core' import {
AttachedDoc,
Blobs,
Class,
CollectionSize,
Doc,
Domain,
MarkupBlobRef,
Mixin,
Rank,
Ref,
Space
} from '@hcengineering/core'
import { Asset, IntlString, plugin, Plugin } from '@hcengineering/platform' import { Asset, IntlString, plugin, Plugin } from '@hcengineering/platform'
import type { AnyComponent, ComponentExtensionId } from '@hcengineering/ui' import type { AnyComponent, ComponentExtensionId } from '@hcengineering/ui'
@ -20,10 +32,16 @@ export * from './analytics'
export interface MasterTag extends Class<Card> { export interface MasterTag extends Class<Card> {
color?: number color?: number
removed?: boolean removed?: boolean
roles?: CollectionSize<Role>
} }
export interface Tag extends MasterTag, Mixin<Card> {} export interface Tag extends MasterTag, Mixin<Card> {}
export interface Role extends AttachedDoc<MasterTag | Tag, 'roles'> {
name: string
attachedTo: Ref<MasterTag | Tag>
}
export interface Card extends Doc { export interface Card extends Doc {
_class: Ref<MasterTag> _class: Ref<MasterTag>
title: string title: string
@ -69,7 +87,8 @@ const cardPlugin = plugin(cardId, {
MasterTag: '' as Ref<Class<MasterTag>>, MasterTag: '' as Ref<Class<MasterTag>>,
Tag: '' as Ref<Class<Tag>>, Tag: '' as Ref<Class<Tag>>,
MasterTagEditorSection: '' as Ref<Class<MasterTagEditorSection>>, MasterTagEditorSection: '' as Ref<Class<MasterTagEditorSection>>,
CardSpace: '' as Ref<Class<CardSpace>> CardSpace: '' as Ref<Class<CardSpace>>,
Role: '' as Ref<Class<Role>>
}, },
space: { space: {
Default: '' as Ref<CardSpace> Default: '' as Ref<CardSpace>

View File

@ -38,7 +38,7 @@ import { TemplateField, TemplateFieldCategory } from '@hcengineering/templates'
import type { AnyComponent, ColorDefinition, ResolvedLocation, Location, ComponentExtensionId } from '@hcengineering/ui' import type { AnyComponent, ColorDefinition, ResolvedLocation, Location, ComponentExtensionId } from '@hcengineering/ui'
import { Action, FilterMode, Viewlet } from '@hcengineering/view' import { Action, FilterMode, Viewlet } from '@hcengineering/view'
import type { Readable } from 'svelte/store' import type { Readable } from 'svelte/store'
import { Card, MasterTag } from '@hcengineering/card' import { Card, MasterTag, Role } from '@hcengineering/card'
import { PermissionsStore } from './types' import { PermissionsStore } from './types'
/** /**
@ -144,6 +144,11 @@ export interface Person extends Contact, BasePerson {
profile?: Ref<Card> profile?: Ref<Card>
} }
export interface UserRole extends Doc {
user: Ref<Employee>
role: Ref<Role>
}
/** /**
* @public * @public
*/ */
@ -213,7 +218,8 @@ export const contactPlugin = plugin(contactId, {
ContactsTab: '' as Ref<Class<ContactsTab>>, ContactsTab: '' as Ref<Class<ContactsTab>>,
PersonSpace: '' as Ref<Class<PersonSpace>>, PersonSpace: '' as Ref<Class<PersonSpace>>,
SocialIdentity: '' as Ref<Class<SocialIdentity>>, SocialIdentity: '' as Ref<Class<SocialIdentity>>,
UserProfile: '' as Ref<MasterTag> UserProfile: '' as Ref<MasterTag>,
UserRole: '' as Ref<Class<UserRole>>
}, },
mixin: { mixin: {
Employee: '' as Ref<Class<Employee>> Employee: '' as Ref<Class<Employee>>

View File

@ -65,6 +65,7 @@
"ObjectNotFound": "Objekt nenalezen: {_id}", "ObjectNotFound": "Objekt nenalezen: {_id}",
"EmptyRelatedObjectValue": "Prázdná hodnota {attr} souvisejícího objektu: {parent}", "EmptyRelatedObjectValue": "Prázdná hodnota {attr} souvisejícího objektu: {parent}",
"InternalServerError": "Vnitřní chyba serveru, kontaktujte podporu, chybové id: {errorId}", "InternalServerError": "Vnitřní chyba serveru, kontaktujte podporu, chybové id: {errorId}",
"ResultNotProvided": "Výsledek nebyl poskytnut" "ResultNotProvided": "Výsledek nebyl poskytnut",
"EmptyFunctionResult": "Prázdný výsledek funkce {func}"
} }
} }

View File

@ -65,6 +65,7 @@
"ObjectNotFound": "Objekt nicht gefunden: {_id}", "ObjectNotFound": "Objekt nicht gefunden: {_id}",
"EmptyRelatedObjectValue": "Leerer verwandtes Objekt {parent} Wert: {attr}", "EmptyRelatedObjectValue": "Leerer verwandtes Objekt {parent} Wert: {attr}",
"InternalServerError": "Interner Serverfehler, bitte kontaktieren Sie den Support, Fehler-ID: {errorId}", "InternalServerError": "Interner Serverfehler, bitte kontaktieren Sie den Support, Fehler-ID: {errorId}",
"ResultNotProvided": "Ergebnis nicht bereitgestellt" "ResultNotProvided": "Ergebnis nicht bereitgestellt",
"EmptyFunctionResult": "Leerer Funktionsresultat {func}"
} }
} }

View File

@ -65,6 +65,7 @@
"ObjectNotFound": "Object not found: {_id}", "ObjectNotFound": "Object not found: {_id}",
"EmptyRelatedObjectValue": "Empty related object {parent} value: {attr}", "EmptyRelatedObjectValue": "Empty related object {parent} value: {attr}",
"InternalServerError": "Internal server error, contact support, error id: {errorId}", "InternalServerError": "Internal server error, contact support, error id: {errorId}",
"ResultNotProvided": "Result not provided" "ResultNotProvided": "Result not provided",
"EmptyFunctionResult": "Empty function result {func}"
} }
} }

View File

@ -65,6 +65,7 @@
"ObjectNotFound": "Objeto no encontrado: {_id}", "ObjectNotFound": "Objeto no encontrado: {_id}",
"EmptyRelatedObjectValue": "Valor de objeto relacionado vacío: {parent} {attr}", "EmptyRelatedObjectValue": "Valor de objeto relacionado vacío: {parent} {attr}",
"InternalServerError": "Error interno del servidor, contacte con el soporte, id de error: {errorId}", "InternalServerError": "Error interno del servidor, contacte con el soporte, id de error: {errorId}",
"ResultNotProvided": "Resultado no proporcionado" "ResultNotProvided": "Resultado no proporcionado",
"EmptyFunctionResult": "Resultado de función vacío {func}"
} }
} }

View File

@ -65,6 +65,7 @@
"ObjectNotFound": "Objet introuvable : {_id}", "ObjectNotFound": "Objet introuvable : {_id}",
"EmptyRelatedObjectValue": "Valeur d'objet lié vide : {parent} {attr}", "EmptyRelatedObjectValue": "Valeur d'objet lié vide : {parent} {attr}",
"InternalServerError": "Erreur interne du serveur, contactez le support, id d'erreur : {errorId}", "InternalServerError": "Erreur interne du serveur, contactez le support, id d'erreur : {errorId}",
"ResultNotProvided": "Résultat non fourni" "ResultNotProvided": "Résultat non fourni",
"EmptyFunctionResult": "Résultat de fonction vide {func}"
} }
} }

View File

@ -65,6 +65,7 @@
"ObjectNotFound": "Oggetto non trovato: {_id}", "ObjectNotFound": "Oggetto non trovato: {_id}",
"EmptyRelatedObjectValue": "Valore oggetto correlato vuoto: {parent} {attr}", "EmptyRelatedObjectValue": "Valore oggetto correlato vuoto: {parent} {attr}",
"InternalServerError": "Errore interno del server, contatta il supporto, id errore: {errorId}", "InternalServerError": "Errore interno del server, contatta il supporto, id errore: {errorId}",
"ResultNotProvided": "Risultato non fornito" "ResultNotProvided": "Risultato non fornito",
"EmptyFunctionResult": "Risultato funzione vuoto {func}"
} }
} }

View File

@ -61,6 +61,7 @@
"UserRequestedValueNotProvided": "ユーザーが要求した値が提供されていません: {attr}", "UserRequestedValueNotProvided": "ユーザーが要求した値が提供されていません: {attr}",
"ObjectNotFound": "オブジェクトが見つかりません: {_id}", "ObjectNotFound": "オブジェクトが見つかりません: {_id}",
"EmptyRelatedObjectValue": "関連オブジェクト {parent} の値が空です: {attr}", "EmptyRelatedObjectValue": "関連オブジェクト {parent} の値が空です: {attr}",
"InternalServerError": "内部サーバーエラー。サポートにお問い合わせください。エラーID: {errorId}" "InternalServerError": "内部サーバーエラー。サポートにお問い合わせください。エラーID: {errorId}",
"EmptyFunctionResult": "関数結果が空です {func}"
} }
} }

View File

@ -65,6 +65,7 @@
"ObjectNotFound": "Objeto não encontrado: {_id}", "ObjectNotFound": "Objeto não encontrado: {_id}",
"EmptyRelatedObjectValue": "Valor de objeto relacionado vazio: {parent} {attr}", "EmptyRelatedObjectValue": "Valor de objeto relacionado vazio: {parent} {attr}",
"InternalServerError": "Erro interno do servidor, contate o suporte, id de erro: {errorId}", "InternalServerError": "Erro interno do servidor, contate o suporte, id de erro: {errorId}",
"ResultNotProvided": "Resultado não fornecido" "ResultNotProvided": "Resultado não fornecido",
"EmptyFunctionResult": "Resultado da função vazio {func}"
} }
} }

View File

@ -65,6 +65,7 @@
"UserRequestedValueNotProvided": "Знвчение не предоставлено пользователем: {attr}", "UserRequestedValueNotProvided": "Знвчение не предоставлено пользователем: {attr}",
"ObjectNotFound": "Объект не найден: {_id}", "ObjectNotFound": "Объект не найден: {_id}",
"AttributeNotExists": "Атрибут не существует: {key}", "AttributeNotExists": "Атрибут не существует: {key}",
"ResultNotProvided": "Результат не предоставлен" "ResultNotProvided": "Результат не предоставлен",
"EmptyFunctionResult": "Отсутствуют результат функции {func}"
} }
} }

View File

@ -65,6 +65,7 @@
"ObjectNotFound": "找不到对象:{_id}", "ObjectNotFound": "找不到对象:{_id}",
"EmptyRelatedObjectValue": "空相关对象值:{parent} {attr}", "EmptyRelatedObjectValue": "空相关对象值:{parent} {attr}",
"InternalServerError": "内部服务器错误,请联系支持,错误 ID{errorId}", "InternalServerError": "内部服务器错误,请联系支持,错误 ID{errorId}",
"ResultNotProvided": "未提供结果" "ResultNotProvided": "未提供结果",
"EmptyFunctionResult": "空函数结果 {func}"
} }
} }

View File

@ -29,7 +29,9 @@
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import ContextSelectorPopup from './attributeEditors/ContextSelectorPopup.svelte' import ContextSelectorPopup from './attributeEditors/ContextSelectorPopup.svelte'
import ContextValue from './attributeEditors/ContextValue.svelte' import ContextValue from './attributeEditors/ContextValue.svelte'
import { MasterTag, Tag } from '@hcengineering/card'
export let masterTag: Ref<MasterTag | Tag>
export let value: any export let value: any
export let context: Context export let context: Context
export let presenterClass: { export let presenterClass: {
@ -52,6 +54,7 @@
showPopup( showPopup(
ContextSelectorPopup, ContextSelectorPopup,
{ {
masterTag,
context, context,
attribute, attribute,
onSelect onSelect
@ -78,6 +81,7 @@
<div class="text-input" class:context={contextValue}> <div class="text-input" class:context={contextValue}>
{#if contextValue} {#if contextValue}
<ContextValue <ContextValue
{masterTag}
{contextValue} {contextValue}
{context} {context}
{attribute} {attribute}

View File

@ -62,6 +62,7 @@
{attribute} {attribute}
{presenterClass} {presenterClass}
{value} {value}
masterTag={process.masterTag}
{allowRemove} {allowRemove}
on:remove on:remove
on:change={(e) => { on:change={(e) => {

View File

@ -13,10 +13,10 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import core, { generateId, Ref } from '@hcengineering/core' import core, { Ref } from '@hcengineering/core'
import { translate } from '@hcengineering/platform' import { translate } from '@hcengineering/platform'
import { createQuery, getClient, MessageBox } from '@hcengineering/presentation' import { createQuery, getClient, MessageBox } from '@hcengineering/presentation'
import { Process, SelectedUserRequest, State } from '@hcengineering/process' import { Process, State } from '@hcengineering/process'
import { settingsStore } from '@hcengineering/setting-resources' import { settingsStore } from '@hcengineering/setting-resources'
import { import {
Button, Button,
@ -29,21 +29,20 @@
IconDescription, IconDescription,
navigate, navigate,
NavItem, NavItem,
ToggleWithLabel,
Scroller, Scroller,
secondNavSeparators, secondNavSeparators,
Separator, Separator,
showPopup showPopup,
ToggleWithLabel
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import process from '../plugin' import process from '../plugin'
import { getToDoEndAction } from '../utils'
import Aside from './Aside.svelte' import Aside from './Aside.svelte'
import ArrowEnd from './icons/ArrowEnd.svelte' import ArrowEnd from './icons/ArrowEnd.svelte'
import ArrowStart from './icons/ArrowStart.svelte' import ArrowStart from './icons/ArrowStart.svelte'
import StateEditor from './StateEditor.svelte' import StateEditor from './StateEditor.svelte'
import TransitionEditor from './TransitionEditor.svelte' import TransitionEditor from './TransitionEditor.svelte'
import { getToDoEndAction } from '../utils'
import ExecutionResultEditor from './ExecutionResultEditor.svelte'
export let _id: Ref<Process> export let _id: Ref<Process>
export let visibleSecondNav: boolean = true export let visibleSecondNav: boolean = true

View File

@ -13,6 +13,9 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { AnyAttribute, Class, Doc, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Context, Func, ProcessFunction, SelectedContext } from '@hcengineering/process'
import { import {
ButtonIcon, ButtonIcon,
CheckBox, CheckBox,
@ -26,14 +29,13 @@
showPopup, showPopup,
Submenu Submenu
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { AttributeCategory } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin' import plugin from '../../plugin'
import core, { AnyAttribute, Class, Doc, Ref } from '@hcengineering/core'
import { Context, Func, ProcessFunction, SelectedContext } from '@hcengineering/process'
import { getClient } from '@hcengineering/presentation'
import { AttributeCategory } from '@hcengineering/view'
import FallbackEditor from '../contextEditors/FallbackEditor.svelte' import FallbackEditor from '../contextEditors/FallbackEditor.svelte'
import { MasterTag, Tag } from '@hcengineering/card'
export let masterTag: Ref<MasterTag | Tag>
export let contextValue: SelectedContext export let contextValue: SelectedContext
export let context: Context export let context: Context
export let attribute: AnyAttribute export let attribute: AnyAttribute
@ -63,7 +65,7 @@
const reduceFuncs = client const reduceFuncs = client
.getModel() .getModel()
.findAllSync(plugin.class.ProcessFunction, { of: core.class.ArrOf }) .findAllSync(plugin.class.ProcessFunction, { type: 'reduce' })
.map((it) => it._id) .map((it) => it._id)
$: availableFunctions = getAvailableFunctions(context, contextValue.functions, attrClass, category) $: availableFunctions = getAvailableFunctions(context, contextValue.functions, attrClass, category)
@ -77,7 +79,9 @@
category: AttributeCategory category: AttributeCategory
): Ref<ProcessFunction>[] { ): Ref<ProcessFunction>[] {
const result: Ref<ProcessFunction>[] = [] const result: Ref<ProcessFunction>[] = []
const allFunctions = client.getModel().findAllSync(plugin.class.ProcessFunction, { of: attrClass, category }) const allFunctions = client
.getModel()
.findAllSync(plugin.class.ProcessFunction, { of: attrClass, category, type: 'transform' })
for (const f of allFunctions) { for (const f of allFunctions) {
if (functions === undefined || f.allowMany === true || functions.findIndex((p) => p.func === f._id) === -1) { if (functions === undefined || f.allowMany === true || functions.findIndex((p) => p.func === f._id) === -1) {
result.push(f._id) result.push(f._id)
@ -179,6 +183,7 @@
func.editor, func.editor,
{ {
func, func,
masterTag,
context, context,
attribute, attribute,
props: val?.props ?? {} props: val?.props ?? {}
@ -192,7 +197,6 @@
func.props = res func.props = res
contextValue.functions[pos] = func contextValue.functions[pos] = func
contextValue.functions = contextValue.functions contextValue.functions = contextValue.functions
console.log(contextValue.functions)
onChange(contextValue) onChange(contextValue)
} }
} }
@ -309,8 +313,8 @@
/> />
<!-- <div class="menu-separator" /> --> <!-- <div class="menu-separator" /> -->
{/if} {/if}
<div class="menu-separator" />
{/if} {/if}
<div class="menu-separator" />
<!-- svelte-ignore a11y-mouse-events-have-key-events --> <!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button <button
bind:this={elements[functionButtonIndex + 1]} bind:this={elements[functionButtonIndex + 1]}

View File

@ -13,13 +13,16 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { AnyAttribute, generateId } from '@hcengineering/core' import { AnyAttribute, generateId, Ref } from '@hcengineering/core'
import { Context, SelectedContext } from '@hcengineering/process' import { getClient } from '@hcengineering/presentation'
import { Context, ProcessFunction, SelectedContext } from '@hcengineering/process'
import { Label, resizeObserver, Scroller, Submenu } from '@hcengineering/ui' import { Label, resizeObserver, Scroller, Submenu } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin' import plugin from '../../plugin'
import { getValueReduceFunc } from '../../utils' import { getValueReduceFunc } from '../../utils'
import { MasterTag, Tag } from '@hcengineering/card'
export let masterTag: Ref<MasterTag | Tag>
export let context: Context export let context: Context
export let attribute: AnyAttribute export let attribute: AnyAttribute
export let onSelect: (val: SelectedContext | null) => void export let onSelect: (val: SelectedContext | null) => void
@ -58,20 +61,20 @@
dispatch('close') dispatch('close')
} }
function getIndex (ownIndex: number, kind: 'attribute' | 'relation' | 'nested' | 'total'): number { function onFunc (func: Ref<ProcessFunction>): void {
if (kind === 'attribute') { onSelect({
return ownIndex + 1 type: 'function',
} key: attribute.name,
if (kind === 'nested') { func,
return ownIndex + context.attributes.length + 1 props: {}
} })
if (kind === 'relation') { dispatch('close')
return ownIndex + context.attributes.length + nested.length + 1 }
}
if (kind === 'total') { function getFunc (func: Ref<ProcessFunction>): ProcessFunction {
return ownIndex + context.attributes.length + relations.length + nested.length + 1 const client = getClient()
} const f = client.getModel().getObject(func)
return ownIndex return f
} }
</script> </script>
@ -89,8 +92,38 @@
</span> </span>
</button> </button>
<div class="menu-separator" /> <div class="menu-separator" />
{#if context.functions.length > 0}
{#each context.functions as f}
{@const func = getFunc(f)}
{#if func.editor !== undefined}
<Submenu
label={func.label}
props={{
masterTag,
context: func,
target: attribute,
onSelect: onClick
}}
options={{ component: func.editor }}
withHover
/>
{:else}
<button
on:click={() => {
onFunc(func._id)
}}
class="menu-item"
>
<span class="overflow-label pr-1">
<Label label={func.label} />
</span>
</button>
{/if}
{/each}
<div class="menu-separator" />
{/if}
{#if context.attributes.length > 0} {#if context.attributes.length > 0}
{#each context.attributes as attr, i} {#each context.attributes as attr}
<button <button
on:click={() => { on:click={() => {
onAttribute(attr) onAttribute(attr)
@ -105,8 +138,7 @@
<div class="menu-separator" /> <div class="menu-separator" />
{/if} {/if}
{#if nested.length > 0} {#if nested.length > 0}
{#each nested as object, i} {#each nested as object}
{@const index = getIndex(i, 'nested')}
<Submenu <Submenu
label={object.attribute.label} label={object.attribute.label}
props={{ props={{
@ -121,8 +153,7 @@
<div class="menu-separator" /> <div class="menu-separator" />
{/if} {/if}
{#if relations.length > 0} {#if relations.length > 0}
{#each relations as object, i} {#each relations as object}
{@const index = getIndex(i, 'relation')}
<Submenu <Submenu
text={object[0]} text={object[0]}
props={{ props={{

View File

@ -20,7 +20,9 @@
import ContextValuePresenter from './ContextValuePresenter.svelte' import ContextValuePresenter from './ContextValuePresenter.svelte'
import { AttributeCategory } from '@hcengineering/view' import { AttributeCategory } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { MasterTag, Tag } from '@hcengineering/card'
export let masterTag: Ref<MasterTag | Tag>
export let contextValue: SelectedContext export let contextValue: SelectedContext
export let context: Context export let context: Context
export let attribute: AnyAttribute export let attribute: AnyAttribute
@ -46,7 +48,7 @@
} }
showPopup( showPopup(
ConfigurePopup, ConfigurePopup,
{ contextValue, attrClass, category, attribute, context, onChange }, { contextValue, attrClass, masterTag, category, attribute, context, onChange },
eventToHTMLElement(e) eventToHTMLElement(e)
) )
} }

View File

@ -19,6 +19,7 @@
import AttrContextPresenter from './AttrContextPresenter.svelte' import AttrContextPresenter from './AttrContextPresenter.svelte'
import NestedContextPresenter from './NestedContextPresenter.svelte' import NestedContextPresenter from './NestedContextPresenter.svelte'
import RelContextPresenter from './RelContextPresenter.svelte' import RelContextPresenter from './RelContextPresenter.svelte'
import FunctionContextPresenter from './FunctionContextPresenter.svelte'
export let contextValue: SelectedContext export let contextValue: SelectedContext
export let context: Context export let context: Context
@ -33,6 +34,8 @@
<NestedContextPresenter {contextValue} {context} /> <NestedContextPresenter {contextValue} {context} />
{:else if contextValue.type === 'userRequest'} {:else if contextValue.type === 'userRequest'}
<Label label={plugin.string.RequestFromUser} /> <Label label={plugin.string.RequestFromUser} />
{:else if contextValue.type === 'function'}
<FunctionContextPresenter {contextValue} {context} />
{/if} {/if}
</div> </div>

View File

@ -0,0 +1,29 @@
<!--
// 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 { getClient } from '@hcengineering/presentation'
import { Context, SelectedContextFunc } from '@hcengineering/process'
import { Label } from '@hcengineering/ui'
export let contextValue: SelectedContextFunc
export let context: Context
const client = getClient()
const func = client.getModel().findObject(contextValue.func)
</script>
{#if func !== undefined}
<Label label={func.label} />
{/if}

View File

@ -13,7 +13,8 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import core, { AnyAttribute } from '@hcengineering/core' import { MasterTag, Tag } from '@hcengineering/card'
import core, { AnyAttribute, Ref } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import presentation, { Card, getClient } from '@hcengineering/presentation' import presentation, { Card, getClient } from '@hcengineering/presentation'
import { Context, ProcessFunction } from '@hcengineering/process' import { Context, ProcessFunction } from '@hcengineering/process'
@ -23,6 +24,7 @@
import ProcessAttribute from '../ProcessAttribute.svelte' import ProcessAttribute from '../ProcessAttribute.svelte'
export let func: ProcessFunction export let func: ProcessFunction
export let masterTag: Ref<MasterTag | Tag>
export let context: Context export let context: Context
export let attribute: AnyAttribute export let attribute: AnyAttribute
export let props: Record<string, any> = {} export let props: Record<string, any> = {}
@ -59,6 +61,7 @@
<Card on:close width={'menu'} label={func.label} canSave okAction={save} okLabel={presentation.string.Save}> <Card on:close width={'menu'} label={func.label} canSave okAction={save} okLabel={presentation.string.Save}>
<ProcessAttribute <ProcessAttribute
{masterTag}
{context} {context}
{attribute} {attribute}
presenterClass={{ presenterClass={{

View File

@ -0,0 +1,91 @@
<!--
// 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 card, { MasterTag, Role, Tag } from '@hcengineering/card'
import { AnyAttribute, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { ProcessFunction, SelectedContext } from '@hcengineering/process'
import { resizeObserver, Scroller } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { getContextFunctionReduce } from '../../utils'
export let context: ProcessFunction
export let masterTag: Ref<MasterTag | Tag>
export let target: AnyAttribute
export let onSelect: (val: SelectedContext) => void
const dispatch = createEventDispatcher()
const client = getClient()
const elements: HTMLButtonElement[] = []
const keyDown = (event: KeyboardEvent, index: number): void => {
if (event.key === 'ArrowDown') {
elements[(index + 1) % elements.length].focus()
}
if (event.key === 'ArrowUp') {
elements[(elements.length + index - 1) % elements.length].focus()
}
if (event.key === 'ArrowLeft') {
dispatch('close')
}
}
function onValue (role: Ref<Role>): void {
const pathReduce = getContextFunctionReduce(context, target)
onSelect({
type: 'function',
func: context._id,
key: target.name,
functions: [],
sourceFunction: pathReduce,
props: {
target: role
}
})
}
const ancestors = client.getHierarchy().getAncestors(masterTag)
const roles = client.getModel().findAllSync(card.class.Role, { attachedTo: { $in: ancestors } })
</script>
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
<div class="menu-space" />
<Scroller>
{#each roles as role, i}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
bind:this={elements[i]}
on:keydown={(event) => {
keyDown(event, i)
}}
on:mouseover={() => {
elements[i]?.focus()
}}
on:click={() => {
onValue(role._id)
}}
class="menu-item"
>
<span class="overflow-label pr-1">
{role.name}
</span>
</button>
{/each}
</Scroller>
<div class="menu-space" />
</div>

View File

@ -35,6 +35,7 @@ import SubProcessEditor from './components/SubProcessEditor.svelte'
import ToDoEditor from './components/ToDoEditor.svelte' import ToDoEditor from './components/ToDoEditor.svelte'
import UpdateCardEditor from './components/UpdateCardEditor.svelte' import UpdateCardEditor from './components/UpdateCardEditor.svelte'
import ResultInput from './components/contextEditors/ResultInput.svelte' import ResultInput from './components/contextEditors/ResultInput.svelte'
import RoleEditor from './components/contextEditors/RoleEditor.svelte'
import { continueExecution, showDoneQuery } from './utils' import { continueExecution, showDoneQuery } from './utils'
import { ProcessMiddleware } from './middleware' import { ProcessMiddleware } from './middleware'
@ -66,7 +67,8 @@ export default async (): Promise<Resources> => ({
NumberOffsetEditor, NumberOffsetEditor,
ErrorPresenter, ErrorPresenter,
RequestUserInput, RequestUserInput,
ResultInput ResultInput,
RoleEditor
}, },
function: { function: {
ShowDoneQuery: showDoneQuery, ShowDoneQuery: showDoneQuery,

View File

@ -46,7 +46,8 @@ export default mergeIds(processId, process, {
NumberOffsetEditor: '' as AnyComponent, NumberOffsetEditor: '' as AnyComponent,
ErrorPresenter: '' as AnyComponent, ErrorPresenter: '' as AnyComponent,
RequestUserInput: '' as AnyComponent, RequestUserInput: '' as AnyComponent,
ResultInput: '' as AnyComponent ResultInput: '' as AnyComponent,
RoleEditor: '' as AnyComponent
}, },
function: { function: {
ShowDoneQuery: '' as ViewQueryAction, ShowDoneQuery: '' as ViewQueryAction,

View File

@ -66,6 +66,8 @@ export function getContext (
if (attr !== undefined && category === 'object') { if (attr !== undefined && category === 'object') {
attributes = attributes.filter((it) => it._id !== attr) attributes = attributes.filter((it) => it._id !== attr)
} }
const functions = getContextFunctions(client, process.masterTag, target, category)
const nested: Record<string, NestedContext> = {} const nested: Record<string, NestedContext> = {}
const relations: Record<string, RelatedContext> = {} const relations: Record<string, RelatedContext> = {}
@ -119,12 +121,55 @@ export function getContext (
} }
return { return {
functions,
attributes, attributes,
nested, nested,
relations relations
} }
} }
function getContextFunctions (
client: Client,
_class: Ref<Class<Doc>>,
target: Ref<Class<Type<any>>>,
category: AttributeCategory
): Array<Ref<ProcessFunction>> {
const matched: Array<Ref<ProcessFunction>> = []
const hierarchy = client.getHierarchy()
const funcs = client.getModel().findAllSync(process.class.ProcessFunction, { type: 'context' })
for (const func of funcs) {
switch (category) {
case 'object': {
if (func.category === 'array') {
if (hierarchy.isDerived(func.of, target)) {
matched.push(func._id)
}
}
if (func.category === 'object') {
if (hierarchy.isDerived(func.of, target)) {
matched.push(func._id)
}
}
break
}
case 'array': {
if (func.category === 'array') {
if (hierarchy.isDerived(func.of, target)) {
matched.push(func._id)
}
}
break
}
default: {
if (func.of === target) {
matched.push(func._id)
}
}
}
}
return matched
}
function getClassAttributes ( function getClassAttributes (
client: Client, client: Client,
_class: Ref<Class<Doc>>, _class: Ref<Class<Doc>>,
@ -192,6 +237,15 @@ export function getValueReduceFunc (source: AnyAttribute, target: AnyAttribute):
return process.function.FirstValue return process.function.FirstValue
} }
export function getContextFunctionReduce (
func: ProcessFunction,
target: AnyAttribute
): Ref<ProcessFunction> | undefined {
if (func.category !== 'array') return undefined
if (target.type._class === core.class.ArrOf) return undefined
return process.function.FirstValue
}
export function showDoneQuery (value: any, query: DocumentQuery<Doc>): DocumentQuery<Doc> { export function showDoneQuery (value: any, query: DocumentQuery<Doc>): DocumentQuery<Doc> {
if (value === false) { if (value === false) {
return { ...query, done: false } return { ...query, done: false }

View File

@ -91,6 +91,7 @@ export interface Method<T extends Doc> extends Doc {
} }
export interface ProcessFunction extends Doc { export interface ProcessFunction extends Doc {
type: 'transform' | 'reduce' | 'context'
of: Ref<Class<Doc>> of: Ref<Class<Doc>>
editor?: AnyComponent editor?: AnyComponent
category: AttributeCategory | undefined category: AttributeCategory | undefined
@ -134,7 +135,8 @@ export default plugin(processId, {
ObjectNotFound: '' as IntlString, ObjectNotFound: '' as IntlString,
AttributeNotExists: '' as IntlString, AttributeNotExists: '' as IntlString,
UserRequestedValueNotProvided: '' as IntlString, UserRequestedValueNotProvided: '' as IntlString,
ResultNotProvided: '' as IntlString ResultNotProvided: '' as IntlString,
EmptyFunctionResult: '' as IntlString
}, },
icon: { icon: {
Process: '' as Asset, Process: '' as Asset,
@ -153,6 +155,7 @@ export default plugin(processId, {
Add: '' as Ref<ProcessFunction>, Add: '' as Ref<ProcessFunction>,
Subtract: '' as Ref<ProcessFunction>, Subtract: '' as Ref<ProcessFunction>,
Offset: '' as Ref<ProcessFunction>, Offset: '' as Ref<ProcessFunction>,
FirstWorkingDayAfter: '' as Ref<ProcessFunction> FirstWorkingDayAfter: '' as Ref<ProcessFunction>,
RoleContext: '' as Ref<ProcessFunction>
} }
}) })

View File

@ -2,6 +2,7 @@ import { Class, Doc, type AnyAttribute, type Association, type Ref } from '@hcen
import { ProcessFunction } from '.' import { ProcessFunction } from '.'
export interface Context { export interface Context {
functions: Ref<ProcessFunction>[]
attributes: AnyAttribute[] attributes: AnyAttribute[]
nested: Record<string, NestedContext> nested: Record<string, NestedContext>
relations: Record<string, RelatedContext> relations: Record<string, RelatedContext>
@ -25,7 +26,7 @@ export interface Func {
} }
interface BaseSelectedContext { interface BaseSelectedContext {
type: 'attribute' | 'relation' | 'nested' | 'userRequest' type: 'attribute' | 'relation' | 'nested' | 'userRequest' | 'function'
// attribute key // attribute key
key: string key: string
@ -60,4 +61,15 @@ export interface SelectedUserRequest extends BaseSelectedContext {
id: string id: string
} }
export type SelectedContext = SelectedAttribute | SelectedRelation | SelectedNested | SelectedUserRequest export interface SelectedContextFunc extends BaseSelectedContext {
type: 'function'
func: Ref<ProcessFunction>
props: Record<string, any>
}
export type SelectedContext =
| SelectedAttribute
| SelectedRelation
| SelectedNested
| SelectedUserRequest
| SelectedContextFunc

View File

@ -39,6 +39,7 @@
"dependencies": { "dependencies": {
"@hcengineering/time": "^0.6.0", "@hcengineering/time": "^0.6.0",
"@hcengineering/core": "^0.6.32", "@hcengineering/core": "^0.6.32",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/card": "^0.6.0", "@hcengineering/card": "^0.6.0",
"@hcengineering/server-process": "^0.6.0", "@hcengineering/server-process": "^0.6.0",
"@hcengineering/server-core": "^0.6.1", "@hcengineering/server-core": "^0.6.1",

View File

@ -14,6 +14,7 @@
// //
import card, { Card } from '@hcengineering/card' import card, { Card } from '@hcengineering/card'
import contact, { Employee } from '@hcengineering/contact'
import core, { import core, {
ArrOf, ArrOf,
Doc, Doc,
@ -40,6 +41,7 @@ import process, {
ProcessError, ProcessError,
ProcessToDo, ProcessToDo,
SelectedContext, SelectedContext,
SelectedContextFunc,
SelectedNested, SelectedNested,
SelectedRelation, SelectedRelation,
SelectedUserRequest, SelectedUserRequest,
@ -342,6 +344,8 @@ async function getContextValue (value: any, control: TriggerControl, execution:
value = await getNestedValue(control, execution, context) value = await getNestedValue(control, execution, context)
} else if (context.type === 'userRequest') { } else if (context.type === 'userRequest') {
value = getUserRequestValue(control, execution, context) value = getUserRequestValue(control, execution, context)
} else if (context.type === 'function') {
value = await getFunctionValue(control, execution, context)
} }
return await fillValue(value, context, control, execution) return await fillValue(value, context, control, execution)
} catch (err: any) { } catch (err: any) {
@ -355,6 +359,39 @@ async function getContextValue (value: any, control: TriggerControl, execution:
} }
} }
async function getFunctionValue (
control: TriggerControl,
execution: Execution,
context: SelectedContextFunc
): Promise<any> {
const func = control.modelDb.findObject(context.func)
if (func === undefined) throw processError(process.error.MethodNotFound, { methodId: context.func }, {}, true)
const impl = control.hierarchy.as(func, serverProcess.mixin.FuncImpl)
if (impl === undefined) throw processError(process.error.MethodNotFound, { methodId: context.func }, {}, true)
const f = await getResource(impl.func)
const res = await f(null, context.props, control, execution)
if (context.sourceFunction !== undefined) {
const transform = control.modelDb.findObject(context.sourceFunction)
if (transform === undefined) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
if (!control.hierarchy.hasMixin(transform, serverProcess.mixin.FuncImpl)) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
const funcImpl = control.hierarchy.as(transform, serverProcess.mixin.FuncImpl)
const f = await getResource(funcImpl.func)
const val = await f(res, {}, control, execution)
if (val == null) {
throw processError(process.error.EmptyFunctionResult, {}, { func: func.label })
}
return val
}
if (res == null) {
throw processError(process.error.EmptyFunctionResult, {}, { func: func.label })
}
return res
}
function getUserRequestValue (control: TriggerControl, execution: Execution, context: SelectedUserRequest): any { function getUserRequestValue (control: TriggerControl, execution: Execution, context: SelectedUserRequest): any {
const userContext = execution.context?.[context.id] const userContext = execution.context?.[context.id]
if (userContext !== undefined) return userContext if (userContext !== undefined) return userContext
@ -682,6 +719,18 @@ export function Trim (value: string): string {
return value.trim() return value.trim()
} }
export async function RoleContext (
value: null,
props: Record<string, any>,
control: TriggerControl,
execution: Execution
): Promise<Ref<Employee>[]> {
const targetRole = props.target
if (targetRole === undefined) return []
const users = await control.findAll(control.ctx, contact.class.UserRole, { role: targetRole })
return users.map((it) => it.user)
}
export async function Add ( export async function Add (
value: number, value: number,
props: Record<string, any>, props: Record<string, any>,
@ -787,7 +836,8 @@ export default async () => ({
Add, Add,
Subtract, Subtract,
Offset, Offset,
FirstWorkingDayAfter FirstWorkingDayAfter,
RoleContext
}, },
trigger: { trigger: {
OnExecutionCreate, OnExecutionCreate,

View File

@ -44,7 +44,8 @@ export default plugin(serverProcessId, {
Add: '' as Resource<TransformFunc>, Add: '' as Resource<TransformFunc>,
Subtract: '' as Resource<TransformFunc>, Subtract: '' as Resource<TransformFunc>,
Offset: '' as Resource<TransformFunc>, Offset: '' as Resource<TransformFunc>,
FirstWorkingDayAfter: '' as Resource<TransformFunc> FirstWorkingDayAfter: '' as Resource<TransformFunc>,
RoleContext: '' as Resource<TransformFunc>
}, },
trigger: { trigger: {
OnExecutionCreate: '' as Resource<TriggerFunc>, OnExecutionCreate: '' as Resource<TriggerFunc>,