WhenFieldChanges trigger (#9913)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2025-09-23 00:01:24 +07:00
committed by GitHub
parent 9ae9453a52
commit e089e201f1
28 changed files with 447 additions and 67 deletions
@@ -0,0 +1,98 @@
<!--
// 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 } from '@hcengineering/card'
import core, { AnyAttribute, Class, Ref } from '@hcengineering/core'
import { getAttributePresenterClass, getClient } from '@hcengineering/presentation'
import { Process } from '@hcengineering/process'
import { Button, eventToHTMLElement, SelectPopup, showPopup } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import { getCirteriaEditor } from '../../utils'
import CriteriasEditor from '../criterias/CriteriasEditor.svelte'
export let readonly: boolean
export let process: Process
export let params: Record<string, any>
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
function change (e: CustomEvent<any>): void {
if (readonly || e.detail == null) return
params = e.detail
dispatch('change', { params })
}
function getKeys (_class: Ref<Class<MasterTag>>): AnyAttribute[] {
const ignoreKeys = ['_class', 'content', 'parent', 'attachments', 'todos']
const attributes = hierarchy.getAllAttributes(_class, core.class.Doc)
const res: AnyAttribute[] = []
for (const [key, attr] of attributes) {
if (attr.hidden === true) continue
if (ignoreKeys.includes(key)) continue
const presenterClass = getAttributePresenterClass(hierarchy, attr.type)
const updateCriteria = getCirteriaEditor(presenterClass.attrClass, presenterClass.category)
const editor = updateCriteria?.editor
if (editor == null) continue
res.push(attr)
}
return res
}
let key: string | undefined = Object.keys(params)[0]
const allAttrs = getKeys(process.masterTag)
function onSelect (e: MouseEvent): void {
const possibleAttrs = allAttrs.filter((attr) => key !== attr.name)
showPopup(
SelectPopup,
{
value: possibleAttrs.map((p) => {
return { id: p.name, label: p.label }
})
},
eventToHTMLElement(e),
(res) => {
if (res != null) {
key = res
}
}
)
}
function remove (e: CustomEvent<any>): void {
if (e.detail !== undefined) {
if (key !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (params as any)[key]
}
key = undefined
dispatch('change', { params })
}
}
</script>
{#if key !== undefined}
<CriteriasEditor keys={[key]} {readonly} {process} {params} on:remove={remove} on:change={change} />
{:else}
<div class="flex-center mt-4">
<Button label={view.string.Select} width={'100%'} kind={'link-bordered'} size={'large'} on:click={onSelect} />
</div>
{/if}
+8 -4
View File
@@ -77,9 +77,11 @@ import {
showDoneQuery,
timeTransitionCheck,
todoTranstionCheck,
updateCardTranstionCheck,
subProcessesDoneCheck
matchCardCheck,
subProcessesDoneCheck,
fieldChangesCheck
} from './utils'
import FieldChangesEditor from './components/settings/FieldChangesEditor.svelte'
export * from './query'
@@ -132,7 +134,8 @@ export default async (): Promise<Resources> => ({
TimePresenter,
AddTagEditor,
AddTagPresenter,
ExecutionMyToDos
ExecutionMyToDos,
FieldChangesEditor
},
criteriaEditor: {
BaseCriteria,
@@ -154,7 +157,8 @@ export default async (): Promise<Resources> => ({
CutEditor
},
triggerCheck: {
UpdateCheck: updateCardTranstionCheck,
MatchCheck: matchCardCheck,
FieldChangedCheck: fieldChangesCheck,
SubProcessesDoneCheck: subProcessesDoneCheck,
ToDo: todoTranstionCheck,
Time: timeTransitionCheck
+23 -11
View File
@@ -21,10 +21,11 @@ import core, {
type TxApplyIf,
type TxResult,
type TxUpdateDoc,
TxProcessor
TxProcessor,
SortingOrder
} from '@hcengineering/core'
import { BasePresentationMiddleware, type PresentationMiddleware } from '@hcengineering/presentation'
import process, { ExecutionStatus, type ProcessToDo } from '@hcengineering/process'
import process, { ExecutionStatus, type ProcessToDo, isUpdateTx } from '@hcengineering/process'
import { createExecution, getNextStateUserInput, requestResult, pickTransition } from './utils'
import cardPlugin, { type Card } from '@hcengineering/card'
@@ -68,8 +69,8 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre
}
private async handleCardUpdate (etx: Tx): Promise<void> {
if (etx._class === core.class.TxUpdateDoc) {
const updateTx = etx as TxUpdateDoc<Card>
if (etx._class === core.class.TxUpdateDoc || etx._class === core.class.TxMixin) {
const updateTx = etx as TxUpdateDoc<Card> | TxMixin<Card, Card>
const hierarchy = this.client.getHierarchy()
if (!hierarchy.isDerived(updateTx.objectClass, cardPlugin.class.Card)) return
const executions = await this.client.findAll(process.class.Execution, {
@@ -81,14 +82,23 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre
_id: updateTx.objectId
})
if (card === undefined) return
const updated = TxProcessor.updateDoc2Doc(hierarchy.clone(card), updateTx)
const updated = isUpdateTx(updateTx)
? TxProcessor.updateDoc2Doc<Card>(hierarchy.clone(card), updateTx)
: TxProcessor.updateMixin4Doc<Card, Card>(hierarchy.clone(card), updateTx)
for (const execution of executions) {
const transitions = this.client.getModel().findAllSync(process.class.Transition, {
process: execution.process,
from: execution.currentState,
trigger: process.trigger.OnCardUpdate
const transitions = this.client.getModel().findAllSync(
process.class.Transition,
{
process: execution.process,
from: execution.currentState,
trigger: { $in: [process.trigger.OnCardUpdate, process.trigger.WhenFieldChanges] }
},
{ sort: { rank: SortingOrder.Ascending } }
)
const transition = await pickTransition(this.client, execution, transitions, {
card: updated,
operations: isUpdateTx(updateTx) ? updateTx.operations : updateTx.attributes
})
const transition = await pickTransition(this.client, execution, transitions, updated)
if (transition === undefined) return
const context = await getNextStateUserInput(execution, transition, execution.context)
const txop = new TxOperations(this.client, getCurrentAccount().primarySocialId)
@@ -153,7 +163,9 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre
from: execution.currentState,
trigger: process.trigger.OnToDoClose
})
const transition = await pickTransition(this.client, execution, transitions, todo)
const transition = await pickTransition(this.client, execution, transitions, {
todo
})
if (transition === undefined) return
const context = await getNextStateUserInput(execution, transition, execution.context)
if (context !== undefined) {
+3 -1
View File
@@ -65,6 +65,7 @@ export default mergeIds(processId, process, {
AddRelationPresenter: '' as AnyComponent,
CardUpdateEditor: '' as AnyComponent,
CardUpdatePresenter: '' as AnyComponent,
FieldChangesEditor: '' as AnyComponent,
ToDoSettingPresenter: '' as AnyComponent,
TimeEditor: '' as AnyComponent,
TimePresenter: '' as AnyComponent,
@@ -161,7 +162,8 @@ export default mergeIds(processId, process, {
OnToDoDone: '' as IntlString,
OnSubProcessesDone: '' as IntlString,
WaitUntil: '' as IntlString,
OnCardUpdate: '' as IntlString,
WhenCardMatches: '' as IntlString,
WhenFieldChanges: '' as IntlString,
Result: '' as IntlString,
RequestResult: '' as IntlString,
NoResultRequired: '' as IntlString,
+22 -6
View File
@@ -113,7 +113,7 @@ export async function pickTransition (
client: Client,
execution: Execution,
transitions: Transition[],
doc: Doc
context: Record<string, any>
): Promise<Transition | undefined> {
for (const tr of transitions) {
const trigger = client.getModel().findObject(tr.trigger)
@@ -122,7 +122,7 @@ export async function pickTransition (
const filled = fillParams(tr.triggerParams, execution)
const checkFunc = await getResource(trigger.checkFunction)
if (checkFunc === undefined) continue
const res = await checkFunc(client, execution, filled, doc)
const res = await checkFunc(client, execution, filled, context)
if (res) return tr
}
}
@@ -600,10 +600,10 @@ export function todoTranstionCheck (
client: Client,
execution: Execution,
params: Record<string, any>,
doc: Doc
context: Record<string, any>
): boolean {
if (params._id === undefined) return false
return doc._id === params._id
return context.card?._id === params._id
}
export function timeTransitionCheck (
@@ -616,12 +616,28 @@ export function timeTransitionCheck (
return params.value <= Date.now()
}
export function updateCardTranstionCheck (
export function matchCardCheck (
client: Client,
execution: Execution,
params: Record<string, any>,
doc: Doc
context: Record<string, any>
): boolean {
const doc = context.card
if (doc === undefined) return false
const res = matchQuery([doc], params, doc._class, client.getHierarchy(), true)
return res.length > 0
}
export function fieldChangesCheck (
client: Client,
execution: Execution,
params: Record<string, any>,
context: Record<string, any>
): boolean {
const doc = context.card
if (doc === undefined) return false
const param = Object.keys(params)[0]
if (!Object.keys(context.operations ?? {}).includes(param)) return false
const res = matchQuery([doc], params, doc._class, client.getHierarchy(), true)
return res.length > 0
}