Merge branch 'develop' of https://github.com/hcengineering/platform into staging-new

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artem Savchenko
2025-09-09 09:12:54 +07:00
58 changed files with 1011 additions and 551 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
services:
cockroach:
image: cockroachdb/cockroach:latest-v24.2
image: cockroachdb/cockroach:latest-v24.3
extra_hosts:
- 'huly.local:host-gateway'
ports:
@@ -11,7 +11,7 @@ services:
- cockroach_db:/cockroach/cockroach-data
restart: unless-stopped
redpanda:
image: docker.redpanda.com/redpandadata/redpanda:v24.3.6
image: docker.redpanda.com/redpandadata/redpanda:v25.2.3
extra_hosts:
- 'huly.local:host-gateway'
command:
+22 -3
View File
@@ -41,7 +41,7 @@ services:
- 4040:4040
restart: unless-stopped
cockroach:
image: cockroachdb/cockroach:latest-v24.2
image: cockroachdb/cockroach:latest-v24.3
extra_hosts:
- 'huly.local:host-gateway'
ports:
@@ -52,7 +52,7 @@ services:
- cockroach_db:/cockroach/cockroach-data
restart: unless-stopped
redpanda:
image: docker.redpanda.com/redpandadata/redpanda:v24.3.6
image: docker.redpanda.com/redpandadata/redpanda:v25.2.3
extra_hosts:
- 'huly.local:host-gateway'
command:
@@ -84,7 +84,7 @@ services:
retries: 10
restart: unless-stopped
redpanda_console:
image: docker.redpanda.com/redpandadata/console:v2.8.3
image: docker.redpanda.com/redpandadata/console:v2.8.10
extra_hosts:
- 'huly.local:host-gateway'
entrypoint: /bin/sh
@@ -499,6 +499,25 @@ services:
- QUEUE_CONFIG=${QUEUE_CONFIG}
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318/v1/traces
restart: unless-stopped
hulylake:
image: hardcoreeng/hulylake
extra_hosts:
- 'huly.local:host-gateway'
depends_on:
minio:
condition: service_healthy
cockroach:
condition: service_started
ports:
- 8096:8096
environment:
- HULY_TOKEN_SECRET=secret
- HULY_DB_CONNECTION=${DB_CR_URL}
- AWS_ENDPOINT_URL=http://minio:9000
- AWS_REGION=local
- AWS_ACCESS_KEY_ID=minioadmin
- AWS_SECRET_ACCESS_KEY=minioadmin
restart: unless-stopped
hulykvs:
image: hardcoreeng/hulykvs
depends_on:
+41 -13
View File
@@ -671,7 +671,8 @@ export function createModel (builder: Builder): void {
of: core.class.ArrOf,
category: 'array',
label: process.string.Insert,
editor: process.transformEditor.ArrayElementEditor,
allowMany: true,
editor: process.transformEditor.MultiArrayElementEditor,
type: 'transform'
},
process.function.Insert
@@ -684,6 +685,7 @@ export function createModel (builder: Builder): void {
of: core.class.ArrOf,
category: 'array',
label: process.string.Remove,
allowMany: true,
editor: process.transformEditor.ArrayElementEditor,
type: 'transform'
},
@@ -697,6 +699,7 @@ export function createModel (builder: Builder): void {
of: core.class.ArrOf,
category: 'array',
label: process.string.RemoveFirst,
allowMany: true,
type: 'transform'
},
process.function.RemoveFirst
@@ -709,18 +712,7 @@ export function createModel (builder: Builder): void {
of: core.class.ArrOf,
category: 'array',
label: process.string.RemoveLast,
type: 'transform'
},
process.function.RemoveLast
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.ArrOf,
category: 'array',
label: process.string.RemoveLast,
allowMany: true,
type: 'transform'
},
process.function.RemoveLast
@@ -739,6 +731,42 @@ export function createModel (builder: Builder): void {
process.function.RoleContext
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: contact.class.Person,
category: 'attribute',
label: process.string.CurrentUser,
type: 'context'
},
process.function.CurrentUser
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: contact.mixin.Employee,
category: 'attribute',
label: process.string.CurrentUser,
type: 'context'
},
process.function.CurrentEmployee
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeDate,
category: 'attribute',
label: process.string.CurrentDate,
type: 'context'
},
process.function.CurrentDate
)
builder.mixin(process.class.Process, core.class.Class, view.mixin.AttributePresenter, {
presenter: process.component.ProcessPresenter
})
+12
View File
@@ -195,6 +195,18 @@ export function createModel (builder: Builder): void {
func: serverProcess.transform.RoleContext
})
builder.mixin(process.function.CurrentDate, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.CurrentDate
})
builder.mixin(process.function.CurrentEmployee, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.CurrentUser
})
builder.mixin(process.function.CurrentUser, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.CurrentUser
})
builder.mixin(process.function.Insert, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.Insert
})
@@ -12,7 +12,7 @@
export let label: IntlString
export let value: string | undefined = undefined
export let kind: 'default' | 'ghost' | 'secondary' = 'default'
export let kind: 'default' | 'ghost' | 'secondary' | 'transparent' = 'default'
export let size: 'small' | 'medium' | 'large' = 'small'
export let disabled: boolean = false
export let error: boolean = false
@@ -185,6 +185,23 @@
}
}
}
&.transparent {
&.small,
&.medium {
// medium/ghost - not designed
padding: var(--spacing-1_5) var(--spacing-2);
height: var(--spacing-5);
}
&.large {
padding: var(--spacing-1) var(--spacing-2);
height: var(--spacing-6);
input {
font-weight: 500;
font-size: 1rem;
}
}
}
&.error {
box-shadow: inset 0 0 0 1px var(--input-error-BorderColor);
@@ -66,6 +66,7 @@
export let enableAttachments: boolean = true
export let isScrollable = true
export let kitOptions: Partial<EditorKitOptions> = {}
export let fullWidth = false
export let useDirectAttachDelete = false
export let boundary: HTMLElement | undefined = undefined
@@ -373,6 +374,10 @@
}
}
export function getAttachments (): Attachment[] {
return Array.from(attachments.values())
}
$: dispatch('attachments', {
size: attachments.size,
values: attachments.size === 0 ? true : attachments
@@ -408,6 +413,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="flex-col clear-mins"
class:w-full={fullWidth}
on:paste={(ev) => pasteAction(ev)}
on:dragover|preventDefault={() => {}}
on:dragleave={() => {}}
@@ -435,7 +441,9 @@
on:blur
on:focus
{attachFile}
/>
>
<slot name="actions" slot="actions" />
</StyledTextBox>
{#if attachments.size > 0 && enableAttachments}
<AttachmentsGrid
attachments={Array.from(attachments.values())}
+3 -2
View File
@@ -42,11 +42,12 @@
"ConfigDescription": "Rozšíření pro správu znalostí ve stylu databáze.",
"AddCollaborators": "Přidat spolupracovníky",
"Home": "Domů",
"CardTitle": "Zadejte název karty...",
"CardTitle": "Název",
"Post": "Přidat",
"Compact": "Kompaktní",
"Comfortable": "Komfortní",
"Comfortable2": "Komfortní 2",
"AdvancedCreateCard": "Přepnout na pokročilé vytváření karet"
"AdvancedCreateCard": "Skrýt detaily",
"CardContent": "Co chcete sdílet?"
}
}
+3 -2
View File
@@ -42,11 +42,12 @@
"ConfigDescription": "Erweiterung für Datenbank-basiertes Wissensmanagement.",
"AddCollaborators": "Mitarbeiter hinzufügen",
"Home": "Startseite",
"CardTitle": "Geben Sie einen Kartentitel ein...",
"CardTitle": "Titel",
"Post": "Beitrag",
"Compact": "Kompakt",
"Comfortable": "Komfortabel",
"Comfortable2": "Komfortabel 2",
"AdvancedCreateCard": "Zum erweiterten Kartenerstellungsmodus wechseln"
"AdvancedCreateCard": "Weniger anzeigen",
"CardContent": "Was möchten Sie teilen?"
}
}
+3 -2
View File
@@ -42,11 +42,12 @@
"ConfigDescription": "Extension for database-style knowledge management.",
"AddCollaborators": "Add collaborators",
"Home": "Home",
"CardTitle": "Enter a card title...",
"CardTitle": "Title",
"Post": "Post",
"Compact": "Compact",
"Comfortable": "Comfortable",
"Comfortable2": "Comfortable 2",
"AdvancedCreateCard": "Switch to advanced card creation"
"AdvancedCreateCard": "Show less",
"CardContent": "What do you want to share?"
}
}
+3 -2
View File
@@ -42,11 +42,12 @@
"ConfigDescription": "Extensión para la gestión del conocimiento al estilo de una base de datos.",
"AddCollaborators": "Agregar colaboradores",
"Home": "Inicio",
"CardTitle": "Introduce un título para la tarjeta...",
"CardTitle": "Título",
"Post": "Publicar",
"Compact": "Compacto",
"Comfortable": "Confortable",
"Comfortable2": "Confortable 2",
"AdvancedCreateCard": "Cambiar a creación avanzada de tarjetas"
"AdvancedCreateCard": "Mostrar menos",
"CardContent": "¿Qué quieres compartir?"
}
}
+3 -2
View File
@@ -42,11 +42,12 @@
"ConfigDescription": "Extension pour la gestion des connaissances de style base de données.",
"AddCollaborators": "Ajouter des collaborateurs",
"Home": "Accueil",
"CardTitle": "Entrez un titre de carte...",
"CardTitle": "Titre",
"Post": "Publier",
"Compact": "Compact",
"Comfortable": "Confortable",
"Comfortable2": "Confortable 2",
"AdvancedCreateCard": "Passer à la création avancée de cartes"
"AdvancedCreateCard": "Afficher moins",
"CardContent": "Que voulez-vous partager ?"
}
}
+3 -2
View File
@@ -42,11 +42,12 @@
"ConfigDescription": "Estensione per la gestione della conoscenza in stile database.",
"AddCollaborators": "Aggiungi collaboratori",
"Home": "Home",
"CardTitle": "Inserisci un titolo per la carta...",
"CardTitle": "Titolo",
"Post": "Pubblica",
"Compact": "Compatto",
"Comfortable": "Confortabile",
"Comfortable2": "Confortabile 2",
"AdvancedCreateCard": "Passa alla creazione avanzata della carta"
"AdvancedCreateCard": "Mostra meno",
"CardContent": "Cosa vuoi condividere?"
}
}
+3 -2
View File
@@ -42,11 +42,12 @@
"ConfigDescription": "データベーススタイルのナレッジマネジメント用の拡張機能。",
"AddCollaborators": "コラボレーターを追加",
"Home": "ホーム",
"CardTitle": "カードのタイトルを入力...",
"CardTitle": "タイトル",
"Post": "投稿",
"Compact": "コンパクト",
"Comfortable": "コンファンティブ",
"Comfortable2": "コンファンティブ 2",
"AdvancedCreateCard": "高度なカード作成に切り替え"
"AdvancedCreateCard": "詳細を隠す",
"CardContent": "何を共有しますか?"
}
}
+3 -2
View File
@@ -42,11 +42,12 @@
"ConfigDescription": "Extensão para gerenciamento de conhecimento no estilo de banco de dados.",
"AddCollaborators": "Adicionar colaboradores",
"Home": "Início",
"CardTitle": "Digite um título para o cartão...",
"CardTitle": "Título",
"Post": "Publicar",
"Compact": "Compacto",
"Comfortable": "Confortável",
"Comfortable2": "Confortável 2",
"AdvancedCreateCard": "Mudar para criação avançada de cartão"
"AdvancedCreateCard": "Mostrar menos",
"CardContent": "O que você quer compartilhar?"
}
}
+3 -2
View File
@@ -42,11 +42,12 @@
"ConfigDescription": "Расширение для управления знаниями в стиле базы данных.",
"AddCollaborators": "Добавить сотрудников",
"Home": "Дом",
"CardTitle": "Введите название карты...",
"CardTitle": "Название",
"Post": "Опубликовать",
"Compact": "Компактный",
"Comfortable": "Комфортный",
"Comfortable2": "Комфортный 2",
"AdvancedCreateCard": "Переключиться на расширенное создание карты"
"AdvancedCreateCard": "Скрыть детали",
"CardContent": "Чем вы хотите поделиться?"
}
}
+3 -2
View File
@@ -42,11 +42,12 @@
"ConfigDescription": "用于数据库风格知识管理的扩展。",
"AddCollaborators": "添加协作者",
"Home": "主页",
"CardTitle": "输入卡片标题...",
"CardTitle": "标题",
"Post": "发布",
"Compact": "紧凑",
"Comfortable": "舒适",
"Comfortable2": "舒适 2",
"AdvancedCreateCard": "切换到高级卡片创建"
"AdvancedCreateCard": "收起详情",
"CardContent": "你想分享什么?"
}
}
+1
View File
@@ -60,6 +60,7 @@
"@hcengineering/view": "^0.6.13",
"@hcengineering/text-editor": "^0.6.0",
"@hcengineering/text-editor-resources": "^0.6.0",
"@hcengineering/text-markdown": "^0.6.0",
"@hcengineering/workbench-resources": "^0.6.1",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/notification": "^0.6.23",
@@ -33,6 +33,7 @@
export let space: CardSpace | undefined = undefined
export let changeType: boolean = false
export let allowChangeSpace: boolean = true
export let description: Markup = EmptyMarkup
const dispatch = createEventDispatcher()
const client = getClient()
@@ -41,16 +42,16 @@
const me = getCurrentEmployee()
const _id = generateId<Card>()
$: extension = type
? client
.getModel()
.findAllSync(card.mixin.CreateCardExtension, {})
.find((it) => hierarchy.isDerived(type, it._id))
: undefined
$: extension =
type != null
? client
.getModel()
.findAllSync(card.mixin.CreateCardExtension, {})
.find((it) => hierarchy.isDerived(type, it._id))
: undefined
let data: Partial<Data<Card>> = { title }
let _space: Ref<CardSpace> | undefined = space?._id
let description: Markup = EmptyMarkup
let collaborators: Ref<Employee>[] = [me]
let creating = false
@@ -73,7 +74,7 @@
try {
creating = true
if (extension?.canCreate) {
if (extension?.canCreate != null) {
const fn = await getResource(extension.canCreate)
const res = await fn(_space, data)
if (res === false) {
@@ -156,7 +157,7 @@
<ModernEditbox
bind:value={data.title}
label={view.string.Title}
size="large"
size="medium"
kind="ghost"
disabled={extension?.disableTitle ?? false}
autoFocus={!(extension?.disableTitle ?? false)}
@@ -0,0 +1,58 @@
<!--
// 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 { ButtonIcon, handler } from '@hcengineering/ui'
import type { RefAction, TextEditorHandler } from '@hcengineering/text-editor'
import { getEditorHandler } from '@hcengineering/text-editor-resources/src/components/editor-context'
export let actions: RefAction[] = []
const editorHandler: TextEditorHandler | undefined = getEditorHandler()
$: sortedActions = actions.slice().sort((a, b) => a.order - b.order)
function handleAction (action: RefAction, evt?: Event): void {
if (editorHandler === undefined) {
console.error('Editor handler is not available')
return
}
action.action(evt?.target as HTMLElement, editorHandler)
}
</script>
<div class="editor-actions">
{#each sortedActions as action}
<ButtonIcon
disabled={action.disabled}
icon={action.icon}
iconSize="small"
size="small"
kind="tertiary"
tooltip={{ label: action.label }}
on:click={handler(action, (a, evt) => {
if (a.disabled !== true) {
handleAction(a, evt)
}
})}
/>
{/each}
</div>
<style lang="scss">
.editor-actions {
display: flex;
gap: 0.375rem;
}
</style>
@@ -1,25 +1,53 @@
<!--
// 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 { getClient, SpaceSelector } from '@hcengineering/presentation'
import { ModernButton, ModernEditbox, ButtonIcon, IconSend, showPopup } from '@hcengineering/ui'
import { getClient, getCommunicationClient, SpaceSelector } from '@hcengineering/presentation'
import { ModernButton, ModernEditbox, ButtonIcon, IconSend, IconMinimize } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import CreateCardPopup from './CreateCardPopup.svelte'
import card from '../plugin'
import { TypeSelector } from '../index'
import core, { Data, Ref } from '@hcengineering/core'
import core, { Data, generateId, Markup, Ref } from '@hcengineering/core'
import { Card, type CardSpace, MasterTag } from '@hcengineering/card'
import { getResource } from '@hcengineering/platform'
import { EmptyMarkup } from '@hcengineering/text'
import { EmptyMarkup, markupToText, isEmptyMarkup, markupToJSON } from '@hcengineering/text'
import { createCard } from '../utils'
import { AttachmentStyledBox } from '@hcengineering/attachment-resources'
import { AttachIcon } from '@hcengineering/text-editor-resources'
import { AttachmentID, BlobParams } from '@hcengineering/communication-types'
import { markupToMarkdown } from '@hcengineering/text-markdown'
import textEditor, { type RefAction } from '@hcengineering/text-editor'
import { defaultMessageInputActions } from '@hcengineering/communication-resources'
import EditorActions from './EditorActions.svelte'
const dispatch = createEventDispatcher()
const communicationClient = getCommunicationClient()
const threadMasterTag = 'chat:masterTag:Thread' as Ref<MasterTag>
let title: string = ''
let space: Ref<CardSpace> | undefined = undefined
let type: Ref<MasterTag> = 'chat:masterTag:Thread' as Ref<MasterTag>
let type: Ref<MasterTag> = threadMasterTag
let description = EmptyMarkup
let _id = generateId<Card>()
let isExpanded = false
let descriptionBox: AttachmentStyledBox
let creating = false
$: applyDisabled = title.trim() === '' || space == null || type == null || creating
$: applyDisabled = (title.trim() === '' && isEmptyMarkup(description)) || space == null || type == null || creating
const client = getClient()
const hierarchy = client.getHierarchy()
@@ -32,30 +60,15 @@
.find((it) => hierarchy.isDerived(type, it._id))
: undefined
function onExtend (): void {
showPopup(
CreateCardPopup,
{ title, type, space, changeType: true, allowChangeSpace: true },
'center',
async (result) => {
if (result !== undefined) {
const doc = await getClient().findOne(card.class.Card, { _id: result })
if (doc === undefined) return
dispatch('selectCard', doc)
}
}
)
title = ''
}
async function okAction (): Promise<void> {
if (space === undefined || type == null || title.trim() === '') return
if (space === undefined || type == null) return
try {
creating = true
const cardTitle = title.trim().length > 0 ? title.trim() : markupToText(description).slice(0, 50)
const data: Partial<Data<Card>> = {
title
title: cardTitle
}
if (extension?.canCreate !== undefined) {
@@ -68,68 +81,152 @@
}
}
const createdCard = await createCard(type, space, data, EmptyMarkup)
const isThread = type === threadMasterTag
const cardDescription = isThread ? EmptyMarkup : description
const createdCard = await createCard(type, space, data, cardDescription, _id)
if (isThread) {
if (createdCard == null) {
console.error('Failed to create thread card')
return
}
const blobs: BlobParams[] = descriptionBox.getAttachments().map((attachment) => ({
blobId: attachment.file,
mimeType: attachment.type,
fileName: attachment.name,
size: attachment.size
}))
await createMessage(createdCard, description, blobs)
} else {
await descriptionBox.createAttachments()
}
if (createdCard != null) {
dispatch('selectCard', createdCard)
}
title = ''
clear()
} finally {
_id = generateId<Card>()
creating = false
}
}
async function createMessage (card: Ref<Card>, markup: Markup, blobs: BlobParams[]): Promise<void> {
const markdown = markupToMarkdown(markupToJSON(markup))
const { messageId } = await communicationClient.createMessage(card, type, markdown)
if (blobs.length > 0) {
void communicationClient.attachmentPatch<BlobParams>(card, messageId, {
add: blobs.map((it) => ({
id: it.blobId as any as AttachmentID,
type: it.mimeType,
params: it
}))
})
}
}
function clear (): void {
title = ''
description = EmptyMarkup
_id = generateId<Card>()
}
function expand (): void {
const prevExpanded = isExpanded
isExpanded = true
if (!prevExpanded) {
setTimeout(() => {
descriptionBox?.focus?.()
}, 0)
}
}
const attachAction: RefAction = {
label: textEditor.string.Attach,
icon: AttachIcon,
action: () => {
dispatch('focus')
descriptionBox.handleAttach()
},
order: 1000
}
</script>
<div class="create-card rounded-form">
<div class="corner-container">
<ButtonIcon
icon={card.icon.Expand}
size="extra-small"
on:click={onExtend}
kind="tertiary"
disabled={creating}
tooltip={{ label: card.string.AdvancedCreateCard }}
/>
</div>
<div class="form-row">
<ModernEditbox
bind:value={title}
label={card.string.CardTitle}
size="medium"
kind="ghost"
width="100%"
disabled={creating}
autoFocus={true}
on:keydown={(evt) => {
if (evt.key === 'Enter') {
void okAction()
}
}}
/>
</div>
<div class="form-row form-row-bottom">
<div class="form-dropdowns">
<SpaceSelector
_class={card.class.CardSpace}
query={{ archived: false }}
label={core.string.Space}
bind:space
focus={false}
readonly={creating}
kind={'regular'}
size={'small'}
{#if isExpanded}
<div class="corner-container">
<ButtonIcon
icon={IconMinimize}
size="extra-small"
on:click={() => (isExpanded = false)}
kind="tertiary"
tooltip={{ label: card.string.ShowLess }}
disabled={creating}
/>
<TypeSelector size={'small'} bind:value={type} disabled={creating} />
</div>
<ModernButton
label={card.string.Post}
icon={IconSend}
iconSize="small"
size="small"
kind="primary"
disabled={applyDisabled}
loading={creating}
on:click={okAction}
/>
<div class="form-row title">
<ModernEditbox
bind:value={title}
label={card.string.CardTitle}
size="large"
kind="transparent"
width="100%"
disabled={creating}
/>
</div>
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="form-row description" class:collapsed={!isExpanded} on:click={expand}>
<AttachmentStyledBox
bind:this={descriptionBox}
objectId={_id}
_class={type}
{space}
alwaysEdit
showButtons={true}
bind:content={description}
placeholder={card.string.CardContent}
kind="indented"
maxHeight="limited"
isScrollable={false}
kitOptions={{ reference: true }}
enableAttachments={true}
fullWidth={true}
on:focus={() => (isExpanded = true)}
>
<svelte:fragment slot="actions">
{#if isExpanded}
<div class="form-row form-row-bottom">
<div class="form-dropdowns">
<SpaceSelector
_class={card.class.CardSpace}
query={{ archived: false }}
label={core.string.Space}
bind:space
focus={false}
readonly={creating}
kind={'regular'}
size={'small'}
/>
<TypeSelector size={'small'} bind:value={type} disabled={creating} />
<div class="spacer" />
<div class="right-divider" />
<EditorActions actions={[...defaultMessageInputActions, attachAction]} />
</div>
<ModernButton
label={card.string.Post}
icon={IconSend}
iconSize="small"
size="small"
kind="primary"
disabled={applyDisabled}
loading={creating}
on:click={okAction}
/>
</div>
{/if}
</svelte:fragment>
</AttachmentStyledBox>
</div>
</div>
@@ -138,7 +235,7 @@
position: absolute;
top: 0.75rem;
right: 1rem;
z-index: 1;
z-index: 100;
display: flex;
align-items: center;
justify-content: flex-end;
@@ -162,13 +259,26 @@
.form-row {
display: flex;
width: 100%;
margin-bottom: 1rem;
&.title {
z-index: 1;
}
&.description {
padding-left: 0.375rem;
margin-top: -2rem;
padding-top: 1rem;
width: 100%;
max-height: 20rem;
&.collapsed {
margin-bottom: -1rem;
}
}
}
.form-row-bottom {
align-items: center;
justify-content: space-between;
margin-bottom: 0;
gap: 1rem;
padding-top: 0.5rem;
}
.form-dropdowns {
display: flex;
+2 -1
View File
@@ -103,10 +103,11 @@ export default mergeIds(cardId, card, {
AddCollaborators: '' as IntlString,
Home: '' as IntlString,
CardTitle: '' as IntlString,
CardContent: '' as IntlString,
Post: '' as IntlString,
Compact: '' as IntlString,
Comfortable: '' as IntlString,
Comfortable2: '' as IntlString,
AdvancedCreateCard: '' as IntlString
ShowLess: '' as IntlString
}
})
@@ -44,6 +44,7 @@ import { createPoll, getPollTitle } from './poll'
export { isActivityMessage } from './activity'
export * from './stores'
export { defaultMessageInputActions } from './utils'
export { default as MessagePresenter } from './components/message/MessagePresenter.svelte'
export { default as MessageInput } from './components/message/MessageInput.svelte'
@@ -59,6 +59,7 @@
export let avatarSize: IconSize = kind === 'regular' ? 'small' : 'card'
export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = undefined
export let height: string | undefined = undefined
export let shrink: number = 0
export let focusIndex = -1
export let showTooltip: LabelAndProps | PersonLabelTooltip | undefined = undefined
@@ -132,7 +133,14 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div {id} bind:this={container} class="min-w-0" class:w-full={width === '100%'} style:flex-shrink={shrink}>
<div
{id}
bind:this={container}
class="min-w-0"
class:w-full={width === '100%'}
class:h-full={height === '100%'}
style:flex-shrink={shrink}
>
{#if $$slots.content}
<div
class="w-full h-full flex-streatch"
@@ -23,6 +23,7 @@
<div class="text-editor-popup w-85">
<ReferenceInput
autofocus
focusable
kindSend="primary"
placeholder={chunter.string.AddCommentPlaceholder}
@@ -204,13 +204,14 @@
if (isOffice(room) && rX === 0 && rY === 0) e.stopPropagation()
}}
>
{#if isOffice(room) && rX === 0 && rY === 0}
{#if isOffice(room) && rX === 0 && rY === 0 && !room.person}
<AssigneeBox
_class={contact.class.Person}
excluded={excludedPersons}
shouldShowName={false}
showNavigate={false}
width={'100%'}
height={'100%'}
label={contact.string.Person}
value={room.person}
avatarSize={'full'}
+3 -1
View File
@@ -97,7 +97,9 @@
"RemoveFirst": "Odstranit první",
"RemoveLast": "Odstranit poslední",
"Cancelled": "Zrušeno",
"WaitUntil": "Čekat do"
"WaitUntil": "Čekat do",
"CurrentUser": "Aktuální uživatel",
"CurrentDate": "Aktuální datum"
},
"error": {
"MethodNotFound": "Metoda nenalezena: {methodId}",
+3 -1
View File
@@ -97,7 +97,9 @@
"RemoveFirst": "Erstes entfernen",
"RemoveLast": "Letztes entfernen",
"Cancelled": "Abgebrochen",
"WaitUntil": "Warten bis"
"WaitUntil": "Warten bis",
"CurrentUser": "Aktueller Benutzer",
"CurrentDate": "Aktuelles Datum"
},
"error": {
"MethodNotFound": "Methode nicht gefunden: {methodId}",
+3 -1
View File
@@ -102,7 +102,9 @@
"RemoveFirst": "Remove first",
"RemoveLast": "Remove last",
"Cancelled": "Cancelled",
"WaitUntil": "Wait until"
"WaitUntil": "Wait until",
"CurrentUser": "Current user",
"CurrentDate": "Current date"
},
"error": {
"MethodNotFound": "Method not found: {methodId}",
+3 -1
View File
@@ -102,7 +102,9 @@
"ArraySizeLt": "Tamaño menor que",
"ArraySizeLte": "Tamaño menor o igual que",
"Cancelled": "Cancelado",
"WaitUntil": "Esperar hasta"
"WaitUntil": "Esperar hasta",
"CurrentUser": "Usuario Actual",
"CurrentDate": "Fecha Actual"
},
"error": {
"MethodNotFound": "Método no encontrado: {methodId}",
+3 -1
View File
@@ -102,7 +102,9 @@
"ArraySizeLt": "Taille inférieure à",
"ArraySizeLte": "Taille inférieure ou égale à",
"Cancelled": "Annulé",
"WaitUntil": "Attendre jusqu'à"
"WaitUntil": "Attendre jusqu'à",
"CurrentUser": "Utilisateur actuel",
"CurrentDate": "Date actuelle"
},
"error": {
"MethodNotFound": "Méthode introuvable : {methodId}",
+3 -1
View File
@@ -102,7 +102,9 @@
"ArraySizeLt": "Dimensione minore di",
"ArraySizeLte": "Dimensione minore o uguale a",
"Cancelled": "Annullato",
"WaitUntil": "Aspetta fino a"
"WaitUntil": "Aspetta fino a",
"CurrentUser": "Utente corrente",
"CurrentDate": "Data corrente"
},
"error": {
"MethodNotFound": "Metodo non trovato: {methodId}",
+3 -1
View File
@@ -101,7 +101,9 @@
"ArraySizeLt": "サイズがより小さい",
"ArraySizeLte": "サイズが以下",
"Cancelled": "キャンセル済み",
"WaitUntil": "待機する"
"WaitUntil": "待機する",
"CurrentUser": "現在のユーザー",
"CurrentDate": "現在の日付"
},
"error": {
"MethodNotFound": "メソッドが見つかりません: {methodId}",
+3 -1
View File
@@ -102,7 +102,9 @@
"ArraySizeLt": "Tamanho menor que",
"ArraySizeLte": "Tamanho menor ou igual a",
"Cancelled": "Cancelado",
"WaitUntil": "Aguardar até"
"WaitUntil": "Aguardar até",
"CurrentUser": "Usuário Atual",
"CurrentDate": "Data Atual"
},
"error": {
"MethodNotFound": "Método não encontrado: {methodId}",
+3 -1
View File
@@ -102,7 +102,9 @@
"ArraySizeLt": "Количество меньше",
"ArraySizeLte": "Количество меньше или равно",
"Cancelled": "Отменено",
"WaitUntil": "Ждать до"
"WaitUntil": "Ждать до",
"CurrentUser": "Текущий пользователь",
"CurrentDate": "Текущая дата"
},
"error": {
"MethodNotFound": "Метод не найден: {methodId}",
+3 -1
View File
@@ -102,7 +102,9 @@
"ArraySizeLt": "大小小于",
"ArraySizeLte": "大小小于等于",
"Cancelled": "已取消",
"WaitUntil": "等待直到"
"WaitUntil": "等待直到",
"CurrentUser": "当前用户",
"CurrentDate": "当前日期"
},
"error": {
"MethodNotFound": "找不到方法:{methodId}",
@@ -187,13 +187,14 @@
onChange(contextValue)
}
function onConfigure (e: MouseEvent, func: ProcessFunction, pos: number): void {
if (contextValue.functions === undefined || func.editor === undefined) return
function onConfigure (e: MouseEvent, func: Func, pos: number): void {
const f = getFunction(func.func)
if (contextValue.functions === undefined || f.editor === undefined) return
const val = contextValue.functions[pos]
showPopup(
func.editor,
f.editor,
{
func,
func: f,
masterTag: process.masterTag,
process,
context,
@@ -224,6 +225,10 @@
}
onChange(contextValue)
}
function getFunction (_id: Ref<ProcessFunction>): ProcessFunction {
return client.getModel().findAllSync(plugin.class.ProcessFunction, { _id })[0]
}
</script>
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
@@ -246,11 +251,13 @@
options={{ component: plugin.component.FunctionSelector }}
withHover
/>
<div class="menu-separator" />
{/if}
{#if availableFunctions.length > 0 || functionsLength > 0}
{#each funcs as f, i}
{#if reduceFuncs.includes(f._id)}
{#if sourceFunc !== undefined}
<div class="menu-separator" />
{/if}
{#each contextValue.functions ?? [] as f, i}
{#if reduceFuncs.includes(f.func)}
<Submenu
bind:element={elements[i + (sourceFunc !== undefined ? 1 : 0)]}
on:keydown={(event) => {
@@ -259,7 +266,7 @@
on:mouseover={() => {
elements[i + (sourceFunc !== undefined ? 1 : 0)]?.focus()
}}
label={f.label}
label={getFunction(f.func)?.label}
props={{
availableFunctions: reduceFuncs,
onSelect: getFunctionChange(i)
@@ -281,10 +288,10 @@
}}
>
<div>
<Label label={f.label} />
<Label label={getFunction(f.func).label} />
</div>
<div>
{#if f.editor}
{#if getFunction(f.func).editor}
<ButtonIcon
icon={IconSettings}
size="small"
@@ -325,7 +332,9 @@
/>
<!-- <div class="menu-separator" /> -->
{/if}
<div class="menu-separator" />
{#if !forbidValue}
<div class="menu-separator" />
{/if}
{/if}
{#if !forbidValue}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
@@ -0,0 +1,67 @@
<!--
// 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 { Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { ExecutionContext, parseContext, Process, SelectedUserRequest, Transition } from '@hcengineering/process'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
import ProcessAttributeEditor from './ProcessAttributeEditor.svelte'
export let process: Process
export let targetProcess: Ref<Process>
export let context: ExecutionContext
const client = getClient()
const dispatch = createEventDispatcher()
$: initTransition = client.getModel().findAllSync(plugin.class.Transition, {
process: targetProcess,
from: null
})[0]
let contexts: SelectedUserRequest[] = []
function fillParams (initTransition: Transition | undefined): void {
contexts = []
for (const action of initTransition?.actions ?? []) {
for (const key in action.params) {
const value = (action.params as any)[key]
const request = parseContext(value)
if (request !== undefined && request.type === 'userRequest') {
contexts.push(request)
}
}
}
contexts = contexts
}
$: fillParams(initTransition)
</script>
{#each contexts as ctx}
<ProcessAttributeEditor
{process}
_class={ctx._class}
key={ctx.key}
objectKey={ctx.id}
object={context}
allowRemove
on:change={(e) => {
context = e.detail.object
dispatch('change', context)
}}
/>
{/each}
@@ -17,9 +17,9 @@
import { getAttributeEditor, getAttributePresenterClass, getClient } from '@hcengineering/presentation'
import { Process } from '@hcengineering/process'
import { AnySvelteComponent } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { getContext } from '../../utils'
import ProcessAttribute from '../ProcessAttribute.svelte'
import { createEventDispatcher } from 'svelte'
export let process: Process
export let _class: Ref<Class<Doc>>
@@ -28,6 +28,7 @@
export let allowRemove: boolean = false
export let forbidValue: boolean = false
export let allowArray: boolean = false
export let objectKey: string | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@@ -36,14 +37,14 @@
function onChange (value: any | undefined): void {
if (value === undefined) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (object as any)[key]
delete (object as any)[objectKey ?? key]
} else {
;(object as any)[key] = value
;(object as any)[objectKey ?? key] = value
}
dispatch('change', { value })
dispatch('change', { value, object })
}
$: value = object[key]
$: value = object[objectKey ?? key]
$: attribute = hierarchy.getAttribute(_class, key)
$: presenterClass = getAttributePresenterClass(hierarchy, attribute.type)
@@ -14,12 +14,14 @@
-->
<script lang="ts">
import card from '@hcengineering/card'
import { Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Execution, parseContext, Process, Step } from '@hcengineering/process'
import { Execution, ExecutionContext, parseContext, Process, Step } from '@hcengineering/process'
import { DropdownLabels, DropdownTextItem, Label, Toggle } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
import { getContextMasterTag } from '../../utils'
import InitParamsEditor from './InitParamsEditor.svelte'
import ProcessAttributeEditor from './ProcessAttributeEditor.svelte'
export let process: Process
@@ -28,15 +30,18 @@
const client = getClient()
const hierarchy = client.getHierarchy()
let _id = step.params._id
let _id: Ref<Process> = step.params._id as any
$: context = parseContext(step.params.card)
$: masterTag = getContextMasterTag(client, context, process.masterTag) ?? process.masterTag
$: executionContext = (step.params.context ?? {}) as ExecutionContext
const dispatch = createEventDispatcher()
function change (e: CustomEvent<any>): void {
if (e.detail !== undefined) {
;(step.params as any)._id = e.detail
_id = e.detail
dispatch('change', step)
}
}
@@ -62,15 +67,20 @@
.findAllSync(plugin.class.Process, { masterTag: { $in: ancestors }, _id: { $ne: process._id } })
function changeThis (): void {
_id = undefined
step.params = {}
dispatch('change', step)
}
function changeTarget (e: CustomEvent<any>): void {
_id = undefined
step.params.card = e.detail.value
}
function changeContext (e: CustomEvent<ExecutionContext>): void {
if (e.detail !== undefined) {
step.params.context = e.detail
dispatch('change', step)
}
}
</script>
<div class="grid">
@@ -103,8 +113,17 @@
on:selected={change}
/>
</div>
<div class="divider" />
<div class="grid">
<InitParamsEditor {process} targetProcess={_id} context={executionContext} on:change={changeContext} />
</div>
<style lang="scss">
.divider {
border-bottom: 1px solid var(--divider-color);
margin: 1rem 0;
}
.grid {
display: grid;
grid-template-columns: 1fr 1.5fr;
@@ -14,12 +14,9 @@
-->
<script lang="ts">
import { MasterTag, Tag } from '@hcengineering/card'
import core, { AnyAttribute, Ref } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { AnyAttribute, Ref } from '@hcengineering/core'
import presentation, { Card, getAttributePresenterClass, getClient } from '@hcengineering/presentation'
import { Process, ProcessFunction } from '@hcengineering/process'
import { AnySvelteComponent } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import { getContext } from '../../utils'
import ProcessAttribute from '../ProcessAttribute.svelte'
@@ -29,6 +26,7 @@
export let masterTag: Ref<MasterTag | Tag>
export let attribute: AnyAttribute
export let props: Record<string, any> = {}
export let allowArray: boolean = false
let value: any = props?.value ?? ''
@@ -40,28 +38,9 @@
dispatch('close', { value })
}
let editor: AnySvelteComponent | undefined
function getEditor (): void {
try {
const inlineEditor = h.as(h.getClass(core.class.ArrOf), view.mixin.AttributeEditor).inlineEditor
void getResource(inlineEditor)
.then((p) => {
editor = p
})
.catch((e) => {
console.error(e)
})
} catch (e) {
console.error(e)
}
}
getEditor()
$: presenterClass = getAttributePresenterClass(h, attribute.type)
$: elementContext = getContext(client, process, presenterClass.attrClass, 'object')
$: elementContext = getContext(client, process, presenterClass.attrClass, 'attribute')
</script>
<Card on:close width={'x-small'} label={func.label} canSave okAction={save} okLabel={presentation.string.Save}>
@@ -71,12 +50,13 @@
{masterTag}
context={elementContext}
{attribute}
editor={undefined}
presenterClass={{
attrClass: presenterClass.attrClass,
category: 'attribute'
}}
{allowArray}
{value}
{editor}
on:change={(e) => {
value = e.detail
}}
@@ -0,0 +1,28 @@
<!--
// 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, Tag } from '@hcengineering/card'
import { AnyAttribute, Ref } from '@hcengineering/core'
import { Process, ProcessFunction } from '@hcengineering/process'
import ArrayElementEditor from './ArrayElementEditor.svelte'
export let func: ProcessFunction
export let process: Process
export let masterTag: Ref<MasterTag | Tag>
export let attribute: AnyAttribute
export let props: Record<string, any> = {}
</script>
<ArrayElementEditor {func} {process} {masterTag} {attribute} {props} allowArray on:close />
+2
View File
@@ -43,6 +43,7 @@ import TransitionEditor from './components/settings/TransitionEditor.svelte'
import TriggerPresenter from './components/settings/TriggerPresenter.svelte'
import UpdateCardEditor from './components/settings/UpdateCardEditor.svelte'
import ArrayElementEditor from './components/transformEditors/ArrayElementEditor.svelte'
import MultiArrayElementEditor from './components/transformEditors/MultiArrayElementEditor.svelte'
import DateOffsetEditor from './components/transformEditors/DateOffsetEditor.svelte'
import NumberEditor from './components/transformEditors/NumberEditor.svelte'
@@ -143,6 +144,7 @@ export default async (): Promise<Resources> => ({
},
transformEditor: {
ArrayElementEditor,
MultiArrayElementEditor,
DateOffsetEditor,
NumberEditor,
AppendEditor,
+4 -1
View File
@@ -78,6 +78,7 @@ export default mergeIds(processId, process, {
RefCriteria: '' as AnyComponent
},
transformEditor: {
MultiArrayElementEditor: '' as AnyComponent,
ArrayElementEditor: '' as AnyComponent,
DateOffsetEditor: '' as AnyComponent,
NumberEditor: '' as AnyComponent,
@@ -188,6 +189,8 @@ export default mergeIds(processId, process, {
ArraySizeGt: '' as IntlString,
ArraySizeGte: '' as IntlString,
ArraySizeLt: '' as IntlString,
ArraySizeLte: '' as IntlString
ArraySizeLte: '' as IntlString,
CurrentUser: '' as IntlString,
CurrentDate: '' as IntlString
}
})
+15 -18
View File
@@ -81,7 +81,9 @@ export function getContextMasterTag (
const model = client.getModel()
if (context.type === 'attribute') {
const attr = h.findAttribute(masterTag, context.key)
return (attr?.type as RefTo<Doc>)?.to
if (attr === undefined) return
const parentType = attr.type._class === core.class.ArrOf ? (attr.type as ArrOf<Doc>).of : attr.type
if (parentType._class === core.class.RefTo) return (parentType as RefTo<Doc>).to
}
if (context.type === 'nested') {
const attr = h.findAttribute(masterTag, context.path)
@@ -284,19 +286,6 @@ function getContextFunctions (
const funcs = client.getModel().findAllSync(process.class.ProcessFunction, { type: 'context' })
for (const func of funcs) {
switch (category) {
case 'object': {
if (func.category === 'array') {
if (hierarchy.isDerived(func.of, target)) {
matched.push(func._id)
}
}
if (func.category === 'object') {
if (hierarchy.isDerived(func.of, target)) {
matched.push(func._id)
}
}
break
}
case 'array': {
if (func.category === 'array') {
if (hierarchy.isDerived(func.of, target)) {
@@ -306,7 +295,7 @@ function getContextFunctions (
break
}
default: {
if (func.of === target) {
if (hierarchy.isDerived(func.of, target)) {
matched.push(func._id)
}
}
@@ -358,6 +347,12 @@ function getClassAttributes (
if (attr[1].type._class === target) {
matchedAttributes.push(attr[1])
}
if (attr[1].type._class === core.class.RefTo) {
const to = (attr[1].type as RefTo<Doc>).to
if (hierarchy.isDerived(to, target)) {
matchedAttributes.push(attr[1])
}
}
}
}
}
@@ -450,13 +445,13 @@ export async function getTransitionUserInput (
transition: Transition,
userContext: ExecutionContext
): Promise<ExecutionContext | undefined> {
const changed = false
let changed = false
for (const action of transition.actions) {
if (action == null) continue
for (const key in action.params) {
const value = (action.params as any)[key]
const context = parseContext(value)
if (context !== undefined && context.type === 'userRequest') {
if (context !== undefined && context.type === 'userRequest' && userContext[context.id] === undefined) {
const promise = new Promise<void>((resolve) => {
showPopup(
process.component.RequestUserInput,
@@ -464,6 +459,7 @@ export async function getTransitionUserInput (
undefined,
(res) => {
if (res?.value !== undefined) {
changed = true
userContext[context.id] = res.value
}
resolve()
@@ -481,7 +477,7 @@ export async function getSubProcessesUserInput (
transition: Transition,
userContext: ExecutionContext
): Promise<ExecutionContext | undefined> {
const changed = false
let changed = false
for (const action of transition.actions) {
if (action.methodId !== process.method.RunSubProcess) continue
const processId = action.params._id as Ref<Process>
@@ -489,6 +485,7 @@ export async function getSubProcessesUserInput (
const res = await newExecutionUserInput(processId)
if (action.context == null || res === undefined) continue
userContext[action.context._id] = res
changed = true
}
return changed ? userContext : undefined
}
+4 -1
View File
@@ -285,6 +285,9 @@ export default plugin(processId, {
Insert: '' as Ref<ProcessFunction>,
Remove: '' as Ref<ProcessFunction>,
RemoveFirst: '' as Ref<ProcessFunction>,
RemoveLast: '' as Ref<ProcessFunction>
RemoveLast: '' as Ref<ProcessFunction>,
CurrentEmployee: '' as Ref<ProcessFunction>,
CurrentUser: '' as Ref<ProcessFunction>,
CurrentDate: '' as Ref<ProcessFunction>
}
})
@@ -312,6 +312,7 @@
dispatch('changeContent', evt.detail)
}}
>
<slot name="actions" slot="actions" />
{#if !alwaysEdit && !hideExtraButtons}
<div class="flex flex-reverse flex-grow gap-2 reverse">
<ActionIcon
@@ -23,6 +23,7 @@
import { EditorKitOptions } from '../kits/editor-kit'
import { defaultRefActions, getModelRefActions } from './editor/actions'
import TextEditor from './TextEditor.svelte'
import { setEditorHandler } from './editor-context'
const dispatch = createEventDispatcher()
@@ -107,6 +108,9 @@
}
}
// Set the editor handler in context so child components can access it
setEditorHandler(editorHandler)
let actions: RefAction[] = defaultRefActions.concat(...extraActions).sort((a, b) => a.order - b.order)
void getModelRefActions().then((modelActions) => {
@@ -198,28 +202,30 @@
{/if}
</div>
</div>
{#if showButtons}
<div class="flex-between">
<div class="buttons-group {buttonsGap} mt-3">
{#each actions as a}
<Button
icon={a.icon}
iconProps={{ size: buttonSize }}
kind="ghost"
showTooltip={{ label: a.label }}
size={buttonSize}
on:click={(evt) => {
handleAction(a, evt)
}}
/>
{#if a.order % 10 === 1}
<div class="buttons-divider {buttonsHeight}" />
{/if}
{/each}
<slot />
<slot name="actions">
{#if showButtons}
<div class="flex-between">
<div class="buttons-group {buttonsGap} mt-3">
{#each actions as a}
<Button
icon={a.icon}
iconProps={{ size: buttonSize }}
kind="ghost"
showTooltip={{ label: a.label }}
size={buttonSize}
on:click={(evt) => {
handleAction(a, evt)
}}
/>
{#if a.order % 10 === 1}
<div class="buttons-divider {buttonsHeight}" />
{/if}
{/each}
<slot />
</div>
</div>
</div>
{/if}
{/if}
</slot>
</div>
<style lang="scss">
@@ -0,0 +1,30 @@
//
// 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.
//
import { getContext, setContext } from 'svelte'
import type { TextEditorHandler } from '@hcengineering/text-editor'
const EDITOR_HANDLER_CONTEXT = Symbol('editor-handler')
export function setEditorHandler (handler: TextEditorHandler): void {
try {
setContext(EDITOR_HANDLER_CONTEXT, handler)
} catch (err: any) {
console.warn('Failed to set editor handler context: ', err)
}
}
export function getEditorHandler (): TextEditorHandler | undefined {
return getContext(EDITOR_HANDLER_CONTEXT)
}
@@ -64,6 +64,7 @@ export { default as TableOfContents } from './components/toc/TableOfContents.sve
export { default as TableOfContentsContent } from './components/toc/TableOfContentsContent.svelte'
export type { EditorKitOptions } from './kits/editor-kit'
export * from './utils'
export * from './components/editor-context'
export * from './command/deleteAttachment'
export { EmojiExtension } from './components/extension/emoji'
+2
View File
@@ -0,0 +1,2 @@
hulylake hardcoreeng/service_hulylake:0.1.1
+2 -2
View File
@@ -10,7 +10,7 @@ services:
command: start-single-node --insecure
restart: unless-stopped
redpanda:
image: docker.redpanda.com/redpandadata/redpanda:v24.3.6
image: docker.redpanda.com/redpandadata/redpanda:v25.2.3
extra_hosts:
- 'huly.local:host-gateway'
command:
@@ -37,7 +37,7 @@ services:
timeout: 5s
retries: 10
redpanda_console:
image: docker.redpanda.com/redpandadata/console:v2.8.3
image: docker.redpanda.com/redpandadata/console:v2.8.10
extra_hosts:
- 'huly.local:host-gateway'
entrypoint: /bin/sh
+23 -2
View File
@@ -14,7 +14,17 @@
//
import cardPlugin, { Card } from '@hcengineering/card'
import core, { Tx, TxCreateDoc, TxCUD, TxProcessor, TxRemoveDoc, TxUpdateDoc } from '@hcengineering/core'
import core, {
ArrOf,
Doc,
RefTo,
Tx,
TxCreateDoc,
TxCUD,
TxProcessor,
TxRemoveDoc,
TxUpdateDoc
} from '@hcengineering/core'
import process, {
ContextId,
Execution,
@@ -33,6 +43,8 @@ import {
All,
Append,
Ceil,
CurrentDate,
CurrentUser,
Cut,
Divide,
FirstValue,
@@ -276,11 +288,16 @@ async function syncContext (control: TriggerControl, _process: Process): Promise
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: transition.result.type._class,
_class,
index: index++,
producer: transition._id,
value: ctx
@@ -325,6 +342,8 @@ async function syncContext (control: TriggerControl, _process: Process): Promise
}
}
export * from './utils'
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
func: {
@@ -338,6 +357,8 @@ export default async () => ({
CheckTime
},
transform: {
CurrentDate,
CurrentUser,
FirstValue,
LastValue,
Random,
@@ -13,11 +13,11 @@
// limitations under the License.
//
import contact, { Employee } from '@hcengineering/contact'
import contact, { Employee, Person } from '@hcengineering/contact'
import { Doc, Ref, Timestamp } from '@hcengineering/core'
import { Execution, parseContext } from '@hcengineering/process'
import { ProcessControl } from '@hcengineering/server-process'
import { getAttributeValue } from './utils'
import { getContextValue } from './utils'
// #region ArrayReduce
@@ -54,10 +54,8 @@ export async function Insert (
if (props.value == null) return value
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const addition = getAttributeValue(control, execution, context)
value.push(addition)
}
const val = await getContextValue(props.value, control, execution)
Array.isArray(val) ? value.push(...val) : value.push(val)
} else {
value.push(props.value)
}
@@ -74,14 +72,11 @@ export async function Remove (
if (props.value == null) return value
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const addition = getAttributeValue(control, execution, context)
return value.filter((item) => item !== addition)
}
const val = await getContextValue(props.value, control, execution)
return value.filter((item) => item !== val)
} else {
return value.filter((item) => item !== props.value)
}
return value
}
export function RemoveFirst (value: Doc[], props: Record<string, any>): Doc[] {
@@ -121,11 +116,9 @@ export async function Prepend (
): Promise<string> {
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const addition = getAttributeValue(control, execution, context)
if (typeof addition !== 'string') return value
return addition + value
}
const val = await getContextValue(props.value, control, execution)
if (typeof val !== 'string') return value
return val + value
} else if (typeof value === 'string') {
return props.value + value
}
@@ -140,11 +133,9 @@ export async function Append (
): Promise<string> {
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const addition = getAttributeValue(control, execution, context)
if (typeof addition !== 'string') return value
return value + addition
}
const val = await getContextValue(props.value, control, execution)
if (typeof val !== 'string') return value
return value + val
} else if (typeof value === 'string') {
return value + props.value
}
@@ -216,10 +207,9 @@ export async function Add (
): Promise<number> {
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const offset = getAttributeValue(control, execution, context)
return value + offset
}
const val = await getContextValue(props.value, control, execution)
if (typeof val !== 'number') return value
return value + val
} else if (typeof value === 'number' && typeof props.value === 'number') {
return value + props.value
}
@@ -234,10 +224,9 @@ export async function Subtract (
): Promise<number> {
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const offset = getAttributeValue(control, execution, context)
return value - offset
}
const val = await getContextValue(props.value, control, execution)
if (typeof val !== 'number') return value
return value - val
} else if (typeof value === 'number' && typeof props.value === 'number') {
return value - props.value
}
@@ -252,10 +241,9 @@ export async function Multiply (
): Promise<number> {
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const val = getAttributeValue(control, execution, context)
return value * val
}
const val = await getContextValue(props.value, control, execution)
if (typeof val !== 'number') return value
return value * val
} else if (typeof value === 'number' && typeof props.value === 'number') {
return value * props.value
}
@@ -270,13 +258,11 @@ export async function Divide (
): Promise<number> {
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const val = getAttributeValue(control, execution, context)
if (val === 0) {
return value // Avoid division by zero
}
return value / val
const val = await getContextValue(props.value, control, execution)
if (val === 0 || typeof val !== 'number') {
return value // Avoid division by zero
}
return value / val
} else if (typeof value === 'number' && typeof props.value === 'number') {
if (props.value === 0) {
return value // Avoid division by zero
@@ -294,13 +280,11 @@ export async function Modulo (
): Promise<number> {
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const val = getAttributeValue(control, execution, context)
if (val === 0) {
return value // Avoid division by zero
}
return value % val
const val = await getContextValue(props.value, control, execution)
if (val === 0 || typeof val !== 'number') {
return value // Avoid division by zero
}
return value % val
} else if (typeof value === 'number' && typeof props.value === 'number') {
if (props.value === 0) {
return value // Avoid division by zero
@@ -318,10 +302,9 @@ export async function Power (
): Promise<number> {
const context = parseContext(props.value)
if (context !== undefined) {
if (context.type === 'attribute') {
const val = getAttributeValue(control, execution, context)
return Math.pow(value, val)
}
const val = await getContextValue(props.value, control, execution)
if (typeof val !== 'number') return value
return Math.pow(value, val)
} else if (typeof value === 'number' && typeof props.value === 'number') {
return Math.pow(value, props.value)
}
@@ -372,4 +355,17 @@ export async function RoleContext (
return users.map((it) => it.user)
}
export async function CurrentUser (
value: null,
props: Record<string, any>,
control: ProcessControl
): Promise<Ref<Person> | undefined> {
const socialId = await control.client.findOne(contact.class.SocialIdentity, { _id: control.client.user as any })
return socialId?.attachedTo
}
export async function CurrentDate (): Promise<Timestamp> {
return Date.now()
}
// #endregion
+253 -9
View File
@@ -13,22 +13,266 @@
// limitations under the License.
//
import { getObjectValue } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import process, { Execution, SelectedContext, processError } from '@hcengineering/process'
import { ProcessControl } from '@hcengineering/server-process'
import core, { ArrOf, Doc, getObjectValue, RefTo } from '@hcengineering/core'
import { getEmbeddedLabel, getResource } from '@hcengineering/platform'
import process, {
Execution,
ExecutionError,
parseContext,
ProcessError,
processError,
SelectedContext,
SelectedContextFunc,
SelectedExecutionContext,
SelectedNested,
SelectedRelation,
SelectedUserRequest
} from '@hcengineering/process'
import serverProcess, { ProcessControl } from '@hcengineering/server-process'
export function getAttributeValue (control: ProcessControl, execution: Execution, context: SelectedContext): any {
export async function getContextValue (value: any, control: ProcessControl, execution: Execution): Promise<any> {
const context = parseContext(value)
if (context !== undefined) {
let value: any | undefined
try {
if (context.type === 'attribute') {
value = getAttributeValue(control, execution, context)
} else if (context.type === 'relation') {
value = await getRelationValue(control, execution, context)
} else if (context.type === 'nested') {
value = await getNestedValue(control, execution, context)
} else if (context.type === 'userRequest') {
value = getUserRequestValue(control, execution, context)
} else if (context.type === 'function') {
value = await getFunctionValue(control, execution, context)
} else if (context.type === 'context') {
value = await getExecutionContextValue(control, execution, context)
}
return await fillValue(value, context, control, execution)
} catch (err: any) {
if (err instanceof ProcessError && context.fallbackValue !== undefined) {
return await fillValue(context.fallbackValue, context, control, execution)
}
throw err
}
} else if (typeof value === 'object' && !Array.isArray(value)) {
for (const key in value) {
value[key] = await getContextValue(value[key], control, execution)
}
return value
} else {
return value
}
}
function getAttributeValue (control: ProcessControl, execution: Execution, context: SelectedContext): any {
const card = control.cache.get(execution.card)
if (card !== undefined) {
const val = getObjectValue(context.key, card)
if (val == null) {
const attr = control.client.getHierarchy().findAttribute(card._class, context.key)
throw processError(
process.error.EmptyAttributeContextValue,
{},
{ attr: attr?.label ?? getEmbeddedLabel(context.key) }
)
}
return val
} else {
throw processError(process.error.ObjectNotFound, { _id: execution.card }, {}, true)
}
}
async function fillValue (
value: any,
context: SelectedContext,
control: ProcessControl,
execution: Execution
): Promise<any> {
for (const func of context.functions ?? []) {
const transform = control.client.getModel().findObject(func.func)
if (transform === undefined) throw processError(process.error.MethodNotFound, { methodId: func.func }, {}, true)
if (!control.client.getHierarchy().hasMixin(transform, serverProcess.mixin.FuncImpl)) {
throw processError(process.error.MethodNotFound, { methodId: func.func }, {}, true)
}
const funcImpl = control.client.getHierarchy().as(transform, serverProcess.mixin.FuncImpl)
const f = await getResource(funcImpl.func)
value = await f(value, func.props, control, execution)
}
return value
}
async function getNestedValue (
control: ProcessControl,
execution: Execution,
context: SelectedNested
): Promise<any | ExecutionError> {
const card = control.cache.get(execution.card)
if (card === undefined) throw processError(process.error.ObjectNotFound, { _id: execution.card }, {}, true)
const val = getObjectValue(context.key, card)
const attr = control.client.getHierarchy().findAttribute(card._class, context.path)
if (attr === undefined) throw processError(process.error.AttributeNotExists, { key: context.path })
const nestedValue = getObjectValue(context.path, card)
if (nestedValue === undefined) throw processError(process.error.EmptyAttributeContextValue, {}, { attr: attr.label })
const parentType = attr.type._class === core.class.ArrOf ? (attr.type as ArrOf<Doc>).of : attr.type
const targetClass = parentType._class === core.class.RefTo ? (parentType as RefTo<Doc>).to : parentType._class
const target = await control.client.findAll(targetClass, {
_id: { $in: Array.isArray(nestedValue) ? nestedValue : [nestedValue] }
})
if (target.length === 0) throw processError(process.error.RelatedObjectNotFound, {}, { attr: attr.label })
const nested = control.client.getHierarchy().findAttribute(targetClass, context.key)
if (context.sourceFunction !== undefined) {
const transform = control.client.getModel().findObject(context.sourceFunction)
if (transform === undefined) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
if (!control.client.getHierarchy().hasMixin(transform, serverProcess.mixin.FuncImpl)) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
const funcImpl = control.client.getHierarchy().as(transform, serverProcess.mixin.FuncImpl)
const f = await getResource(funcImpl.func)
const reduced = await f(target, {}, control, execution)
const val = Array.isArray(reduced)
? reduced.map((v) => getObjectValue(context.key, v))
: getObjectValue(context.key, reduced)
if (val == null) {
throw processError(
process.error.EmptyRelatedObjectValue,
{},
{ parent: attr.label, attr: nested?.label ?? getEmbeddedLabel(context.key) }
)
}
return val
}
const val =
Array.isArray(target) && target.length > 1
? target.map((v) => getObjectValue(context.key, v))
: getObjectValue(context.key, target[0])
if (val == null) {
const attr = control.client.getHierarchy().findAttribute(card._class, context.key)
throw processError(
process.error.EmptyAttributeContextValue,
process.error.EmptyRelatedObjectValue,
{},
{ attr: attr?.label ?? getEmbeddedLabel(context.key) }
{ parent: attr.label, attr: nested?.label ?? getEmbeddedLabel(context.key) }
)
}
return val
}
async function getRelationValue (
control: ProcessControl,
execution: Execution,
context: SelectedRelation
): Promise<any> {
const assoc = control.client.getModel().findObject(context.association)
if (assoc === undefined) throw processError(process.error.RelationNotExists, {})
const targetClass = context.direction === 'A' ? assoc.classA : assoc.classB
const q = context.direction === 'A' ? { docB: execution.card } : { docA: execution.card }
const relations = await control.client.findAll(core.class.Relation, { association: assoc._id, ...q })
const name = context.direction === 'A' ? assoc.nameA : assoc.nameB
if (relations.length === 0) throw processError(process.error.RelatedObjectNotFound, { attr: name })
const ids = relations.map((it) => {
return context.direction === 'A' ? it.docA : it.docB
})
const target = await control.client.findAll(targetClass, { _id: { $in: ids } })
if (target.length === 0) throw processError(process.error.RelatedObjectNotFound, { attr: context.name })
const attr = context.key !== '' ? control.client.getHierarchy().findAttribute(targetClass, context.key) : undefined
if (context.sourceFunction !== undefined) {
const transform = control.client.getModel().findObject(context.sourceFunction)
if (transform === undefined) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
if (!control.client.getHierarchy().hasMixin(transform, serverProcess.mixin.FuncImpl)) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
const funcImpl = control.client.getHierarchy().as(transform, serverProcess.mixin.FuncImpl)
const f = await getResource(funcImpl.func)
const reduced = await f(target, {}, control, execution)
const val = Array.isArray(reduced)
? reduced.map((v) => getObjectValue(context.key, v))
: getObjectValue(context.key, reduced)
if (val == null) {
throw processError(
process.error.EmptyRelatedObjectValue,
{ parent: name },
{ attr: attr?.label ?? getEmbeddedLabel(context.name) }
)
}
return val
}
const val =
Array.isArray(target) && target.length > 1
? target.map((v) => getObjectValue(context.key, v))
: getObjectValue(context.key, target[0])
if (val == null) {
throw processError(
process.error.EmptyRelatedObjectValue,
{ parent: name },
{ attr: attr?.label ?? getEmbeddedLabel(context.name) }
)
}
return val
}
async function getFunctionValue (
control: ProcessControl,
execution: Execution,
context: SelectedContextFunc
): Promise<any> {
const func = control.client.getModel().findObject(context.func)
if (func === undefined) throw processError(process.error.MethodNotFound, { methodId: context.func }, {}, true)
const impl = control.client.getHierarchy().as(func, serverProcess.mixin.FuncImpl)
if (impl === undefined) throw processError(process.error.MethodNotFound, { methodId: context.func }, {}, true)
const f = await getResource(impl.func)
const res = await f(null, context.props, control, execution)
if (context.sourceFunction !== undefined) {
const transform = control.client.getModel().findObject(context.sourceFunction)
if (transform === undefined) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
if (!control.client.getHierarchy().hasMixin(transform, serverProcess.mixin.FuncImpl)) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
const funcImpl = control.client.getHierarchy().as(transform, serverProcess.mixin.FuncImpl)
const f = await getResource(funcImpl.func)
const val = await f(res, {}, control, execution)
if (val == null) {
throw processError(process.error.EmptyFunctionResult, {}, { func: func.label })
}
return val
}
if (res == null) {
throw processError(process.error.EmptyFunctionResult, {}, { func: func.label })
}
return res
}
function getUserRequestValue (control: ProcessControl, execution: Execution, context: SelectedUserRequest): any {
const userContext = execution.context[context.id]
if (userContext !== undefined) return userContext
const attr = control.client.getHierarchy().findAttribute(context._class, context.key)
throw processError(
process.error.UserRequestedValueNotProvided,
{},
{ attr: attr?.label ?? getEmbeddedLabel(context.key) }
)
}
async function getExecutionContextValue (
control: ProcessControl,
execution: Execution,
context: SelectedExecutionContext
): Promise<any> {
const userContext = execution.context[context.id]
const _process = control.client.getModel().findObject(execution.process)
const processContext = _process?.context?.[context.id]
if (userContext !== undefined) {
if (context.key === '' || context.key === '_id') return userContext
if (processContext !== undefined) {
const contextVal = await control.client.findOne(processContext?._class, { _id: userContext })
if (contextVal !== undefined) {
const val = getObjectValue(context.key, contextVal)
return val
}
}
}
throw processError(process.error.ContextValueNotProvided, { name: processContext?.name ?? context.id })
}
+3 -1
View File
@@ -80,7 +80,9 @@ export default plugin(serverProcessId, {
Insert: '' as Resource<TransformFunc>,
Remove: '' as Resource<TransformFunc>,
RemoveFirst: '' as Resource<TransformFunc>,
RemoveLast: '' as Resource<TransformFunc>
RemoveLast: '' as Resource<TransformFunc>,
CurrentUser: '' as Resource<TransformFunc>,
CurrentDate: '' as Resource<TransformFunc>
},
trigger: {
OnTransition: '' as Resource<TriggerFunc>,
+10 -258
View File
@@ -14,20 +14,17 @@
//
import cardPlugin from '@hcengineering/card'
import core, {
ArrOf,
import {
Doc,
generateId,
getDiffUpdate,
getObjectValue,
MeasureContext,
Ref,
RefTo,
Tx,
TxProcessor,
WorkspaceUuid
} from '@hcengineering/core'
import { getEmbeddedLabel, getResource } from '@hcengineering/platform'
import { getResource } from '@hcengineering/platform'
import process, {
Execution,
ExecutionError,
@@ -38,12 +35,6 @@ import process, {
parseError,
processError,
ProcessError,
SelectedContext,
SelectedContextFunc,
SelectedExecutionContext,
SelectedNested,
SelectedRelation,
SelectedUserRequest,
State,
Step,
Transition
@@ -55,11 +46,12 @@ import serverProcess, {
ProcessMessage,
TriggerImpl
} from '@hcengineering/server-process'
import { isError } from './errors'
import { getClient, releaseClient } from './utils'
import { getTemporalClient } from './temporal'
import { getContextValue } from '@hcengineering/server-process-resources'
import { Client as TemporalClient } from '@temporalio/client'
import config from './config'
import { isError } from './errors'
import { getTemporalClient } from './temporal'
import { getClient, releaseClient } from './utils'
const activeExecutions = new Set<Ref<Execution>>()
@@ -149,11 +141,13 @@ async function findTransitions (
if (record.event === process.trigger.OnExecutionContinue) {
const transition = execution.error?.[0].transition
if (transition === undefined) return
return control.client.getModel().findAllSync(process.class.Transition, {
const res = control.client.getModel().findAllSync(process.class.Transition, {
_id: transition,
from: execution.currentState,
process: execution.process
})[0]
if (res.from === execution.currentState || res.from === null) {
return res
}
}
const transitions = control.client.getModel().findAllSync(process.class.Transition, {
from: execution.currentState,
@@ -449,130 +443,6 @@ async function executeAction<T extends Doc> (
}
}
async function fillValue (
value: any,
context: SelectedContext,
control: ProcessControl,
execution: Execution
): Promise<any> {
for (const func of context.functions ?? []) {
const transform = control.client.getModel().findObject(func.func)
if (transform === undefined) throw processError(process.error.MethodNotFound, { methodId: func.func }, {}, true)
if (!control.client.getHierarchy().hasMixin(transform, serverProcess.mixin.FuncImpl)) {
throw processError(process.error.MethodNotFound, { methodId: func.func }, {}, true)
}
const funcImpl = control.client.getHierarchy().as(transform, serverProcess.mixin.FuncImpl)
const f = await getResource(funcImpl.func)
value = await f(value, func.props, control, execution)
}
return value
}
async function getNestedValue (
control: ProcessControl,
execution: Execution,
context: SelectedNested
): Promise<any | ExecutionError> {
const card = control.cache.get(execution.card)
if (card === undefined) throw processError(process.error.ObjectNotFound, { _id: execution.card }, {}, true)
const attr = control.client.getHierarchy().findAttribute(card._class, context.path)
if (attr === undefined) throw processError(process.error.AttributeNotExists, { key: context.path })
const nestedValue = getObjectValue(context.path, card)
if (nestedValue === undefined) throw processError(process.error.EmptyAttributeContextValue, {}, { attr: attr.label })
const parentType = attr.type._class === core.class.ArrOf ? (attr.type as ArrOf<Doc>).of : attr.type
const targetClass = parentType._class === core.class.RefTo ? (parentType as RefTo<Doc>).to : parentType._class
const target = await control.client.findAll(targetClass, {
_id: { $in: Array.isArray(nestedValue) ? nestedValue : [nestedValue] }
})
if (target.length === 0) throw processError(process.error.RelatedObjectNotFound, {}, { attr: attr.label })
const nested = control.client.getHierarchy().findAttribute(targetClass, context.key)
if (context.sourceFunction !== undefined) {
const transform = control.client.getModel().findObject(context.sourceFunction)
if (transform === undefined) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
if (!control.client.getHierarchy().hasMixin(transform, serverProcess.mixin.FuncImpl)) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
const funcImpl = control.client.getHierarchy().as(transform, serverProcess.mixin.FuncImpl)
const f = await getResource(funcImpl.func)
const reduced = await f(target, {}, control, execution)
const val = getObjectValue(context.key, reduced)
if (val == null) {
throw processError(
process.error.EmptyRelatedObjectValue,
{},
{ parent: attr.label, attr: nested?.label ?? getEmbeddedLabel(context.key) }
)
}
return val
}
const val = getObjectValue(context.key, target[0])
if (val == null) {
throw processError(
process.error.EmptyRelatedObjectValue,
{},
{ parent: attr.label, attr: nested?.label ?? getEmbeddedLabel(context.key) }
)
}
return val
}
async function getRelationValue (
control: ProcessControl,
execution: Execution,
context: SelectedRelation
): Promise<any> {
const assoc = control.client.getModel().findObject(context.association)
if (assoc === undefined) throw processError(process.error.RelationNotExists, {})
const targetClass = context.direction === 'A' ? assoc.classA : assoc.classB
const q = context.direction === 'A' ? { docB: execution.card } : { docA: execution.card }
const relations = await control.client.findAll(core.class.Relation, { association: assoc._id, ...q })
const name = context.direction === 'A' ? assoc.nameA : assoc.nameB
if (relations.length === 0) throw processError(process.error.RelatedObjectNotFound, { attr: name })
const ids = relations.map((it) => {
return context.direction === 'A' ? it.docA : it.docB
})
const target = await control.client.findAll(targetClass, { _id: { $in: ids } })
if (target.length === 0) throw processError(process.error.RelatedObjectNotFound, { attr: context.name })
const attr = context.key !== '' ? control.client.getHierarchy().findAttribute(targetClass, context.key) : undefined
if (context.sourceFunction !== undefined) {
const transform = control.client.getModel().findObject(context.sourceFunction)
if (transform === undefined) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
if (!control.client.getHierarchy().hasMixin(transform, serverProcess.mixin.FuncImpl)) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
const funcImpl = control.client.getHierarchy().as(transform, serverProcess.mixin.FuncImpl)
const f = await getResource(funcImpl.func)
const reduced = await f(target, {}, control, execution)
const val = Array.isArray(reduced)
? reduced.map((v) => getObjectValue(context.key, v))
: getObjectValue(context.key, reduced)
if (val == null) {
throw processError(
process.error.EmptyRelatedObjectValue,
{ parent: name },
{ attr: attr?.label ?? getEmbeddedLabel(context.name) }
)
}
return val
}
const val =
Array.isArray(target) && target.length > 1
? target.map((v) => getObjectValue(context.key, v))
: getObjectValue(context.key, target[0])
if (val == null) {
throw processError(
process.error.EmptyRelatedObjectValue,
{ parent: name },
{ attr: attr?.label ?? getEmbeddedLabel(context.name) }
)
}
return val
}
async function fillParams<T extends Doc> (
params: MethodParams<T>,
execution: Execution,
@@ -587,124 +457,6 @@ async function fillParams<T extends Doc> (
return res
}
function getAttributeValue (control: ProcessControl, execution: Execution, context: SelectedContext): any {
const card = control.cache.get(execution.card)
if (card !== undefined) {
const val = getObjectValue(context.key, card)
if (val == null) {
const attr = control.client.getHierarchy().findAttribute(card._class, context.key)
throw processError(
process.error.EmptyAttributeContextValue,
{},
{ attr: attr?.label ?? getEmbeddedLabel(context.key) }
)
}
return val
} else {
throw processError(process.error.ObjectNotFound, { _id: execution.card }, {}, true)
}
}
async function getContextValue (value: any, control: ProcessControl, execution: Execution): Promise<any> {
const context = parseContext(value)
if (context !== undefined) {
let value: any | undefined
try {
if (context.type === 'attribute') {
value = getAttributeValue(control, execution, context)
} else if (context.type === 'relation') {
value = await getRelationValue(control, execution, context)
} else if (context.type === 'nested') {
value = await getNestedValue(control, execution, context)
} else if (context.type === 'userRequest') {
value = getUserRequestValue(control, execution, context)
} else if (context.type === 'function') {
value = await getFunctionValue(control, execution, context)
} else if (context.type === 'context') {
value = await getExecutionContextValue(control, execution, context)
}
return await fillValue(value, context, control, execution)
} catch (err: any) {
if (err instanceof ProcessError && context.fallbackValue !== undefined) {
return await fillValue(context.fallbackValue, context, control, execution)
}
throw err
}
} else if (typeof value === 'object' && !Array.isArray(value)) {
for (const key in value) {
value[key] = await getContextValue(value[key], control, execution)
}
return value
} else {
return value
}
}
async function getFunctionValue (
control: ProcessControl,
execution: Execution,
context: SelectedContextFunc
): Promise<any> {
const func = control.client.getModel().findObject(context.func)
if (func === undefined) throw processError(process.error.MethodNotFound, { methodId: context.func }, {}, true)
const impl = control.client.getHierarchy().as(func, serverProcess.mixin.FuncImpl)
if (impl === undefined) throw processError(process.error.MethodNotFound, { methodId: context.func }, {}, true)
const f = await getResource(impl.func)
const res = await f(null, context.props, control, execution)
if (context.sourceFunction !== undefined) {
const transform = control.client.getModel().findObject(context.sourceFunction)
if (transform === undefined) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
if (!control.client.getHierarchy().hasMixin(transform, serverProcess.mixin.FuncImpl)) {
throw processError(process.error.MethodNotFound, { methodId: context.sourceFunction }, {}, true)
}
const funcImpl = control.client.getHierarchy().as(transform, serverProcess.mixin.FuncImpl)
const f = await getResource(funcImpl.func)
const val = await f(res, {}, control, execution)
if (val == null) {
throw processError(process.error.EmptyFunctionResult, {}, { func: func.label })
}
return val
}
if (res == null) {
throw processError(process.error.EmptyFunctionResult, {}, { func: func.label })
}
return res
}
function getUserRequestValue (control: ProcessControl, execution: Execution, context: SelectedUserRequest): any {
const userContext = execution.context[context.id]
if (userContext !== undefined) return userContext
const attr = control.client.getHierarchy().findAttribute(context._class, context.key)
throw processError(
process.error.UserRequestedValueNotProvided,
{},
{ attr: attr?.label ?? getEmbeddedLabel(context.key) }
)
}
async function getExecutionContextValue (
control: ProcessControl,
execution: Execution,
context: SelectedExecutionContext
): Promise<any> {
const userContext = execution.context[context.id]
const _process = control.client.getModel().findObject(execution.process)
const processContext = _process?.context?.[context.id]
if (userContext !== undefined) {
if (context.key === '' || context.key === '_id') return userContext
if (processContext !== undefined) {
const contextVal = await control.client.findOne(processContext?._class, { _id: userContext })
if (contextVal !== undefined) {
const val = getObjectValue(context.key, contextVal)
return val
}
}
}
throw processError(process.error.ContextValueNotProvided, { name: processContext?.name ?? context.id })
}
async function checkParent (execution: Execution, control: ProcessControl): Promise<void> {
const subProcesses = await control.client.findAll(process.class.Execution, {
parentId: execution.parentId,
+2 -2
View File
@@ -35,7 +35,7 @@ services:
command: start-single-node --insecure
restart: unless-stopped
redpanda:
image: docker.redpanda.com/redpandadata/redpanda:v24.3.6
image: docker.redpanda.com/redpandadata/redpanda:v25.2.3
command:
- redpanda
- start
@@ -60,7 +60,7 @@ services:
timeout: 5s
retries: 10
redpanda_console:
image: docker.redpanda.com/redpandadata/console:v2.8.3
image: docker.redpanda.com/redpandadata/console:v2.8.10
entrypoint: /bin/sh
command: -c 'echo "$$CONSOLE_CONFIG_FILE" > /tmp/config.yml; /app/console'
environment:
+2 -2
View File
@@ -39,7 +39,7 @@ services:
command: start-single-node --insecure
restart: unless-stopped
redpanda:
image: docker.redpanda.com/redpandadata/redpanda:v24.3.6
image: docker.redpanda.com/redpandadata/redpanda:v25.2.3
extra_hosts:
- 'huly.local:host-gateway'
command:
@@ -66,7 +66,7 @@ services:
timeout: 5s
retries: 10
redpanda_console:
image: docker.redpanda.com/redpandadata/console:v2.8.3
image: docker.redpanda.com/redpandadata/console:v2.8.10
extra_hosts:
- 'huly.local:host-gateway'
entrypoint: /bin/sh