Create card process (#9522)

This commit is contained in:
Denis Bykhov
2025-07-11 09:11:58 +07:00
committed by GitHub
parent 881a12cc6e
commit cd3396cf9d
48 changed files with 1005 additions and 467 deletions
+28
View File
@@ -898,6 +898,34 @@ export function createModel (builder: Builder): void {
process.method.UpdateCard
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: process.string.CreateCard,
objectClass: card.class.Card,
editor: process.component.CreateCardEditor,
presenter: process.component.CreateCardPresenter,
contextClass: card.class.Card,
requiredParams: ['title', '_class']
},
process.method.CreateCard
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: core.string.AddRelation,
objectClass: core.class.Relation,
editor: process.component.AddRelationEditor,
presenter: process.component.AddRelationPresenter,
contextClass: core.class.Relation,
requiredParams: ['association', 'direction', '_id']
},
process.method.AddRelation
)
builder.createDoc(
process.class.Trigger,
core.space.Model,
+4 -1
View File
@@ -104,7 +104,10 @@ export function createModel (builder: Builder): void {
})
builder.mixin(card.class.Card, core.class.Class, serverCore.mixin.SearchPresenter, {
searchIcon: card.icon.Card,
iconConfig: {
component: card.component.CardIcon,
fields: [['_id']]
},
title: [['title']]
})
}
+8 -8
View File
@@ -65,6 +65,14 @@ export function createModel (builder: Builder): void {
func: serverProcess.func.UpdateCard
})
builder.mixin(process.method.CreateCard, process.class.Method, serverProcess.mixin.MethodImpl, {
func: serverProcess.func.CreateCard
})
builder.mixin(process.method.AddRelation, process.class.Method, serverProcess.mixin.MethodImpl, {
func: serverProcess.func.AddRelation
})
builder.mixin(process.function.FirstValue, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.FirstValue
})
@@ -222,14 +230,6 @@ export function createModel (builder: Builder): void {
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverProcess.trigger.OnStateActionsUpdate,
txMatch: {
_class: core.class.TxUpdateDoc,
objectClass: process.class.State
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverProcess.trigger.OnTransition,
txMatch: {
@@ -17,7 +17,7 @@
import { createEventDispatcher } from 'svelte'
import ui from '../plugin'
import { showPopup } from '../popups'
import type { DropdownIntlItem } from '../types'
import type { ButtonKind, DropdownIntlItem } from '../types'
import Button from './Button.svelte'
import DropdownIcon from './icons/Dropdown.svelte'
import NestedMenu from './NestedMenu.svelte'
@@ -28,6 +28,9 @@
export let disabled: boolean = false
export let selected: DropdownIntlItem | undefined = undefined
export let width: string | undefined = undefined
export let kind: ButtonKind = 'regular'
let container: HTMLElement
let opened: boolean = false
@@ -48,7 +51,7 @@
</script>
<div bind:this={container}>
<Button width={'min-content'} {disabled} on:click={openPopup}>
<Button width={width ?? 'min-content'} {kind} {disabled} on:click={openPopup}>
<span slot="content" class="overflow-label disabled flex-grow text-left mr-2">
<Label label={selected !== undefined ? selected.label : label} />
</span>
@@ -14,15 +14,16 @@
-->
<script lang="ts">
import { Card, MasterTag } from '@hcengineering/card'
import { Asset, getEmbeddedLabel } from '@hcengineering/platform'
import { AnySvelteComponent, Icon, tooltip } from '@hcengineering/ui'
import view, { ObjectPresenterType } from '@hcengineering/view'
import { DocNavLink, ObjectMention } from '@hcengineering/view-resources'
import { getClient, IconWithEmoji } from '@hcengineering/presentation'
import { Ref } from '@hcengineering/core'
import { Asset, getEmbeddedLabel } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { AnySvelteComponent, tooltip } from '@hcengineering/ui'
import { ObjectPresenterType } from '@hcengineering/view'
import { DocNavLink, ObjectMention } from '@hcengineering/view-resources'
import ParentNamesPresenter from './ParentNamesPresenter.svelte'
import card from '../plugin'
import CardIcon from './CardIcon.svelte'
import ParentNamesPresenter from './ParentNamesPresenter.svelte'
export let value: Card | Ref<Card> | undefined
export let disabled: boolean = false
@@ -77,11 +78,7 @@
>
{#if shouldShowAvatar}
<div class="icon" use:tooltip={{ label: _class?.label ?? card.string.Card }}>
<Icon
icon={icon === view.ids.IconWithEmoji ? IconWithEmoji : icon ?? card.icon.Card}
iconProps={{ icon: _class?.color }}
size={'small'}
/>
<CardIcon value={cardObj} />
</div>
{/if}
<span class="overflow-label">
@@ -105,11 +102,7 @@
>
{#if shouldShowAvatar}
<div class="icon" use:tooltip={{ label: _class?.label ?? card.string.Card }}>
<Icon
icon={icon === view.ids.IconWithEmoji ? IconWithEmoji : icon ?? card.icon.Card}
iconProps={{ icon: _class?.color }}
size={'small'}
/>
<CardIcon value={cardObj} />
</div>
{/if}
<span class="overflow-label cropped-text-presenter">
@@ -14,16 +14,16 @@
//
-->
<script lang="ts">
import { Card } from '@hcengineering/card'
import { WithLookup } from '@hcengineering/core'
import card, { Card } from '@hcengineering/card'
import { Icon } from '@hcengineering/ui'
import CardIcon from './CardIcon.svelte'
export let value: WithLookup<Card>
</script>
<div class="flex-row-center">
<div class="flex-center p-1 content-dark-color flex-no-shrink mr-2-5">
<Icon icon={card.icon.Card} size={'medium'} />
<CardIcon {value} size={'medium'} />
</div>
<span class="overflow-label">
{value.title}
@@ -15,19 +15,20 @@
<script lang="ts">
import { Analytics } from '@hcengineering/analytics'
import { Card, CardEvents, MasterTag } from '@hcengineering/card'
import { AnyAttribute, Class, ClassifierKind, Doc, fillDefaults, Ref } from '@hcengineering/core'
import { AnyAttribute, fillDefaults, Ref } from '@hcengineering/core'
import { Card as CardModal, getClient } from '@hcengineering/presentation'
import { DropdownIntlItem, Label, NestedDropdown } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { Label } from '@hcengineering/ui'
import { deepEqual } from 'fast-equals'
import { createEventDispatcher } from 'svelte'
import card from '../plugin'
import TypeSelector from './TypeSelector.svelte'
export let value: Card
const client = getClient()
const hierarchy = client.getHierarchy()
let selected: Ref<MasterTag> | undefined = value._class
let selected: Ref<MasterTag> = value._class
$: mapping = buildMapping(selected, value._class)
@@ -74,47 +75,6 @@
}
const dispatch = createEventDispatcher()
function filterClasses (): [DropdownIntlItem, DropdownIntlItem[]][] {
const descendants = hierarchy.getDescendants(card.class.Card).filter((p) => p !== card.class.Card)
const added = new Set<Ref<Class<Doc>>>()
const base = new Map<Ref<Class<Doc>>, Class<Doc>[]>()
for (const _id of descendants) {
if (added.has(_id)) continue
const _class = hierarchy.getClass(_id)
if (_class.label === undefined) continue
if (_class.kind !== ClassifierKind.CLASS) continue
if ((_class as MasterTag).removed === true) continue
added.add(_id)
const descendants = hierarchy.getDescendants(_id)
const toAdd: Class<Doc>[] = []
for (const desc of descendants) {
if (added.has(desc)) continue
const _class = hierarchy.getClass(desc)
if (_class.label === undefined) continue
if (_class.kind !== ClassifierKind.CLASS) continue
if ((_class as MasterTag).removed === true) continue
added.add(desc)
toAdd.push(_class)
}
base.set(_id, toAdd)
}
const result: [DropdownIntlItem, DropdownIntlItem[]][] = []
for (const [key, value] of base) {
try {
const clazz = hierarchy.getClass(key)
result.push([
{ id: key, label: clazz.label, icon: clazz.icon },
value
.map((it) => ({ id: it._id, label: it.label, icon: it.icon }))
.sort((a, b) => a.label.localeCompare(b.label))
])
} catch {}
}
return result
}
const classes = filterClasses()
</script>
<CardModal
@@ -130,10 +90,5 @@
<div class="mb-2">
<Label label={card.string.ChangeTypeWarning} />
</div>
<NestedDropdown
items={classes}
on:selected={(e) => {
selected = e.detail
}}
/>
<TypeSelector bind:value={selected} />
</CardModal>
@@ -0,0 +1,84 @@
<!--
// 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 { Class, ClassifierKind, Doc, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { DropdownIntlItem, NestedDropdown } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import card from '../plugin'
export let value: Ref<MasterTag>
export let width: string | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
function filterClasses (): [DropdownIntlItem, DropdownIntlItem[]][] {
const descendants = hierarchy.getDescendants(card.class.Card).filter((p) => p !== card.class.Card)
const added = new Set<Ref<Class<Doc>>>()
const base = new Map<Ref<Class<Doc>>, Class<Doc>[]>()
for (const _id of descendants) {
if (added.has(_id)) continue
const _class = hierarchy.getClass(_id)
if (_class.label === undefined) continue
if (_class.kind !== ClassifierKind.CLASS) continue
if ((_class as MasterTag).removed === true) continue
added.add(_id)
const descendants = hierarchy.getDescendants(_id)
const toAdd: Class<Doc>[] = []
for (const desc of descendants) {
if (added.has(desc)) continue
const _class = hierarchy.getClass(desc)
if (_class.label === undefined) continue
if (_class.kind !== ClassifierKind.CLASS) continue
if ((_class as MasterTag).removed === true) continue
added.add(desc)
toAdd.push(_class)
}
base.set(_id, toAdd)
}
const result: [DropdownIntlItem, DropdownIntlItem[]][] = []
for (const [key, value] of base) {
try {
const clazz = hierarchy.getClass(key)
result.push([
{ id: key, label: clazz.label },
value.map((it) => ({ id: it._id, label: it.label })).sort((a, b) => a.label.localeCompare(b.label))
])
} catch {}
}
return result
}
const classes = filterClasses()
const dispatch = createEventDispatcher()
$: selectedClass = hierarchy.getClass(value)
$: selected = {
id: selectedClass._id,
label: selectedClass.label
}
</script>
<NestedDropdown
items={classes}
{width}
{selected}
on:selected={(e) => {
value = e.detail
dispatch('change', value)
}}
/>
+2
View File
@@ -72,6 +72,8 @@ export { default as CardIcon } from './components/CardIcon.svelte'
export { default as Navigator } from './components/navigator-next/Navigator.svelte'
export { default as Favorites } from './components/Favorites.svelte'
export { default as CardPresenter } from './components/CardPresenter.svelte'
export { default as TypeSelector } from './components/TypeSelector.svelte'
export { default as AssociationsSelect } from './components/settings/view/AssociationsSelect.svelte'
export * from './types'
export { getCardIconInfo } from './utils'
+4 -2
View File
@@ -89,7 +89,8 @@
"Absolute": "Absolutní hodnota",
"LogAction": "Událost",
"Started": "Spuštěno",
"Each": "Každý"
"Each": "Každý",
"CreateCard": "Vytvořit kartu"
},
"error": {
"MethodNotFound": "Metoda nenalezena: {methodId}",
@@ -103,6 +104,7 @@
"InternalServerError": "Vnitřní chyba serveru, kontaktujte podporu, chybové id: {errorId}",
"ResultNotProvided": "Výsledek nebyl poskytnut",
"EmptyFunctionResult": "Prázdný výsledek funkce {func}",
"ContextValueNotProvided": "Hodnota kontextu nebyla poskytnuta: {name}"
"ContextValueNotProvided": "Hodnota kontextu nebyla poskytnuta: {name}",
"RequiredParamsNotProvided": "Chybí požadované parametry: {params}"
}
}
+4 -2
View File
@@ -89,7 +89,8 @@
"Absolute": "Absolutwert",
"LogAction": "Ereignis",
"Started": "Gestartet",
"Each": "Jeder"
"Each": "Jeder",
"CreateCard": "Karte erstellen"
},
"error": {
"MethodNotFound": "Methode nicht gefunden: {methodId}",
@@ -103,6 +104,7 @@
"InternalServerError": "Interner Serverfehler, bitte kontaktieren Sie den Support, Fehler-ID: {errorId}",
"ResultNotProvided": "Ergebnis nicht bereitgestellt",
"EmptyFunctionResult": "Leerer Funktionsresultat {func}",
"ContextValueNotProvided": "Kontextwert nicht bereitgestellt: {name}"
"ContextValueNotProvided": "Kontextwert nicht bereitgestellt: {name}",
"RequiredParamsNotProvided": "Erforderliche Parameter nicht bereitgestellt: {params}"
}
}
+4 -2
View File
@@ -89,7 +89,8 @@
"Floor": "Floor",
"LogAction": "Event",
"Started": "Started",
"Each": "Each"
"Each": "Each",
"CreateCard": "Create card"
},
"error": {
"MethodNotFound": "Method not found: {methodId}",
@@ -103,6 +104,7 @@
"InternalServerError": "Internal server error, contact support, error id: {errorId}",
"ResultNotProvided": "Result not provided",
"EmptyFunctionResult": "Empty function result {func}",
"ContextValueNotProvided": "Context value not provided: {name}"
"ContextValueNotProvided": "Context value not provided: {name}",
"RequiredParamsNotProvided": "Required parameters not provided: {params}"
}
}
+4 -2
View File
@@ -89,7 +89,8 @@
"Absolute": "Valor absoluto",
"LogAction": "Evento",
"Started": "Iniciado",
"Each": "Cada"
"Each": "Cada",
"CreateCard": "Crear tarjeta"
},
"error": {
"MethodNotFound": "Método no encontrado: {methodId}",
@@ -103,6 +104,7 @@
"InternalServerError": "Error interno del servidor, contacte con el soporte, id de error: {errorId}",
"ResultNotProvided": "Resultado no proporcionado",
"EmptyFunctionResult": "Resultado de función vacío {func}",
"ContextValueNotProvided": "Valor de contexto no proporcionado: {name}"
"ContextValueNotProvided": "Valor de contexto no proporcionado: {name}",
"RequiredParamsNotProvided": "Faltan parámetros requeridos: {params}"
}
}
+4 -2
View File
@@ -89,7 +89,8 @@
"Absolute": "Valeur absolue",
"LogAction": "Événement",
"Started": "Démarré",
"Each": "Chaque"
"Each": "Chaque",
"CreateCard": "Créer une carte"
},
"error": {
"MethodNotFound": "Méthode introuvable : {methodId}",
@@ -103,6 +104,7 @@
"InternalServerError": "Erreur interne du serveur, contactez le support, id d'erreur : {errorId}",
"ResultNotProvided": "Résultat non fourni",
"EmptyFunctionResult": "Résultat de fonction vide {func}",
"ContextValueNotProvided": "Valeur de contexte non fournie : {name}"
"ContextValueNotProvided": "Valeur de contexte non fournie : {name}",
"RequiredParamsNotProvided": "Paramètres requis non fournis : {params}"
}
}
+4 -2
View File
@@ -89,7 +89,8 @@
"Absolute": "Valore assoluto",
"LogAction": "Evento",
"Started": "Avviato",
"Each": "Ogni"
"Each": "Ogni",
"CreateCard": "Crea scheda"
},
"error": {
"MethodNotFound": "Metodo non trovato: {methodId}",
@@ -103,6 +104,7 @@
"InternalServerError": "Errore interno del server, contatta il supporto, id errore: {errorId}",
"ResultNotProvided": "Risultato non fornito",
"EmptyFunctionResult": "Risultato funzione vuoto {func}",
"ContextValueNotProvided": "Valore di contesto non fornito: {name}"
"ContextValueNotProvided": "Valore di contesto non fornito: {name}",
"RequiredParamsNotProvided": "Parametri richiesti non forniti: {params}"
}
}
+4 -2
View File
@@ -88,7 +88,8 @@
"Absolute": "絶対値",
"LogAction": "イベント",
"Started": "開始",
"Each": "各"
"Each": "各",
"CreateCard": "カードを作成"
},
"error": {
"MethodNotFound": "メソッドが見つかりません: {methodId}",
@@ -101,6 +102,7 @@
"EmptyRelatedObjectValue": "関連オブジェクト {parent} の値が空です: {attr}",
"InternalServerError": "内部サーバーエラー。サポートにお問い合わせください。エラーID: {errorId}",
"EmptyFunctionResult": "関数結果が空です {func}",
"ContextValueNotProvided": "コンテキスト値が提供されていません: {name}"
"ContextValueNotProvided": "コンテキスト値が提供されていません: {name}",
"RequiredParamsNotProvided": "必須パラメータが提供されていません: {params}"
}
}
+4 -2
View File
@@ -89,7 +89,8 @@
"Absolute": "Valor absoluto",
"LogAction": "Evento",
"Started": "Iniciado",
"Each": "Cada"
"Each": "Cada",
"CreateCard": "Criar Cartão"
},
"error": {
"MethodNotFound": "Método não encontrado: {methodId}",
@@ -103,6 +104,7 @@
"InternalServerError": "Erro interno do servidor, contate o suporte, id de erro: {errorId}",
"ResultNotProvided": "Resultado não fornecido",
"EmptyFunctionResult": "Resultado da função vazio {func}",
"ContextValueNotProvided": "Valor de contexto não fornecido: {name}"
"ContextValueNotProvided": "Valor de contexto não fornecido: {name}",
"RequiredParamsNotProvided": "Parâmetros obrigatórios não fornecidos: {params}"
}
}
+4 -2
View File
@@ -89,7 +89,8 @@
"Absolute": "Абсолютное значение",
"LogAction": "Событие",
"Started": "Начат",
"Each": "Каждый"
"Each": "Каждый",
"CreateCard": "Создать карточку"
},
"error": {
"MethodNotFound": "Метод не найден: {methodId}",
@@ -103,6 +104,7 @@
"AttributeNotExists": "Атрибут не существует: {key}",
"ResultNotProvided": "Результат не предоставлен",
"EmptyFunctionResult": "Отсутствуют результат функции {func}",
"ContextValueNotProvided": "Значение контекста не предоставлено: {name}"
"ContextValueNotProvided": "Значение контекста не предоставлено: {name}",
"RequiredParamsNotProvided": "Отсутствуют обязательные параметры: {params}"
}
}
+4 -2
View File
@@ -89,7 +89,8 @@
"Absolute": "绝对值",
"LogAction": "事件",
"Started": "已启动",
"Each": "每个"
"Each": "每个",
"CreateCard": "创建卡片"
},
"error": {
"MethodNotFound": "找不到方法:{methodId}",
@@ -103,6 +104,7 @@
"InternalServerError": "内部服务器错误,请联系支持,错误 ID:{errorId}",
"ResultNotProvided": "未提供结果",
"EmptyFunctionResult": "空函数结果 {func}",
"ContextValueNotProvided": "未提供上下文值:{name}"
"ContextValueNotProvided": "未提供上下文值:{name}",
"RequiredParamsNotProvided": "未提供必需的参数:{params}"
}
}
@@ -13,10 +13,11 @@
// limitations under the License.
-->
<script lang="ts">
import { Execution } from '@hcengineering/process'
import { Doc, WithLookup } from '@hcengineering/core'
import presentation, { ActionContext, Card, createQuery } from '@hcengineering/presentation'
import presentation, { ActionContext, createQuery } from '@hcengineering/presentation'
import { Execution } from '@hcengineering/process'
import { Modal, registerFocus } from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference, ViewOptions } from '@hcengineering/view'
import {
List,
ListSelectionProvider,
@@ -26,7 +27,6 @@
} from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import process from '../plugin'
import view, { Viewlet, ViewletPreference, ViewOptions } from '@hcengineering/view'
export let execution: Execution
@@ -43,7 +43,7 @@
}
)
let docs: Doc[] = []
function select () {
function select (): void {
listProvider.update(docs)
listProvider.updateFocus(docs[0])
list?.select(0, undefined)
@@ -68,6 +68,7 @@
let preference: ViewletPreference | undefined = undefined
const query = createQuery()
const viewletId = process.viewlet.ExecutionLogList
$: query.query(
view.class.Viewlet,
@@ -86,8 +87,6 @@
const preferenceQuery = createQuery()
const viewletId = process.viewlet.ExecutionLogList
$: if (viewlet != null) {
preferenceQuery.query(
view.class.ViewletPreference,
@@ -13,8 +13,9 @@
// limitations under the License.
-->
<script lang="ts">
import { MasterTag, Tag } from '@hcengineering/card'
import { AnyAttribute, Class, Doc, Ref } from '@hcengineering/core'
import { Context, parseContext, SelectedContext, Process } from '@hcengineering/process'
import { Context, parseContext, Process, SelectedContext } from '@hcengineering/process'
import {
AnySvelteComponent,
Button,
@@ -29,7 +30,6 @@
import { createEventDispatcher } from 'svelte'
import ContextSelectorPopup from './attributeEditors/ContextSelectorPopup.svelte'
import ContextValue from './attributeEditors/ContextValue.svelte'
import { MasterTag, Tag } from '@hcengineering/card'
export let process: Process
export let masterTag: Ref<MasterTag | Tag>
@@ -61,6 +61,7 @@
masterTag,
context,
attribute,
forbidValue,
onSelect
},
eventToHTMLElement(e)
@@ -73,69 +74,68 @@
}
</script>
{#if editor}
<span
class="labelOnPanel"
use:tooltip={{
props: { label: attribute.label }
}}
>
<Label label={attribute.label} />
</span>
<div class="text-input" class:context={contextValue}>
{#if contextValue}
<ContextValue
{process}
{masterTag}
{contextValue}
{context}
{attribute}
{allowArray}
category={presenterClass.category}
attrClass={presenterClass.attrClass}
on:update={(e) => {
onSelect(e.detail)
}}
/>
{:else}
<div class="w-full">
{#if !forbidValue}
<svelte:component
this={editor}
label={attribute?.label}
placeholder={attribute?.label}
kind={'ghost'}
size={'large'}
width={'100%'}
justify={'left'}
type={attribute?.type}
{value}
{onChange}
{focus}
/>
{/if}
</div>
{/if}
<div class="button flex-row-center">
<Button
icon={IconAdd}
kind="ghost"
on:click={(e) => {
selectContext(e)
}}
/>
{#if allowRemove}
<Button
icon={IconClose}
kind="ghost"
on:click={() => {
dispatch('remove', { key: attribute.name })
}}
<span
class="labelOnPanel"
use:tooltip={{
props: { label: attribute.label }
}}
>
<Label label={attribute.label} />
</span>
<div class="text-input" class:context={contextValue}>
{#if contextValue}
<ContextValue
{process}
{masterTag}
{contextValue}
{context}
{attribute}
{allowArray}
category={presenterClass.category}
attrClass={presenterClass.attrClass}
{forbidValue}
on:update={(e) => {
onSelect(e.detail)
}}
/>
{:else}
<div class="w-full">
{#if !forbidValue && editor}
<svelte:component
this={editor}
label={attribute?.label}
placeholder={attribute?.label}
kind={'ghost'}
size={'large'}
width={'100%'}
justify={'left'}
type={attribute?.type}
{value}
{onChange}
{focus}
/>
{/if}
</div>
{/if}
<div class="button flex-row-center">
<Button
icon={IconAdd}
kind="ghost"
on:click={(e) => {
selectContext(e)
}}
/>
{#if allowRemove}
<Button
icon={IconClose}
kind="ghost"
on:click={() => {
dispatch('remove', { key: attribute.name })
}}
/>
{/if}
</div>
{/if}
</div>
<style lang="scss">
.text-input {
@@ -13,6 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { MasterTag, Tag } from '@hcengineering/card'
import { AnyAttribute, Class, Doc, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Context, Func, Process, ProcessFunction, SelectedContext } from '@hcengineering/process'
@@ -33,7 +34,6 @@
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
import FallbackEditor from '../contextEditors/FallbackEditor.svelte'
import { MasterTag, Tag } from '@hcengineering/card'
export let process: Process
export let masterTag: Ref<MasterTag | Tag>
@@ -44,6 +44,7 @@
export let category: AttributeCategory
export let onChange: (contextValue: SelectedContext) => void
export let allowArray: boolean = false
export let forbidValue: boolean = false
const client = getClient()
@@ -321,50 +322,52 @@
{/if}
<div class="menu-separator" />
{/if}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
bind:this={elements[functionButtonIndex + 1]}
on:keydown={(event) => {
keyDown(event, functionButtonIndex + 1)
}}
on:mouseover={() => {
elements[functionButtonIndex + 1]?.focus()
}}
on:click={onFallbackChange}
class="menu-item flex-gap-2 fallback"
>
<div>
<div class="label">
<Label label={plugin.string.Required} />
</div>
<div class="text-sm">
<Label label={plugin.string.FallbackValueError} />
</div>
</div>
<CheckBox
on:click={onFallbackChange}
checked={contextValue.fallbackValue === undefined}
size={'medium'}
kind={'primary'}
/>
</button>
{#if contextValue.fallbackValue !== undefined}
{#if !forbidValue}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
bind:this={elements[functionButtonIndex + 2]}
bind:this={elements[functionButtonIndex + 1]}
on:keydown={(event) => {
keyDown(event, functionButtonIndex + 2)
keyDown(event, functionButtonIndex + 1)
}}
on:mouseover={() => {
elements[functionButtonIndex + 2]?.focus()
elements[functionButtonIndex + 1]?.focus()
}}
on:click={onFallback}
class="menu-item"
on:click={onFallbackChange}
class="menu-item flex-gap-2 fallback"
>
<span class="overflow-label pr-1">
<Label label={plugin.string.FallbackValue} />
</span>
<div>
<div class="label">
<Label label={plugin.string.Required} />
</div>
<div class="text-sm">
<Label label={plugin.string.FallbackValueError} />
</div>
</div>
<CheckBox
on:click={onFallbackChange}
checked={contextValue.fallbackValue === undefined}
size={'medium'}
kind={'primary'}
/>
</button>
{#if contextValue.fallbackValue !== undefined}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
bind:this={elements[functionButtonIndex + 2]}
on:keydown={(event) => {
keyDown(event, functionButtonIndex + 2)
}}
on:mouseover={() => {
elements[functionButtonIndex + 2]?.focus()
}}
on:click={onFallback}
class="menu-item"
>
<span class="overflow-label pr-1">
<Label label={plugin.string.FallbackValue} />
</span>
</button>
{/if}
{/if}
</Scroller>
<div class="menu-space" />
@@ -18,7 +18,6 @@
import { getClient } from '@hcengineering/presentation'
import {
Context,
ContextId,
Process,
ProcessContext,
ProcessFunction,
@@ -28,7 +27,7 @@
import { Label, resizeObserver, Scroller, Submenu } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
import { generateContextId, getRelationObjectReduceFunc, getValueReduceFunc, isTypeEqual } from '../../utils'
import { generateContextId, getRelationObjectReduceFunc, getValueReduceFunc } from '../../utils'
import ExecutionContextPresenter from './ExecutionContextPresenter.svelte'
export let process: Process
@@ -36,6 +35,7 @@
export let context: Context
export let attribute: AnyAttribute
export let onSelect: (val: SelectedContext | null) => void
export let forbidValue: boolean = false
const dispatch = createEventDispatcher()
@@ -58,18 +58,7 @@
})
}
$: processContext = getProcessContext(process, attribute)
function getProcessContext (process: Process, attribute: AnyAttribute): ProcessContext[] {
const res: ProcessContext[] = []
for (const key in process?.context ?? {}) {
const ctx = process.context[key as ContextId]
if (ctx._class === attribute.type._class && isTypeEqual(ctx.type, attribute.type)) {
res.push(ctx)
}
}
return res
}
$: processContext = Object.values(context.executionContext)
$: nested = Object.values(context.nested)
$: relations = Object.entries(context.relations)
@@ -236,17 +225,19 @@
{/each}
<div class="menu-separator" />
{/if}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
on:click={() => {
onCustom()
}}
class="menu-item"
>
<span class="overflow-label pr-1">
<Label label={plugin.string.CustomValue} />
</span>
</button>
{#if !forbidValue}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
on:click={() => {
onCustom()
}}
class="menu-item"
>
<span class="overflow-label pr-1">
<Label label={plugin.string.CustomValue} />
</span>
</button>
{/if}
</Scroller>
<div class="menu-space" />
</div>
@@ -13,14 +13,14 @@
// limitations under the License.
-->
<script lang="ts">
import { SelectedContext, Context, Process } from '@hcengineering/process'
import { MasterTag, Tag } from '@hcengineering/card'
import { AnyAttribute, Class, Doc, Ref } from '@hcengineering/core'
import { Context, Process, SelectedContext } from '@hcengineering/process'
import { eventToHTMLElement, showPopup } from '@hcengineering/ui'
import ConfigurePopup from './ConfigurePopup.svelte'
import { Ref, Class, Doc, AnyAttribute } from '@hcengineering/core'
import ContextValuePresenter from './ContextValuePresenter.svelte'
import { AttributeCategory } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import { MasterTag, Tag } from '@hcengineering/card'
import ConfigurePopup from './ConfigurePopup.svelte'
import ContextValuePresenter from './ContextValuePresenter.svelte'
export let process: Process
export let masterTag: Ref<MasterTag | Tag>
@@ -30,6 +30,7 @@
export let attrClass: Ref<Class<Doc>>
export let category: AttributeCategory
export let allowArray: boolean = false
export let forbidValue: boolean = false
const dispatch = createEventDispatcher()
@@ -50,7 +51,7 @@
}
showPopup(
ConfigurePopup,
{ contextValue, attrClass, masterTag, category, attribute, context, onChange, allowArray },
{ contextValue, attrClass, masterTag, category, attribute, context, onChange, allowArray, forbidValue },
eventToHTMLElement(e)
)
}
@@ -1,12 +0,0 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large' | 'full'
const fill: string = 'currentColor'
</script>
<svg xmlns="http://www.w3.org/2000/svg" class="svg-{size}" viewBox="0 0 16 45" fill="none">
<path d="M8 0L8 43" stroke={fill} />
<path
d="M12.295 37.295L8.5 41.085V31.5C8.5 31.22386 8.27614 31 8 31C7.72386 31 7.5 31.22386 7.5 31.5V41.085L3.705 37.295L3 38L8 43L13 38L12.295 37.295Z"
{fill}
/>
</svg>
@@ -1,11 +0,0 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large' | 'full'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" viewBox="0 0 6 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M3 0.333333C1.52724 0.333333 0.333333 1.52724 0.333333 3C0.333333 4.47276 1.52724 5.66667 3 5.66667C4.47276 5.66667 5.66667 4.47276 5.66667 3C5.66667 1.52724 4.47276 0.333333 3 0.333333ZM2.5 3L2.5 48L3.5 48L3.5 3L2.5 3Z"
{fill}
/>
</svg>
@@ -1,33 +0,0 @@
<script lang="ts">
import { ColorDefinition, IconSize, getPlatformColorDef, themeStore } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
export let size: IconSize = 'small'
export let fill: number = 21
const dispatch = createEventDispatcher()
const dispatchAccentColor = (color?: ColorDefinition) => dispatch('accent-color', color)
$: color = getPlatformColorDef(fill, $themeStore.dark)
$: dispatchAccentColor(color)
onMount(() => {
dispatchAccentColor(color)
})
</script>
<svg
class="svg-{size}"
fill={color?.icon ?? 'currentColor'}
style:flex-shrink={0}
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.97975 1.07389C3.92356 1.52014 1.50831 3.94118 1.0708 7.0002H2.08287C2.50337 4.49345 4.47822 2.51374 6.9825 2.08599L6.97975 1.07389ZM8.98249 2.08014L8.97974 1.06812C12.055 1.49885 14.4896 3.92773 14.9291 7.0002H13.917C13.4945 4.48183 11.5033 2.4954 8.98249 2.08014ZM9.01467 13.9146C11.5201 13.4879 13.4962 11.5077 13.917 9.00019H14.929C14.4913 12.06 12.0748 14.4815 9.01742 14.9267L9.01467 13.9146ZM2.0829 9.0002C2.50529 11.5176 4.49522 13.5034 7.01468 13.9196L7.01743 14.9317C3.94351 14.4999 1.51022 12.0717 1.07083 9.0002H2.0829Z"
/>
</svg>
@@ -1,34 +0,0 @@
<script lang="ts">
import { ColorDefinition, IconSize, getPlatformColorDef, themeStore } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
export let size: IconSize = 'small'
export let fill: number = 17
const dispatch = createEventDispatcher()
const dispatchAccentColor = (color?: ColorDefinition) => dispatch('accent-color', color)
$: color = getPlatformColorDef(fill, $themeStore.dark)
$: dispatchAccentColor(color)
onMount(() => {
dispatchAccentColor(color)
})
</script>
<svg
class="svg-{size}"
fill={color?.icon ?? 'currentColor'}
style:flex-shrink={0}
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8,1C4.1,1,1,4.1,1,8c0,3.9,3.1,7,7,7c3.9,0,7-3.1,7-7C15,4.1,11.9,1,8,1z M8,14c-3.3,0-6-2.7-6-6s2.7-6,6-6s6,2.7,6,6S11.3,14,8,14z"
/>
<path
d="M10.6,6.1L7.5,9.2L5.9,7.6c-0.2-0.2-0.6-0.2-0.8,0s-0.2,0.6,0,0.8l2,2c0.1,0.1,0.3,0.2,0.4,0.2s0.3-0.1,0.4-0.2l3.5-3.5c0.2-0.2,0.2-0.6,0-0.8S10.8,5.8,10.6,6.1z"
/>
</svg>
@@ -1,53 +0,0 @@
<script lang="ts">
import { ColorDefinition, IconSize, getPlatformColorDef, themeStore } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
export let size: IconSize = 'small'
export let fill: number = 11
const dispatch = createEventDispatcher()
const dispatchAccentColor = (color?: ColorDefinition) => dispatch('accent-color', color)
export let count: number | undefined = undefined
export let index: number | undefined = undefined
$: color = getPlatformColorDef(fill, $themeStore.dark)
$: dispatchAccentColor(color)
onMount(() => {
dispatchAccentColor(color)
})
</script>
<svg
class="svg-{size}"
fill={color?.icon ?? 'currentColor'}
style:flex-shrink={0}
style:transform={'rotate(-90deg)'}
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14ZM8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15Z"
/>
{#if count !== undefined && index !== undefined}
<path
d="M 4.5 4.5 L 9 4.5 A 4.5 4.5 0 {index > (count - 1) / 2 ? 1 : 0} 1 {Math.cos(
((2 * Math.PI) / count) * index - 0.01
) *
4.5 +
4.5} {Math.sin(((2 * Math.PI) / count) * index - 0.01) * 4.5 + 4.5} Z"
transform="translate(3.5,3.5)"
/>
{:else}
<path
d="M 4.5 4.5 L 9 4.5 A 4.5 4.5 0 1 1 {Math.cos(Math.PI - 0.01) * 4.5 + 4.5} {Math.sin(Math.PI - 0.01) * 4.5 +
4.5} Z"
transform="translate(3.5,3.5)"
/>
{/if}
</svg>
@@ -0,0 +1,62 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Card } from '@hcengineering/card'
import { Association, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { MethodParams, parseContext, Process, Step } from '@hcengineering/process'
import { Label } from '@hcengineering/ui'
import plugin from '../../plugin'
import { getContext } from '../../utils'
import ContextValuePresenter from '../attributeEditors/ContextValuePresenter.svelte'
export let step: Step<Card>
export let process: Process
export let params: MethodParams<Card>
const client = getClient()
$: method = client.getModel().findAllSync(plugin.class.Method, { _id: step.methodId })[0]
$: association = params.association as Ref<Association> | undefined
$: selected = association && client.getModel().findObject(association)
$: direction = params.direction as 'A' | 'B' | undefined
$: assoc = association && client.getModel().findObject(association)
$: targetClass = assoc && direction ? (direction === 'A' ? assoc.classA : assoc.classB) : undefined
$: contextValue = params._id !== undefined ? parseContext(params._id) : undefined
$: context = targetClass !== undefined ? getContext(client, process, targetClass, 'object') : undefined
</script>
<div class="flex-row-center flex-gap-1">
<Label label={method.label} />:
<div class="title">
{#if selected && direction}
{direction === 'A' ? selected.nameA : selected.nameB}
{/if}
</div>
{#if contextValue !== undefined && context !== undefined}
<ContextValuePresenter {contextValue} {context} {process} />
{/if}
</div>
<style lang="scss">
.title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--theme-caption-color);
}
</style>
@@ -0,0 +1,71 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import card, { Card, MasterTag } from '@hcengineering/card'
import core, { Ref } from '@hcengineering/core'
import { getClient, IconWithEmoji } from '@hcengineering/presentation'
import { MethodParams, parseContext, Process, Step } from '@hcengineering/process'
import { Icon, Label, tooltip } from '@hcengineering/ui'
import view from '@hcengineering/view'
import plugin from '../../plugin'
import { getContext } from '../../utils'
import ContextValuePresenter from '../attributeEditors/ContextValuePresenter.svelte'
export let step: Step<Card>
export let process: Process
export let params: MethodParams<Card>
const client = getClient()
$: method = client.getModel().findAllSync(plugin.class.Method, { _id: step.methodId })[0]
$: contextValue = params.title !== undefined ? parseContext(params.title) : undefined
$: context = getContext(client, process, core.class.TypeString, 'attribute')
$: _class = params?._class
? (client.getHierarchy().getClass(params._class as Ref<MasterTag>) as MasterTag)
: undefined
$: icon = _class?.icon
</script>
<div class="flex-row-center flex-gap-1">
<Label label={method.label} />:
{#if icon}
<div class="icon" use:tooltip={{ label: _class?.label ?? card.string.Card }}>
<Icon
icon={icon === view.ids.IconWithEmoji ? IconWithEmoji : icon ?? card.icon.Card}
iconProps={{ icon: _class?.color }}
size={'small'}
/>
</div>
{/if}
{#if params.title}
<div class="title">
{#if contextValue}
<ContextValuePresenter {contextValue} {context} {process} />
{:else}
{params.title}
{/if}
</div>
{/if}
</div>
<style lang="scss">
.title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--theme-caption-color);
}
</style>
@@ -0,0 +1,120 @@
<!--
// 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, { AnyAttribute, Association, Doc, generateId, Ref, RefTo, Relation } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Process, Step } from '@hcengineering/process'
import { Label, tooltip } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { getContext } from '../../utils'
import ProcessAttribute from '../ProcessAttribute.svelte'
import AssociationSelector from './AssociationSelector.svelte'
export let process: Process
export let step: Step<Relation>
const dispatch = createEventDispatcher()
const params = step.params
let association = params.association as Ref<Association> | undefined
let direction = params.direction as 'A' | 'B' | undefined
const _id = params._id
const client = getClient()
$: assoc = association && client.getModel().findObject(association)
$: targetClass = assoc && direction ? (direction === 'A' ? assoc.classA : assoc.classB) : undefined
function changeAssociation (e: CustomEvent<any>): void {
if (e.detail !== undefined) {
association = e.detail.association
direction = e.detail.direction
params.association = association
params.direction = direction
;(step.params as any) = params
dispatch('change', step)
}
}
function changeParam (e: CustomEvent<any>): void {
if (e.detail !== undefined) {
params._id = e.detail
;(step.params as any)._id = e.detail
dispatch('change', step)
}
}
let attribute: AnyAttribute
$: attribute = {
attributeOf: process.masterTag,
name: '',
type: {
label: core.string.Ref,
_class: core.class.RefTo,
to: targetClass
},
_id: generateId(),
space: core.space.Model,
modifiedOn: 0,
modifiedBy: core.account.System,
_class: core.class.Attribute,
label: core.string.Object
}
$: context = targetClass && getContext(client, process, targetClass, 'object')
</script>
<div class="grid">
<span
class="labelOnPanel"
use:tooltip={{
props: { label: core.string.Relation }
}}
>
<Label label={core.string.Relation} />
</span>
<AssociationSelector {process} {association} {direction} on:change={changeAssociation} />
{#if targetClass && context && attribute}
<ProcessAttribute
value={_id}
{process}
{attribute}
{context}
masterTag={process.masterTag}
editor={undefined}
presenterClass={{
attrClass: targetClass,
category: 'object'
}}
forbidValue
on:change={changeParam}
/>
{/if}
</div>
<style lang="scss">
.grid {
display: grid;
grid-template-columns: 1fr 1.5fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
margin: 0.25rem 2rem 0;
width: calc(100% - 4rem);
height: min-content;
}
</style>
@@ -0,0 +1,84 @@
<!--
// 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, { Association, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Process } from '@hcengineering/process'
import { Button, eventToHTMLElement, Label, SelectPopup, SelectPopupValueType, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
export let process: Process
export let association: Ref<Association> | undefined = undefined
export let direction: 'A' | 'B' | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
function open (e: MouseEvent): void {
const descendants = hierarchy.getDescendants(process.masterTag)
const leftAssociations = client.getModel().findAllSync(core.class.Association, { classA: { $in: descendants } })
const rightAssociations = client.getModel().findAllSync(core.class.Association, { classB: { $in: descendants } })
const items: SelectPopupValueType[] = []
rightAssociations.forEach((a) => {
items.push({
id: 'A_' + a._id,
text: a.nameA,
isSelected: association !== undefined && association === a._id && direction === 'A'
})
})
leftAssociations.forEach((a) => {
items.push({
id: 'B_' + a._id,
text: a.nameB,
isSelected: association !== undefined && association === a._id && direction === 'B'
})
})
showPopup(
SelectPopup,
{
value: items
},
eventToHTMLElement(e),
(res) => {
if (res !== undefined) {
const [dir, id] = res.split('_')
association = id as Ref<Association>
direction = dir as 'A' | 'B'
} else {
association = undefined
direction = undefined
}
dispatch('change', {
association,
direction
})
}
)
}
$: selected = association !== undefined && client.getModel().findObject(association)
</script>
<Button on:click={open} width={'100%'}>
<div slot="content">
{#if selected && direction !== undefined}
{direction === 'A' ? selected.nameA : selected.nameB}
{:else}
<Label label={core.string.Relation} />
{/if}
</div>
</Button>
@@ -27,12 +27,12 @@
const client = getClient()
async function save () {
async function save (): Promise<void> {
await client.update(process, { context: process.context })
clearSettingsStore()
}
function onNameChange (ev: Event, _id: string, ctx: ProcessContext) {
function onNameChange (ev: Event, _id: string, ctx: ProcessContext): void {
const value = (ev.target as HTMLInputElement).value?.trim()
ctx.name = value
process.context[_id as ContextId] = ctx
@@ -0,0 +1,170 @@
<!--
// 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 cardPlugin, { Card, MasterTag } from '@hcengineering/card'
import { TypeSelector } from '@hcengineering/card-resources'
import core, { AnyAttribute, Class, Ref } from '@hcengineering/core'
import { translateCB } from '@hcengineering/platform'
import presentation, { getClient } from '@hcengineering/presentation'
import { MethodParams, Process, Step } from '@hcengineering/process'
import { Button, eventToHTMLElement, Label, SelectPopup, showPopup, tooltip } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import ParamsEditor from './ParamsEditor.svelte'
export let process: Process
export let step: Step<Card>
const dispatch = createEventDispatcher()
step.params.title = step.params.title ?? ''
step.params._class = step.params._class ?? process.masterTag
let params = step.params
let _class: Ref<Class<MasterTag>> = (params._class as Ref<Class<MasterTag>>) ?? process.masterTag
const client = getClient()
const hierarchy = client.getHierarchy()
translateCB(cardPlugin.string.Card, {}, undefined, (res) => {
if (params.title === undefined || params.title === '') {
params.title = res
;(step.params as any) = params
dispatch('change', step)
}
})
function change (e: CustomEvent<any>): void {
if (e.detail !== undefined) {
params = e.detail
;(step.params as any) = e.detail
dispatch('change', step)
}
}
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
}
let keys = Object.keys(params).filter((key) => {
return key !== '_class'
})
$: allAttrs = getKeys(_class)
$: possibleAttrs = allAttrs.filter((attr) => !keys.includes(attr.name))
function addKey (key: string): void {
keys = [...keys, key]
}
function onAdd (e: MouseEvent): void {
showPopup(
SelectPopup,
{
value: possibleAttrs.map((p) => {
return { id: p.name, label: p.label }
})
},
eventToHTMLElement(e),
(res) => {
if (res != null) {
addKey(res)
}
}
)
}
function remove (e: CustomEvent<any>): void {
if (e.detail !== undefined) {
const key = e.detail.key
if (key === 'title') return
keys = keys.filter((k) => k !== key)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (params as any)[key]
;(step.params as any) = params
dispatch('change', step)
}
}
function typeChange (e: CustomEvent<Ref<Class<MasterTag>>>): void {
if (e.detail === undefined || e.detail === _class) return
allAttrs = getKeys(e.detail)
const oldParams = { ...params }
params = {}
for (const attr of allAttrs) {
const key = attr.name
if (oldParams[key] !== undefined) {
;(params as any)[key] = oldParams[key]
}
}
_class = e.detail
keys = Object.keys(params)
params._class = _class
step.params = params
if (step.context != null) {
step.context._class = _class
}
dispatch('change', step)
}
</script>
<div class="grid">
<span
class="labelOnPanel"
use:tooltip={{
props: { label: cardPlugin.string.MasterTag }
}}
>
<Label label={cardPlugin.string.MasterTag} />
</span>
<TypeSelector value={_class} width={'100%'} on:change={typeChange} />
</div>
<div class="divider" />
{#key _class}
<ParamsEditor {_class} {process} {keys} {params} allowRemove on:remove={remove} on:change={change} />
{/key}
{#if possibleAttrs.length > 0}
<div class="flex-center mt-4">
<Button label={presentation.string.Add} width={'100%'} kind={'link-bordered'} size={'large'} on:click={onAdd} />
</div>
{/if}
<style lang="scss">
.divider {
border-bottom: 1px solid var(--divider-color);
margin: 1rem 0;
}
.grid {
display: grid;
grid-template-columns: 1fr 1.5fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
margin: 0.25rem 2rem 0;
width: calc(100% - 4rem);
height: min-content;
}
</style>
@@ -14,7 +14,7 @@
-->
<script lang="ts">
import { Class, Doc, Ref } from '@hcengineering/core'
import { MethodParams, Process, State } from '@hcengineering/process'
import { MethodParams, Process } from '@hcengineering/process'
import { createEventDispatcher } from 'svelte'
import ProcessAttributeEditor from './ProcessAttributeEditor.svelte'
@@ -15,7 +15,7 @@
<script lang="ts">
import core, { Class, Doc, PropertyType, Ref, Type } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { ContextId, Step } from '@hcengineering/process'
import { Step } from '@hcengineering/process'
import setting from '@hcengineering/setting-resources/src/plugin'
import {
AnyComponent,
@@ -24,13 +24,13 @@
const client = getClient()
const query = createQuery()
$: from = transition.from && client.getModel().findObject(transition.from)
$: to = client.getModel().findObject(transition.to)
$: query.query(plugin.class.State, { process: transition.process }, () => {
from = transition.from && client.getModel().findObject(transition.from)
to = client.getModel().findObject(transition.to)
})
$: from = transition.from && client.getModel().findObject(transition.from)
$: to = client.getModel().findObject(transition.to)
</script>
<span>
@@ -14,7 +14,6 @@
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Process, Transition } from '@hcengineering/process'
import { Button, getCurrentLocation, IconAdd, Label, navigate, showPopup } from '@hcengineering/ui'
import plugin from '../../plugin'
@@ -25,8 +24,6 @@
export let transitions: Transition[]
export let readonly: boolean
const client = getClient()
function addTransition (): void {
showPopup(AddTransitionPopup, { process }, 'top')
}
@@ -16,10 +16,10 @@
import { Card, MasterTag } from '@hcengineering/card'
import core, { AnyAttribute, Class, Ref } from '@hcengineering/core'
import presentation, { getClient } from '@hcengineering/presentation'
import { Process, State, Step } from '@hcengineering/process'
import { Process, Step } from '@hcengineering/process'
import { Button, eventToHTMLElement, SelectPopup, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import ParamsEditor from './ParamsEditor.svelte'
import { Button, eventToHTMLElement, SelectPopup, showPopup } from '@hcengineering/ui'
export let process: Process
export let step: Step<Card>
+12 -4
View File
@@ -44,15 +44,19 @@ import UpdateCardEditor from './components/settings/UpdateCardEditor.svelte'
import DateOffsetEditor from './components/transformEditors/DateOffsetEditor.svelte'
import NumberEditor from './components/transformEditors/NumberEditor.svelte'
import LogActionPresenter from './components/LogActionPresenter.svelte'
import NotifierExtension from './components/NotifierExtension.svelte'
import AddRelationPresenter from './components/presenters/AddRelationPresenter.svelte'
import CreateCardPresenter from './components/presenters/CreateCardPresenter.svelte'
import AddRelationEditor from './components/settings/AddRelationEditor.svelte'
import CreateCardEditor from './components/settings/CreateCardEditor.svelte'
import TransitionRefPresenter from './components/settings/TransitionRefPresenter.svelte'
import AppendEditor from './components/transformEditors/AppendEditor.svelte'
import CutEditor from './components/transformEditors/CutEditor.svelte'
import ReplaceEditor from './components/transformEditors/ReplaceEditor.svelte'
import SplitEditor from './components/transformEditors/SplitEditor.svelte'
import { ProcessMiddleware } from './middleware'
import { continueExecution, showDoneQuery, todoTranstionCheck } from './utils'
import LogActionPresenter from './components/LogActionPresenter.svelte'
import TransitionRefPresenter from './components/settings/TransitionRefPresenter.svelte'
import NotifierExtension from './components/NotifierExtension.svelte'
export default async (): Promise<Resources> => ({
actionImpl: {
@@ -90,7 +94,11 @@ export default async (): Promise<Resources> => ({
TransitionEditor,
TransitionRefPresenter,
LogActionPresenter,
NotifierExtension
NotifierExtension,
CreateCardEditor,
CreateCardPresenter,
AddRelationEditor,
AddRelationPresenter
},
transformEditor: {
DateOffsetEditor,
+7 -2
View File
@@ -56,7 +56,11 @@ export default mergeIds(processId, process, {
StateEditor: '' as AnyComponent,
TransitionRefPresenter: '' as AnyComponent,
LogActionPresenter: '' as AnyComponent,
NotifierExtension: '' as AnyComponent
NotifierExtension: '' as AnyComponent,
CreateCardEditor: '' as AnyComponent,
CreateCardPresenter: '' as AnyComponent,
AddRelationEditor: '' as AnyComponent,
AddRelationPresenter: '' as AnyComponent
},
transformEditor: {
DateOffsetEditor: '' as AnyComponent,
@@ -151,6 +155,7 @@ export default mergeIds(processId, process, {
Start: '' as IntlString,
End: '' as IntlString,
Started: '' as IntlString,
Each: '' as IntlString
Each: '' as IntlString,
CreateCard: '' as IntlString
}
})
+27 -7
View File
@@ -40,6 +40,7 @@ import {
type Method,
type NestedContext,
type Process,
type ProcessContext,
type ProcessFunction,
type RelatedContext,
type SelectedContext,
@@ -140,7 +141,13 @@ export async function initState<T extends Doc> (methodId: Ref<Method<T>>): Promi
}
const step: Step<T> = {
_id: generateId() as string as StepId,
contextId: method.contextClass !== null ? generateContextId() : null,
context:
method.contextClass !== null
? {
_id: generateContextId(),
_class: method.contextClass
}
: null,
methodId,
params: {}
}
@@ -164,6 +171,7 @@ export function getContext (
const functions = getContextFunctions(client, process.masterTag, target, category)
const nested: Record<string, NestedContext> = {}
const relations: Record<string, RelatedContext> = {}
const executionContext: Record<string, ProcessContext> = {}
const refs = getClassAttributes(client, process.masterTag, core.class.RefTo, 'attribute')
for (const ref of refs) {
@@ -230,11 +238,21 @@ export function getContext (
}
}
if (category === 'object') {
for (const key in process.context) {
const value = process.context[key as ContextId]
if (client.getHierarchy().isDerived(value._class, target)) {
executionContext[key] = value
}
}
}
return {
functions,
attributes,
nested,
relations
relations,
executionContext
}
}
@@ -443,8 +461,8 @@ export async function getSubProcessesUserInput (
if (processId === undefined) continue
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const res = await newExecutionUserInput(processId, {} as ExecutionContext)
if (action.contextId == null) continue
userContext[action.contextId] = res
if (action.context == null) continue
userContext[action.context._id] = res
}
return userContext
}
@@ -501,11 +519,13 @@ export function getToDoEndAction (prevState: State): Step<Doc> {
key: 'user',
_class: process.class.ProcessToDo
}
const endAction = {
const endAction: Step<Doc> = {
_id: generateId() as string as StepId,
contextId: generateContextId(),
context: {
_id: context.id,
_class: process.class.ProcessToDo
},
methodId: process.method.CreateToDo,
system: true,
params: {
state: prevState._id,
title: prevState.title,
+14 -5
View File
@@ -12,7 +12,7 @@
// limitations under the License.
import { Card, MasterTag, Tag } from '@hcengineering/card'
import { Class, Doc, DocumentUpdate, ObjQueryType, Ref, Tx, Type } from '@hcengineering/core'
import { Association, Class, Doc, DocumentUpdate, ObjQueryType, Ref, Tx, Type } from '@hcengineering/core'
import { Asset, IntlString, Plugin, plugin, Resource } from '@hcengineering/platform'
import { ToDo } from '@hcengineering/time'
import { AnyComponent } from '@hcengineering/ui'
@@ -116,7 +116,8 @@ export interface ProcessToDo extends ToDo {
export type MethodParams<T extends Doc> = {
[P in keyof T]?: ObjQueryType<T[P]> | string
} & DocumentUpdate<T>
} & DocumentUpdate<T> &
Record<string, any>
export interface State extends Doc {
process: Ref<Process>
@@ -127,12 +128,17 @@ export type StepId = string & { __stepId: true }
export interface Step<T extends Doc> {
_id: StepId
contextId: ContextId | null
context: StepContext | null
methodId: Ref<Method<T>>
params: MethodParams<T>
result?: StepResult
}
export interface StepContext {
_id: ContextId // context id
_class?: Ref<Class<Doc>> // class of the context
}
export interface StepResult {
_id: ContextId // context id
name: string
@@ -177,7 +183,9 @@ export default plugin(processId, {
method: {
RunSubProcess: '' as Ref<Method<Process>>,
CreateToDo: '' as Ref<Method<ProcessToDo>>,
UpdateCard: '' as Ref<Method<Card>>
UpdateCard: '' as Ref<Method<Card>>,
CreateCard: '' as Ref<Method<Card>>,
AddRelation: '' as Ref<Method<Association>>
},
trigger: {
OnSubProcessesDone: '' as Ref<Trigger>,
@@ -211,7 +219,8 @@ export default plugin(processId, {
UserRequestedValueNotProvided: '' as IntlString,
ResultNotProvided: '' as IntlString,
EmptyFunctionResult: '' as IntlString,
ContextValueNotProvided: '' as IntlString
ContextValueNotProvided: '' as IntlString,
RequiredParamsNotProvided: '' as IntlString
},
icon: {
Process: '' as Asset,
+2 -1
View File
@@ -1,11 +1,12 @@
import { Class, Doc, type AnyAttribute, type Association, type Ref } from '@hcengineering/core'
import { ContextId, ProcessFunction } from '.'
import { ContextId, ProcessContext, ProcessFunction } from '.'
export interface Context {
functions: Ref<ProcessFunction>[]
attributes: AnyAttribute[]
nested: Record<string, NestedContext>
relations: Record<string, RelatedContext>
executionContext: Record<ContextId, ProcessContext>
}
export interface NestedContext {
+106 -56
View File
@@ -13,15 +13,18 @@
// limitations under the License.
//
import card, { Card } from '@hcengineering/card'
import card, { Card, MasterTag } from '@hcengineering/card'
import contact, { Employee } from '@hcengineering/contact'
import core, {
ArrOf,
Association,
Data,
Doc,
generateId,
getObjectValue,
Ref,
RefTo,
Relation,
Tx,
TxCreateDoc,
TxCUD,
@@ -205,11 +208,8 @@ async function executeAction<T extends Doc> (
const params = await fillParams(action.params, execution, control)
const f = await getResource(impl.func)
const res = await f(params, execution, control)
if (action.contextId != null && !isError(res)) {
const resId = (res.txes[0] as TxCUD<Doc>)?.objectId
if (resId !== undefined) {
execution.context[action.contextId] = resId
}
if (!isError(res) && action.context?._id != null && res.context != null) {
execution.context[action.context._id] = res.context
}
return res
} catch (err) {
@@ -593,6 +593,12 @@ export async function CreateToDo (
execution: Execution,
control: TriggerControl
): Promise<ExecuteResult | undefined> {
for (const key in { user: params.user, title: params.title }) {
const val = (params as any)[key]
if (isEmpty(val)) {
throw processError(process.error.RequiredParamsNotProvided, { params: key })
}
}
if (params.user === undefined || params.title === undefined) return
const res: Tx[] = []
const rollback: Tx[] = []
@@ -619,7 +625,59 @@ export async function CreateToDo (
id
)
)
return { txes: res, rollback }
return { txes: res, rollback, context: id }
}
export async function CreateCard (
params: MethodParams<Card>,
execution: Execution,
control: TriggerControl
): Promise<ExecuteResult | undefined> {
const { _class, title, ...attrs } = params
for (const key in { _class, title }) {
const val = (params as any)[key]
if (isEmpty(val)) {
throw processError(process.error.RequiredParamsNotProvided, { params: key })
}
}
const _id = generateId<Card>()
const data = {
title,
...attrs
} as any
const res: Tx[] = [control.txFactory.createTxCreateDoc(_class as Ref<MasterTag>, execution.space, data, _id)]
const rollback: Tx[] = [control.txFactory.createTxRemoveDoc(_class as Ref<MasterTag>, execution.space, _id)]
return { txes: res, rollback, context: _id }
}
export async function AddRelation (
params: MethodParams<Relation>,
execution: Execution,
control: TriggerControl
): Promise<ExecuteResult | undefined> {
const _id = generateId<Relation>()
const association = params.association as Ref<Association>
if (isEmpty(association)) {
throw processError(process.error.RequiredParamsNotProvided, { params: 'association' })
}
if (isEmpty(params._id)) {
throw processError(process.error.RequiredParamsNotProvided, { params: '_id' })
}
if (isEmpty(params.direction)) {
throw processError(process.error.RequiredParamsNotProvided, { params: 'direction' })
}
const targetId = params._id as Ref<Doc>
const direction = params.direction as 'A' | 'B'
const docA = direction === 'A' ? targetId : execution.card
const docB = direction === 'A' ? execution.card : targetId
const data: Data<Relation> = {
association,
docA,
docB
}
const res: Tx[] = [control.txFactory.createTxCreateDoc(core.class.Relation, core.space.Workspace, data, _id)]
const rollback: Tx[] = [control.txFactory.createTxRemoveDoc(core.class.Relation, core.space.Workspace, _id)]
return { txes: res, rollback, context: _id }
}
export async function UpdateCard (
@@ -638,7 +696,7 @@ export async function UpdateCard (
}
const res: Tx[] = [control.txFactory.createTxUpdateDoc(target._class, target.space, target._id, update)]
const rollback: Tx[] = [control.txFactory.createTxUpdateDoc(target._class, target.space, target._id, prevValue)]
return { txes: res, rollback }
return { txes: res, rollback, context: null }
}
export async function RunSubProcess (
@@ -652,6 +710,7 @@ export async function RunSubProcess (
const target = control.modelDb.findObject(processId)
if (target === undefined) return
const res: Tx[] = []
const context: Ref<Execution>[] = []
for (const _card of Array.isArray(card) ? card : [card]) {
if (target.parallelExecutionForbidden === true) {
const currentExecution = await control.findAll(control.ctx, process.class.Execution, {
@@ -667,19 +726,26 @@ export async function RunSubProcess (
const initTransition = control.modelDb.findAllSync(process.class.Transition, { process: target._id, from: null })[0]
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const emptyContext = {} as ExecutionContext
const id = generateId<Execution>()
res.push(
control.txFactory.createTxCreateDoc(process.class.Execution, core.space.Workspace, {
process: processId,
currentState: initTransition.to,
card: _card,
context: emptyContext,
status: ExecutionStatus.Active,
rollback: [],
parentId: execution._id
})
control.txFactory.createTxCreateDoc(
process.class.Execution,
core.space.Workspace,
{
process: processId,
currentState: initTransition.to,
card: _card,
context: emptyContext,
status: ExecutionStatus.Active,
rollback: [],
parentId: execution._id
},
id
)
)
context.push(id)
}
return { txes: res, rollback: undefined }
return { txes: res, rollback: undefined, context }
}
export async function RoleContext (
@@ -763,24 +829,6 @@ export function CheckToDo (params: Record<string, any>, doc: Doc): boolean {
return doc._id === params._id
}
export async function OnStateActionsUpdate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of txes) {
if (!control.hierarchy.isDerived(tx._class, core.class.TxCUD)) continue
const cudTx = tx as TxUpdateDoc<State>
if (!control.hierarchy.isDerived(cudTx.objectClass, process.class.State)) continue
const state = control.modelDb.findObject(cudTx.objectId)
if (state === undefined) continue
const _process = control.modelDb.findObject(state.process)
if (_process === undefined) continue
const syncTx = await syncContext(control, _process)
if (syncTx !== undefined) {
res.push(syncTx)
}
}
return res
}
export async function OnTransition (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of txes) {
@@ -806,25 +854,22 @@ async function syncContext (control: TriggerControl, _process: Process): Promise
let changed = false
for (const transition of transitions) {
for (const action of transition.actions) {
if (action.contextId != null) {
exists.add(action.contextId)
const context = _process.context[action.contextId]
if (context === undefined) {
const method = control.modelDb.findObject(action.methodId)
if (method?.contextClass != null) {
changed = true
const ctx: SelectedExecutonContext = {
type: 'context',
id: action.contextId,
key: ''
}
_process.context[action.contextId] = {
name: '',
_class: method.contextClass,
action: action._id,
producer: transition._id,
value: ctx
}
if (action.context != null) {
exists.add(action.context._id)
const method = control.modelDb.findObject(action.methodId)
if (method?.contextClass != null) {
changed = true
const ctx: SelectedExecutonContext = {
type: 'context',
id: action.context._id,
key: ''
}
_process.context[action.context._id] = {
name: '',
_class: action.context._class ?? method.contextClass,
action: action._id,
producer: transition._id,
value: ctx
}
}
}
@@ -865,12 +910,18 @@ async function syncContext (control: TriggerControl, _process: Process): Promise
}
}
function isEmpty (value: any): boolean {
return value === undefined || value === null || (typeof value === 'string' && value.trim() === '')
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
func: {
RunSubProcess,
CreateToDo,
UpdateCard,
CreateCard,
AddRelation,
CheckToDo
},
transform: {
@@ -904,7 +955,6 @@ export default async () => ({
trigger: {
OnProcessRemove,
OnStateRemove,
OnStateActionsUpdate,
OnTransition,
OnExecutionCreate,
OnProcessToDoClose,
+2 -1
View File
@@ -37,6 +37,8 @@ export default plugin(serverProcessId, {
RunSubProcess: '' as Resource<ExecuteFunc>,
CreateToDo: '' as Resource<ExecuteFunc>,
UpdateCard: '' as Resource<ExecuteFunc>,
CreateCard: '' as Resource<ExecuteFunc>,
AddRelation: '' as Resource<ExecuteFunc>,
WaitSubProcess: '' as Resource<ExecuteFunc>,
CheckToDo: '' as Resource<CheckFunc>
},
@@ -69,7 +71,6 @@ export default plugin(serverProcessId, {
RoleContext: '' as Resource<TransformFunc>
},
trigger: {
OnStateActionsUpdate: '' as Resource<TriggerFunc>,
OnTransition: '' as Resource<TriggerFunc>,
OnProcessRemove: '' as Resource<TriggerFunc>,
OnStateRemove: '' as Resource<TriggerFunc>,
+1
View File
@@ -13,6 +13,7 @@ export type ExecuteResult = SuccessExecutionResult | ExecutionError
export interface SuccessExecutionResult {
txes: Tx[]
rollback: Tx[] | undefined
context: any | null
}
export type TransformFunc = (