Merge branch 'develop' into staging-new

This commit is contained in:
Denis Bykhov
2026-02-27 14:42:52 +05:00
6 changed files with 213 additions and 73 deletions
+8 -1
View File
@@ -14,7 +14,7 @@
// limitations under the License.
//
import activity from '@hcengineering/activity'
import activity, { type ActivityMessageControl } from '@hcengineering/activity'
import { type Role, type Card } from '@hcengineering/card'
import {
AvatarType,
@@ -340,6 +340,13 @@ export function createModel (builder: Builder): void {
preposition: contact.string.For
})
// Prevent leaking private social identifiers (emails, external handles) via activity updates.
builder.createDoc<ActivityMessageControl<Person>>(activity.class.ActivityMessageControl, core.space.Model, {
objectClass: contact.class.Person,
skip: [],
skipFields: ['socialIds']
})
builder.mixin(contact.mixin.Employee, core.class.Class, activity.mixin.ActivityDoc, {
preposition: contact.string.For
})
@@ -15,7 +15,7 @@
<script lang="ts">
import { onMount } from 'svelte'
import { Icon, Label, tooltip } from '@hcengineering/ui'
import contact, { SocialIdentity, SocialIdentityProvider } from '@hcengineering/contact'
import contact, { SocialIdentity, SocialIdentityProvider, getCurrentEmployee } from '@hcengineering/contact'
import { getClient } from '@hcengineering/presentation'
export let value: SocialIdentity
@@ -23,6 +23,7 @@
export let shouldShowAvatar = true
const client = getClient()
let isOwner = false
onMount(() => {
if (socialIdProvider == null) {
@@ -31,6 +32,10 @@
})
$: icon = socialIdProvider?.icon ?? contact.icon.Profile
$: {
const me = getCurrentEmployee()
isOwner = me != null && value.attachedTo === me
}
</script>
{#if socialIdProvider != null}
@@ -46,8 +51,12 @@
</div>
<div class="flex-col flex-gap-0-5">
<div>{value.displayValue ?? value.value}</div>
{#if shouldShowAvatar}
{#if isOwner}
<div>{value.displayValue ?? value.value}</div>
{#if shouldShowAvatar}
<div class="type"><Label label={socialIdProvider.label} /></div>
{/if}
{:else}
<div class="type"><Label label={socialIdProvider.label} /></div>
{/if}
</div>
@@ -41,7 +41,8 @@
async function runProcess (): Promise<void> {
if (process === undefined || card === undefined) return
await createExecution(card._id, process, card.space)
const tx = await createExecution(card._id, process, card.space, client.txFactory)
if (tx) await client.tx(tx)
dispatch('close')
}
@@ -94,7 +94,8 @@
async function runProcess (_id: Ref<Process>): Promise<void> {
if (!value) return
for (const element of values) {
await createExecution(element._id, _id, element.space)
const tx = await createExecution(element._id, _id, element.space, client.txFactory)
if (tx) await client.tx(tx)
}
dispatch('close')
}
+94 -39
View File
@@ -13,14 +13,17 @@
import cardPlugin, { type Card } from '@hcengineering/card'
import core, {
generateId,
getCurrentAccount,
SortingOrder,
TxOperations,
TxProcessor,
type Client,
type Doc,
type Tx,
type TxApplyIf,
type TxCreateDoc,
type TxCUD,
type TxMixin,
type TxResult,
type TxUpdateDoc
@@ -50,27 +53,47 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre
return new ProcessMiddleware(client, next)
}
private readonly txFactory = new TxOperations(this.client, getCurrentAccount().primarySocialId).txFactory
async tx (tx: Tx): Promise<TxResult> {
await this.handleTx(tx)
const preTx: Array<TxCUD<Doc>> = []
const postTx: Array<TxCUD<Doc>> = []
await this.handleTx(preTx, postTx, tx)
if (preTx.length > 0 || postTx.length > 0) {
if (TxProcessor.isExtendsCUD(tx._class)) {
const applyIf = this.txFactory.createTxApplyIf(
core.space.Tx,
generateId(),
[],
[],
[...preTx, tx as TxCUD<Doc>, ...postTx],
'process',
true
)
return await this.provideTx(applyIf)
}
}
return await this.provideTx(tx)
}
private async handleTx (...txes: Tx[]): Promise<void> {
private async handleTx (preTx: Array<TxCUD<Doc>>, postTx: Array<TxCUD<Doc>>, ...txes: Tx[]): Promise<void> {
for (const etx of txes) {
if (etx._class === core.class.TxApplyIf) {
const applyIf = etx as TxApplyIf
await this.handleTx(...applyIf.txes)
await this.handleTx(preTx, postTx, ...applyIf.txes)
}
await this.handleCardCreate(etx)
await this.handleCardUpdate(etx)
await this.handleTagAdd(etx)
await this.handleToDoDone(etx)
await this.handleApproveRequest(etx)
await this.handleCardCreate(postTx, etx)
await this.handleCardUpdate(preTx, etx)
await this.handleTagAdd(postTx, etx)
await this.handleToDoDone(preTx, etx)
await this.handleApproveRequest(preTx, etx)
}
}
private async handleCardUpdate (etx: Tx): Promise<void> {
private async handleCardUpdate (preTx: Array<TxCUD<Doc>>, etx: Tx): Promise<void> {
if (etx._class === core.class.TxUpdateDoc || etx._class === core.class.TxMixin) {
const updateTx = etx as TxUpdateDoc<Card> | TxMixin<Card, Card>
const hierarchy = this.client.getHierarchy()
@@ -97,21 +120,26 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre
},
{ sort: { rank: SortingOrder.Ascending } }
)
const transition = await pickTransition(this.client, execution, transitions, {
const inputContext = {
...execution.context,
card: updated,
operations: isUpdateTx(updateTx) ? updateTx.operations : updateTx.attributes
})
}
const transition = await pickTransition(this.client, execution, transitions, inputContext)
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
})
const result = await getNextStateUserInput(execution, transition, execution.context, inputContext)
if (result?.changed === true) {
preTx.push(
this.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, {
context: result.context
})
)
}
}
}
}
private async handleCardCreate (etx: Tx): Promise<void> {
private async handleCardCreate (postTx: Array<TxCUD<Doc>>, etx: Tx): Promise<void> {
if (etx._class === core.class.TxCreateDoc) {
const createTx = etx as TxCreateDoc<Card>
const doc = TxProcessor.createDoc2Doc(createTx)
@@ -130,12 +158,13 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre
autoStart: true
})
for (const proc of processes) {
await createExecution(createTx.objectId, proc._id, createTx.objectSpace)
const res = await createExecution(createTx.objectId, proc._id, createTx.objectSpace, this.txFactory)
if (res !== undefined) postTx.push(res)
}
}
}
private async handleTagAdd (tx: Tx): Promise<void> {
private async handleTagAdd (postTx: Array<TxCUD<Doc>>, tx: Tx): Promise<void> {
if (tx._class !== core.class.TxMixin) return
const mixinTx = tx as TxMixin<Card, Card>
const hierarchy = this.client.getHierarchy()
@@ -146,11 +175,12 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre
.getModel()
.findAllSync(process.class.Process, { masterTag: mixinTx.mixin, autoStart: true })
for (const proc of processes) {
await createExecution(mixinTx.objectId, proc._id, mixinTx.objectSpace)
const res = await createExecution(mixinTx.objectId, proc._id, mixinTx.objectSpace, this.txFactory)
if (res !== undefined) postTx.push(res)
}
}
private async handleApproveRequest (etx: Tx): Promise<void> {
private async handleApproveRequest (preTx: Array<TxCUD<Doc>>, etx: Tx): Promise<void> {
if (etx._class === core.class.TxUpdateDoc) {
const cud = etx as TxUpdateDoc<ApproveRequest>
if (cud.objectClass !== process.class.ApproveRequest) return
@@ -163,7 +193,6 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre
_id: approveRequest.execution
})
if (execution === undefined) return
const txop = new TxOperations(this.client, getCurrentAccount().primarySocialId)
const transitions = this.client.getModel().findAllSync(
process.class.Transition,
{
@@ -176,18 +205,24 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre
{ sort: { rank: SortingOrder.Ascending } }
)
const updatedApproveRequest = TxProcessor.updateDoc2Doc(approveRequest, cud)
const transition = await pickTransition(this.client, execution, transitions, {
const inputContext = {
...execution.context,
todo: updatedApproveRequest
})
}
const transition = await pickTransition(this.client, execution, transitions, inputContext)
if (transition === undefined) return
const context = await getNextStateUserInput(execution, transition, execution.context)
await txop.update(execution, {
context
})
const result = await getNextStateUserInput(execution, transition, execution.context, inputContext)
if (result?.changed === true) {
preTx.push(
this.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, {
context: result.context
})
)
}
}
}
private async handleToDoDone (etx: Tx): Promise<void> {
private async handleToDoDone (preTx: Array<TxCUD<Doc>>, etx: Tx): Promise<void> {
if (etx._class === core.class.TxUpdateDoc) {
const cud = etx as TxUpdateDoc<ProcessToDo>
if (cud.objectClass !== process.class.ProcessToDo) return
@@ -200,22 +235,42 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre
_id: todo.execution
})
if (execution === undefined) return
const txop = new TxOperations(this.client, getCurrentAccount().primarySocialId)
await requestResult(txop, execution, todo.results, execution.context)
const context = await requestResult(execution, todo.results, execution.context)
const transitions = this.client.getModel().findAllSync(process.class.Transition, {
process: execution.process,
from: execution.currentState,
trigger: process.trigger.OnToDoClose
})
const transition = await pickTransition(this.client, execution, transitions, {
const inputContext = {
...(context ?? execution.context),
todo
})
if (transition === undefined) return
const context = await getNextStateUserInput(execution, transition, execution.context)
if (context !== undefined) {
await txop.update(execution, {
context
})
}
const transition = await pickTransition(this.client, execution, transitions, inputContext)
if (transition === undefined) {
if (context !== undefined) {
preTx.push(
this.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, {
context
})
)
}
return
}
const finalResult = await getNextStateUserInput(execution, transition, context ?? execution.context, inputContext)
if (finalResult?.changed === true) {
preTx.push(
this.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, {
context: finalResult.context
})
)
} else if (context !== undefined) {
preTx.push(
this.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, {
context
})
)
}
}
}
+95 -28
View File
@@ -26,7 +26,8 @@ import core, {
type Ref,
type RefTo,
type Space,
type TxOperations,
type TxCUD,
type TxFactory,
TxProcessor,
type Type
} from '@hcengineering/core'
@@ -422,13 +423,13 @@ export async function continueExecution (value: Execution): Promise<void> {
let context = value.context
const transition = value.error[0].transition
if (transition == null) {
const res = await newExecutionUserInput(value.process, value.space, context)
context = res ?? context
const res = await newExecutionUserInput(value.process, value.space, value)
context = res?.context ?? context
} else {
const _transition = client.getModel().findObject(transition)
if (_transition === undefined) return
const res = await getNextStateUserInput(value, _transition, context)
context = res ?? context
context = res?.context ?? context
}
await client.update(value, { status: ExecutionStatus.Active, context })
}
@@ -437,17 +438,57 @@ export async function requestUserInput (
processId: Ref<Process>,
space: Ref<Space>,
target: Transition,
userContext: ExecutionContext
): Promise<ExecutionContext | undefined> {
execution: Execution,
userContext: ExecutionContext,
inputContext: Record<string, any> = {}
): Promise<{ context: ExecutionContext, state: Ref<State>, changed: boolean }> {
const client = getClient()
let changed = false
const tr = await getTransitionUserInput(processId, space, target, userContext)
if (tr !== undefined) {
userContext = { ...userContext, ...tr }
changed = true
}
const sub = await getSubProcessesUserInput(space, target, userContext)
if (sub !== undefined) {
userContext = { ...userContext, ...sub }
changed = true
}
return sub !== undefined || tr !== undefined ? userContext : undefined
// Follow auto transitions
const nextAutoTransitions = client
.getModel()
.findAllSync(process.class.Transition, {
process: processId,
from: target.to
})
.filter((t) => client.getModel().findObject(t.trigger)?.auto === true)
if (nextAutoTransitions.length > 0) {
const newExecution = {
...execution,
context: userContext,
currentState: target.to
}
const nextTransition = await pickTransition(client, newExecution, nextAutoTransitions, inputContext)
if (nextTransition !== undefined) {
const recursive = await requestUserInput(
processId,
space,
nextTransition,
newExecution,
userContext,
inputContext
)
return {
context: recursive.context,
state: recursive.state,
changed: changed || recursive.changed
}
}
}
return { context: userContext, state: target.to, changed }
}
export async function getTransitionUserInput (
@@ -523,47 +564,76 @@ export async function getSubProcessesUserInput (
export async function newExecutionUserInput (
_id: Ref<Process>,
space: Ref<Space>,
userContext?: ExecutionContext
): Promise<ExecutionContext | undefined> {
execution: Execution
): Promise<{ context: ExecutionContext, state: Ref<State>, changed: boolean } | 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, space, initTransition, userContext ?? getEmptyContext())
if (initTransition === undefined) return undefined
return await requestUserInput(_id, space, initTransition, execution, execution.context)
}
export async function getNextStateUserInput (
execution: Execution,
transition: Transition,
userContext: ExecutionContext
): Promise<ExecutionContext | undefined> {
userContext: ExecutionContext,
inputContext: Record<string, any> = {}
): Promise<{ context: ExecutionContext, state: Ref<State>, changed: boolean } | undefined> {
const client = getClient()
const process = client.getModel().findObject(execution.process)
if (process === undefined) return userContext
return await requestUserInput(execution.process, execution.space, transition, userContext)
const _process = client.getModel().findObject(execution.process)
if (_process === undefined) return undefined
return await requestUserInput(execution.process, execution.space, transition, execution, userContext, inputContext)
}
export async function createExecution (card: Ref<Card>, _id: Ref<Process>, space: Ref<Space>): Promise<void> {
export async function createExecution (
card: Ref<Card>,
_id: Ref<Process>,
space: Ref<Space>,
txFactory: TxFactory
): Promise<TxCUD<Doc> | undefined> {
const client = getClient()
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const context = await newExecutionUserInput(_id, space)
const _process = client.getModel().findObject(_id)
if (_process === undefined) return
const initTransition = client.getModel().findAllSync(process.class.Transition, {
process: _id,
from: null
})[0]
if (initTransition === undefined) return
await client.createDoc(process.class.Execution, space, {
const executionId = generateId<Execution>()
const mockExecution: Execution = {
_id: executionId,
process: _id,
currentState: initTransition.to,
card,
rollback: [],
context: context ?? getEmptyContext(),
status: ExecutionStatus.Active
})
context: getEmptyContext(),
status: ExecutionStatus.Active,
space,
_class: process.class.Execution,
modifiedOn: 0,
modifiedBy: client.user
}
const result = await newExecutionUserInput(_id, space, mockExecution)
return txFactory.createTxCreateDoc(
process.class.Execution,
space,
{
process: _id,
currentState: initTransition.to,
card,
rollback: [],
context: result?.context ?? getEmptyContext(),
status: ExecutionStatus.Active
},
executionId
)
}
export function getToDoEndAction (prevState: State): Step<Doc> {
@@ -590,11 +660,10 @@ export function getToDoEndAction (prevState: State): Step<Doc> {
}
export async function requestResult (
txop: TxOperations,
execution: Execution,
results: UserResult[] | undefined,
context: ExecutionContext
): Promise<void> {
): Promise<ExecutionContext | undefined> {
if (results == null || results.length === 0) return
const promise = new Promise<void>((resolve, reject) => {
showPopup(process.component.ResultInput, { results, context }, undefined, (res) => {
@@ -610,9 +679,7 @@ export async function requestResult (
})
})
await promise
await txop.update(execution, {
context
})
return context
}
export function todoTranstionCheck (