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-05 09:46:54 +07:00
35 changed files with 753 additions and 215 deletions
+209 -24
View File
@@ -55,7 +55,7 @@ import { type MongoClient } from 'mongodb'
import type postgres from 'postgres'
import { type Row } from 'postgres'
import { getToolToken } from './utils'
import { createFileBackupStorage, restore } from '@hcengineering/server-backup'
import { type BackupStorage, createFileBackupStorage, restore } from '@hcengineering/server-backup'
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
import { getPlatformQueue } from '@hcengineering/kafka'
import {
@@ -1167,7 +1167,8 @@ export async function migrateTrustedV6Accounts (
accountDB: AccountDB,
mongoDb: v6MongoAccountDB,
dryRun: boolean,
skipWorkspaces: Set<string>
skipWorkspaces: Set<string>,
conflictSuffix?: string
): Promise<void> {
// Mapping between <ObjectId, UUID>
const accountsIdToUuid: Record<string, AccountUuid> = {}
@@ -1229,13 +1230,16 @@ export async function migrateTrustedV6Accounts (
}
try {
const workspaceUuid = await migrateWorkspace(
workspace,
accountDB,
accountsIdToUuid,
accountsEmailToUuid,
dryRun
)
const [workspaceUuid] =
(await migrateWorkspace(
workspace,
accountDB,
accountsIdToUuid,
accountsEmailToUuid,
dryRun,
'manual-creation',
conflictSuffix
)) ?? []
if (workspaceUuid !== undefined) {
workspacesIdToUuid[workspace.workspace] = workspaceUuid
@@ -1394,8 +1398,12 @@ async function migrateWorkspace (
accountsIdToUuid: Record<string, AccountUuid>,
accountsEmailToUuid: Record<string, AccountUuid>,
dryRun = true,
forcedMode?: WorkspaceMode
): Promise<WorkspaceUuid | undefined> {
forcedMode?: WorkspaceMode,
conflictSuffix?: string,
throwExisting?: boolean,
region?: string,
branding?: string
): Promise<[WorkspaceUuid, string] | undefined> {
if (workspace.workspaceUrl == null) {
console.log('No workspace url, skipping', workspace.workspace)
return
@@ -1406,14 +1414,19 @@ async function migrateWorkspace (
console.log('No account found for workspace', workspace.workspace, 'created by', workspace.createdBy)
}
const existingByUrl = await accountDB.workspace.findOne({ url: workspace.workspaceUrl })
let existingByUrl = await accountDB.workspace.findOne({ url: workspace.workspaceUrl })
const existingByUuid = await accountDB.workspace.findOne({ uuid: workspace.uuid })
let workspaceUuid: WorkspaceUuid
let url = workspace.workspaceUrl
if (existingByUuid == null) {
let url = workspace.workspaceUrl
if (existingByUrl != null && (conflictSuffix ?? '') !== '') {
url = `${url}-${conflictSuffix}`
existingByUrl = await accountDB.workspace.findOne({ url })
}
if (existingByUrl != null) {
console.log('Conflicting workspace url', url)
// generate new url
url = `${url}-${generateId('-')}`
console.log('Generating new url', url)
@@ -1424,8 +1437,8 @@ async function migrateWorkspace (
name: workspace.workspaceName,
url,
dataId: workspace.workspace,
branding: workspace.branding,
region: workspace.region,
branding: branding ?? workspace.branding,
region: region ?? workspace.region,
createdBy,
billingAccount: createdBy,
createdOn: workspace.createdOn ?? Date.now()
@@ -1438,6 +1451,10 @@ async function migrateWorkspace (
workspaceUuid = generateUuid() as WorkspaceUuid
}
} else {
if (throwExisting === true) {
throw new Error(`Workspace with the same uuid ${workspace.uuid} already exists`)
}
workspaceUuid = existingByUuid.uuid
}
@@ -1488,7 +1505,7 @@ async function migrateWorkspace (
}
}
return workspaceUuid
return [workspaceUuid, url]
}
export async function restoreFromv6All (
@@ -1581,14 +1598,15 @@ export async function restoreFromv6All (
try {
// Create active workspaces as archived until they are actually restored
const workspaceUuid = await migrateWorkspace(
workspace,
accountDB,
accountsIdToUuid,
accountsEmailToUuid,
false,
isActive ? 'archived' : undefined
)
const [workspaceUuid] =
(await migrateWorkspace(
workspace,
accountDB,
accountsIdToUuid,
accountsEmailToUuid,
false,
isActive ? 'archived' : undefined
)) ?? []
if (workspaceUuid !== undefined) {
workspacesIdToUuid[workspace.workspace] = workspaceUuid
@@ -1720,3 +1738,170 @@ export async function restoreFromv6All (
ctx.error('Failed to restore v6 dump', { err })
}
}
export async function restoreTrustedV6Workspace (
ctx: MeasureMetricsContext,
accountDB: AccountDB,
workspace: OldWorkspace,
accounts: OldAccount[],
invites: any[],
backupWsStorage: BackupStorage,
workspaceStorage: StorageAdapter,
txes: Tx[],
dbUrl: string,
opts?: {
conflictSuffix?: string
region?: string
branding?: string
force?: boolean
}
): Promise<void> {
const { conflictSuffix, region, branding, force } = opts ?? {}
// Mapping between <ObjectId, UUID>
const accountsIdToUuid: Record<string, AccountUuid> = {}
// Mapping between <email, UUID>
const accountsEmailToUuid: Record<string, AccountUuid> = {}
let workspaceUuid: WorkspaceUuid | undefined
let newWorkspaceUrl: string | undefined
ctx.info('Restoring workspace accounts...')
let accountsProcessed = 0
for (const account of accounts) {
try {
const accountUuid = await migrateAccount(account, accountDB, false)
if (accountUuid == null) {
ctx.warn('Account not restored', account)
continue
}
accountsIdToUuid[account._id.toString()] = accountUuid
accountsEmailToUuid[account.email] = accountUuid
accountsProcessed++
if (accountsProcessed % 100 === 0) {
ctx.info('Processed accounts:', { accountsProcessed })
}
} catch (err: any) {
ctx.error('Failed to restore account', { _id: account._id, email: account.email, err })
}
}
ctx.info('Total accounts processed:', { accountsProcessed })
const oldMode = workspace.mode
try {
// Create workspace with manual-creation mode until it is restored
;[workspaceUuid, newWorkspaceUrl] =
(await migrateWorkspace(
workspace,
accountDB,
accountsIdToUuid,
accountsEmailToUuid,
false,
'manual-creation',
conflictSuffix,
force !== true,
region,
branding
)) ?? []
if (workspaceUuid === undefined) {
ctx.error('Workspace uuid not set', { workspace: workspace.workspace })
throw new Error(`Workspace uuid not set ${workspace.workspace}`)
}
if (newWorkspaceUrl == null) {
ctx.error('Workspace url not set', { workspace: workspace.workspace })
throw new Error(`Workspace url not set ${workspace.workspace}`)
}
let invitesProcessed = 0
for (const invite of invites) {
try {
if (workspace.workspace !== invite.workspace.name) {
ctx.error(
`Invite workspace ${invite.workspace.name} doesn't match workspace being restored ${workspace.workspace}`
)
continue
}
const existing = await accountDB.invite.findOne({ migratedFrom: invite._id.toString() })
if (existing != null) {
continue
}
const inviteRecord = {
migratedFrom: invite._id.toString(),
workspaceUuid,
expiresOn: invite.exp,
emailPattern: invite.emailMask,
remainingUses: invite.limit,
role: invite.role ?? AccountRole.User
}
await accountDB.invite.insertOne(inviteRecord)
invitesProcessed++
if (invitesProcessed % 100 === 0) {
ctx.info('Processed invites:', { invitesProcessed })
}
} catch (err: any) {
ctx.error('Failed to restore invite', { _id: invite._id, err })
}
}
ctx.info('Total invites processed:', { invitesProcessed })
const dataId = workspace.workspace
const url = newWorkspaceUrl
const uuid = workspaceUuid
const wsIds = {
uuid,
dataId,
url
}
const queue = getPlatformQueue('tool', workspace.region)
const wsProducer = queue.getProducer<QueueWorkspaceMessage>(ctx, QueueTopic.Workspace)
await wsProducer.send(ctx, uuid, [workspaceEvents.restoring()])
let pipeline: Pipeline | undefined
try {
pipeline = await createBackupPipeline(ctx, dbUrl, txes, {
externalStorage: workspaceStorage,
usePassedCtx: true
})(ctx, wsIds, createEmptyBroadcastOps(), null)
if (pipeline === undefined) {
ctx.error('failed to restore, pipeline is undefined', { dataId })
return
}
await sendTransactorEvent(uuid, 'force-maintenance')
await restore(ctx, pipeline, wsIds, backupWsStorage, {
date: -1,
merge: false,
parallel: 1,
recheck: false
})
await sendTransactorEvent(uuid, 'force-close')
ctx.info('workspace restored', { dataId })
await wsProducer.send(ctx, uuid, [workspaceEvents.restored()])
await accountDB.workspaceStatus.update({ workspaceUuid: uuid }, { mode: oldMode })
} catch (err) {
ctx.error('failed to restore backup of the workspace', { url, dataId, err })
} finally {
await pipeline?.close()
await queue.shutdown()
await workspaceStorage?.close()
}
} catch (err: any) {
ctx.error('Failed to restore workspace', { url: workspace.workspaceUrl, workspace: workspace.workspace, err })
}
}
+97 -2
View File
@@ -51,7 +51,11 @@ import {
} from '@hcengineering/server-pipeline'
import serverToken, { decodeToken, generateToken } from '@hcengineering/server-token'
import { createWorkspace, upgradeWorkspace } from '@hcengineering/workspace-service'
import { getMongoAccountDB } from '@hcengineering/account-service'
import {
getMongoAccountDB,
type Account as OldAccount,
type Workspace as OldWorkspace
} from '@hcengineering/account-service'
import { faker } from '@faker-js/faker'
import { getPlatformQueue } from '@hcengineering/kafka'
@@ -113,7 +117,8 @@ import {
migrateMergedAccounts,
migrateTrustedV6Accounts,
moveAccountDbFromMongoToPG,
restoreFromv6All
restoreFromv6All,
restoreTrustedV6Workspace
} from './db'
import { performGithubAccountMigrations } from './github'
import { performGmailAccountMigrations } from './gmail'
@@ -2750,6 +2755,96 @@ export function devTool (
}, dbUrl)
})
program
.command('restore-v6-from-storage <workspace> <accsRoot>')
.description('Restore a workspace from v6 backup storage with accounts info')
.option('-r, --region <region>', 'Region to restore workspace to')
.option('-b, --branding <branding>', 'Branding to restore workspace with', 'huly')
.option('-s, --suffix <suffix>', 'Url suffix if conflicting', 'bold')
.option('-f, --force', 'Force restore if the same uuid', false)
.action(async (workspace, accsRoot, cmd: { suffix: string, region: string, branding: string, force: boolean }) => {
const bucketName = process.env.BUCKET_NAME
if (bucketName === '' || bucketName == null) {
console.error('please provide bucket name env')
process.exit(1)
}
const backupStorageConfig = storageConfigFromEnv(process.env.BACKUP_STORAGE)
const backupStorageAdapter = createStorageFromConfig(backupStorageConfig.storages[0])
const backupIds = { uuid: bucketName as WorkspaceUuid, dataId: bucketName as WorkspaceDataId, url: '' }
const backupAccsStorage = await createStorageBackupStorage(toolCtx, backupStorageAdapter, backupIds, accsRoot)
const v6AccountsFile = 'account.accounts.json'
const v6WorkspacesFile = 'account.workspaces.json'
const v6InvitesFile = 'account.invites.json'
if (!(await backupAccsStorage.exists(v6AccountsFile))) {
toolCtx.error('file not present', { file: v6AccountsFile })
throw new Error(`${v6AccountsFile} should be present to restore`)
}
if (!(await backupAccsStorage.exists(v6WorkspacesFile))) {
toolCtx.error('file not present', { file: v6WorkspacesFile })
throw new Error(`${v6WorkspacesFile} should be present to restore`)
}
if (!(await backupAccsStorage.exists(v6InvitesFile))) {
toolCtx.error('file not present', { file: v6InvitesFile })
throw new Error(`${v6InvitesFile} should be present to restore`)
}
const v6Workspaces = JSON.parse((await backupAccsStorage.loadFile(v6WorkspacesFile)).toString()) as OldWorkspace[]
const v6Workspace = v6Workspaces.find((it) => it.workspace === workspace)
if (v6Workspace == null) {
toolCtx.error('workspace not found in the accounts backup', { workspace })
throw new Error(`workspace ${workspace} not found in the accounts backup`)
}
const uniqueWorkspaceAccounts = new Set((v6Workspace.accounts ?? []).map((it) => it.toString()))
const v6AccountsRaw = JSON.parse((await backupAccsStorage.loadFile(v6AccountsFile)).toString()) as any[]
const v6WorkspaceAccountsRaw = v6AccountsRaw.filter((acc) => uniqueWorkspaceAccounts.has(acc._id.toString()))
const v6WorkspaceAccounts: OldAccount[] = []
for (const rawAccount of v6WorkspaceAccountsRaw) {
const hashTypedArray = rawAccount.hash != null ? new Uint8Array(rawAccount.hash.data) : null
const saltTypedArray = new Uint8Array(rawAccount.salt.data)
v6WorkspaceAccounts.push({
...rawAccount,
hash: hashTypedArray != null ? Buffer.from(hashTypedArray.buffer) : null,
salt: Buffer.from(saltTypedArray.buffer)
})
}
let v6Invites = JSON.parse((await backupAccsStorage.loadFile(v6InvitesFile)).toString()) as any[]
v6Invites = v6Invites.filter((invite: any) => invite.workspace.name === v6Workspace.workspace)
const { txes, dbUrl } = prepareTools()
const backupWsStorage = await createStorageBackupStorage(
toolCtx,
backupStorageAdapter,
backupIds,
v6Workspace.uuid ?? v6Workspace.workspace
)
const storageConfig = storageConfigFromEnv()
const workspaceStorage: StorageAdapter = buildStorageFromConfig(storageConfig)
const { suffix, region, branding, force } = cmd
await withAccountDatabase(async (pgDb) => {
await restoreTrustedV6Workspace(
toolCtx,
pgDb,
v6Workspace,
v6WorkspaceAccounts,
v6Invites,
backupWsStorage,
workspaceStorage,
txes,
dbUrl,
{ conflictSuffix: suffix, region, branding, force }
)
}, dbUrl)
})
extendProgram?.(program)
process.on('unhandledRejection', (reason, promise) => {
+1 -1
View File
@@ -452,7 +452,7 @@ export function createModel (builder: Builder): void {
label: core.string.Spaces,
spaceClass: card.class.CardSpace,
addSpaceLabel: core.string.Space,
icon: card.icon.Card,
icon: card.icon.Space,
// intentionally left empty in order to make space presenter working
specials: []
}
+73 -2
View File
@@ -13,8 +13,15 @@
// limitations under the License.
//
import card, { type Card, cardId, DOMAIN_CARD, type MasterTag } from '@hcengineering/card'
import type { Doc, Ref } from '@hcengineering/core'
import card, { type Card, cardId, type CardSpace, DOMAIN_CARD, type MasterTag } from '@hcengineering/card'
import core, {
type Doc,
type Ref,
type Class,
DOMAIN_MODEL_TX,
type TxCreateDoc,
DOMAIN_SPACE
} from '@hcengineering/core'
import {
type MigrateOperation,
type MigrationClient,
@@ -40,6 +47,16 @@ export const chatOperation: MigrateOperation = {
state: 'migrate-parent-info',
mode: 'upgrade',
func: migrateParentInfo
},
{
state: 'migrate-channel-tags',
mode: 'upgrade',
func: migrateChannelTags
},
{
state: 'migrate-card-spaces',
mode: 'upgrade',
func: migrateCardSpaces
}
])
},
@@ -58,6 +75,34 @@ async function migrateChannelsToThreads (client: MigrationClient): Promise<void>
)
}
async function migrateChannelTags (client: MigrationClient): Promise<void> {
const tagTxes = await client.find<TxCreateDoc<Class<Card>>>(DOMAIN_MODEL_TX, {
_class: core.class.TxCreateDoc,
objectClass: { $in: [card.class.MasterTag, card.class.Tag] }
})
const updates: {
filter: MigrationDocumentQuery<TxCreateDoc<Class<Card>>>
update: MigrateUpdate<TxCreateDoc<Class<Card>>>
}[] = []
for (const tagTx of tagTxes) {
if (tagTx.attributes.extends !== channelMasterTag) {
continue
}
updates.push({
filter: { _id: tagTx._id },
update: {
attributes: {
...tagTx.attributes,
extends: chat.masterTag.Thread
}
}
})
}
await client.bulk(DOMAIN_MODEL_TX, updates)
client.logger.log('Migrated channel tags', { allTags: tagTxes.length, updatedTags: updates.length })
}
async function migrateParentInfo (client: MigrationClient, mode: MigrateMode): Promise<void> {
await performParentInfoMigration(client, 1000)
}
@@ -106,3 +151,29 @@ export async function performParentInfoMigration (client: MigrationClient, bulkS
await iterator.close()
}
}
async function migrateCardSpaces (client: MigrationClient): Promise<void> {
const cardSpaces = await client.find<CardSpace>(DOMAIN_SPACE, {
_class: card.class.CardSpace
})
const updates: {
filter: MigrationDocumentQuery<CardSpace>
update: MigrateUpdate<CardSpace>
}[] = []
for (const cs of cardSpaces) {
if (cs.types == null || !cs.types.includes(channelMasterTag)) {
continue
}
const types = cs.types.filter((t) => t !== channelMasterTag)
if (!types.includes(chat.masterTag.Thread)) {
types.push(chat.masterTag.Thread)
}
updates.push({
filter: { _id: cs._id },
update: { types }
})
}
await client.bulk(DOMAIN_SPACE, updates)
client.logger.log('Migrated card spaces', { allSpaces: cardSpaces.length, updatedSpaces: updates.length })
}
+3
View File
@@ -60,6 +60,7 @@ import {
type Step,
type Transition,
type Trigger,
type TriggerResult,
type UpdateCriteriaComponent,
processId
} from '@hcengineering/process'
@@ -131,6 +132,8 @@ export class TTransition extends TDoc implements Transition {
trigger!: Ref<Trigger>
triggerParams!: Record<string, any>
result?: TriggerResult | null
}
@Model(process.class.ExecutionLog, core.class.Doc, DOMAIN_PROCESS_LOG)
+3
View File
@@ -49,4 +49,7 @@
<symbol id="home" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.2929 2.29302C15.6834 1.90249 16.3166 1.90249 16.7071 2.29302L29.7071 15.293C30.0976 15.6835 30.0976 16.3167 29.7071 16.7072C29.3166 17.0978 28.6834 17.0978 28.2929 16.7072L28 16.4143V26.0001C28 27.1047 27.1046 28.0001 26 28.0001H6C4.89543 28.0001 4 27.1047 4 26.0001V16.4143L3.70711 16.7072C3.31658 17.0978 2.68342 17.0978 2.29289 16.7072C1.90237 16.3167 1.90237 15.6835 2.29289 15.293L15.2929 2.29302ZM6 14.4143V26.0001H12V17.0001C12 16.4478 12.4477 16.0001 13 16.0001H19C19.5523 16.0001 20 16.4478 20 17.0001V26.0001H26V14.4143L16 4.41434L6 14.4143ZM18 26.0001V18.0001H14V26.0001H18Z" fill="currentColor"/>
</symbol>
<symbol id="space" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 9C2 6.23858 4.23858 4 7 4H10.6716C11.4672 4 12.2303 4.31607 12.7929 4.87868L15.6213 7.70711C15.8089 7.89464 16.0632 8 16.3284 8H25C27.7614 8 30 10.2386 30 13V23C30 25.7614 27.7614 28 25 28H7C4.23858 28 2 25.7614 2 23V9ZM7 6C5.34315 6 4 7.34315 4 9V23C4 24.6569 5.34315 26 7 26H25C26.6569 26 28 24.6569 28 23V13C28 11.3431 26.6569 10 25 10H16.3284C15.5328 10 14.7697 9.68393 14.2071 9.12132L11.3787 6.29289C11.1911 6.10536 10.9368 6 10.6716 6H7Z" fill="currentColor"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

+2 -1
View File
@@ -24,5 +24,6 @@ loadMetadata(card.icon, {
File: `${icons}#file`,
View: `${icons}#view`,
Document: `${icons}#document`,
Home: `${icons}#home`
Home: `${icons}#home`,
Space: `${icons}#space`
})
@@ -30,7 +30,7 @@
<div class="flex-presenter flex-gap-0-5">
{#if displaySpace && card.$lookup?.space !== undefined}
<div class="card-presenter">
<Icon icon={cardPlugin.icon.Card} size="small" />
<Icon icon={cardPlugin.icon.Space} size="small" />
<span class="overflow-label max-w-40">
{card.$lookup?.space.name}
</span>
@@ -25,6 +25,7 @@
Loading,
showPopup
} from '@hcengineering/ui'
import { FilterBar, FilterButton } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import HomeCardPresenter from './HomeCardPresenter.svelte'
@@ -42,12 +43,13 @@
let cards: Card[] = []
let total = -1
let isLoading = true
let searchQuery: string = ''
let search: string = ''
$: query = searchQuery != null && searchQuery.trim() !== '' ? { $search: searchQuery } : {}
$: searchQuery = search != null && search.trim() !== '' ? { $search: search } : {}
$: resultQuery = { ...searchQuery }
$: cardsQuery.query(
card.class.Card,
query,
resultQuery,
(res) => {
cards = res
total = res.total
@@ -131,10 +133,18 @@
<Label label={card.string.Home} />
</div>
<div class="flex flex-gap-2">
<SearchInput bind:value={searchQuery} collapsed />
<SearchInput bind:value={search} collapsed />
<FilterButton _class={card.class.Card} />
<div class="hulyHeader-divider" />
<ModernButton icon={IconSettings} on:click={onSettings} size="small" iconSize="small" kind="tertiary" />
</div>
</div>
<FilterBar
_class={card.class.Card}
query={searchQuery}
space={undefined}
on:change={({ detail }) => (resultQuery = detail)}
/>
<div class="create-card">
<ModernEditbox
bind:value={title}
@@ -39,7 +39,7 @@
<TreeNode
_id={space._id}
icon={cardPlugin.icon.Card}
icon={cardPlugin.icon.Space}
title={space.name}
type={'nested'}
on:dragstart={(evt) => {
+2 -1
View File
@@ -150,7 +150,8 @@ const cardPlugin = plugin(cardId, {
File: '' as Asset,
View: '' as Asset,
Document: '' as Asset,
Home: '' as Asset
Home: '' as Asset,
Space: '' as Asset
},
extensions: {
EditCardExtension: '' as ComponentExtensionId
@@ -19,7 +19,7 @@
import {
Context,
Process,
ProcessContext,
ProcessExecutionContext,
ProcessFunction,
RelatedContext,
SelectedContext
@@ -83,7 +83,7 @@
dispatch('close')
}
function onProcessContext (ctx: ProcessContext): void {
function onProcessContext (ctx: ProcessExecutionContext): void {
onSelect(ctx.value)
dispatch('close')
}
@@ -155,15 +155,30 @@
<div class="menu-separator" />
{/if}
{#if processContext.length > 0}
{#each processContext as f}
<button
on:click={() => {
onProcessContext(f)
}}
class="menu-item"
>
<ExecutionContextPresenter {process} contextValue={f.value} />
</button>
{#each processContext as pc}
{#if pc.attributes.length > 0}
<Submenu
component={ExecutionContextPresenter}
props={{
context: pc,
target: attribute,
contextValue: pc.value,
process,
onSelect: onClick
}}
options={{ component: plugin.component.ExecutionContextSelector }}
withHover
/>
{:else}
<button
on:click={() => {
onProcessContext(pc)
}}
class="menu-item"
>
<ExecutionContextPresenter {process} contextValue={pc.value} />
</button>
{/if}
{/each}
<div class="menu-separator" />
{/if}
@@ -13,18 +13,28 @@
// limitations under the License.
-->
<script lang="ts">
import { Process, SelectedExecutonContext } from '@hcengineering/process'
import { Context, Process, SelectedExecutionContext } from '@hcengineering/process'
import ui, { Label } from '@hcengineering/ui'
import ProcessContextPresenter from '../contextEditors/ProcessContextPresenter.svelte'
import { getClient } from '@hcengineering/presentation'
export let contextValue: SelectedExecutonContext
export let contextValue: SelectedExecutionContext
export let process: Process
const client = getClient()
$: ctx = process.context[contextValue.id]
$: attr = contextValue.key !== '' ? client.getHierarchy().findAttribute(ctx._class, contextValue.key) : undefined
</script>
{#if ctx !== undefined}
<ProcessContextPresenter context={ctx} />
{#if attr !== undefined}
<span class="attr">
<Label label={attr.label} />
</span>
{/if}
{:else}
<Label label={ui.string.NotSelected} />
{/if}
@@ -0,0 +1,80 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { AnyAttribute } from '@hcengineering/core'
import { ProcessExecutionContext, SelectedContext } from '@hcengineering/process'
import { Label, resizeObserver, Scroller } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { getValueReduceFunc } from '../../utils'
export let context: ProcessExecutionContext
export let target: AnyAttribute
export let onSelect: (val: SelectedContext) => void
const dispatch = createEventDispatcher()
const elements: HTMLButtonElement[] = []
const keyDown = (event: KeyboardEvent, index: number): void => {
if (event.key === 'ArrowDown') {
elements[(index + 1) % elements.length].focus()
}
if (event.key === 'ArrowUp') {
elements[(elements.length + index - 1) % elements.length].focus()
}
if (event.key === 'ArrowLeft') {
dispatch('close')
}
}
function onAttribute (attr: AnyAttribute): void {
const valueFunc = getValueReduceFunc(attr, target)
onSelect({
type: 'context',
key: attr.name,
id: context.context,
functions: valueFunc !== undefined ? [{ func: valueFunc, props: {} }] : []
})
}
</script>
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
<div class="menu-space" />
<Scroller>
{#each context.attributes as attr, i}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
bind:this={elements[i]}
on:keydown={(event) => {
keyDown(event, i)
}}
on:mouseover={() => {
elements[i]?.focus()
}}
on:click={() => {
onAttribute(attr)
}}
class="menu-item"
>
<span class="overflow-label pr-1">
<Label label={attr.label} />
</span>
</button>
{/each}
</Scroller>
<div class="menu-space" />
</div>
@@ -16,7 +16,6 @@
import { getClient } from '@hcengineering/presentation'
import { ProcessContext } from '@hcengineering/process'
import { Label } from '@hcengineering/ui'
import process from '../../plugin'
export let context: ProcessContext
@@ -21,7 +21,7 @@
Process,
ProcessToDo,
SelectedContext,
SelectedExecutonContext
SelectedExecutionContext
} from '@hcengineering/process'
import ui, {
Button,
@@ -52,14 +52,14 @@
const client = getClient()
function getContext (value: string): SelectedExecutonContext | undefined {
function getContext (value: string): SelectedExecutionContext | undefined {
const context = parseContext(value)
if (context !== undefined && isExecutionContext(context)) {
return context
}
}
function isExecutionContext (context: SelectedContext): context is SelectedExecutonContext {
function isExecutionContext (context: SelectedContext): context is SelectedExecutionContext {
return context.type === 'context'
}
@@ -15,7 +15,7 @@
<script lang="ts">
import core, { Ref } from '@hcengineering/core'
import { Card, createQuery, getClient } from '@hcengineering/presentation'
import { Process, State, Trigger } from '@hcengineering/process'
import { Process, State, Trigger, TriggerResult } from '@hcengineering/process'
import { Component, Dropdown, DropdownIntlItem, DropdownLabelsIntl, Label, ListItem } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
@@ -25,6 +25,7 @@
const dispatch = createEventDispatcher()
let to: Ref<State> | undefined = undefined
let states: State[] = []
let result: TriggerResult | null = null
const query = createQuery()
query.query(plugin.class.State, { process: process._id }, (res) => {
@@ -66,13 +67,19 @@
trigger,
triggerParams: params,
process: process._id,
result,
actions: []
})
dispatch('close')
}
function change (e: CustomEvent<Record<string, any>>): void {
params = e.detail
if (e.detail?.params !== undefined) {
params = e.detail.params
}
if (e.detail?.result !== undefined) {
result = e.detail.result
}
}
</script>
@@ -111,13 +118,17 @@
items={triggersItems}
bind:selected={trigger}
label={plugin.string.Trigger}
on:selected={() => {
params = {}
result = null
}}
justify={'left'}
width={'100%'}
kind={'no-border'}
/>
</div>
{#if triggerValue?.editor}
<Component is={triggerValue.editor} props={{ process, params }} on:change={change} />
<Component is={triggerValue.editor} props={{ process, params, result }} on:change={change} />
{/if}
</Card>
@@ -20,11 +20,12 @@
export let transition: Transition
let params = transition.triggerParams ?? {}
let result = transition.result ?? null
const client = getClient()
async function save (): Promise<void> {
await client.update(transition, { triggerParams: params, trigger: selectedTrigger })
await client.update(transition, { triggerParams: params, trigger: selectedTrigger, result })
clearSettingsStore()
}
@@ -34,7 +35,12 @@
}
function change (e: CustomEvent<Record<string, any>>): void {
params = e.detail
if (e.detail?.params !== undefined) {
params = e.detail.params
}
if (e.detail?.result !== undefined) {
result = e.detail.result
}
}
let selectedTrigger: Ref<Trigger> = transition.trigger
@@ -79,7 +85,7 @@
</div>
{#if trigger.editor !== undefined}
<div class="editor">
<Component is={trigger.editor} props={{ process, params, readonly }} on:change={change} />
<Component is={trigger.editor} props={{ process, params, result, readonly }} on:change={change} />
</div>
{/if}
{/if}
@@ -35,7 +35,7 @@
function change (e: CustomEvent<any>): void {
if (readonly || e.detail == null) return
params = e.detail
dispatch('change', params)
dispatch('change', { params })
}
function getKeys (_class: Ref<Class<MasterTag>>): AnyAttribute[] {
@@ -87,7 +87,7 @@
keys = keys.filter((k) => k !== key)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (params as any)[key]
dispatch('change', params)
dispatch('change', { params })
}
}
</script>
@@ -16,33 +16,23 @@
<script lang="ts">
import { Doc } from '@hcengineering/core'
import { Process, Step } from '@hcengineering/process'
import ProcessContextPresenter from '../contextEditors/ProcessContextPresenter.svelte'
import { Label } from '@hcengineering/ui'
import processPlugin from '../../plugin'
import ProcessContextPresenter from '../contextEditors/ProcessContextPresenter.svelte'
export let process: Process
export let step: Step<Doc>
$: currentContext = step.context ? process.context[step.context._id] : undefined
$: currentResultContext = step.result ? process.context[step.result._id] : undefined
</script>
{#if currentContext || currentResultContext}
<Label label={processPlugin.string.Result} />
{/if}
{#if currentContext}
<Label label={processPlugin.string.Result} />
<div class="container">
<ProcessContextPresenter context={currentContext} />
</div>
{/if}
{#if currentResultContext}
<div class="container">
<ProcessContextPresenter context={currentResultContext} />
</div>
{/if}
<style lang="scss">
.container {
padding: 0.25rem 0.5rem;
@@ -13,9 +13,9 @@
// limitations under the License.
-->
<script lang="ts">
import core, { Class, Doc, PropertyType, Ref, Type } from '@hcengineering/core'
import core, { Class, PropertyType, Ref, Type } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Step } from '@hcengineering/process'
import { TriggerResult } from '@hcengineering/process'
import setting from '@hcengineering/setting-resources/src/plugin'
import {
AnyComponent,
@@ -32,10 +32,10 @@
import plugin from '../../plugin'
import { generateContextId } from '../../utils'
export let step: Step<Doc>
export let result: TriggerResult | null
let type: Type<any> | undefined | null = step.result?.type
let name: string = step.result?.name ?? ''
let type: Type<any> | undefined | null = result?.type
let name: string = result?.name ?? ''
let is: AnyComponent | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@@ -43,15 +43,15 @@
function update (): void {
if (type == null) {
step.result = undefined
result = null
} else {
step.result = {
result = {
_id: generateContextId(),
name,
type
}
}
dispatch('change', step)
dispatch('change', result)
}
function getTypes (): DropdownIntlItem[] {
@@ -91,9 +91,9 @@
}
function handleNameChange (e: any): void {
if (step.result != null) {
step.result.name = name
dispatch('change', step)
if (result != null) {
result.name = name
dispatch('change', result)
}
}
@@ -110,7 +110,7 @@
<div class="grid">
<Label label={plugin.string.Result} />
<Toggle
on={step.result !== undefined}
on={result != null}
on:change={(e) => {
changeRequired(e.detail)
}}
@@ -150,14 +150,12 @@
<style lang="scss">
.grid {
display: grid;
grid-template-columns: 1fr 1.5fr;
grid-template-columns: 1fr 3fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
margin: 0.25rem 2rem 0;
width: calc(100% - 4rem);
height: min-content;
}
</style>
@@ -36,13 +36,6 @@
}
const keys = ['title', 'user', 'dueDate']
function changeResult (e: CustomEvent<any>): void {
if (e.detail !== undefined) {
step = e.detail
dispatch('change', step)
}
}
</script>
<ParamsEditor _class={plugin.class.ProcessToDo} {process} {keys} {params} on:change={changeParams} />
@@ -60,10 +53,6 @@
}}
/>
</div>
<div class="divider" />
{#key step._id}
<ResultEditor {step} on:change={changeResult} />
{/key}
<style lang="scss">
.divider {
@@ -14,23 +14,32 @@
-->
<script lang="ts">
import { Process } from '@hcengineering/process'
import { Process, TriggerResult } from '@hcengineering/process'
import { Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
import ToDoContextSelector from '../contextEditors/ToDoContextSelector.svelte'
import ResultEditor from './ResultEditor.svelte'
export let readonly: boolean
export let process: Process
export let params: Record<string, any>
export let skipRollback: boolean = false
export let result: TriggerResult | null = null
const dispatch = createEventDispatcher()
function change (e: CustomEvent<string>): void {
if (readonly || e.detail == null) return
params._id = e.detail
dispatch('change', params)
dispatch('change', { params })
}
function changeResult (e: CustomEvent<any>): void {
if (e.detail !== undefined) {
result = e.detail
dispatch('change', { result })
}
}
</script>
@@ -38,8 +47,17 @@
<Label label={plugin.string.ToDo} />
<ToDoContextSelector {readonly} {skipRollback} {process} value={params._id} on:change={change} />
</div>
{#if !skipRollback}
<div class="divider" />
<ResultEditor {result} on:change={changeResult} />
{/if}
<style lang="scss">
.divider {
border-bottom: 1px solid var(--divider-color);
margin: 1rem 0;
}
.grid {
display: grid;
grid-template-columns: 1fr 3fr;
@@ -14,7 +14,7 @@
-->
<script lang="ts">
import { parseContext, Process, SelectedContext, SelectedExecutonContext } from '@hcengineering/process'
import { parseContext, Process, SelectedContext, SelectedExecutionContext } from '@hcengineering/process'
import ui, { Label } from '@hcengineering/ui'
import ExecutionContextPresenter from '../attributeEditors/ExecutionContextPresenter.svelte'
@@ -23,7 +23,7 @@
$: context = getContext(params._id)
function getContext (value: string | undefined): SelectedExecutonContext | undefined {
function getContext (value: string | undefined): SelectedExecutionContext | undefined {
if (value === undefined) return
const context = parseContext(value)
if (context !== undefined && isExecutionContext(context)) {
@@ -31,7 +31,7 @@
}
}
function isExecutionContext (context: SelectedContext): context is SelectedExecutonContext {
function isExecutionContext (context: SelectedContext): context is SelectedExecutionContext {
return context.type === 'context'
}
</script>
+2
View File
@@ -15,6 +15,7 @@ import { type Resources } from '@hcengineering/platform'
import FunctionSelector from './components/attributeEditors/FunctionSelector.svelte'
import NestedContextSelector from './components/attributeEditors/NestedContextSelector.svelte'
import RelatedContextSelector from './components/attributeEditors/RelatedContextSelector.svelte'
import ExecutionContextSelector from './components/attributeEditors/ExecutionContextSelector.svelte'
import RequestUserInput from './components/contextEditors/RequestUserInput.svelte'
import ResultInput from './components/contextEditors/ResultInput.svelte'
import RoleEditor from './components/contextEditors/RoleEditor.svelte'
@@ -100,6 +101,7 @@ export default async (): Promise<Resources> => ({
ProcessPresenter,
NestedContextSelector,
RelatedContextSelector,
ExecutionContextSelector,
FunctionSelector,
Main,
RunProcessCardPopup,
+5 -3
View File
@@ -168,9 +168,11 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre
if (transition === undefined) return
const context = await getNextStateUserInput(execution, transition, execution.context)
const txop = new TxOperations(this.client, getCurrentAccount().primarySocialId)
await txop.update(execution, {
context
})
if (context !== undefined) {
await txop.update(execution, {
context
})
}
await requestResult(txop, execution, transition, execution.context)
}
}
+1
View File
@@ -38,6 +38,7 @@ export default mergeIds(processId, process, {
ProcessPresenter: '' as AnyComponent,
ExecutonPresenter: '' as AnyComponent,
ExecutonProgressPresenter: '' as AnyComponent,
ExecutionContextSelector: '' as AnyComponent,
NestedContextSelector: '' as AnyComponent,
RelatedContextSelector: '' as AnyComponent,
FunctionSelector: '' as AnyComponent,
+78 -53
View File
@@ -13,9 +13,6 @@
import { type Card, type MasterTag } from '@hcengineering/card'
import core, {
generateId,
type Hierarchy,
matchQuery,
type AnyAttribute,
type ArrOf,
type Association,
@@ -23,6 +20,9 @@ import core, {
type Client,
type Doc,
type DocumentQuery,
generateId,
type Hierarchy,
matchQuery,
type ModelDb,
type Ref,
type RefTo,
@@ -33,16 +33,16 @@ import core, {
import { getResource, PlatformError, Severity, Status } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import {
ExecutionStatus,
parseContext,
type Context,
type ContextId,
type Execution,
type ExecutionContext,
ExecutionStatus,
type Method,
type NestedContext,
parseContext,
type Process,
type ProcessContext,
type ProcessExecutionContext,
type ProcessFunction,
type RelatedContext,
type SelectedContext,
@@ -174,7 +174,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 executionContext: Record<string, ProcessExecutionContext> = {}
const refs = getClassAttributes(client, process.masterTag, core.class.RefTo, 'attribute')
for (const ref of refs) {
@@ -241,11 +241,25 @@ 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
for (const key in process.context) {
const contextId = key as ContextId
const value = process.context[contextId]
if (client.getHierarchy().isDerived(value._class, target)) {
executionContext[key] = {
attributes: [],
name: value.name,
context: contextId,
value: value.value
}
} else {
const contextAttributes = getClassAttributes(client, value._class, target, category)
if (contextAttributes.length > 0) {
executionContext[key] = {
name: value.name,
context: contextId,
value: value.value,
attributes: contextAttributes
}
}
}
}
@@ -404,11 +418,13 @@ export async function continueExecution (value: Execution): Promise<void> {
let context = value.context
const transition = value.error[0].transition
if (transition == null) {
context = await newExecutionUserInput(value.process, context)
const res = await newExecutionUserInput(value.process, context)
context = res ?? context
} else {
const _transition = client.getModel().findObject(transition)
if (_transition === undefined) return
context = await getNextStateUserInput(value, _transition, value.context)
const res = await getNextStateUserInput(value, _transition, context)
context = res ?? context
}
await client.update(value, { status: ExecutionStatus.Active, context })
}
@@ -417,17 +433,24 @@ export async function requestUserInput (
processId: Ref<Process>,
target: Transition,
userContext: ExecutionContext
): Promise<ExecutionContext> {
userContext = await getTransitionUserInput(processId, target, userContext)
userContext = await getSubProcessesUserInput(target, userContext)
return userContext
): Promise<ExecutionContext | undefined> {
const tr = await getTransitionUserInput(processId, target, userContext)
if (tr !== undefined) {
userContext = { ...userContext, ...tr }
}
const sub = await getSubProcessesUserInput(target, userContext)
if (sub !== undefined) {
userContext = { ...userContext, ...sub }
}
return sub !== undefined || tr !== undefined ? userContext : undefined
}
export async function getTransitionUserInput (
processId: Ref<Process>,
transition: Transition,
userContext: ExecutionContext
): Promise<ExecutionContext> {
): Promise<ExecutionContext | undefined> {
const changed = false
for (const action of transition.actions) {
if (action == null) continue
for (const key in action.params) {
@@ -451,43 +474,45 @@ export async function getTransitionUserInput (
}
}
}
return userContext
return changed ? userContext : undefined
}
export async function getSubProcessesUserInput (
transition: Transition,
userContext: ExecutionContext
): Promise<ExecutionContext> {
): Promise<ExecutionContext | undefined> {
const changed = false
for (const action of transition.actions) {
if (action.methodId !== process.method.RunSubProcess) continue
const processId = action.params._id as Ref<Process>
if (processId === undefined) continue
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const res = await newExecutionUserInput(processId, {} as ExecutionContext)
if (action.context == null) continue
const res = await newExecutionUserInput(processId)
if (action.context == null || res === undefined) continue
userContext[action.context._id] = res
}
return userContext
return changed ? userContext : undefined
}
export async function newExecutionUserInput (
_id: Ref<Process>,
userContext: ExecutionContext
): Promise<ExecutionContext> {
userContext?: ExecutionContext
): Promise<ExecutionContext | undefined> {
const client = getClient()
const initTransition = client.getModel().findAllSync(process.class.Transition, {
process: _id,
from: null
})[0]
if (initTransition === undefined) return userContext
return await requestUserInput(_id, initTransition, userContext)
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const emptyContext: ExecutionContext = {} as ExecutionContext
return await requestUserInput(_id, initTransition, userContext ?? emptyContext)
}
export async function getNextStateUserInput (
execution: Execution,
transition: Transition,
userContext: ExecutionContext
): Promise<ExecutionContext> {
): Promise<ExecutionContext | undefined> {
const client = getClient()
const process = client.getModel().findObject(execution.process)
if (process === undefined) return userContext
@@ -497,7 +522,7 @@ export async function getNextStateUserInput (
export async function createExecution (card: Ref<Card>, _id: Ref<Process>, space: Ref<Space>): Promise<void> {
const client = getClient()
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const context = await newExecutionUserInput(_id, {} as ExecutionContext)
const context = await newExecutionUserInput(_id)
const _process = client.getModel().findObject(_id)
if (_process === undefined) return
const initTransition = client.getModel().findAllSync(process.class.Transition, {
@@ -505,12 +530,14 @@ export async function createExecution (card: Ref<Card>, _id: Ref<Process>, space
from: null
})[0]
if (initTransition === undefined) return
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const emptyContext: ExecutionContext = {} as ExecutionContext
await client.createDoc(process.class.Execution, space, {
process: _id,
currentState: initTransition.to,
card,
rollback: [],
context,
context: context ?? emptyContext,
status: ExecutionStatus.Active
})
}
@@ -544,29 +571,27 @@ export async function requestResult (
transition: Transition,
context: ExecutionContext
): Promise<void> {
for (const action of transition.actions) {
if (action.result == null) continue
const promise = new Promise<void>((resolve, reject) => {
showPopup(
process.component.ResultInput,
{ type: action.result?.type, name: action.result?.name },
undefined,
(res) => {
if (action.result?._id === undefined) return
if (res?.value !== undefined) {
context[action.result._id] = res.value
resolve()
} else {
reject(new PlatformError(new Status(Severity.ERROR, process.error.ResultNotProvided, {})))
}
if (transition.result == null) return
const promise = new Promise<void>((resolve, reject) => {
showPopup(
process.component.ResultInput,
{ type: transition.result?.type, name: transition.result?.name },
undefined,
(res) => {
if (transition.result?._id === undefined) return
if (res?.value !== undefined) {
context[transition.result._id] = res.value
resolve()
} else {
reject(new PlatformError(new Status(Severity.ERROR, process.error.ResultNotProvided, {})))
}
)
})
await promise
await txop.update(execution, {
context
})
}
}
)
})
await promise
await txop.update(execution, {
context
})
}
export function todoTranstionCheck (params: Record<string, any>, doc: Doc): boolean {
+5 -5
View File
@@ -17,7 +17,7 @@ import { Asset, IntlString, Plugin, plugin, Resource } from '@hcengineering/plat
import { ToDo } from '@hcengineering/time'
import { AnyComponent } from '@hcengineering/ui'
import { AttributeCategory } from '@hcengineering/view'
import { SelectedExecutonContext } from './types'
import { SelectedExecutionContext } from './types'
/**
* @public
@@ -38,12 +38,12 @@ export interface Process extends Doc {
export interface ProcessContext {
name: string
_class: Ref<Class<Doc>>
action: StepId
action?: StepId
index: number
producer: Ref<Transition>
isResult?: boolean
type?: Type<any>
value: SelectedExecutonContext
value: SelectedExecutionContext
}
export type ContextId = string & { __contextId: true }
@@ -66,6 +66,7 @@ export interface Transition extends Doc {
actions: Step<Doc>[]
trigger: Ref<Trigger>
triggerParams: Record<string, any>
result?: TriggerResult | null
}
export interface ExecutionLog extends Doc {
@@ -138,7 +139,6 @@ export interface Step<T extends Doc> {
context: StepContext | null
methodId: Ref<Method<T>>
params: MethodParams<T>
result?: StepResult
}
export interface StepContext {
@@ -146,7 +146,7 @@ export interface StepContext {
_class?: Ref<Class<Doc>> // class of the context
}
export interface StepResult {
export interface TriggerResult {
_id: ContextId // context id
name: string
type: Type<any>
+11 -4
View File
@@ -1,12 +1,12 @@
import { Class, Doc, type AnyAttribute, type Association, type Ref } from '@hcengineering/core'
import { ContextId, ProcessContext, ProcessFunction } from '.'
import { ContextId, ProcessFunction } from '.'
export interface Context {
functions: Ref<ProcessFunction>[]
attributes: AnyAttribute[]
nested: Record<string, NestedContext>
relations: Record<string, RelatedContext>
executionContext: Record<ContextId, ProcessContext>
executionContext: Record<ContextId, ProcessExecutionContext>
}
export interface NestedContext {
@@ -21,6 +21,13 @@ export interface RelatedContext {
attributes: AnyAttribute[]
}
export interface ProcessExecutionContext {
name: string
context: ContextId
value: SelectedExecutionContext
attributes: AnyAttribute[]
}
export interface Func {
func: Ref<ProcessFunction>
props: Record<string, any>
@@ -62,7 +69,7 @@ export interface SelectedUserRequest extends BaseSelectedContext {
id: ContextId
}
export interface SelectedExecutonContext extends BaseSelectedContext {
export interface SelectedExecutionContext extends BaseSelectedContext {
type: 'context'
id: ContextId
}
@@ -79,4 +86,4 @@ export type SelectedContext =
| SelectedNested
| SelectedUserRequest
| SelectedContextFunc
| SelectedExecutonContext
| SelectedExecutionContext
+4 -4
View File
@@ -131,16 +131,16 @@ export class WorkspaceClient {
const txOps = new TxOperations(this.client, core.account.System)
await txOps.domainRequest('communication' as OperationDomain, {
event: {
type: MessageEventType.BlobPatch,
type: MessageEventType.AttachmentPatch,
cardId: result.source.cardId,
messageId: result.source.messageId,
operations: [
{
opcode: 'update',
blobs: [
attachments: [
{
blobId: result.blobId,
metadata
id: result.blobId,
params: { metadata }
}
]
}
+12 -7
View File
@@ -15,6 +15,7 @@
import attachment, { type Attachment } from '@hcengineering/attachment'
import { Event, MessageEventType } from '@hcengineering/communication-sdk-types'
import { BlobAttachment } from '@hcengineering/communication-types'
import drive, { type FileVersion } from '@hcengineering/drive'
import core, {
type Blob,
@@ -94,22 +95,26 @@ async function handleCommunicationTx (
tx: TxDomainEvent<Event>,
producer: PlatformQueueProducer<VideoTranscodeRequest>
): Promise<void> {
if (tx.domain === COMMUNICATION && tx.event.type === MessageEventType.BlobPatch) {
if (tx.domain === COMMUNICATION && tx.event.type === MessageEventType.AttachmentPatch) {
const event = tx.event
const source: BlobSource = {
source: BlobSourceType.Message,
cardId: event.cardId,
messageId: event.messageId
}
const blobs = event.operations
.filter((it) => it.opcode === 'attach' || it.opcode === 'set')
.flatMap((it) => it.blobs)
const messages: VideoTranscodeRequest[] = blobs.map(({ blobId, mimeType }) => ({
const attachments = event.operations
.filter((it) => it.opcode === 'add' || it.opcode === 'set')
.flatMap((it) => it.attachments)
.filter((it): it is BlobAttachment => 'blobId' in it.params)
const messages: VideoTranscodeRequest[] = attachments.map(({ params }) => ({
workspaceUuid,
blobId,
contentType: mimeType,
blobId: params.blobId,
contentType: params.mimeType,
source
}))
if (messages.length > 0) {
await producer.send(ctx, workspaceUuid, messages)
}
+7 -2
View File
@@ -39,6 +39,9 @@ const ODP_MIME_TYPE = 'application/vnd.oasis.opendocument.presentation'
// RTF
const RTF_MIME_TYPE_1 = 'application/rtf'
const RTF_MIME_TYPE_2 = 'text/rtf'
// JSON
const JSON_MIME_TYPE = 'application/json'
const YAML_MIME_TYPE = 'application/yaml'
const extensions: Record<string, string> = {
[DOCX_MIME_TYPE]: '.docx',
@@ -51,7 +54,9 @@ const extensions: Record<string, string> = {
[RTF_MIME_TYPE_2]: '.rtf',
[ODT_MIME_TYPE]: '.odt',
[ODS_MIME_TYPE]: '.ods',
[ODP_MIME_TYPE]: '.odp'
[ODP_MIME_TYPE]: '.odp',
[JSON_MIME_TYPE]: '.json',
[YAML_MIME_TYPE]: '.yaml'
}
function getFileExtension (contentType: string): string {
@@ -67,7 +72,7 @@ export class DocProvider implements PreviewProvider {
supports (contentType: string): boolean {
const mimeType = contentType.split(';')[0].trim().toLowerCase()
return extensions[mimeType] !== undefined
return extensions[mimeType] !== undefined || contentType.startsWith('text/')
}
async image (ctx: MeasureContext, workspace: WorkspaceUuid, name: string, contentType: string): Promise<PreviewFile> {
+21 -22
View File
@@ -21,7 +21,7 @@ import process, {
Process,
ProcessContext,
ProcessToDo,
SelectedExecutonContext,
SelectedExecutionContext,
State,
Transition
} from '@hcengineering/process'
@@ -267,6 +267,25 @@ async function syncContext (control: TriggerControl, _process: Process): Promise
let changed = false
let index = 1
for (const transition of transitions) {
if (transition.result?._id != null) {
exists.add(transition.result._id)
const context = _process.context[transition.result._id]
changed = true
const ctx: SelectedExecutionContext = {
type: 'context',
id: transition.result._id,
key: ''
}
_process.context[transition.result._id] = {
name: context?.name ?? transition.result.name,
isResult: true,
type: transition.result.type,
_class: transition.result.type._class,
index: index++,
producer: transition._id,
value: ctx
}
}
for (const action of transition.actions) {
if (action.context != null) {
exists.add(action.context._id)
@@ -274,7 +293,7 @@ async function syncContext (control: TriggerControl, _process: Process): Promise
const current = _process.context[action.context._id]
if (method?.contextClass != null) {
changed = true
const ctx: SelectedExecutonContext = {
const ctx: SelectedExecutionContext = {
type: 'context',
id: action.context._id,
key: ''
@@ -289,26 +308,6 @@ async function syncContext (control: TriggerControl, _process: Process): Promise
}
}
}
if (action.result?._id != null) {
exists.add(action.result._id)
const context = _process.context[action.result._id]
changed = true
const ctx: SelectedExecutonContext = {
type: 'context',
id: action.result._id,
key: ''
}
_process.context[action.result._id] = {
name: context?.name ?? action.result.name,
isResult: true,
type: action.result.type,
_class: action.result.type._class,
action: action._id,
index: index++,
producer: transition._id,
value: ctx
}
}
}
}
const newContext: Record<ContextId, ProcessContext> = {}
+17 -10
View File
@@ -40,7 +40,7 @@ import process, {
ProcessError,
SelectedContext,
SelectedContextFunc,
SelectedExecutonContext,
SelectedExecutionContext,
SelectedNested,
SelectedRelation,
SelectedUserRequest,
@@ -621,7 +621,7 @@ async function getContextValue (value: any, control: ProcessControl, execution:
} else if (context.type === 'function') {
value = await getFunctionValue(control, execution, context)
} else if (context.type === 'context') {
value = getExecutionContextValue(control, execution, context)
value = await getExecutionContextValue(control, execution, context)
}
return await fillValue(value, context, control, execution)
} catch (err: any) {
@@ -684,18 +684,25 @@ function getUserRequestValue (control: ProcessControl, execution: Execution, con
)
}
function getExecutionContextValue (
async function getExecutionContextValue (
control: ProcessControl,
execution: Execution,
context: SelectedExecutonContext
): any {
context: SelectedExecutionContext
): Promise<any> {
const userContext = execution.context[context.id]
if (userContext !== undefined) return userContext
const _process = control.client.getModel().findObject(execution.process)
if (_process === undefined) return
const ctx = _process.context[context.id]
if (ctx === undefined) return
throw processError(process.error.ContextValueNotProvided, { name: ctx.name })
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> {