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 MasterTag,
type ParentInfo,
type Tag
type Tag,
type Role
} from '@hcengineering/card'
import chunter from '@hcengineering/chunter'
import core, {
@ -47,7 +48,7 @@ import {
type Builder
} from '@hcengineering/model'
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 setting from '@hcengineering/model-setting'
import view, { createAction } from '@hcengineering/model-view'
@ -115,6 +116,13 @@ export class MasterTagEditorSection extends TDoc implements MasterTagEditorSecti
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'
const listConfig: (BuildModelKey | string)[] = [
@ -204,7 +212,7 @@ export function createSystemType (
}
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.Document, card.icon.Document, card.string.Document, card.string.Documents)
@ -681,6 +689,12 @@ export function createModel (builder: Builder): void {
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, {
id: 'views',
label: card.string.Views,

View File

@ -15,11 +15,12 @@
//
import activity from '@hcengineering/activity'
import card, { type Role, type Card } from '@hcengineering/card'
import {
AvatarType,
type UserRole,
contactId,
type AvatarProvider,
type SocialIdentity,
type Channel,
type ChannelProvider,
type Contact,
@ -29,26 +30,27 @@ import {
type Member,
type Organization,
type Person,
type Status,
type PersonSpace
type PersonSpace,
type SocialIdentity,
type Status
} from '@hcengineering/contact'
import {
AccountRole,
ClassifierKind,
DOMAIN_MODEL,
DateRangeMode,
IndexKind,
type Collection,
type AccountUuid,
type Blob,
type Class,
type MarkupBlobRef,
type Collection,
type Domain,
type Ref,
type Timestamp,
type SocialIdType,
type PersonUuid,
type MarkupBlobRef,
type PersonId,
type AccountUuid,
ClassifierKind
type PersonUuid,
type Ref,
type SocialIdType,
type Timestamp
} from '@hcengineering/core'
import {
Collection as CollectionType,
@ -84,7 +86,6 @@ import setting from '@hcengineering/setting'
import templates from '@hcengineering/templates'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import { type Action } from '@hcengineering/view'
import card, { type Card } from '@hcengineering/card'
import contact from './plugin'
export { contactId } from '@hcengineering/contact'
@ -92,6 +93,7 @@ export { contactOperation } from './migration'
export { contact as default }
export const DOMAIN_CONTACT = 'contact' as Domain
export const DOMAIN_ROLE = 'role' as Domain
export const DOMAIN_CHANNEL = 'channel' as Domain
@Model(contact.class.AvatarProvider, core.class.Doc, DOMAIN_MODEL)
@ -272,6 +274,12 @@ export class TPersonSpace extends TSpace implements PersonSpace {
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 {
builder.createDoc(
card.class.MasterTag,
@ -334,7 +342,8 @@ export function createModel (builder: Builder): void {
TStatus,
TMember,
TContactsTab,
TPersonSpace
TPersonSpace,
TUserRole
)
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
editor?: AnyComponent
allowMany?: boolean
type!: 'transform' | 'reduce' | 'context'
}
export * from './migration'
@ -239,7 +240,8 @@ export function createModel (builder: Builder): void {
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.UpperCase
label: process.string.UpperCase,
type: 'transform'
},
process.function.UpperCase
)
@ -250,7 +252,8 @@ export function createModel (builder: Builder): void {
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.LowerCase
label: process.string.LowerCase,
type: 'transform'
},
process.function.LowerCase
)
@ -261,7 +264,8 @@ export function createModel (builder: Builder): void {
{
of: core.class.TypeString,
category: 'attribute',
label: process.string.Trim
label: process.string.Trim,
type: 'transform'
},
process.function.Trim
)
@ -272,7 +276,8 @@ export function createModel (builder: Builder): void {
{
of: core.class.ArrOf,
category: undefined,
label: process.string.FirstValue
label: process.string.FirstValue,
type: 'reduce'
},
process.function.FirstValue
)
@ -282,6 +287,7 @@ export function createModel (builder: Builder): void {
core.space.Model,
{
of: core.class.ArrOf,
type: 'reduce',
category: undefined,
label: process.string.LastValue
},
@ -293,6 +299,7 @@ export function createModel (builder: Builder): void {
core.space.Model,
{
of: core.class.ArrOf,
type: 'reduce',
category: undefined,
label: process.string.Random
},
@ -304,6 +311,7 @@ export function createModel (builder: Builder): void {
core.space.Model,
{
of: core.class.TypeNumber,
type: 'transform',
category: 'attribute',
label: process.string.Add,
allowMany: true,
@ -320,7 +328,8 @@ export function createModel (builder: Builder): void {
category: 'attribute',
label: process.string.Subtract,
allowMany: true,
editor: process.component.NumberOffsetEditor
editor: process.component.NumberOffsetEditor,
type: 'transform'
},
process.function.Subtract
)
@ -332,7 +341,8 @@ export function createModel (builder: Builder): void {
of: core.class.TypeDate,
category: 'attribute',
label: process.string.Offset,
editor: process.component.DateOffsetEditor
editor: process.component.DateOffsetEditor,
type: 'transform'
},
process.function.Offset
)
@ -343,11 +353,25 @@ export function createModel (builder: Builder): void {
{
of: core.class.TypeDate,
category: 'attribute',
label: process.string.FirstWorkingDayAfter
label: process.string.FirstWorkingDayAfter,
type: 'transform'
},
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, {
presenter: process.component.ProcessPresenter
})

View File

@ -95,6 +95,10 @@ export function createModel (builder: Builder): void {
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, {
trigger: serverProcess.trigger.OnExecutionContinue,
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 SpacePresenter from './components/navigator/SpacePresenter.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'
@ -76,7 +78,9 @@ export default async (): Promise<Resources> => ({
CardArrayEditor,
NewCardHeader,
SpacePresenter,
LabelsPresenter
LabelsPresenter,
RolesSection,
EditRole
},
completion: {
CardQuery: queryCard

View File

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

View File

@ -11,7 +11,19 @@
// See the License for the specific language governing permissions and
// 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 type { AnyComponent, ComponentExtensionId } from '@hcengineering/ui'
@ -20,10 +32,16 @@ export * from './analytics'
export interface MasterTag extends Class<Card> {
color?: number
removed?: boolean
roles?: CollectionSize<Role>
}
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 {
_class: Ref<MasterTag>
title: string
@ -69,7 +87,8 @@ const cardPlugin = plugin(cardId, {
MasterTag: '' as Ref<Class<MasterTag>>,
Tag: '' as Ref<Class<Tag>>,
MasterTagEditorSection: '' as Ref<Class<MasterTagEditorSection>>,
CardSpace: '' as Ref<Class<CardSpace>>
CardSpace: '' as Ref<Class<CardSpace>>,
Role: '' as Ref<Class<Role>>
},
space: {
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 { Action, FilterMode, Viewlet } from '@hcengineering/view'
import type { Readable } from 'svelte/store'
import { Card, MasterTag } from '@hcengineering/card'
import { Card, MasterTag, Role } from '@hcengineering/card'
import { PermissionsStore } from './types'
/**
@ -144,6 +144,11 @@ export interface Person extends Contact, BasePerson {
profile?: Ref<Card>
}
export interface UserRole extends Doc {
user: Ref<Employee>
role: Ref<Role>
}
/**
* @public
*/
@ -213,7 +218,8 @@ export const contactPlugin = plugin(contactId, {
ContactsTab: '' as Ref<Class<ContactsTab>>,
PersonSpace: '' as Ref<Class<PersonSpace>>,
SocialIdentity: '' as Ref<Class<SocialIdentity>>,
UserProfile: '' as Ref<MasterTag>
UserProfile: '' as Ref<MasterTag>,
UserRole: '' as Ref<Class<UserRole>>
},
mixin: {
Employee: '' as Ref<Class<Employee>>

View File

@ -65,6 +65,7 @@
"ObjectNotFound": "Objekt nenalezen: {_id}",
"EmptyRelatedObjectValue": "Prázdná hodnota {attr} souvisejícího objektu: {parent}",
"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}",
"EmptyRelatedObjectValue": "Leerer verwandtes Objekt {parent} Wert: {attr}",
"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}",
"EmptyRelatedObjectValue": "Empty related object {parent} value: {attr}",
"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}",
"EmptyRelatedObjectValue": "Valor de objeto relacionado vacío: {parent} {attr}",
"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}",
"EmptyRelatedObjectValue": "Valeur d'objet lié vide : {parent} {attr}",
"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}",
"EmptyRelatedObjectValue": "Valore oggetto correlato vuoto: {parent} {attr}",
"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}",
"ObjectNotFound": "オブジェクトが見つかりません: {_id}",
"EmptyRelatedObjectValue": "関連オブジェクト {parent} の値が空です: {attr}",
"InternalServerError": "内部サーバーエラー。サポートにお問い合わせください。エラーID: {errorId}"
"InternalServerError": "内部サーバーエラー。サポートにお問い合わせください。エラーID: {errorId}",
"EmptyFunctionResult": "関数結果が空です {func}"
}
}

View File

@ -65,6 +65,7 @@
"ObjectNotFound": "Objeto não encontrado: {_id}",
"EmptyRelatedObjectValue": "Valor de objeto relacionado vazio: {parent} {attr}",
"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}",
"ObjectNotFound": "Объект не найден: {_id}",
"AttributeNotExists": "Атрибут не существует: {key}",
"ResultNotProvided": "Результат не предоставлен"
"ResultNotProvided": "Результат не предоставлен",
"EmptyFunctionResult": "Отсутствуют результат функции {func}"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@
import AttrContextPresenter from './AttrContextPresenter.svelte'
import NestedContextPresenter from './NestedContextPresenter.svelte'
import RelContextPresenter from './RelContextPresenter.svelte'
import FunctionContextPresenter from './FunctionContextPresenter.svelte'
export let contextValue: SelectedContext
export let context: Context
@ -33,6 +34,8 @@
<NestedContextPresenter {contextValue} {context} />
{:else if contextValue.type === 'userRequest'}
<Label label={plugin.string.RequestFromUser} />
{:else if contextValue.type === 'function'}
<FunctionContextPresenter {contextValue} {context} />
{/if}
</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.
-->
<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 presentation, { Card, getClient } from '@hcengineering/presentation'
import { Context, ProcessFunction } from '@hcengineering/process'
@ -23,6 +24,7 @@
import ProcessAttribute from '../ProcessAttribute.svelte'
export let func: ProcessFunction
export let masterTag: Ref<MasterTag | Tag>
export let context: Context
export let attribute: AnyAttribute
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}>
<ProcessAttribute
{masterTag}
{context}
{attribute}
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 UpdateCardEditor from './components/UpdateCardEditor.svelte'
import ResultInput from './components/contextEditors/ResultInput.svelte'
import RoleEditor from './components/contextEditors/RoleEditor.svelte'
import { continueExecution, showDoneQuery } from './utils'
import { ProcessMiddleware } from './middleware'
@ -66,7 +67,8 @@ export default async (): Promise<Resources> => ({
NumberOffsetEditor,
ErrorPresenter,
RequestUserInput,
ResultInput
ResultInput,
RoleEditor
},
function: {
ShowDoneQuery: showDoneQuery,

View File

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

View File

@ -66,6 +66,8 @@ export function getContext (
if (attr !== undefined && category === 'object') {
attributes = attributes.filter((it) => it._id !== attr)
}
const functions = getContextFunctions(client, process.masterTag, target, category)
const nested: Record<string, NestedContext> = {}
const relations: Record<string, RelatedContext> = {}
@ -119,12 +121,55 @@ export function getContext (
}
return {
functions,
attributes,
nested,
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 (
client: Client,
_class: Ref<Class<Doc>>,
@ -192,6 +237,15 @@ export function getValueReduceFunc (source: AnyAttribute, target: AnyAttribute):
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> {
if (value === false) {
return { ...query, done: false }

View File

@ -91,6 +91,7 @@ export interface Method<T extends Doc> extends Doc {
}
export interface ProcessFunction extends Doc {
type: 'transform' | 'reduce' | 'context'
of: Ref<Class<Doc>>
editor?: AnyComponent
category: AttributeCategory | undefined
@ -134,7 +135,8 @@ export default plugin(processId, {
ObjectNotFound: '' as IntlString,
AttributeNotExists: '' as IntlString,
UserRequestedValueNotProvided: '' as IntlString,
ResultNotProvided: '' as IntlString
ResultNotProvided: '' as IntlString,
EmptyFunctionResult: '' as IntlString
},
icon: {
Process: '' as Asset,
@ -153,6 +155,7 @@ export default plugin(processId, {
Add: '' as Ref<ProcessFunction>,
Subtract: '' 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 '.'
export interface Context {
functions: Ref<ProcessFunction>[]
attributes: AnyAttribute[]
nested: Record<string, NestedContext>
relations: Record<string, RelatedContext>
@ -25,7 +26,7 @@ export interface Func {
}
interface BaseSelectedContext {
type: 'attribute' | 'relation' | 'nested' | 'userRequest'
type: 'attribute' | 'relation' | 'nested' | 'userRequest' | 'function'
// attribute key
key: string
@ -60,4 +61,15 @@ export interface SelectedUserRequest extends BaseSelectedContext {
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": {
"@hcengineering/time": "^0.6.0",
"@hcengineering/core": "^0.6.32",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/card": "^0.6.0",
"@hcengineering/server-process": "^0.6.0",
"@hcengineering/server-core": "^0.6.1",

View File

@ -14,6 +14,7 @@
//
import card, { Card } from '@hcengineering/card'
import contact, { Employee } from '@hcengineering/contact'
import core, {
ArrOf,
Doc,
@ -40,6 +41,7 @@ import process, {
ProcessError,
ProcessToDo,
SelectedContext,
SelectedContextFunc,
SelectedNested,
SelectedRelation,
SelectedUserRequest,
@ -342,6 +344,8 @@ async function getContextValue (value: any, control: TriggerControl, execution:
value = await getNestedValue(control, execution, context)
} else if (context.type === 'userRequest') {
value = getUserRequestValue(control, execution, context)
} else if (context.type === 'function') {
value = await getFunctionValue(control, execution, context)
}
return await fillValue(value, context, control, execution)
} 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 {
const userContext = execution.context?.[context.id]
if (userContext !== undefined) return userContext
@ -682,6 +719,18 @@ export function Trim (value: string): string {
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 (
value: number,
props: Record<string, any>,
@ -787,7 +836,8 @@ export default async () => ({
Add,
Subtract,
Offset,
FirstWorkingDayAfter
FirstWorkingDayAfter,
RoleContext
},
trigger: {
OnExecutionCreate,

View File

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