Add parallel execution restriction feature to processes (#8477)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2025-04-07 12:28:25 +05:00 committed by GitHub
parent b9c6d337d8
commit e2a2011ad8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 114 additions and 13 deletions

View File

@ -74,6 +74,9 @@ export class TProcess extends TDoc implements Process {
@Prop(ArrOf(TypeRef(process.class.State)), process.string.States)
states!: Ref<State>[]
@Prop(TypeBoolean(), process.string.ParallelExecutionForbidden)
parallelExecutionForbidden?: boolean
@Prop(TypeBoolean(), process.string.StartAutomatically)
autoStart: boolean | undefined
}

View File

@ -35,6 +35,7 @@
export let readonly: boolean = false
export let label: IntlString = card.string.Card
export let _class: Ref<Class<Card>>
export let ignoreObjects: Ref<Card>[] | undefined = undefined
export let focusIndex: number | undefined = undefined
export let kind: ButtonKind = 'no-border'
@ -53,7 +54,7 @@
return
}
showPopup(CardsPopup, { selected: value, _class }, eventToHTMLElement(event), change)
showPopup(CardsPopup, { selected: value, _class, ignoreObjects }, eventToHTMLElement(event), change)
}
const change = (val: Card | undefined): void => {

View File

@ -23,6 +23,7 @@
export let _class: Ref<Class<Card>>
export let selected: Ref<Card> | undefined
export let selectedObjects: Ref<Card>[] | undefined
export let ignoreObjects: Ref<Card>[] | undefined = undefined
export let multiSelect: boolean = false
export let allowDeselect: boolean = true
export let titleDeselect: IntlString | undefined = undefined
@ -41,6 +42,7 @@
{multiSelect}
{allowDeselect}
{titleDeselect}
{ignoreObjects}
type={'object'}
groupBy={'_class'}
on:update

View File

@ -43,6 +43,7 @@
"FirstWorkingDayAfter": "První pracovní den po",
"FallbackValueError": "Zobrazit chybu, pokud není k dispozici",
"Required": "Povinné",
"ParallelExecutionForbidden": "Nedovoluj paralelní spuštění",
"StartAutomatically": "Spustit automaticky",
"Error": "Chyba",
"Continue": "Pokračovat"

View File

@ -43,6 +43,7 @@
"FirstWorkingDayAfter": "Erste Arbeitstag nach",
"FallbackValueError": "Fehler anzeigen, wenn leer",
"Required": "Erforderlich",
"ParallelExecutionForbidden": "Parallele Ausführung verboten",
"StartAutomatically": "Automatisch starten",
"Error": "Fehler",
"Continue": "Fortsetzen"

View File

@ -43,6 +43,7 @@
"FirstWorkingDayAfter": "First working day after",
"FallbackValueError": "Show error if empty",
"Required": "Required",
"ParallelExecutionForbidden": "Parallel execution forbidden",
"StartAutomatically": "Start automatically",
"Error": "Error",
"Continue": "Continue"

View File

@ -43,6 +43,7 @@
"FirstWorkingDayAfter": "Primer día de trabajo después",
"FallbackValueError": "Mostrar error si está vacío",
"Required": "Requerido",
"ParallelExecutionForbidden": "Ejecución paralela prohibida",
"StartAutomatically": "Iniciar automáticamente",
"Error": "Error",
"Continue": "Continuar"

View File

@ -43,6 +43,7 @@
"FirstWorkingDayAfter": "Premier jour de travail après",
"FallbackValueError": "Afficher l'erreur si vide",
"Required": "Requis",
"ParallelExecutionForbidden": "Exécution parallèle interdite",
"StartAutomatically": "Démarrer automatiquement",
"Error": "Erreur",
"Continue": "Continuer"

View File

@ -43,6 +43,7 @@
"FirstWorkingDayAfter": "Primo giorno lavorativo dopo",
"FallbackValueError": "Mostra errore se vuoto",
"Required": "Obbligatorio",
"ParallelExecutionForbidden": "Esecuzione parallela vietata",
"StartAutomatically": "Avvia automaticamente",
"Error": "Errore",
"Continue": "Continua"

View File

@ -43,6 +43,7 @@
"FirstWorkingDayAfter": "Primeiro Dia de Trabalho Após",
"FallbackValueError": "Mostrar erro se vazio",
"Required": "Obrigatório",
"ParallelExecutionForbidden": "Execução Paralela Proibida",
"StartAutomatically": "Iniciar automaticamente",
"Error": "Erro",
"Continue": "Continuar"

View File

@ -43,6 +43,7 @@
"FirstWorkingDayAfter": "Первый рабочий день после",
"FallbackValueError": "Показывать ошибку, если недоступно",
"Required": "Обязательно",
"ParallelExecutionForbidden": "Одновременное выполнение запрещено",
"StartAutomatically": "Запускать автоматически",
"Error": "Ошибка",
"Continue": "Продолжить"

View File

@ -43,6 +43,7 @@
"FirstWorkingDayAfter": "在之后的第一个工作日",
"FallbackValueError": "显示错误,如果为空",
"Required": "必需",
"ParallelExecutionForbidden": "禁止并行执行",
"StartAutomatically": "自动启动",
"Error": "错误",
"Continue": "继续"

View File

@ -78,6 +78,12 @@
}
}
async function saveRestriction (e: CustomEvent<boolean>): Promise<void> {
if (value !== undefined) {
await client.update(value, { parallelExecutionForbidden: e.detail })
}
}
async function saveAutoStart (e: CustomEvent<boolean>): Promise<void> {
if (value !== undefined) {
await client.update(value, { autoStart: e.detail })
@ -167,6 +173,13 @@
<EditBox bind:value={value.name} on:change={saveName} placeholder={process.string.Untitled} />
<ButtonIcon icon={IconDelete} size="small" kind="secondary" on:click={handleDelete} />
</div>
<div>
<ToggleWithLabel
on={value.parallelExecutionForbidden ?? false}
on:change={saveRestriction}
label={process.string.ParallelExecutionForbidden}
/>
</div>
<div>
<ToggleWithLabel
on={value.autoStart ?? false}

View File

@ -50,6 +50,30 @@
}
$: selectedProcess = processes.find((p) => p._id === process)
let ignoreObjects: Ref<Card>[] = []
$: void filter(process)
async function filter (process: Ref<Process> | undefined): Promise<void> {
if (process === undefined) {
ignoreObjects = []
return
}
const pr = client.getModel().findObject(process)
if (pr === undefined) {
ignoreObjects = []
return
}
if (pr.parallelExecutionForbidden !== true) {
ignoreObjects = []
return
}
const executions = await client.findAll(plugin.class.Execution, { process, done: false }, {})
const cards = new Set(executions.map((it) => it.card))
ignoreObjects = [...cards]
}
</script>
<CardPopup
@ -74,6 +98,12 @@
/>
</div>
{#if selectedProcess !== undefined}
<CardSelector kind={'regular'} size={'medium'} bind:value={card} _class={selectedProcess.masterTag} />
<CardSelector
kind={'regular'}
size={'medium'}
bind:value={card}
{ignoreObjects}
_class={selectedProcess.masterTag}
/>
{/if}
</CardPopup>

View File

@ -33,6 +33,31 @@
const res = client.getModel().findAllSync(process.class.Process, {})
const processes = res.filter((it) => resClasses.includes(it.masterTag))
async function filterProcesses (processes: Process[]): Promise<Process[]> {
const res: Process[] = []
const shouldCheck: Process[] = []
for (const val of processes) {
if (val.parallelExecutionForbidden === true) {
shouldCheck.push(val)
} else {
res.push(val)
}
}
if (shouldCheck.length === 0) return res
const executions = await client.findAll(process.class.Execution, {
process: { $in: shouldCheck.map((it) => it._id) },
done: false
})
const notAllowed = new Set(executions.map((it) => it.process))
for (const val of shouldCheck) {
if (!notAllowed.has(val._id)) {
res.push(val)
}
}
return res
}
const dispatch = createEventDispatcher()
async function runProcess (_id: Ref<Process>): Promise<void> {
@ -57,16 +82,18 @@
<Label label={process.string.NoProcesses} />
</div>
{:else}
{#each processes as process}
<button
class="ap-menuItem flex-row-center withIcon w-full"
on:click|preventDefault|stopPropagation={async () => {
await runProcess(process._id)
}}
>
{process.name}
</button>
{/each}
{#await filterProcesses(processes) then processes}
{#each processes as process}
<button
class="ap-menuItem flex-row-center withIcon w-full"
on:click|preventDefault|stopPropagation={async () => {
await runProcess(process._id)
}}
>
{process.name}
</button>
{/each}
{/await}
{/if}
</div>
<div class="ap-space x2" />

View File

@ -89,6 +89,7 @@ export default mergeIds(processId, process, {
FirstWorkingDayAfter: '' as IntlString,
FallbackValueError: '' as IntlString,
Required: '' as IntlString,
ParallelExecutionForbidden: '' as IntlString,
StartAutomatically: '' as IntlString,
Continue: '' as IntlString
}

View File

@ -29,6 +29,7 @@ export interface Process extends Doc {
name: string
description: string
states: Ref<State>[]
parallelExecutionForbidden?: boolean
autoStart?: boolean
}

View File

@ -528,10 +528,24 @@ export async function RunSubProcess (
control: TriggerControl
): Promise<ExecuteResult | undefined> {
if (params._id === undefined) return
const processId = params._id as Ref<Process>
const target = control.modelDb.findObject(processId)
if (target === undefined) return
if (target.parallelExecutionForbidden === true) {
const currentExecution = await control.findAll(control.ctx, process.class.Execution, {
process: target._id,
card: execution.card,
done: false
})
if (currentExecution.length > 0) {
// todo, show erro after merge another pr
return
}
}
const res: Tx[] = []
res.push(
control.txFactory.createTxCreateDoc(process.class.Execution, core.space.Workspace, {
process: params._id as Ref<Process>,
process: processId,
currentState: null,
currentToDo: null,
card: execution.card,