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) @Prop(ArrOf(TypeRef(process.class.State)), process.string.States)
states!: Ref<State>[] states!: Ref<State>[]
@Prop(TypeBoolean(), process.string.ParallelExecutionForbidden)
parallelExecutionForbidden?: boolean
@Prop(TypeBoolean(), process.string.StartAutomatically) @Prop(TypeBoolean(), process.string.StartAutomatically)
autoStart: boolean | undefined autoStart: boolean | undefined
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -43,6 +43,7 @@
"FirstWorkingDayAfter": "在之后的第一个工作日", "FirstWorkingDayAfter": "在之后的第一个工作日",
"FallbackValueError": "显示错误,如果为空", "FallbackValueError": "显示错误,如果为空",
"Required": "必需", "Required": "必需",
"ParallelExecutionForbidden": "禁止并行执行",
"StartAutomatically": "自动启动", "StartAutomatically": "自动启动",
"Error": "错误", "Error": "错误",
"Continue": "继续" "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> { async function saveAutoStart (e: CustomEvent<boolean>): Promise<void> {
if (value !== undefined) { if (value !== undefined) {
await client.update(value, { autoStart: e.detail }) await client.update(value, { autoStart: e.detail })
@ -167,6 +173,13 @@
<EditBox bind:value={value.name} on:change={saveName} placeholder={process.string.Untitled} /> <EditBox bind:value={value.name} on:change={saveName} placeholder={process.string.Untitled} />
<ButtonIcon icon={IconDelete} size="small" kind="secondary" on:click={handleDelete} /> <ButtonIcon icon={IconDelete} size="small" kind="secondary" on:click={handleDelete} />
</div> </div>
<div>
<ToggleWithLabel
on={value.parallelExecutionForbidden ?? false}
on:change={saveRestriction}
label={process.string.ParallelExecutionForbidden}
/>
</div>
<div> <div>
<ToggleWithLabel <ToggleWithLabel
on={value.autoStart ?? false} on={value.autoStart ?? false}

View File

@ -50,6 +50,30 @@
} }
$: selectedProcess = processes.find((p) => p._id === process) $: 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> </script>
<CardPopup <CardPopup
@ -74,6 +98,12 @@
/> />
</div> </div>
{#if selectedProcess !== undefined} {#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} {/if}
</CardPopup> </CardPopup>

View File

@ -33,6 +33,31 @@
const res = client.getModel().findAllSync(process.class.Process, {}) const res = client.getModel().findAllSync(process.class.Process, {})
const processes = res.filter((it) => resClasses.includes(it.masterTag)) 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() const dispatch = createEventDispatcher()
async function runProcess (_id: Ref<Process>): Promise<void> { async function runProcess (_id: Ref<Process>): Promise<void> {
@ -57,6 +82,7 @@
<Label label={process.string.NoProcesses} /> <Label label={process.string.NoProcesses} />
</div> </div>
{:else} {:else}
{#await filterProcesses(processes) then processes}
{#each processes as process} {#each processes as process}
<button <button
class="ap-menuItem flex-row-center withIcon w-full" class="ap-menuItem flex-row-center withIcon w-full"
@ -67,6 +93,7 @@
{process.name} {process.name}
</button> </button>
{/each} {/each}
{/await}
{/if} {/if}
</div> </div>
<div class="ap-space x2" /> <div class="ap-space x2" />

View File

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

View File

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

View File

@ -528,10 +528,24 @@ export async function RunSubProcess (
control: TriggerControl control: TriggerControl
): Promise<ExecuteResult | undefined> { ): Promise<ExecuteResult | undefined> {
if (params._id === undefined) return 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[] = [] const res: Tx[] = []
res.push( res.push(
control.txFactory.createTxCreateDoc(process.class.Execution, core.space.Workspace, { control.txFactory.createTxCreateDoc(process.class.Execution, core.space.Workspace, {
process: params._id as Ref<Process>, process: processId,
currentState: null, currentState: null,
currentToDo: null, currentToDo: null,
card: execution.card, card: execution.card,