Rework todo result (#9893)

This commit is contained in:
Denis Bykhov
2025-09-19 10:41:21 +07:00
committed by GitHub
parent 1e7b531564
commit a7afa2b1dc
34 changed files with 555 additions and 317 deletions
-3
View File
@@ -63,7 +63,6 @@ import {
type Step,
type Transition,
type Trigger,
type TriggerResult,
type UpdateCriteriaComponent,
processId
} from '@hcengineering/process'
@@ -136,8 +135,6 @@ export class TTransition extends TDoc implements Transition {
triggerParams!: Record<string, any>
result?: TriggerResult | null
@Prop(TypeRank(), core.string.Rank)
@Hidden()
rank!: Rank
+14
View File
@@ -906,3 +906,17 @@
.emoji.fitSize > img {
height: 1em;
}
.editor-grid {
display: grid;
grid-template-columns: 1fr 3fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
align-items: center;
row-gap: 0.5rem;
margin: 0.25rem 2rem 0;
width: calc(100% - 4rem);
column-gap: 1rem;
margin-top: 0.5rem;
height: min-content;
}
+5 -1
View File
@@ -99,7 +99,11 @@
"Cancelled": "Zrušeno",
"WaitUntil": "Čekat do",
"CurrentUser": "Aktuální uživatel",
"CurrentDate": "Aktuální datum"
"CurrentDate": "Aktuální datum",
"AddResult": "Přidat výsledek",
"For": "Pro",
"Context": "Kontext",
"Attribute": "Atribut"
},
"error": {
"MethodNotFound": "Metoda nenalezena: {methodId}",
+5 -1
View File
@@ -99,7 +99,11 @@
"Cancelled": "Abgebrochen",
"WaitUntil": "Warten bis",
"CurrentUser": "Aktueller Benutzer",
"CurrentDate": "Aktuelles Datum"
"CurrentDate": "Aktuelles Datum",
"AddResult": "Ergebnis hinzufügen",
"For": "Für",
"Context": "Kontext",
"Attribute": "Attribut"
},
"error": {
"MethodNotFound": "Methode nicht gefunden: {methodId}",
+5 -1
View File
@@ -104,7 +104,11 @@
"Cancelled": "Cancelled",
"WaitUntil": "Wait until",
"CurrentUser": "Current user",
"CurrentDate": "Current date"
"CurrentDate": "Current date",
"AddResult": "Add result",
"For": "For",
"Context": "Context",
"Attribute": "Attribute"
},
"error": {
"MethodNotFound": "Method not found: {methodId}",
+5 -1
View File
@@ -104,7 +104,11 @@
"Cancelled": "Cancelado",
"WaitUntil": "Esperar hasta",
"CurrentUser": "Usuario Actual",
"CurrentDate": "Fecha Actual"
"CurrentDate": "Fecha Actual",
"AddResult": "Agregar resultado",
"For": "Para",
"Context": "Contexto",
"Attribute": "Atributo"
},
"error": {
"MethodNotFound": "Método no encontrado: {methodId}",
+5 -1
View File
@@ -104,7 +104,11 @@
"Cancelled": "Annulé",
"WaitUntil": "Attendre jusqu'à",
"CurrentUser": "Utilisateur actuel",
"CurrentDate": "Date actuelle"
"CurrentDate": "Date actuelle",
"AddResult": "Ajouter un résultat",
"For": "Pour",
"Context": "Contexte",
"Attribute": "Attribut"
},
"error": {
"MethodNotFound": "Méthode introuvable : {methodId}",
+5 -1
View File
@@ -104,7 +104,11 @@
"Cancelled": "Annullato",
"WaitUntil": "Aspetta fino a",
"CurrentUser": "Utente corrente",
"CurrentDate": "Data corrente"
"CurrentDate": "Data corrente",
"AddResult": "Aggiungi risultato",
"For": "Per",
"Context": "Contesto",
"Attribute": "Attributo"
},
"error": {
"MethodNotFound": "Metodo non trovato: {methodId}",
+5 -1
View File
@@ -103,7 +103,11 @@
"Cancelled": "キャンセル済み",
"WaitUntil": "待機する",
"CurrentUser": "現在のユーザー",
"CurrentDate": "現在の日付"
"CurrentDate": "現在の日付",
"AddResult": "結果を追加",
"For": "〜のために",
"Context": "コンテキスト",
"Attribute": "属性"
},
"error": {
"MethodNotFound": "メソッドが見つかりません: {methodId}",
+5 -1
View File
@@ -104,7 +104,11 @@
"Cancelled": "Cancelado",
"WaitUntil": "Aguardar até",
"CurrentUser": "Usuário Atual",
"CurrentDate": "Data Atual"
"CurrentDate": "Data Atual",
"AddResult": "Adicionar resultado",
"For": "Para",
"Attribute": "Atributo",
"Context": "Contexto"
},
"error": {
"MethodNotFound": "Método não encontrado: {methodId}",
+5 -1
View File
@@ -104,7 +104,11 @@
"Cancelled": "Отменено",
"WaitUntil": "Ждать до",
"CurrentUser": "Текущий пользователь",
"CurrentDate": "Текущая дата"
"CurrentDate": "Текущая дата",
"AddResult": "Добавить результат",
"For": "Для",
"Context": "Контекст",
"Attribute": "Аттрибут"
},
"error": {
"MethodNotFound": "Метод не найден: {methodId}",
+5 -1
View File
@@ -104,7 +104,11 @@
"Cancelled": "已取消",
"WaitUntil": "等待直到",
"CurrentUser": "当前用户",
"CurrentDate": "当前日期"
"CurrentDate": "当前日期",
"AddResult": "添加结果",
"For": "对于",
"Context": "上下文",
"Attribute": "属性"
},
"error": {
"MethodNotFound": "找不到方法:{methodId}",
@@ -38,7 +38,7 @@
}
</script>
<div class="grid">
<div class="editor-grid">
{#each keys as key}
<AttributeCriteria
{process}
@@ -54,17 +54,3 @@
/>
{/each}
</div>
<style lang="scss">
.grid {
display: grid;
grid-template-columns: 1fr 3fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
margin-top: 0.5rem;
height: min-content;
}
</style>
@@ -93,7 +93,7 @@
}
</script>
<div class="grid">
<div class="editor-grid">
<span
class="labelOnPanel"
use:tooltip={{
@@ -118,18 +118,3 @@
<Button label={presentation.string.Add} width={'100%'} kind={'link-bordered'} size={'large'} on:click={onAdd} />
</div>
{/if}
<style lang="scss">
.grid {
display: grid;
grid-template-columns: 1fr 1.5fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
width: calc(100% - 4rem);
align-items: center;
margin: 0.25rem 2rem 0.5rems;
row-gap: 0.5rem;
column-gap: 1rem;
height: min-content;
}
</style>
@@ -15,7 +15,7 @@
<script lang="ts">
import core, { Ref, SortingOrder } from '@hcengineering/core'
import { Card, createQuery, getClient } from '@hcengineering/presentation'
import { Process, State, Transition, Trigger, TriggerResult } from '@hcengineering/process'
import { Process, State, Transition, Trigger } from '@hcengineering/process'
import { Component, Dropdown, DropdownIntlItem, DropdownLabelsIntl, Label, ListItem } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
@@ -26,7 +26,6 @@
const dispatch = createEventDispatcher()
let to: Ref<State> | undefined = undefined
let states: State[] = []
let result: TriggerResult | null = null
const query = createQuery()
query.query(
@@ -96,7 +95,6 @@
triggerParams: params,
process: process._id,
rank,
result,
actions: []
})
dispatch('close')
@@ -106,9 +104,6 @@
if (e.detail?.params !== undefined) {
params = e.detail.params
}
if (e.detail?.result !== undefined) {
result = e.detail.result
}
}
</script>
@@ -119,7 +114,7 @@
width={'medium'}
on:close
>
<div class="grid">
<div class="editor-grid">
<Label label={plugin.string.From} />
{#if !withoutFrom}
<Dropdown
@@ -149,7 +144,6 @@
label={plugin.string.Trigger}
on:selected={() => {
params = {}
result = null
}}
justify={'left'}
width={'100%'}
@@ -157,19 +151,6 @@
/>
</div>
{#if triggerValue?.editor}
<Component is={triggerValue.editor} props={{ process, params, result }} on:change={change} />
<Component is={triggerValue.editor} props={{ process, params }} on:change={change} />
{/if}
</Card>
<style lang="scss">
.grid {
display: grid;
grid-template-columns: 1fr 3fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
height: min-content;
}
</style>
@@ -20,12 +20,11 @@
export let transition: Transition
let params = transition.triggerParams ?? {}
let result = transition.result ?? null
const client = getClient()
async function save (): Promise<void> {
await client.update(transition, { triggerParams: params, trigger: selectedTrigger, result })
await client.update(transition, { triggerParams: params, trigger: selectedTrigger })
clearSettingsStore()
}
@@ -38,9 +37,6 @@
if (e.detail?.params !== undefined) {
params = e.detail.params
}
if (e.detail?.result !== undefined) {
result = e.detail.result
}
}
let selectedTrigger: Ref<Trigger> = transition.trigger
@@ -72,7 +68,7 @@
</div>
</div>
{#if trigger !== undefined}
<div class="grid">
<div class="editor-grid">
<Label label={plugin.string.Trigger} />
<DropdownLabelsIntl
items={triggersItems}
@@ -80,7 +76,6 @@
label={plugin.string.Trigger}
on:selected={() => {
params = {}
result = null
}}
justify={'left'}
width={'100%'}
@@ -89,7 +84,7 @@
</div>
{#if trigger.editor !== undefined}
<div class="editor">
<Component is={trigger.editor} props={{ process, params, result, readonly }} on:change={change} />
<Component is={trigger.editor} props={{ process, params, readonly }} on:change={change} />
</div>
{/if}
{/if}
@@ -105,18 +100,6 @@
margin: 0.25rem 2rem 0;
}
.grid {
display: grid;
grid-template-columns: 1fr 3fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
align-items: center;
row-gap: 0.25rem;
column-gap: 1rem;
margin: 0.25rem 2rem 0;
height: min-content;
}
.title {
padding-bottom: 1rem;
}
@@ -24,13 +24,23 @@
export let step: Step<Doc>
$: currentContext = step.context ? process.context[step.context._id] : undefined
$: resultContext =
step.results != null ? step.results.map((r) => process.context[r._id]).filter((ctx) => ctx != null) : []
</script>
{#if currentContext}
<Label label={processPlugin.string.Result} />
<div class="container">
<ProcessContextPresenter context={currentContext} />
</div>
{#if currentContext || resultContext.length > 0}
<Label label={processPlugin.string.Result} />:
{#if currentContext}
<div class="container">
<ProcessContextPresenter context={currentContext} />
</div>
{/if}
{#each resultContext as ctx}
<div class="container">
<ProcessContextPresenter context={ctx} />
</div>
{/each}
{/if}
<style lang="scss">
@@ -13,32 +13,20 @@
// limitations under the License.
-->
<script lang="ts">
import core, { Class, PropertyType, Ref, Type } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { TriggerResult } from '@hcengineering/process'
import setting from '@hcengineering/setting-resources/src/plugin'
import {
AnyComponent,
Component,
DropdownIntlItem,
DropdownLabelsIntl,
EditBox,
Label,
Toggle
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import { deepEqual } from 'fast-equals'
import core, { Type } from '@hcengineering/core'
import { Process, UserResult } from '@hcengineering/process'
import { Button, EditBox, IconClose, Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
import { generateContextId } from '../../utils'
import ResultTypeSelector from './ResultTypeSelector.svelte'
export let result: TriggerResult | null
export let result: UserResult | null
export let process: Process
let type: Type<any> | undefined | null = result?.type
let name: string = result?.name ?? ''
let is: AnyComponent | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
let key: string | undefined = result?.key
const dispatch = createEventDispatcher()
function update (): void {
@@ -48,114 +36,34 @@
result = {
_id: generateContextId(),
name,
key,
type
}
}
dispatch('change', result)
}
function getTypes (): DropdownIntlItem[] {
const descendants = hierarchy.getDescendants(core.class.Type)
const res: DropdownIntlItem[] = []
for (const descendant of descendants) {
const _class = hierarchy.getClass(descendant)
if (_class.label !== undefined && hierarchy.hasMixin(_class, view.mixin.ObjectEditor)) {
res.push({
label: _class.label,
id: _class._id
})
}
}
return res
}
const items = getTypes()
let selectedType: Ref<Class<Type<PropertyType>>> | undefined = type?._class
$: selectType(selectedType)
function selectType (type: Ref<Class<Type<PropertyType>>> | undefined): void {
if (type == null) return
const _class = hierarchy.getClass(type)
const editor = hierarchy.as(_class, view.mixin.ObjectEditor)
if (editor.editor !== undefined) {
is = editor.editor
}
}
const handleSelection = (e: { detail: Ref<Class<Type<any>>> }): void => {
selectType(e.detail)
}
const handleChange = (e: any): void => {
if (e.detail?.type === undefined) return
if (!deepEqual(e.detail.type, type)) {
type = e.detail?.type
update()
}
}
function handleNameChange (e: any): void {
function handleNameChange (): void {
if (result != null) {
result.name = name
dispatch('change', result)
}
}
function changeRequired (e: boolean): void {
if (!e) {
type = undefined
} else {
type = null
}
update()
}
</script>
<div class="grid">
<Label label={plugin.string.Result} />
<Toggle
on={result != null}
on:change={(e) => {
changeRequired(e.detail)
}}
/>
{#if type !== undefined}
<span class="label">
<Label label={core.string.Description} />
</span>
<div class="editor-grid">
<span class="label">
<Label label={core.string.Description} />
</span>
<div class="flex-row-center flex-gap-2">
<EditBox bind:value={name} placeholder={core.string.Description} on:change={handleNameChange} kind={'default'} />
<span class="label">
<Label label={setting.string.Type} />
</span>
<DropdownLabelsIntl
label={setting.string.Type}
{items}
size={'large'}
width={'100%'}
bind:selected={selectedType}
on:selected={handleSelection}
<Button
icon={IconClose}
kind="ghost"
on:click={() => {
dispatch('remove')
}}
/>
{#if is}
<Component
{is}
props={{
type,
width: '100%',
isCard: true,
kind: 'regular',
size: 'large'
}}
on:change={handleChange}
/>
{/if}
{/if}
</div>
<ResultTypeSelector {process} bind:key bind:type on:change={update} />
</div>
<style lang="scss">
.grid {
display: grid;
grid-template-columns: 1fr 3fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
height: min-content;
}
</style>
@@ -0,0 +1,80 @@
<!--
// 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, Type } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Process } from '@hcengineering/process'
import { DropdownLabelsIntl, Label, tooltip } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
export let process: Process
export let key: string | undefined
export let type: Type<any> | null | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
$: setType(key)
function setType (key: string | undefined): void {
const attr = key != null ? hierarchy.findAttribute(process.masterTag, key) : undefined
if (attr != null) {
type = attr.type
} else {
type = null
}
dispatch('change')
}
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
res.push(attr)
}
return res
}
const allAttrs = getKeys(process.masterTag)
const items = allAttrs.map((attr) => {
return {
id: attr.name,
label: attr.label ?? attr.name
}
})
</script>
<span
class="labelOnPanel"
use:tooltip={{
props: { label: plugin.string.Attribute }
}}
>
<Label label={plugin.string.Attribute} />
</span>
<DropdownLabelsIntl
label={plugin.string.Attribute}
{items}
size={'large'}
width={'100%'}
shouldUpdateUndefined={false}
bind:selected={key}
/>
@@ -0,0 +1,92 @@
<!--
// 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 core, { Class, PropertyType, Ref, Type } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import setting from '@hcengineering/setting-resources/src/plugin'
import { AnyComponent, Component, DropdownIntlItem, DropdownLabelsIntl, Label } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { deepEqual } from 'fast-equals'
import { createEventDispatcher } from 'svelte'
export let type: Type<any> | null | undefined
let is: AnyComponent | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
function getTypes (): DropdownIntlItem[] {
const descendants = hierarchy.getDescendants(core.class.Type)
const res: DropdownIntlItem[] = []
for (const descendant of descendants) {
const _class = hierarchy.getClass(descendant)
if (_class.label !== undefined && hierarchy.hasMixin(_class, view.mixin.ObjectEditor)) {
res.push({
label: _class.label,
id: _class._id
})
}
}
return res
}
const items = getTypes()
let selectedType: Ref<Class<Type<PropertyType>>> | undefined = type?._class
$: selectType(selectedType)
function selectType (type: Ref<Class<Type<PropertyType>>> | undefined): void {
if (type == null) return
const _class = hierarchy.getClass(type)
const editor = hierarchy.as(_class, view.mixin.ObjectEditor)
if (editor.editor !== undefined) {
is = editor.editor
}
}
const handleSelection = (e: { detail: Ref<Class<Type<any>>> }): void => {
selectType(e.detail)
}
const handleChange = (e: any): void => {
if (e.detail?.type === undefined) return
if (!deepEqual(e.detail.type, type)) {
type = e.detail?.type
dispatch('change', type)
}
}
</script>
<span class="label">
<Label label={setting.string.Type} />
</span>
<DropdownLabelsIntl
label={setting.string.Type}
{items}
size={'large'}
width={'100%'}
bind:selected={selectedType}
on:selected={handleSelection}
/>
{#if is}
<Component
{is}
props={{
type,
width: '100%',
isCard: true,
kind: 'regular',
size: 'large'
}}
on:change={handleChange}
/>
{/if}
@@ -0,0 +1,54 @@
<!--
// 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 { Type } from '@hcengineering/core'
import { Process } from '@hcengineering/process'
import { DropdownIntlItem, DropdownLabelsIntl, Label } from '@hcengineering/ui'
import plugin from '../../plugin'
import ResultFieldTypeEditor from './ResultFieldTypeEditor.svelte'
import ResultTypeEditor from './ResultTypeEditor.svelte'
export let process: Process
export let type: Type<any> | null | undefined
export let key: string | undefined
enum Modes {
Attribute = 'attribute',
Context = 'context'
}
const items: DropdownIntlItem[] = [
{
id: Modes.Attribute,
label: plugin.string.Attribute
},
{
id: Modes.Context,
label: plugin.string.Context
}
]
let selected: Modes = key !== undefined || type == null ? Modes.Attribute : Modes.Context
</script>
<span class="label">
<Label label={plugin.string.For} />
</span>
<DropdownLabelsIntl label={plugin.string.For} {items} size={'large'} width={'100%'} bind:selected />
{#if selected === Modes.Attribute}
<ResultFieldTypeEditor {process} bind:key bind:type on:change />
{:else}
<ResultTypeEditor bind:type on:change />
{/if}
@@ -0,0 +1,64 @@
<!--
// 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 { Process, UserResult } from '@hcengineering/process'
import { Button } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
import ResultEditor from './ResultEditor.svelte'
export let process: Process
export let result: UserResult[] | undefined
const dispatch = createEventDispatcher()
let items: (UserResult | null)[] = result ?? []
function handleChange (e: CustomEvent<UserResult>, index: number) {
if (e.detail !== undefined) {
items[index === -1 ? items.length : index] = e.detail
result = items.filter((r) => r != null)
dispatch('change', result)
}
}
function onAdd (): void {
items.push(null)
items = items
}
function remove (index: number): void {
items.splice(index, 1)
items = items
result = items.filter((r) => r != null)
dispatch('change', result)
}
</script>
{#each items as item, i}
<ResultEditor
{process}
result={item}
on:change={(e) => {
handleChange(e, i)
}}
on:remove={() => {
remove(i)
}}
/>
{/each}
<div class="flex-center mt-4">
<Button label={plugin.string.AddResult} width={'100%'} kind={'link-bordered'} size={'large'} on:click={onAdd} />
</div>
@@ -70,7 +70,7 @@
const dispatch = createEventDispatcher()
</script>
<div class="grid">
<div class="editor-grid">
<ProcessAttribute
{process}
masterTag={process.masterTag}
@@ -88,17 +88,3 @@
}}
/>
</div>
<style lang="scss">
.grid {
display: grid;
grid-template-columns: 1fr 3fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
margin-top: 0.5rem;
height: min-content;
}
</style>
@@ -13,12 +13,12 @@
// limitations under the License.
-->
<script lang="ts">
import { Process, ProcessToDo, Step } from '@hcengineering/process'
import { Process, ProcessToDo, Step, UserResult } from '@hcengineering/process'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
import ParamsEditor from './ParamsEditor.svelte'
import ResultEditor from './ResultEditor.svelte'
import { Label, Toggle } from '@hcengineering/ui'
import ResultsEditor from './ResultsEditor.svelte'
export let process: Process
export let step: Step<ProcessToDo>
@@ -35,6 +35,13 @@
}
}
function changeResults (e: CustomEvent<UserResult[]>): void {
if (e.detail !== undefined) {
step.results = e.detail
dispatch('change', step)
}
}
const keys = ['title', 'user', 'dueDate']
</script>
@@ -53,6 +60,8 @@
}}
/>
</div>
<div class="divider" />
<ResultsEditor {process} result={step.results} on:change={changeResults} />
<style lang="scss">
.divider {
@@ -14,18 +14,16 @@
-->
<script lang="ts">
import { Process, TriggerResult } from '@hcengineering/process'
import { Process } from '@hcengineering/process'
import { Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
import ToDoContextSelector from '../contextEditors/ToDoContextSelector.svelte'
import ResultEditor from './ResultEditor.svelte'
export let readonly: boolean
export let process: Process
export let params: Record<string, any>
export let skipRollback: boolean = false
export let result: TriggerResult | null = null
const dispatch = createEventDispatcher()
@@ -34,39 +32,9 @@
params._id = e.detail
dispatch('change', { params })
}
function changeResult (e: CustomEvent<any>): void {
if (e.detail !== undefined) {
result = e.detail
dispatch('change', { result })
}
}
</script>
<div class="grid">
<div class="editor-grid">
<Label label={plugin.string.ToDo} />
<ToDoContextSelector {readonly} {skipRollback} {process} value={params._id} on:change={change} />
</div>
{#if !skipRollback}
<div class="divider" />
<ResultEditor {result} on:change={changeResult} />
{/if}
<style lang="scss">
.divider {
border-bottom: 1px solid var(--divider-color);
margin: 1rem 0;
}
.grid {
display: grid;
grid-template-columns: 1fr 3fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
margin-top: 0.5rem;
height: min-content;
}
</style>
+2 -3
View File
@@ -95,7 +95,6 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre
await txop.update(execution, {
context
})
await requestResult(txop, execution, transition, execution.context)
}
}
}
@@ -147,6 +146,8 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre
_id: todo.execution
})
if (execution === undefined) return
const txop = new TxOperations(this.client, getCurrentAccount().primarySocialId)
await requestResult(txop, execution, todo.results, execution.context)
const transitions = this.client.getModel().findAllSync(process.class.Transition, {
process: execution.process,
from: execution.currentState,
@@ -155,13 +156,11 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre
const transition = await pickTransition(this.client, execution, transitions, todo)
if (transition === undefined) return
const context = await getNextStateUserInput(execution, transition, execution.context)
const txop = new TxOperations(this.client, getCurrentAccount().primarySocialId)
if (context !== undefined) {
await txop.update(execution, {
context
})
}
await requestResult(txop, execution, transition, execution.context)
}
}
}
+5 -1
View File
@@ -195,6 +195,10 @@ export default mergeIds(processId, process, {
ArraySizeLt: '' as IntlString,
ArraySizeLte: '' as IntlString,
CurrentUser: '' as IntlString,
CurrentDate: '' as IntlString
CurrentDate: '' as IntlString,
AddResult: '' as IntlString,
For: '' as IntlString,
Attribute: '' as IntlString,
Context: '' as IntlString
}
})
+13 -15
View File
@@ -49,7 +49,8 @@ import {
type State,
type Step,
type StepId,
type Transition
type Transition,
type UserResult
} from '@hcengineering/process'
import { type AnyComponent, showPopup } from '@hcengineering/ui'
import { type AttributeCategory } from '@hcengineering/view'
@@ -567,27 +568,24 @@ export function getToDoEndAction (prevState: State): Step<Doc> {
export async function requestResult (
txop: TxOperations,
execution: Execution,
transition: Transition,
results: UserResult[] | undefined,
context: ExecutionContext
): Promise<void> {
if (transition.result == null) return
const promise = new Promise<void>((resolve, reject) => {
showPopup(
process.component.ResultInput,
{ type: transition.result?.type, name: transition.result?.name },
undefined,
(res) => {
if (transition.result?._id === undefined) return
if (results == null) return
for (const result of results) {
const promise = new Promise<void>((resolve, reject) => {
showPopup(process.component.ResultInput, { type: result.type, name: result.name }, undefined, (res) => {
if (result._id === undefined) return
if (res?.value !== undefined) {
context[transition.result._id] = res.value
context[result._id] = res.value
resolve()
} else {
reject(new PlatformError(new Status(Severity.ERROR, process.error.ResultNotProvided, {})))
}
}
)
})
await promise
})
})
await promise
}
await txop.update(execution, {
context
})
+1 -1
View File
@@ -38,6 +38,6 @@ export function processError (
return new ProcessError(message, message, props, intlProps, shouldLog)
}
export function parseError (err: ProcessError, transition: Ref<Transition>): ExecutionError {
export function parseError (err: ProcessError, transition: Ref<Transition> | undefined): ExecutionError {
return { error: err.message, props: err.props, intlProps: err.intlProps, transition }
}
+6 -3
View File
@@ -66,7 +66,6 @@ export interface Transition extends Doc {
actions: Step<Doc>[]
trigger: Ref<Trigger>
triggerParams: Record<string, any>
result?: TriggerResult | null
rank: Rank
}
@@ -115,13 +114,15 @@ export interface ExecutionError {
error: IntlString
props: Record<string, any>
intlProps: Record<string, IntlString>
transition: Ref<Transition>
transition: Ref<Transition> | undefined
}
export interface ProcessToDo extends ToDo {
execution: Ref<Execution>
withRollback: boolean
results?: UserResult[]
}
export type MethodParams<T extends Doc> = {
@@ -142,6 +143,7 @@ export interface Step<T extends Doc> {
context: StepContext | null
methodId: Ref<Method<T>>
params: MethodParams<T>
results?: UserResult[]
}
export interface StepContext {
@@ -149,9 +151,10 @@ export interface StepContext {
_class?: Ref<Class<Doc>> // class of the context
}
export interface TriggerResult {
export interface UserResult {
_id: ContextId // context id
name: string
key?: string
type: Type<any>
}
@@ -35,7 +35,8 @@ import process, {
MethodParams,
Process,
processError,
ProcessToDo
ProcessToDo,
UserResult
} from '@hcengineering/process'
import { ExecuteResult, ProcessControl, SuccessExecutionContext } from '@hcengineering/server-process'
import time, { ToDoPriority } from '@hcengineering/time'
@@ -282,7 +283,8 @@ export async function RunSubProcess (
export async function CreateToDo (
params: MethodParams<ProcessToDo>,
execution: Execution,
control: ProcessControl
control: ProcessControl,
results: UserResult[] | undefined
): Promise<ExecuteResult> {
for (const key in { user: params.user, title: params.title }) {
const val = (params as any)[key]
@@ -311,7 +313,8 @@ export async function CreateToDo (
visibility: 'public',
doneOn: null,
rank: '',
withRollback: params.withRollback ?? false
withRollback: params.withRollback ?? false,
results
},
id
)
+26 -30
View File
@@ -128,7 +128,6 @@ export async function OnProcessToDoClose (txes: Tx[], control: TriggerControl):
}
export async function OnExecutionCreate (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<Execution>
@@ -143,11 +142,10 @@ export async function OnExecutionCreate (txes: Tx[], control: TriggerControl): P
control
)
}
return res
return []
}
export async function OnProcessToDoRemove (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of txes) {
if (tx._class !== core.class.TxRemoveDoc) continue
const removeTx = tx as TxRemoveDoc<ProcessToDo>
@@ -165,11 +163,10 @@ export async function OnProcessToDoRemove (txes: Tx[], control: TriggerControl):
control
)
}
return res
return []
}
export async function OnExecutionContinue (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of txes) {
if (tx._class !== core.class.TxUpdateDoc) continue
if (tx.space !== core.space.Tx) continue
@@ -192,7 +189,7 @@ export async function OnExecutionContinue (txes: Tx[], control: TriggerControl):
control
)
}
return res
return []
}
export async function OnProcessRemove (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
@@ -282,30 +279,6 @@ async function syncContext (control: TriggerControl, _process: Process): Promise
let changed = false
let index = 1
for (const transition of transitions) {
if (transition.result?._id != null) {
exists.add(transition.result._id)
const context = _process.context[transition.result._id]
changed = true
const ctx: SelectedExecutionContext = {
type: 'context',
id: transition.result._id,
key: ''
}
const parentType =
transition.result.type._class === core.class.ArrOf
? (transition.result.type as ArrOf<Doc>).of
: transition.result.type
const _class = parentType._class === core.class.RefTo ? (parentType as RefTo<Doc>).to : parentType._class
_process.context[transition.result._id] = {
name: context?.name ?? transition.result.name,
isResult: true,
type: transition.result.type,
_class,
index: index++,
producer: transition._id,
value: ctx
}
}
for (const action of transition.actions) {
if (action.context != null) {
exists.add(action.context._id)
@@ -328,6 +301,29 @@ async function syncContext (control: TriggerControl, _process: Process): Promise
}
}
}
if (action.results != null) {
for (const result of action.results) {
exists.add(result._id)
const context = _process.context[result._id]
changed = true
const ctx: SelectedExecutionContext = {
type: 'context',
id: result._id,
key: ''
}
const parentType = result.type._class === core.class.ArrOf ? (result.type as ArrOf<Doc>).of : result.type
const _class = parentType._class === core.class.RefTo ? (parentType as RefTo<Doc>).to : parentType._class
_process.context[result._id] = {
name: context?.name ?? result.name,
isResult: true,
type: result.type,
_class,
index: index++,
producer: transition._id,
value: ctx
}
}
}
}
}
const newContext: Record<ContextId, ProcessContext> = {}
+3 -2
View File
@@ -1,11 +1,12 @@
import { Card } from '@hcengineering/card'
import { Doc, MeasureContext, PersonId, Ref, Tx, TxOperations } from '@hcengineering/core'
import { Execution, ExecutionError, MethodParams, Trigger } from '@hcengineering/process'
import { Execution, ExecutionError, MethodParams, Trigger, UserResult } from '@hcengineering/process'
export type ExecuteFunc = (
params: MethodParams<Doc>,
execution: Execution,
control: ProcessControl
control: ProcessControl,
results: UserResult[] | undefined
) => Promise<ExecuteResult>
export type ExecuteResult = SuccessExecutionResult | ExecutionError
+79 -2
View File
@@ -40,7 +40,8 @@ import process, {
ProcessToDo,
State,
Step,
Transition
Transition,
UserResult
} from '@hcengineering/process'
import serverProcess, {
ExecuteResult,
@@ -165,7 +166,83 @@ async function findTransitions (
return await pickTransition(control, execution, transitions, record.context)
}
async function checkToDoResult (control: ProcessControl, record: ProcessMessage, execution: Execution): Promise<void> {
if (record.event !== process.trigger.OnToDoClose) return
const todo = record.context.todo as ProcessToDo
if (todo === undefined) return
if (todo.results == null) return
const results = todo.results.filter((it) => it.key != null)
if (results.length === 0) return
const res = await executeResultSet(control, results, execution)
if (!isError(res)) {
for (const tx of res.txes) {
const updateTx = tx as TxUpdateDoc<Doc>
if (updateTx._class === core.class.TxUpdateDoc && updateTx.objectId === execution.card) {
const updatedCard = TxProcessor.updateDoc2Doc(
control.client.getHierarchy().clone(control.cache.get(execution.card)),
updateTx
)
control.cache.set(execution.card, updatedCard)
}
}
for (const tx of res.txes) {
const timeout = setTimeout(() => {
control.ctx.warn('TX HANG', tx)
}, 30000)
await control.client.tx(tx)
clearTimeout(timeout)
}
if (res.rollback != null) {
execution.rollback = execution.rollback.length > 30 ? execution.rollback.slice(-30) : execution.rollback
await control.client.update(execution, { rollback: execution.rollback })
}
}
}
async function executeResultSet (
control: ProcessControl,
results: UserResult[],
execution: Execution
): Promise<ExecuteResult> {
try {
const method = control.client.getModel().findObject(process.method.UpdateCard)
if (method === undefined) {
throw processError(process.error.MethodNotFound, { methodId: process.method.UpdateCard }, {}, true)
}
const impl = control.client.getHierarchy().as(method, serverProcess.mixin.MethodImpl) as MethodImpl<any>
if (impl === undefined) {
throw processError(process.error.MethodNotFound, { methodId: process.method.UpdateCard }, {}, true)
}
const params = {}
for (const res of results) {
if (res.key == null) continue
;(params as any)[res.key] = execution.context[res._id]
}
if (!control.cache.has(execution.card)) {
const card = await control.client.findOne(cardPlugin.class.Card, { _id: execution.card })
if (card !== undefined) {
control.cache.set(execution.card, card)
}
}
const f = await getResource(impl.func)
const res = await f(params, execution, control, undefined)
return res
} catch (err) {
if (err instanceof ProcessError) {
if (err.shouldLog) {
control.ctx.error(err.message, { props: err.props })
}
return parseError(err, undefined)
} else {
const errorId = generateId()
control.ctx.error(err instanceof Error ? err.message : String(err), { errorId })
return parseError(processError(process.error.InternalServerError, { errorId }), undefined)
}
}
}
async function processExecution (control: ProcessControl, record: ProcessMessage, execution: Execution): Promise<void> {
await checkToDoResult(control, record, execution)
const transition = await findTransitions(control, record, execution)
if (transition !== undefined) {
await execute(execution, transition, control)
@@ -476,7 +553,7 @@ async function executeAction<T extends Doc> (
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)
const res = await f(params, execution, control, action.results)
if (!isError(res) && action.context?._id != null && res.context != null) {
execution.context[action.context._id] =
res.context.length === 1 ? res.context[0]._id : res.context.map((it) => it._id)