mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-23 01:55:00 +02:00
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:
+209
-24
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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: []
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 |
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
+26
-11
@@ -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}
|
||||
|
||||
+12
-2
@@ -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}
|
||||
|
||||
+80
@@ -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>
|
||||
-1
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,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> = {}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user