Add auto-start functionality and related triggers for processes (#8478)

This commit is contained in:
Denis Bykhov 2025-04-07 09:06:58 +05:00 committed by GitHub
parent d2bdf88249
commit 507078d7ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 116 additions and 6 deletions

View File

@ -11,7 +11,7 @@
// 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 card, { type Card, type MasterTag } from '@hcengineering/card' import card, { type Tag, type Card, type MasterTag } from '@hcengineering/card'
import contact, { type Employee } from '@hcengineering/contact' import contact, { type Employee } from '@hcengineering/contact'
import core, { import core, {
AccountRole, AccountRole,
@ -68,11 +68,14 @@ export class TProcess extends TDoc implements Process {
@Prop(TypeString(), core.string.Description) @Prop(TypeString(), core.string.Description)
description!: string description!: string
@Prop(TypeRef(card.class.MasterTag), core.string.Name) @Prop(TypeRef(card.class.MasterTag), card.string.MasterTag)
masterTag!: Ref<MasterTag> masterTag!: Ref<MasterTag | Tag>
@Prop(ArrOf(TypeRef(process.class.State)), process.string.States) @Prop(ArrOf(TypeRef(process.class.State)), process.string.States)
states!: Ref<State>[] states!: Ref<State>[]
@Prop(TypeBoolean(), process.string.StartAutomatically)
autoStart: boolean | undefined
} }
@Model(process.class.Execution, core.class.Doc, DOMAIN_PROCESS) @Model(process.class.Execution, core.class.Doc, DOMAIN_PROCESS)

View File

@ -33,6 +33,7 @@
"@hcengineering/model": "^0.6.11", "@hcengineering/model": "^0.6.11",
"@hcengineering/model-process": "^0.6.0", "@hcengineering/model-process": "^0.6.0",
"@hcengineering/process": "^0.6.0", "@hcengineering/process": "^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

@ -11,6 +11,7 @@
// 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 card from '@hcengineering/card'
import core, { type Doc } from '@hcengineering/core' import core, { type Doc } from '@hcengineering/core'
import { Mixin, type Builder } from '@hcengineering/model' import { Mixin, type Builder } from '@hcengineering/model'
import { TMethod, TProcessFunction } from '@hcengineering/model-process' import { TMethod, TProcessFunction } from '@hcengineering/model-process'
@ -19,8 +20,8 @@ import process from '@hcengineering/process'
import serverCore from '@hcengineering/server-core' import serverCore from '@hcengineering/server-core'
import serverProcess, { import serverProcess, {
type ExecuteFunc, type ExecuteFunc,
type MethodImpl,
type FuncImpl, type FuncImpl,
type MethodImpl,
type TransformFunc type TransformFunc
} from '@hcengineering/server-process' } from '@hcengineering/server-process'
@ -110,6 +111,24 @@ export function createModel (builder: Builder): void {
isAsync: true isAsync: true
}) })
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverProcess.trigger.OnCardCreate,
txMatch: {
_class: core.class.TxCreateDoc,
objectClass: card.class.Card
},
isAsync: true
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverProcess.trigger.OnTagAdd,
txMatch: {
_class: core.class.TxMixin,
objectClass: card.class.Card
},
isAsync: true
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverProcess.trigger.OnExecutionCreate, trigger: serverProcess.trigger.OnExecutionCreate,
txMatch: { txMatch: {

View File

@ -43,6 +43,7 @@
"FirstWorkingDayAfter": "První pracovní den po", "FirstWorkingDayAfter": "První pracovní den po",
"FallbackValueError": "Zobrazit chybu, pokud není k dispozici", "FallbackValueError": "Zobrazit chybu, pokud není k dispozici",
"Required": "Povinné", "Required": "Povinné",
"StartAutomatically": "Spustit automaticky",
"Error": "Chyba", "Error": "Chyba",
"Continue": "Pokračovat" "Continue": "Pokračovat"
}, },

View File

@ -43,6 +43,7 @@
"FirstWorkingDayAfter": "Erste Arbeitstag nach", "FirstWorkingDayAfter": "Erste Arbeitstag nach",
"FallbackValueError": "Fehler anzeigen, wenn leer", "FallbackValueError": "Fehler anzeigen, wenn leer",
"Required": "Erforderlich", "Required": "Erforderlich",
"StartAutomatically": "Automatisch starten",
"Error": "Fehler", "Error": "Fehler",
"Continue": "Fortsetzen" "Continue": "Fortsetzen"
}, },

View File

@ -43,6 +43,7 @@
"FirstWorkingDayAfter": "First working day after", "FirstWorkingDayAfter": "First working day after",
"FallbackValueError": "Show error if empty", "FallbackValueError": "Show error if empty",
"Required": "Required", "Required": "Required",
"StartAutomatically": "Start automatically",
"Error": "Error", "Error": "Error",
"Continue": "Continue" "Continue": "Continue"
}, },

View File

@ -43,6 +43,7 @@
"FirstWorkingDayAfter": "Primer día de trabajo después", "FirstWorkingDayAfter": "Primer día de trabajo después",
"FallbackValueError": "Mostrar error si está vacío", "FallbackValueError": "Mostrar error si está vacío",
"Required": "Requerido", "Required": "Requerido",
"StartAutomatically": "Iniciar automáticamente",
"Error": "Error", "Error": "Error",
"Continue": "Continuar" "Continue": "Continuar"
}, },

View File

@ -43,6 +43,7 @@
"FirstWorkingDayAfter": "Premier jour de travail après", "FirstWorkingDayAfter": "Premier jour de travail après",
"FallbackValueError": "Afficher l'erreur si vide", "FallbackValueError": "Afficher l'erreur si vide",
"Required": "Requis", "Required": "Requis",
"StartAutomatically": "Démarrer automatiquement",
"Error": "Erreur", "Error": "Erreur",
"Continue": "Continuer" "Continue": "Continuer"
}, },

View File

@ -43,6 +43,7 @@
"FirstWorkingDayAfter": "Primo giorno lavorativo dopo", "FirstWorkingDayAfter": "Primo giorno lavorativo dopo",
"FallbackValueError": "Mostra errore se vuoto", "FallbackValueError": "Mostra errore se vuoto",
"Required": "Obbligatorio", "Required": "Obbligatorio",
"StartAutomatically": "Avvia automaticamente",
"Error": "Errore", "Error": "Errore",
"Continue": "Continua" "Continue": "Continua"
}, },

View File

@ -43,6 +43,7 @@
"FirstWorkingDayAfter": "Primeiro Dia de Trabalho Após", "FirstWorkingDayAfter": "Primeiro Dia de Trabalho Após",
"FallbackValueError": "Mostrar erro se vazio", "FallbackValueError": "Mostrar erro se vazio",
"Required": "Obrigatório", "Required": "Obrigatório",
"StartAutomatically": "Iniciar automaticamente",
"Error": "Erro", "Error": "Erro",
"Continue": "Continuar" "Continue": "Continuar"
}, },

View File

@ -43,6 +43,7 @@
"FirstWorkingDayAfter": "Первый рабочий день после", "FirstWorkingDayAfter": "Первый рабочий день после",
"FallbackValueError": "Показывать ошибку, если недоступно", "FallbackValueError": "Показывать ошибку, если недоступно",
"Required": "Обязательно", "Required": "Обязательно",
"StartAutomatically": "Запускать автоматически",
"Error": "Ошибка", "Error": "Ошибка",
"Continue": "Продолжить" "Continue": "Продолжить"
}, },

View File

@ -43,6 +43,7 @@
"FirstWorkingDayAfter": "在之后的第一个工作日", "FirstWorkingDayAfter": "在之后的第一个工作日",
"FallbackValueError": "显示错误,如果为空", "FallbackValueError": "显示错误,如果为空",
"Required": "必需", "Required": "必需",
"StartAutomatically": "自动启动",
"Error": "错误", "Error": "错误",
"Continue": "继续" "Continue": "继续"
}, },

View File

@ -29,6 +29,7 @@
IconDescription, IconDescription,
navigate, navigate,
NavItem, NavItem,
ToggleWithLabel,
Scroller, Scroller,
secondNavSeparators, secondNavSeparators,
Separator, Separator,
@ -77,6 +78,12 @@
} }
} }
async function saveAutoStart (e: CustomEvent<boolean>): Promise<void> {
if (value !== undefined) {
await client.update(value, { autoStart: e.detail })
}
}
async function addState (): Promise<void> { async function addState (): Promise<void> {
if (value === undefined) return if (value === undefined) return
const prevState = states[states.length - 1] const prevState = states[states.length - 1]
@ -160,6 +167,13 @@
<EditBox bind:value={value.name} on:change={saveName} placeholder={process.string.Untitled} /> <EditBox bind:value={value.name} on:change={saveName} placeholder={process.string.Untitled} />
<ButtonIcon icon={IconDelete} size="small" kind="secondary" on:click={handleDelete} /> <ButtonIcon icon={IconDelete} size="small" kind="secondary" on:click={handleDelete} />
</div> </div>
<div>
<ToggleWithLabel
on={value.autoStart ?? false}
on:change={saveAutoStart}
label={process.string.StartAutomatically}
/>
</div>
<div class="hulyComponent-content flex-col-center"> <div class="hulyComponent-content flex-col-center">
<div class="flex-col-center"> <div class="flex-col-center">
{#each sortedStates as state (state._id)} {#each sortedStates as state (state._id)}

View File

@ -89,6 +89,7 @@ export default mergeIds(processId, process, {
FirstWorkingDayAfter: '' as IntlString, FirstWorkingDayAfter: '' as IntlString,
FallbackValueError: '' as IntlString, FallbackValueError: '' as IntlString,
Required: '' as IntlString, Required: '' as IntlString,
StartAutomatically: '' as IntlString,
Continue: '' as IntlString Continue: '' as IntlString
} }
}) })

View File

@ -11,7 +11,7 @@
// 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 { Card, MasterTag } from '@hcengineering/card' import { Card, MasterTag, Tag } from '@hcengineering/card'
import { Employee } from '@hcengineering/contact' import { Employee } from '@hcengineering/contact'
import { Class, Doc, DocumentUpdate, ObjQueryType, Ref, Tx } from '@hcengineering/core' import { Class, Doc, DocumentUpdate, ObjQueryType, Ref, Tx } from '@hcengineering/core'
import { Asset, IntlString, Plugin, plugin } from '@hcengineering/platform' import { Asset, IntlString, Plugin, plugin } from '@hcengineering/platform'
@ -25,10 +25,11 @@ import { AttributeCategory } from '@hcengineering/view'
export const processId = 'process' as Plugin export const processId = 'process' as Plugin
export interface Process extends Doc { export interface Process extends Doc {
masterTag: Ref<MasterTag> masterTag: Ref<MasterTag | Tag>
name: string name: string
description: string description: string
states: Ref<State>[] states: Ref<State>[]
autoStart?: boolean
} }
export interface Execution extends Doc { export interface Execution extends Doc {

View File

@ -25,6 +25,7 @@ import core, {
Timestamp, Timestamp,
Tx, Tx,
TxCreateDoc, TxCreateDoc,
TxMixin,
TxProcessor, TxProcessor,
TxRemoveDoc, TxRemoveDoc,
TxUpdateDoc TxUpdateDoc
@ -635,6 +636,63 @@ export function FirstWorkingDayAfter (val: Timestamp): Timestamp {
return val return val
} }
export async function OnCardCreate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of txes) {
if (tx._class !== core.class.TxCreateDoc) continue
const createTx = tx as TxCreateDoc<Card>
if (!control.hierarchy.isDerived(createTx.objectClass, card.class.Card)) continue
const ancestors = control.hierarchy
.getAncestors(createTx.objectClass)
.filter((p) => control.hierarchy.isDerived(p, card.class.Card))
const processes = control.modelDb.findAllSync(process.class.Process, {
masterTag: { $in: ancestors },
autoStart: true
})
for (const proc of processes) {
res.push(
control.txFactory.createTxCreateDoc(process.class.Execution, core.space.Workspace, {
process: proc._id,
currentState: null,
card: createTx.objectId,
done: false,
rollback: {},
currentToDo: null,
assignee: null
})
)
}
}
return res
}
export async function OnTagAdd (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of txes) {
if (tx._class !== core.class.TxMixin) continue
const mixinTx = tx as TxMixin<Card, Card>
if (!control.hierarchy.isDerived(mixinTx.objectClass, card.class.Card)) continue
if (Object.keys(mixinTx.attributes).length !== 0) continue
const processes = control.modelDb.findAllSync(process.class.Process, { masterTag: mixinTx.mixin, autoStart: true })
for (const proc of processes) {
res.push(
control.txFactory.createTxCreateDoc(process.class.Execution, core.space.Workspace, {
process: proc._id,
currentState: null,
card: mixinTx.objectId,
done: false,
rollback: {},
currentToDo: null,
assignee: null
})
)
}
}
return res
}
export async function OnExecutionContinue (txes: Tx[], control: TriggerControl): Promise<Tx[]> { export async function OnExecutionContinue (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = [] const res: Tx[] = []
for (const tx of txes) { for (const tx of txes) {
@ -679,6 +737,8 @@ export default async () => ({
FirstWorkingDayAfter FirstWorkingDayAfter
}, },
trigger: { trigger: {
OnCardCreate,
OnTagAdd,
OnExecutionCreate, OnExecutionCreate,
OnStateRemove, OnStateRemove,
OnProcessRemove, OnProcessRemove,

View File

@ -46,6 +46,8 @@ export default plugin(serverProcessId, {
FirstWorkingDayAfter: '' as Resource<TransformFunc> FirstWorkingDayAfter: '' as Resource<TransformFunc>
}, },
trigger: { trigger: {
OnCardCreate: '' as Resource<TriggerFunc>,
OnTagAdd: '' as Resource<TriggerFunc>,
OnExecutionCreate: '' as Resource<TriggerFunc>, OnExecutionCreate: '' as Resource<TriggerFunc>,
OnStateRemove: '' as Resource<TriggerFunc>, OnStateRemove: '' as Resource<TriggerFunc>,
OnProcessRemove: '' as Resource<TriggerFunc>, OnProcessRemove: '' as Resource<TriggerFunc>,