diff --git a/models/process/src/index.ts b/models/process/src/index.ts index abc4d630a7..f9466bc5e8 100644 --- a/models/process/src/index.ts +++ b/models/process/src/index.ts @@ -30,6 +30,7 @@ import { Model, Prop, ReadOnly, + TypeAny, TypeBoolean, TypeRecord, TypeRef, @@ -43,6 +44,7 @@ import workbench from '@hcengineering/model-workbench' import { type IntlString } from '@hcengineering/platform' import { type Execution, + type ExecutionError, type Method, type Process, type ProcessFunction, @@ -102,6 +104,10 @@ export class TExecution extends TDoc implements Execution { @Prop(TypeRef(card.class.Card), card.string.Card) @ReadOnly() card!: Ref + + @Prop(TypeAny(process.component.ErrorPresenter, process.string.Error), process.string.Error) + @ReadOnly() + error?: ExecutionError[] | null } @Model(process.class.ProcessToDo, time.class.ToDo) @@ -170,6 +176,25 @@ export function createModel (builder: Builder): void { process.action.RunProcess ) + createAction( + builder, + { + action: process.actionImpl.ContinueExecution, + query: { + error: { $exists: true, $ne: null } + }, + label: process.string.Continue, + icon: process.icon.Process, + input: 'focus', + category: view.category.General, + target: process.class.Execution, + context: { + mode: ['context', 'browser'] + } + }, + process.action.ContinueExecution + ) + builder.createDoc( workbench.class.Application, core.space.Model, @@ -310,6 +335,9 @@ export function createModel (builder: Builder): void { variant: 'cardExecutions', attachTo: process.class.Execution, descriptor: view.viewlet.List, + props: { + baseMenuClass: process.class.Execution + }, viewOptions: { groupBy: ['process', 'assignee', 'done'], orderBy: [ @@ -369,6 +397,9 @@ export function createModel (builder: Builder): void { { attachTo: process.class.Execution, descriptor: view.viewlet.List, + props: { + baseMenuClass: process.class.Execution + }, viewOptions: { groupBy: ['process', 'assignee', 'done'], orderBy: [ diff --git a/models/process/src/plugin.ts b/models/process/src/plugin.ts index 98bd825f30..a932532279 100644 --- a/models/process/src/plugin.ts +++ b/models/process/src/plugin.ts @@ -15,7 +15,7 @@ import { type Doc, type Ref } from '@hcengineering/core' import { type IntlString, mergeIds } from '@hcengineering/platform' import { processId } from '@hcengineering/process' import process from '@hcengineering/process-resources/src/plugin' -import { type Action } from '@hcengineering/view' +import { type ViewAction, type Action } from '@hcengineering/view' export default mergeIds(processId, process, { app: { @@ -24,8 +24,12 @@ export default mergeIds(processId, process, { ids: { ProcessSettings: '' as Ref }, + actionImpl: { + ContinueExecution: '' as ViewAction + }, action: { - RunProcess: '' as Ref> + RunProcess: '' as Ref>, + ContinueExecution: '' as Ref> }, string: { Done: '' as IntlString, diff --git a/models/server-process/src/index.ts b/models/server-process/src/index.ts index 4c77613fdc..785a52bac1 100644 --- a/models/server-process/src/index.ts +++ b/models/server-process/src/index.ts @@ -91,6 +91,16 @@ export function createModel (builder: Builder): void { func: serverProcess.transform.FirstWorkingDayAfter }) + builder.createDoc(serverCore.class.Trigger, core.space.Model, { + trigger: serverProcess.trigger.OnExecutionContinue, + txMatch: { + _class: core.class.TxUpdateDoc, + objectClass: process.class.Execution, + 'operations.error': null + }, + isAsync: true + }) + builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverProcess.trigger.OnProcessRemove, txMatch: { diff --git a/plugins/process-assets/lang/cs.json b/plugins/process-assets/lang/cs.json index cc08cc4d85..02140dd469 100644 --- a/plugins/process-assets/lang/cs.json +++ b/plugins/process-assets/lang/cs.json @@ -42,6 +42,18 @@ "Offset": "Posunout", "FirstWorkingDayAfter": "První pracovní den po", "FallbackValueError": "Zobrazit chybu, pokud není k dispozici", - "Required": "Povinné" + "Required": "Povinné", + "Error": "Chyba", + "Continue": "Pokračovat" + }, + "error": { + "MethodNotFound": "Metoda nenalezena: {methodId}", + "AttributeNotExists": "Atribut neexistuje: {key}", + "RelatedObjectNotFound": "Související objekt nenalezen: {attr}", + "RelationNotExists": "Relace neexistuje: {association}", + "EmptyAttributeContextValue": "Prázdná hodnota atributu: {attr}", + "ObjectNotFound": "Objekt nenalezen: {_id}", + "EmptyRelatedObjectValue": "Prázdná hodnota {attr} souvisejícího objektu: {parent}", + "InternalServerError": "Vnitřní chyba serveru, kontaktujte podporu, chybové id: {errorId}" } } diff --git a/plugins/process-assets/lang/de.json b/plugins/process-assets/lang/de.json index d4c0d8d684..a6899a05f9 100644 --- a/plugins/process-assets/lang/de.json +++ b/plugins/process-assets/lang/de.json @@ -42,6 +42,18 @@ "Offset": "Verschieben", "FirstWorkingDayAfter": "Erste Arbeitstag nach", "FallbackValueError": "Fehler anzeigen, wenn leer", - "Required": "Erforderlich" + "Required": "Erforderlich", + "Error": "Fehler", + "Continue": "Fortsetzen" + }, + "error": { + "MethodNotFound": "Methode nicht gefunden: {methodId}", + "AttributeNotExists": "Attribut existiert nicht: {key}", + "RelatedObjectNotFound": "Verwandtes Objekt nicht gefunden: {attr}", + "RelationNotExists": "Beziehung existiert nicht: {association}", + "EmptyAttributeContextValue": "Leerer Attributwert: {attr}", + "ObjectNotFound": "Objekt nicht gefunden: {_id}", + "EmptyRelatedObjectValue": "Leerer verwandtes Objekt {parent} Wert: {attr}", + "InternalServerError": "Interner Serverfehler, bitte kontaktieren Sie den Support, Fehler-ID: {errorId}" } } diff --git a/plugins/process-assets/lang/en.json b/plugins/process-assets/lang/en.json index d57f6bc56e..719748e947 100644 --- a/plugins/process-assets/lang/en.json +++ b/plugins/process-assets/lang/en.json @@ -42,6 +42,18 @@ "Offset": "Offset", "FirstWorkingDayAfter": "First working day after", "FallbackValueError": "Show error if empty", - "Required": "Required" + "Required": "Required", + "Error": "Error", + "Continue": "Continue" + }, + "error": { + "MethodNotFound": "Method not found: {methodId}", + "AttributeNotExists": "Attribute not exists: {key}", + "RelatedObjectNotFound": "Related object not found: {attr}", + "RelationNotExists": "Relation not exists: {association}", + "EmptyAttributeContextValue": "Empty attribute value: {attr}", + "ObjectNotFound": "Object not found: {_id}", + "EmptyRelatedObjectValue": "Empty related object {parent} value: {attr}", + "InternalServerError": "Internal server error, contact support, error id: {errorId}" } } diff --git a/plugins/process-assets/lang/es.json b/plugins/process-assets/lang/es.json index 40b0bff66d..1b89e7c041 100644 --- a/plugins/process-assets/lang/es.json +++ b/plugins/process-assets/lang/es.json @@ -42,6 +42,18 @@ "Offset": "Desplazar", "FirstWorkingDayAfter": "Primer día de trabajo después", "FallbackValueError": "Mostrar error si está vacío", - "Required": "Requerido" + "Required": "Requerido", + "Error": "Error", + "Continue": "Continuar" + }, + "error": { + "MethodNotFound": "Método no encontrado: {methodId}", + "AttributeNotExists": "Atributo no existe: {key}", + "RelatedObjectNotFound": "Objeto relacionado no encontrado: {attr}", + "RelationNotExists": "Relación no existe: {association}", + "EmptyAttributeContextValue": "Valor de atributo vacío: {attr}", + "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}" } } diff --git a/plugins/process-assets/lang/fr.json b/plugins/process-assets/lang/fr.json index d404f68be9..bef7828326 100644 --- a/plugins/process-assets/lang/fr.json +++ b/plugins/process-assets/lang/fr.json @@ -42,6 +42,18 @@ "Offset": "Décalage", "FirstWorkingDayAfter": "Premier jour de travail après", "FallbackValueError": "Afficher l'erreur si vide", - "Required": "Requis" + "Required": "Requis", + "Error": "Erreur", + "Continue": "Continuer" + }, + "error": { + "MethodNotFound": "Méthode introuvable : {methodId}", + "AttributeNotExists": "Attribut inexistant : {key}", + "RelatedObjectNotFound": "Objet lié introuvable : {attr}", + "RelationNotExists": "Relation inexistante : {association}", + "EmptyAttributeContextValue": "Valeur d'attribut vide : {attr}", + "ObjectNotFound": "Objet introuvable : {_id}", + "EmptyRelatedObjectValue": "Valeur d'objet lié vide : {parent} {attr}", + "InternalServerError": "Erreur interne du serveur, contactez le support, id d'erreur : {errorId}" } } \ No newline at end of file diff --git a/plugins/process-assets/lang/it.json b/plugins/process-assets/lang/it.json index ae24db49b0..088e354630 100644 --- a/plugins/process-assets/lang/it.json +++ b/plugins/process-assets/lang/it.json @@ -42,6 +42,18 @@ "Offset": "Offset", "FirstWorkingDayAfter": "Primo giorno lavorativo dopo", "FallbackValueError": "Mostra errore se vuoto", - "Required": "Obbligatorio" + "Required": "Obbligatorio", + "Error": "Errore", + "Continue": "Continua" + }, + "error": { + "MethodNotFound": "Metodo non trovato: {methodId}", + "AttributeNotExists": "Attributo non esiste: {key}", + "RelatedObjectNotFound": "Oggetto correlato non trovato: {attr}", + "RelationNotExists": "Relazione non esiste: {association}", + "EmptyAttributeContextValue": "Valore attributo vuoto: {attr}", + "ObjectNotFound": "Oggetto non trovato: {_id}", + "EmptyRelatedObjectValue": "Valore oggetto correlato vuoto: {parent} {attr}", + "InternalServerError": "Errore interno del server, contatta il supporto, id errore: {errorId}" } } diff --git a/plugins/process-assets/lang/pt.json b/plugins/process-assets/lang/pt.json index dd5a3f5630..49660a3896 100644 --- a/plugins/process-assets/lang/pt.json +++ b/plugins/process-assets/lang/pt.json @@ -42,6 +42,18 @@ "Offset": "Deslocar", "FirstWorkingDayAfter": "Primeiro Dia de Trabalho Após", "FallbackValueError": "Mostrar erro se vazio", - "Required": "Obrigatório" + "Required": "Obrigatório", + "Error": "Erro", + "Continue": "Continuar" + }, + "error": { + "MethodNotFound": "Método não encontrado: {methodId}", + "AttributeNotExists": "Atributo não existe: {key}", + "RelatedObjectNotFound": "Objeto relacionado não encontrado: {attr}", + "RelationNotExists": "Relação não existe: {association}", + "EmptyAttributeContextValue": "Valor de atributo vazio: {attr}", + "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}" } } diff --git a/plugins/process-assets/lang/ru.json b/plugins/process-assets/lang/ru.json index 9694e509db..1f38ab60fe 100644 --- a/plugins/process-assets/lang/ru.json +++ b/plugins/process-assets/lang/ru.json @@ -1,4 +1,3 @@ - { "string": { "Method": "Метод", @@ -43,6 +42,18 @@ "Offset": "Смещение", "FirstWorkingDayAfter": "Первый рабочий день после", "FallbackValueError": "Показывать ошибку, если недоступно", - "Required": "Обязательно" + "Required": "Обязательно", + "Error": "Ошибка", + "Continue": "Продолжить" + }, + "error": { + "MethodNotFound": "Метод не найден: {methodId}", + "InternalServerError": "Внутренняя ошибка сервера, свяжитесь с поддержкой, идентификатор ошибки: {errorId}", + "EmptyRelatedObjectValue": "Пустое значение {attr} для связанного объекта: {parent}", + "RelatedObjectNotFound": "Связанный объект не найден: {attr}", + "RelationNotExists": "Связь не существует: {association}", + "EmptyAttributeContextValue": "Пустое значение атрибута: {attr}", + "ObjectNotFound": "Объект не найден: {_id}", + "AttributeNotExists": "Атрибут не существует: {key}" } } diff --git a/plugins/process-assets/lang/zh.json b/plugins/process-assets/lang/zh.json index 92c40264a3..b657f8e124 100644 --- a/plugins/process-assets/lang/zh.json +++ b/plugins/process-assets/lang/zh.json @@ -42,6 +42,18 @@ "Offset": "偏移", "FirstWorkingDayAfter": "在之后的第一个工作日", "FallbackValueError": "显示错误,如果为空", - "Required": "必需" + "Required": "必需", + "Error": "错误", + "Continue": "继续" + }, + "error": { + "MethodNotFound": "找不到方法:{methodId}", + "AttributeNotExists": "属性不存在:{key}", + "RelatedObjectNotFound": "找不到相关对象:{attr}", + "RelationNotExists": "关系不存在:{association}", + "EmptyAttributeContextValue": "空属性上下文值:{attr}", + "ObjectNotFound": "找不到对象:{_id}", + "EmptyRelatedObjectValue": "空相关对象值:{parent} {attr}", + "InternalServerError": "内部服务器错误,请联系支持,错误 ID:{errorId}" } } diff --git a/plugins/process-resources/src/components/ErrorPresenter.svelte b/plugins/process-resources/src/components/ErrorPresenter.svelte new file mode 100644 index 0000000000..6451d000ba --- /dev/null +++ b/plugins/process-resources/src/components/ErrorPresenter.svelte @@ -0,0 +1,27 @@ + + + +{#if value} +
+ +
+{/if} diff --git a/plugins/process-resources/src/components/ErrorTooltip.svelte b/plugins/process-resources/src/components/ErrorTooltip.svelte new file mode 100644 index 0000000000..48fe9cb343 --- /dev/null +++ b/plugins/process-resources/src/components/ErrorTooltip.svelte @@ -0,0 +1,42 @@ + + + +
+ {#await fillProps(value) then res} + {#each res as error} +
+ {/each} + {/await} +
diff --git a/plugins/process-resources/src/components/ExecutonPresenter.svelte b/plugins/process-resources/src/components/ExecutonPresenter.svelte index f3e2b31c1a..58857d6118 100644 --- a/plugins/process-resources/src/components/ExecutonPresenter.svelte +++ b/plugins/process-resources/src/components/ExecutonPresenter.svelte @@ -16,6 +16,7 @@ import { WithLookup } from '@hcengineering/core' import { getClient } from '@hcengineering/presentation' import { Execution } from '@hcengineering/process' + import ErrorPresenter from './ErrorPresenter.svelte' export let value: WithLookup @@ -25,7 +26,8 @@ {#if process} -
+
+ {process.name}
{/if} diff --git a/plugins/process-resources/src/components/ProcessesExtension.svelte b/plugins/process-resources/src/components/ProcessesExtension.svelte index 10e1c6dc14..7af8a13f00 100644 --- a/plugins/process-resources/src/components/ProcessesExtension.svelte +++ b/plugins/process-resources/src/components/ProcessesExtension.svelte @@ -122,6 +122,7 @@ bind:this={list} _class={process.class.Execution} {viewOptions} + baseMenuClass={process.class.Execution} viewOptionsConfig={viewlet.viewOptions?.other} config={preference?.config ?? viewlet.config} configurations={undefined} diff --git a/plugins/process-resources/src/components/contextEditors/DateOffsetEditor.svelte b/plugins/process-resources/src/components/contextEditors/DateOffsetEditor.svelte index a7e45b87a2..902b1ac769 100644 --- a/plugins/process-resources/src/components/contextEditors/DateOffsetEditor.svelte +++ b/plugins/process-resources/src/components/contextEditors/DateOffsetEditor.svelte @@ -55,7 +55,7 @@
- +
diff --git a/plugins/process-resources/src/index.ts b/plugins/process-resources/src/index.ts index d463f151c1..d9059a7b62 100644 --- a/plugins/process-resources/src/index.ts +++ b/plugins/process-resources/src/index.ts @@ -12,29 +12,33 @@ // limitations under the License. import { type Resources } from '@hcengineering/platform' -import ProcessEditor from './components/ProcessEditor.svelte' -import SubProcessEditor from './components/SubProcessEditor.svelte' -import UpdateCardEditor from './components/UpdateCardEditor.svelte' -import ProcessesSettingSection from './components/ProcessesSection.svelte' -import ToDoEditor from './components/ToDoEditor.svelte' -import RunProcessPopup from './components/RunProcessPopup.svelte' +import FunctionSelector from './components/attributeEditors/FunctionSelector.svelte' +import NestedContextSelector from './components/attributeEditors/NestedContextSelector.svelte' +import RelatedContextSelector from './components/attributeEditors/RelatedContextSelector.svelte' +import DateOffsetEditor from './components/contextEditors/DateOffsetEditor.svelte' +import NumberOffsetEditor from './components/contextEditors/NumberOffsetEditor.svelte' +import ExecutonPresenter from './components/ExecutonPresenter.svelte' +import ExecutonProgressPresenter from './components/ExecutonProgressPresenter.svelte' +import Main from './components/Main.svelte' import SubProcessPresenter from './components/presenters/SubProcessPresenter.svelte' import ToDoPresenter from './components/presenters/ToDoPresenter.svelte' import UpdateCardPresenter from './components/presenters/UpdateCardPresenter.svelte' +import ProcessEditor from './components/ProcessEditor.svelte' import ProcessesExtension from './components/ProcessesExtension.svelte' -import ExecutonPresenter from './components/ExecutonPresenter.svelte' -import ExecutonProgressPresenter from './components/ExecutonProgressPresenter.svelte' +import ProcessesSettingSection from './components/ProcessesSection.svelte' import ProcessPresenter from './components/ProcessPresenter.svelte' -import NestedContextSelector from './components/attributeEditors/NestedContextSelector.svelte' -import RelatedContextSelector from './components/attributeEditors/RelatedContextSelector.svelte' -import FunctionSelector from './components/attributeEditors/FunctionSelector.svelte' -import Main from './components/Main.svelte' import RunProcessCardPopup from './components/RunProcessCardPopup.svelte' -import { showDoneQuery } from './utils' -import DateOffsetEditor from './components/contextEditors/DateOffsetEditor.svelte' -import NumberOffsetEditor from './components/contextEditors/NumberOffsetEditor.svelte' +import RunProcessPopup from './components/RunProcessPopup.svelte' +import SubProcessEditor from './components/SubProcessEditor.svelte' +import ToDoEditor from './components/ToDoEditor.svelte' +import UpdateCardEditor from './components/UpdateCardEditor.svelte' +import ErrorPresenter from './components/ErrorPresenter.svelte' +import { continueExecution, showDoneQuery } from './utils' export default async (): Promise => ({ + actionImpl: { + ContinueExecution: continueExecution + }, component: { UpdateCardEditor, SubProcessEditor, @@ -55,7 +59,8 @@ export default async (): Promise => ({ Main, RunProcessCardPopup, DateOffsetEditor, - NumberOffsetEditor + NumberOffsetEditor, + ErrorPresenter }, function: { ShowDoneQuery: showDoneQuery diff --git a/plugins/process-resources/src/plugin.ts b/plugins/process-resources/src/plugin.ts index e22ac8a297..de09f252c4 100644 --- a/plugins/process-resources/src/plugin.ts +++ b/plugins/process-resources/src/plugin.ts @@ -42,7 +42,8 @@ export default mergeIds(processId, process, { FunctionSelector: '' as AnyComponent, RunProcessCardPopup: '' as AnyComponent, DateOffsetEditor: '' as AnyComponent, - NumberOffsetEditor: '' as AnyComponent + NumberOffsetEditor: '' as AnyComponent, + ErrorPresenter: '' as AnyComponent }, function: { ShowDoneQuery: '' as ViewQueryAction @@ -87,6 +88,7 @@ export default mergeIds(processId, process, { Value: '' as IntlString, FirstWorkingDayAfter: '' as IntlString, FallbackValueError: '' as IntlString, - Required: '' as IntlString + Required: '' as IntlString, + Continue: '' as IntlString } }) diff --git a/plugins/process-resources/src/utils.ts b/plugins/process-resources/src/utils.ts index 650ad81f35..d3d0c9c9ab 100644 --- a/plugins/process-resources/src/utils.ts +++ b/plugins/process-resources/src/utils.ts @@ -23,6 +23,7 @@ import core, { type Type, type DocumentQuery } from '@hcengineering/core' +import { getClient } from '@hcengineering/presentation' import process, { type Context, type NestedContext, @@ -30,7 +31,8 @@ import process, { type Method, type Process, type Step, - type ProcessFunction + type ProcessFunction, + type Execution } from '@hcengineering/process' import { type AttributeCategory } from '@hcengineering/view' @@ -186,3 +188,9 @@ export function showDoneQuery (value: any, query: DocumentQuery): DocumentQ } return query } + +export async function continueExecution (value: Execution): Promise { + if (value.error == null) return + const client = getClient() + await client.update(value, { error: null }) +} diff --git a/plugins/process/src/index.ts b/plugins/process/src/index.ts index 6fdc061d34..7dca55fd00 100644 --- a/plugins/process/src/index.ts +++ b/plugins/process/src/index.ts @@ -39,6 +39,13 @@ export interface Execution extends Doc { card: Ref done: boolean rollback: Record, Tx[]> + error?: ExecutionError[] | null +} + +export interface ExecutionError { + error: IntlString + props: Record + intlProps: Record } export interface ProcessToDo extends ToDo { @@ -101,7 +108,18 @@ export default plugin(processId, { Method: '' as IntlString, Execution: '' as IntlString, Process: '' as IntlString, - Step: '' as IntlString + Step: '' as IntlString, + Error: '' as IntlString + }, + error: { + MethodNotFound: '' as IntlString, + InternalServerError: '' as IntlString, + EmptyRelatedObjectValue: '' as IntlString, + RelatedObjectNotFound: '' as IntlString, + RelationNotExists: '' as IntlString, + EmptyAttributeContextValue: '' as IntlString, + ObjectNotFound: '' as IntlString, + AttributeNotExists: '' as IntlString }, icon: { Process: '' as Asset, diff --git a/plugins/view-resources/src/components/list/ListView.svelte b/plugins/view-resources/src/components/list/ListView.svelte index ebd437c797..decb32ea4c 100644 --- a/plugins/view-resources/src/components/list/ListView.svelte +++ b/plugins/view-resources/src/components/list/ListView.svelte @@ -39,6 +39,7 @@ export let viewOptions: ViewOptions export let viewOptionsConfig: ViewOptionModel[] | undefined = undefined export let props: Record = {} + export let baseMenuClass: Ref> | undefined = undefined let list: List let scroll: Scroller @@ -89,6 +90,7 @@ {options} {createItemDialog} {createItemDialogProps} + {baseMenuClass} {createItemLabel} {createItemEvent} {viewOptions} diff --git a/server-plugins/process-resources/src/errors.ts b/server-plugins/process-resources/src/errors.ts new file mode 100644 index 0000000000..a781b1ad38 --- /dev/null +++ b/server-plugins/process-resources/src/errors.ts @@ -0,0 +1,47 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { IntlString } from '@hcengineering/platform' +import { ExecutionError } from '@hcengineering/process' +import { ExecuteResult } from '@hcengineering/server-process' + +export function isError (value: ExecuteResult | any): value is ExecutionError { + return (value as ExecutionError).error !== undefined +} + +export class ProcessError extends Error { + constructor ( + public readonly error: string, + public readonly message: IntlString, + public readonly props: Record, + public readonly intlProps: Record, + public readonly shouldLog: boolean = false + ) { + super(error) + } +} + +export function processError ( + message: IntlString, + props: Record = {}, + intlProps: Record = {}, + shouldLog: boolean = false +): ProcessError { + return new ProcessError(message, message, props, intlProps, shouldLog) +} + +export function parseError (err: ProcessError): ExecutionError { + return { error: err.message, props: err.props, intlProps: err.intlProps } +} diff --git a/server-plugins/process-resources/src/index.ts b/server-plugins/process-resources/src/index.ts index 22fef959a4..9dea5c6a58 100644 --- a/server-plugins/process-resources/src/index.ts +++ b/server-plugins/process-resources/src/index.ts @@ -1,5 +1,5 @@ // -// Copyright © 2024 Hardcore Engineering Inc. +// 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 @@ -29,9 +29,10 @@ import core, { TxRemoveDoc, TxUpdateDoc } from '@hcengineering/core' -import { getResource } from '@hcengineering/platform' +import { getEmbeddedLabel, getResource } from '@hcengineering/platform' import process, { Execution, + ExecutionError, MethodParams, parseContext, Process, @@ -45,6 +46,7 @@ import process, { import { TriggerControl } from '@hcengineering/server-core' import serverProcess, { ExecuteResult } from '@hcengineering/server-process' import time, { ToDoPriority } from '@hcengineering/time' +import { isError, parseError, ProcessError, processError } from './errors' export async function OnStateRemove (txes: Tx[], control: TriggerControl): Promise { const res: Tx[] = [] @@ -147,19 +149,27 @@ async function executeAction ( action: Step, execution: Execution, control: TriggerControl -): Promise { +): Promise { try { const method = control.modelDb.findObject(action.methodId) - if (method === undefined) return undefined + if (method === undefined) throw processError(process.error.MethodNotFound, { methodId: action.methodId }, {}, true) const impl = control.hierarchy.as(method, serverProcess.mixin.MethodImpl) - if (impl === undefined) return undefined - const f = await getResource(impl.func) + if (impl === undefined) throw processError(process.error.MethodNotFound, { methodId: action.methodId }, {}, true) const params = await fillParams(action.params, execution, control) + const f = await getResource(impl.func) const res = await f(params, execution, control) return res } catch (err) { - control.ctx.error(err instanceof Error ? err.message : String(err)) - return undefined + if (err instanceof ProcessError) { + if (err.shouldLog) { + control.ctx.error(err.message, { props: err.props }) + } + return parseError(err) + } else { + const errorId = generateId() + control.ctx.error(err instanceof Error ? err.message : String(err), { errorId }) + return parseError(processError(process.error.InternalServerError, { errorId })) + } } } @@ -169,21 +179,15 @@ async function fillValue ( control: TriggerControl, execution: Execution ): Promise { - if (value === undefined) { - // we should add error to execution - throw new Error('Value not found') - } for (const func of context.functions ?? []) { - try { - const transform = control.modelDb.findObject(func.func) - if (transform === undefined) continue - if (!control.hierarchy.hasMixin(transform, serverProcess.mixin.FuncImpl)) continue - const funcImpl = control.hierarchy.as(transform, serverProcess.mixin.FuncImpl) - const f = await getResource(funcImpl.func) - value = await f(value, func.props, control, execution) - } catch (err: any) { - control.ctx.error(err) + const transform = control.modelDb.findObject(func.func) + if (transform === undefined) throw processError(process.error.MethodNotFound, { methodId: func.func }, {}, true) + if (!control.hierarchy.hasMixin(transform, serverProcess.mixin.FuncImpl)) { + throw processError(process.error.MethodNotFound, { methodId: func.func }, {}, true) } + const funcImpl = control.hierarchy.as(transform, serverProcess.mixin.FuncImpl) + const f = await getResource(funcImpl.func) + value = await f(value, func.props, control, execution) } return value } @@ -195,32 +199,69 @@ async function getAttributeValue ( ): Promise { const cardValue = await control.findAll(control.ctx, card.class.Card, { _id: execution.card }, { limit: 1 }) if (cardValue.length > 0) { - return getObjectValue(context.key, cardValue[0]) + const val = getObjectValue(context.key, cardValue[0]) + if (val == null) { + const attr = control.hierarchy.findAttribute(cardValue[0]._class, context.key) + throw processError( + process.error.EmptyAttributeContextValue, + {}, + { attr: attr?.label ?? getEmbeddedLabel(context.key) } + ) + } + return val + } else { + throw processError(process.error.ObjectNotFound, { _id: execution.card }, {}, true) } } -async function getNestedValue (control: TriggerControl, execution: Execution, context: SelectedNested): Promise { +async function getNestedValue ( + control: TriggerControl, + execution: Execution, + context: SelectedNested +): Promise { const cardValue = await control.findAll(control.ctx, card.class.Card, { _id: execution.card }, { limit: 1 }) - if (cardValue.length === 0) return + if (cardValue.length === 0) throw processError(process.error.ObjectNotFound, { _id: execution.card }, {}, true) const attr = control.hierarchy.findAttribute(cardValue[0]._class, context.path) - if (attr === undefined) return + if (attr === undefined) throw processError(process.error.AttributeNotExists, { key: context.path }) const nestedValue = getObjectValue(context.path, cardValue[0]) - if (nestedValue === undefined) return + if (nestedValue === undefined) throw processError(process.error.EmptyAttributeContextValue, {}, { attr: attr.label }) const parentType = attr.type._class === core.class.ArrOf ? (attr.type as ArrOf).of : attr.type const targetClass = parentType._class === core.class.RefTo ? (parentType as RefTo).to : parentType._class const target = await control.findAll(control.ctx, targetClass, { _id: { $in: Array.isArray(nestedValue) ? nestedValue : [nestedValue] } }) + if (target.length === 0) throw processError(process.error.RelatedObjectNotFound, {}, { attr: attr.label }) + const nested = control.hierarchy.findAttribute(targetClass, context.key) if (context.sourceFunction !== undefined) { const transform = control.modelDb.findObject(context.sourceFunction) - if (transform === undefined) return - if (!control.hierarchy.hasMixin(transform, serverProcess.mixin.FuncImpl)) return + 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 reduced = await f(target, {}, control, execution) - return getObjectValue(context.key, reduced) + const val = getObjectValue(context.key, reduced) + if (val == null) { + throw processError( + process.error.EmptyRelatedObjectValue, + {}, + { parent: attr.label, attr: nested?.label ?? getEmbeddedLabel(context.key) } + ) + } + return val } - return getObjectValue(context.key, target[0]) + const val = getObjectValue(context.key, target[0]) + if (val == null) { + throw processError( + process.error.EmptyRelatedObjectValue, + {}, + { parent: attr.label, attr: nested?.label ?? getEmbeddedLabel(context.key) } + ) + } + return val } async function getRelationValue ( @@ -229,25 +270,48 @@ async function getRelationValue ( context: SelectedRelation ): Promise { const assoc = control.modelDb.findObject(context.association) - if (assoc === undefined) return + if (assoc === undefined) throw processError(process.error.RelationNotExists, {}) const targetClass = context.direction === 'A' ? assoc.classA : assoc.classB const q = context.direction === 'A' ? { docB: execution.card } : { docA: execution.card } const relations = await control.findAll(control.ctx, core.class.Relation, { association: assoc._id, ...q }) - if (relations.length === 0) return + const name = context.direction === 'A' ? assoc.nameA : assoc.nameB + if (relations.length === 0) throw processError(process.error.RelatedObjectNotFound, { attr: name }) const ids = relations.map((it) => { return context.direction === 'A' ? it.docA : it.docB }) const target = await control.findAll(control.ctx, targetClass, { _id: { $in: ids } }) + const attr = control.hierarchy.findAttribute(targetClass, context.key) + if (target.length === 0) throw processError(process.error.RelatedObjectNotFound, { attr: name }) if (context.sourceFunction !== undefined) { const transform = control.modelDb.findObject(context.sourceFunction) - if (transform === undefined) return - if (!control.hierarchy.hasMixin(transform, serverProcess.mixin.FuncImpl)) return + 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 reduced = await f(target, {}, control, execution) - return getObjectValue(context.key, reduced) + const val = getObjectValue(context.key, reduced) + if (val == null) { + throw processError( + process.error.EmptyRelatedObjectValue, + { parent: name }, + { attr: attr?.label ?? getEmbeddedLabel(context.key) } + ) + } + return val } - return getObjectValue(context.key, target[0]) + const val = getObjectValue(context.key, target[0]) + if (val == null) { + throw processError( + process.error.EmptyRelatedObjectValue, + { parent: name }, + { attr: attr?.label ?? getEmbeddedLabel(context.key) } + ) + } + return val } async function fillParams ( @@ -267,15 +331,22 @@ async function fillParams ( async function getContextValue (value: any, control: TriggerControl, execution: Execution): Promise { const context = parseContext(value) if (context !== undefined) { - let value = context.fallbackValue - if (context.type === 'attribute') { - value = await getAttributeValue(control, execution, context) - } else if (context.type === 'relation') { - value = await getRelationValue(control, execution, context) - } else if (context.type === 'nested') { - value = await getNestedValue(control, execution, context) + let value: any | undefined + try { + if (context.type === 'attribute') { + value = await getAttributeValue(control, execution, context) + } else if (context.type === 'relation') { + value = await getRelationValue(control, execution, context) + } else if (context.type === 'nested') { + value = await getNestedValue(control, execution, context) + } + return await fillValue(value, context, control, execution) + } catch (err: any) { + if (err instanceof ProcessError && context.fallbackValue !== undefined) { + return await fillValue(context.fallbackValue, context, control, execution) + } + throw err } - return await fillValue(value === undefined ? context.fallbackValue : value, context, control, execution) } else { return value } @@ -287,40 +358,51 @@ async function changeState ( control: TriggerControl, isDone: boolean = false ): Promise { + const errors: ExecutionError[] = [] const res: Tx[] = [] const rollback: Tx[] = [] for (const action of state.actions) { const actionResult = await executeAction(action, execution, control) - if (actionResult === undefined) continue - if (actionResult.rollback !== undefined) { - rollback.push(...actionResult.rollback) - } - res.push(...actionResult.txes) - } - if (state.endAction != null) { - const actionResult = await executeAction(state.endAction, execution, control) - if (actionResult !== undefined) { + if (isError(actionResult)) { + errors.push(actionResult) + } else { if (actionResult.rollback !== undefined) { rollback.push(...actionResult.rollback) } res.push(...actionResult.txes) } } - if (rollback.length > 0) { - execution.rollback[state._id] = rollback + if (state.endAction != null) { + const actionResult = await executeAction(state.endAction, execution, control) + if (isError(actionResult)) { + errors.push(actionResult) + } else { + if (actionResult.rollback !== undefined) { + rollback.push(...actionResult.rollback) + } + res.push(...actionResult.txes) + } + } + + if (errors.length === 0) { + if (rollback.length > 0) { + execution.rollback[state._id] = rollback + res.push( + control.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, { + rollback: execution.rollback + }) + ) + } res.push( control.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, { - rollback: execution.rollback + currentState: state._id, + done: isDone }) ) + return res + } else { + return [control.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, { error: errors })] } - res.push( - control.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, { - currentState: state._id, - done: isDone - }) - ) - return res } export async function OnExecutionCreate (txes: Tx[], control: TriggerControl): Promise { @@ -553,6 +635,30 @@ export function FirstWorkingDayAfter (val: Timestamp): Timestamp { return val } +export async function OnExecutionContinue (txes: Tx[], control: TriggerControl): Promise { + const res: Tx[] = [] + for (const tx of txes) { + if (tx._class !== core.class.TxUpdateDoc) continue + const updateTx = tx as TxUpdateDoc + if (!control.hierarchy.isDerived(updateTx.objectClass, process.class.Execution)) continue + if (updateTx.operations.error !== null) continue + const execution = ( + await control.findAll(control.ctx, process.class.Execution, { _id: updateTx.objectId }, { limit: 1 }) + )[0] + if (execution === undefined) continue + const _process = await control.modelDb.findOne(process.class.Process, { _id: execution.process }) + if (_process === undefined) continue + const currentIndex = _process.states.findIndex((it) => it === execution.currentState) + const nextState = _process.states[currentIndex + 1] + if (nextState === undefined) continue + const states = await control.findAll(control.ctx, process.class.State, { _id: nextState }) + if (states.length === 0) continue + const isDone = _process.states[currentIndex + 2] === undefined + res.push(...(await changeState(execution, states[0], control, isDone))) + } + return res +} + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ func: { @@ -577,6 +683,7 @@ export default async () => ({ OnStateRemove, OnProcessRemove, OnProcessToDoClose, - OnProcessToDoRemove + OnProcessToDoRemove, + OnExecutionContinue } }) diff --git a/server-plugins/process/src/index.ts b/server-plugins/process/src/index.ts index 2be857e1c3..1bbe3cf813 100644 --- a/server-plugins/process/src/index.ts +++ b/server-plugins/process/src/index.ts @@ -50,6 +50,7 @@ export default plugin(serverProcessId, { OnStateRemove: '' as Resource, OnProcessRemove: '' as Resource, OnProcessToDoClose: '' as Resource, - OnProcessToDoRemove: '' as Resource + OnProcessToDoRemove: '' as Resource, + OnExecutionContinue: '' as Resource } }) diff --git a/server-plugins/process/src/types.ts b/server-plugins/process/src/types.ts index 7b6555c0c1..79c47b88ce 100644 --- a/server-plugins/process/src/types.ts +++ b/server-plugins/process/src/types.ts @@ -1,5 +1,5 @@ import { Doc, Tx } from '@hcengineering/core' -import { Execution, MethodParams } from '@hcengineering/process' +import { Execution, ExecutionError, MethodParams } from '@hcengineering/process' import { TriggerControl } from '@hcengineering/server-core' export type ExecuteFunc = ( @@ -8,7 +8,9 @@ export type ExecuteFunc = ( control: TriggerControl ) => Promise -export interface ExecuteResult { +export type ExecuteResult = SuccessExecutionResult | ExecutionError + +export interface SuccessExecutionResult { txes: Tx[] rollback: Tx[] | undefined }