From 83028b5a0d3188711f438e0585a4935ff90c9905 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Wed, 27 May 2026 20:21:17 +0200 Subject: [PATCH 01/13] Changed the schema for user tasks in the database to link them to instance entries and to reflect that we will always initialize them with all the required data --- .../lib/user-task-schema.tsx | 22 ++++++++-------- .../migration.sql | 25 +++++++++++++++++++ src/management-system-v2/prisma/schema.prisma | 19 +++++++++----- 3 files changed, 49 insertions(+), 17 deletions(-) create mode 100644 src/management-system-v2/prisma/migrations/20260527181958_user_task_instance_link/migration.sql diff --git a/src/management-system-v2/lib/user-task-schema.tsx b/src/management-system-v2/lib/user-task-schema.tsx index 0a403d19a..e247dbab8 100644 --- a/src/management-system-v2/lib/user-task-schema.tsx +++ b/src/management-system-v2/lib/user-task-schema.tsx @@ -3,14 +3,15 @@ import { z } from 'zod'; export const UserTaskInputSchema = z.object({ id: z.string(), taskId: z.string(), + environmentId: z.string(), + instanceID: z.string().nullable(), name: z.string().nullish(), - instanceID: z.string(), fileName: z.string(), - html: z.string().nullish(), + html: z.string(), state: z.string(), priority: z.number(), progress: z.number(), - startTime: z.number(), + startTime: z.union([z.number().transform((val) => new Date(val)), z.date()]), owner: z.string().optional(), actualOwner: z.string().array(), potentialOwners: z @@ -20,9 +21,9 @@ export const UserTaskInputSchema = z.object({ }) .optional() .default({}), - endTime: z.number().nullish(), - initialVariables: z.record(z.string(), z.any()).optional(), - variableChanges: z.record(z.string(), z.any()).optional(), + endTime: z.union([z.number().transform((val) => new Date(val)), z.date()]).nullish(), + initialVariables: z.record(z.string(), z.any()), + variableChanges: z.record(z.string(), z.any()), milestones: z .object({ id: z.string(), @@ -30,16 +31,15 @@ export const UserTaskInputSchema = z.object({ description: z.string().optional(), value: z.number(), }) - .array() - .optional(), - milestonesChanges: z.record(z.string(), z.number()).optional(), + .array(), + milestonesChanges: z.record(z.string(), z.number()), machineId: z.string(), offline: z.boolean().optional(), }); -export type UserTaskInput = z.infer; +export type UserTaskInput = z.input; -export type UserTask = UserTaskInput; +export type UserTask = z.output; export type ExtendedTaskListEntry = Omit & { actualOwner: { id: string; name: string; userName?: string }[]; diff --git a/src/management-system-v2/prisma/migrations/20260527181958_user_task_instance_link/migration.sql b/src/management-system-v2/prisma/migrations/20260527181958_user_task_instance_link/migration.sql new file mode 100644 index 000000000..a7933a235 --- /dev/null +++ b/src/management-system-v2/prisma/migrations/20260527181958_user_task_instance_link/migration.sql @@ -0,0 +1,25 @@ +/* + Warnings: + + - Added the required column `environmentId` to the `userTask` table without a default value. This is not possible if the table is not empty. + - Made the column `html` on table `userTask` required. This step will fail if there are existing NULL values in that column. + - Made the column `initialVariables` on table `userTask` required. This step will fail if there are existing NULL values in that column. + - Made the column `variableChanges` on table `userTask` required. This step will fail if there are existing NULL values in that column. + - Made the column `milestones` on table `userTask` required. This step will fail if there are existing NULL values in that column. + - Made the column `milestonesChanges` on table `userTask` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "userTask" ADD COLUMN "environmentId" TEXT NOT NULL, +ALTER COLUMN "instanceID" DROP NOT NULL, +ALTER COLUMN "html" SET NOT NULL, +ALTER COLUMN "initialVariables" SET NOT NULL, +ALTER COLUMN "variableChanges" SET NOT NULL, +ALTER COLUMN "milestones" SET NOT NULL, +ALTER COLUMN "milestonesChanges" SET NOT NULL; + +-- AddForeignKey +ALTER TABLE "userTask" ADD CONSTRAINT "userTask_instanceID_fkey" FOREIGN KEY ("instanceID") REFERENCES "process_instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "userTask" ADD CONSTRAINT "userTask_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "space"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/management-system-v2/prisma/schema.prisma b/src/management-system-v2/prisma/schema.prisma index c0c41703e..7fd29af5d 100644 --- a/src/management-system-v2/prisma/schema.prisma +++ b/src/management-system-v2/prisma/schema.prisma @@ -160,6 +160,7 @@ model Space { folders Folder[] processes Process[] htmlForms HtmlForm[] + userTasks UserTask[] roles Role[] engines Engine[] settings SpaceSettings? @relation("spaceSettings") @@ -334,22 +335,26 @@ model UserTask { id String @id taskId String name String? - instanceID String + instanceID String? + instance ProcessInstance? @relation(fields: [instanceID], references: [id], onDelete: Cascade) fileName String - html String? + html String state String priority Int progress Int startTime DateTime endTime DateTime? - initialVariables Json? - variableChanges Json? - milestones Json? - milestonesChanges Json? + initialVariables Json + variableChanges Json + milestones Json + milestonesChanges Json machineId String actualOwner String[] potentialOwners Json + space Space @relation(fields: [environmentId], references: [id], onDelete: Cascade) + environmentId String + @@map("userTask") } @@ -469,5 +474,7 @@ model ProcessInstance { engineIds String[] state Json + userTasks UserTask[] + @@map("process_instance") } From 1110bf86573404ed4060d6f5680bffb02362ebd3 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Thu, 28 May 2026 10:42:29 +0200 Subject: [PATCH 02/13] Adjusted the user task logic to the new db schema; added user task data fetching to the background refetching loop; users will now fetch user task data from the db instead of directly from the engines --- .../[environmentId]/tasklist/tasklist.tsx | 10 +- .../[environmentId]/tasklist/userTaskCard.tsx | 4 +- .../[environmentId]/tasks/form-list.tsx | 5 +- .../lib/data/db/iam/users.ts | 12 + .../lib/data/db/user-tasks.ts | 28 +- src/management-system-v2/lib/data/engines.ts | 22 +- .../lib/data/processes.tsx | 14 +- .../lib/data/user-tasks.ts | 40 ++- .../executions/deployment-server-actions.ts | 42 ++- .../lib/tasks/server-actions.ts | 292 ++++++++++-------- .../lib/use-user-tasks.ts | 6 +- 11 files changed, 316 insertions(+), 159 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/tasklist.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/tasklist.tsx index c6ae3488a..c721db48a 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/tasklist.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/tasklist.tsx @@ -83,7 +83,9 @@ const Tasklist: React.FC = ({ userId, pollingInterval }) => { return (a: ExtendedTaskListEntry, b: ExtendedTaskListEntry) => { // tiebreak equal value by comparing the startTime if (a[key] === b[key]) { - return selectedSortItem.ascending ? a.startTime - b.startTime : b.startTime - a.startTime; + return selectedSortItem.ascending + ? a.startTime.getTime() - b.startTime.getTime() + : b.startTime.getTime() - a.startTime.getTime(); } if (key === 'state') { @@ -92,6 +94,12 @@ const Tasklist: React.FC = ({ userId, pollingInterval }) => { return selectedSortItem.ascending ? indexA - indexB : indexB - indexA; } + if (key === 'startTime' || key === 'endTime') { + return selectedSortItem.ascending + ? (a[key]?.getTime() || 0) - (b[key]?.getTime() || 0) + : (b[key]?.getTime() || 0) - (a[key]?.getTime() || 0); + } + return selectedSortItem.ascending ? (a[key] || 0) - (b[key] || 0) : (b[key] || 0) - (a[key] || 0); diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/userTaskCard.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/userTaskCard.tsx index 39a3867a8..7ed0693d0 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/userTaskCard.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/userTaskCard.tsx @@ -64,7 +64,7 @@ const UserTaskCard = ({ const endTime = userTaskData.endTime; const durationValues = transformMilisecondsToDurationValues( - (endTime || +new Date()) - userTaskData.startTime, + (endTime?.getTime() || +new Date()) - userTaskData.startTime.getTime(), true, ); @@ -106,7 +106,7 @@ const UserTaskCard = ({ {userTaskData.name} - {userTaskData.offline && !userTaskData.endTime && ( + {userTaskData.offline && !userTaskData.endTime?.getTime() && ( Offline diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/tasks/form-list.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/tasks/form-list.tsx index 29dd6693a..3d3d2a0b1 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/tasks/form-list.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/tasks/form-list.tsx @@ -195,7 +195,8 @@ const FormList: React.FC = ({ data }) => { id: v4(), name: task.name, taskId: '', - instanceID: '', + instanceID: null, + environmentId: space.spaceId, fileName: '', state: 'READY', machineId: 'ms-local', @@ -206,7 +207,9 @@ const FormList: React.FC = ({ data }) => { startTime: Date.now(), html, milestones: [], + milestonesChanges: {}, initialVariables: {}, + variableChanges: {}, }; } else { message.error(`Failed to get the form data for ${task.name}.`); diff --git a/src/management-system-v2/lib/data/db/iam/users.ts b/src/management-system-v2/lib/data/db/iam/users.ts index f07db76f6..a59b3714a 100644 --- a/src/management-system-v2/lib/data/db/iam/users.ts +++ b/src/management-system-v2/lib/data/db/iam/users.ts @@ -41,6 +41,18 @@ export async function getUsers(page: number = 1, pageSize: number = 10) { }; } +export async function getSpaceUsers(spaceId: string, isOrganization = false) { + // get the users for the current space + if (isOrganization) { + return await db.user.findMany({ + where: { memberIn: { some: { environmentId: spaceId } } }, + }); + } else { + const user = await db.user.findUnique({ where: { id: spaceId } }); + return user ? [user] : []; + } +} + export async function getUserById( id: string, opts?: { throwIfNotFound?: boolean }, diff --git a/src/management-system-v2/lib/data/db/user-tasks.ts b/src/management-system-v2/lib/data/db/user-tasks.ts index e8696b0df..f450e806f 100644 --- a/src/management-system-v2/lib/data/db/user-tasks.ts +++ b/src/management-system-v2/lib/data/db/user-tasks.ts @@ -1,14 +1,24 @@ import db from '@/lib/data/db'; import { z } from 'zod'; -import { UserTask, UserTaskInput, UserTaskInputSchema } from '@/lib/user-task-schema'; - -export async function getUserTasks() { - const userTasks = await db.userTask.findMany(); - - return userTasks.map((userTask) => ({ - ...userTask, - offline: userTask.machineId !== 'ms-local', - })) as unknown as UserTask[]; +import { + ExtendedTaskListEntry, + UserTask, + UserTaskInput, + UserTaskInputSchema, +} from '@/lib/user-task-schema'; +import { truthyFilter } from '@/lib/typescript-utils'; + +export async function getUserTasks(spaceId: string) { + return (await db.userTask.findMany({ + where: { + OR: [ + // get all user tasks that were created in the task editor of the MS + { instance: null }, + // and all user tasks that belong to instances of processes belonging to this space + { instance: { deployment: { version: { process: { environmentId: spaceId } } } } }, + ], + }, + })) as UserTask[]; } export async function getUserTaskById(userTaskId: string) { diff --git a/src/management-system-v2/lib/data/engines.ts b/src/management-system-v2/lib/data/engines.ts index 46e270be1..2c4cfe380 100644 --- a/src/management-system-v2/lib/data/engines.ts +++ b/src/management-system-v2/lib/data/engines.ts @@ -3,7 +3,13 @@ import Ability, { UnauthorizedError } from '@/lib/ability/abilityHelper'; import { SpaceEngineInput, SpaceEngineInputSchema } from '@/lib/space-engine-schema'; import { getCurrentEnvironment, getCurrentUser } from '@/components/auth'; -import { getErrorMessage, permissionDenied, schemaValidationError, userError } from '../user-error'; +import { + getErrorMessage, + isUserErrorResponse, + permissionDenied, + schemaValidationError, + userError, +} from '../user-error'; import { savedEnginesToEngines } from '../engines/saved-engines-helpers'; import { Engine, SpaceEngine } from '../engines/types'; import { SystemAdmin } from '@prisma/client'; @@ -98,9 +104,13 @@ export async function getAvailableSpaceEngines(spaceId: string) { } } -export async function getAllAvailableEngines(spaceId: string, ability?: Ability) { +export async function getAllAvailableEngines( + spaceId: string, + ability?: Ability, + skipAbilityCheck = false, +) { try { - if (!ability) ({ ability } = await getCurrentEnvironment(spaceId)); + if (!ability && !skipAbilityCheck) ({ ability } = await getCurrentEnvironment(spaceId)); let engines: Engine[] = []; const [proceedEngines, spaceEngines] = await Promise.allSettled([ @@ -118,6 +128,12 @@ export async function getAllAvailableEngines(spaceId: string, ability?: Ability) } } +export async function getEngineIfAvailable(environmentId: string, engineId: string) { + const engines = await getAvailableSpaceEngines(environmentId); + if (isUserErrorResponse(engines)) return engines; + return engines.find((e) => e.id === engineId); +} + const SpaceEngineArraySchema = SpaceEngineInputSchema.array(); export async function addDbEngines(enginesInput: SpaceEngineInput[], environmentId: string | null) { const newEngines = SpaceEngineArraySchema.safeParse(enginesInput); diff --git a/src/management-system-v2/lib/data/processes.tsx b/src/management-system-v2/lib/data/processes.tsx index 1a6e7ece3..b55a97e2b 100644 --- a/src/management-system-v2/lib/data/processes.tsx +++ b/src/management-system-v2/lib/data/processes.tsx @@ -164,10 +164,13 @@ export const getProcessBPMN = async ( spaceId: string, versionId?: string, ability?: Ability, + skipAbilityCheck = false, ) => { - const error = await checkValidity(definitionId, 'view', spaceId, ability); + if (!skipAbilityCheck) { + const error = await checkValidity(definitionId, 'view', spaceId, ability); - if (error) return error; + if (error) return error; + } return await getBpmnVersion(definitionId, versionId); }; @@ -700,10 +703,13 @@ export const getProcessHtmlFormHTML = async ( fileName: string, spaceId: string, ability?: Ability, + skipAbilityCheck = false, ) => { - const error = await checkValidity(definitionId, 'view', spaceId, ability); + if (!skipAbilityCheck) { + const error = await checkValidity(definitionId, 'view', spaceId, ability); - if (error) return error; + if (error) return error; + } try { return await _getHtmlForm(definitionId, fileName); diff --git a/src/management-system-v2/lib/data/user-tasks.ts b/src/management-system-v2/lib/data/user-tasks.ts index c583dc303..206a42828 100644 --- a/src/management-system-v2/lib/data/user-tasks.ts +++ b/src/management-system-v2/lib/data/user-tasks.ts @@ -9,12 +9,44 @@ import { deleteUserTask as _deleteUserTask, } from './db/user-tasks'; import { UnauthorizedError } from '../ability/abilityHelper'; -import { UserErrorType, userError } from '../user-error'; -import { UserTaskInput } from '../user-task-schema'; +import { UserErrorType, isUserErrorResponse, userError } from '../user-error'; +import { ExtendedTaskListEntry, UserTaskInput } from '../user-task-schema'; +import { getCurrentEnvironment } from '@/components/auth'; +import { getAllAvailableEngines } from './engines'; +import { getSpaceUsers } from './db/iam/users'; +import { truthyFilter } from '../typescript-utils'; + +export async function getUserTasks(spaceId: string) { + const { + activeEnvironment: { isOrganization }, + } = await getCurrentEnvironment(spaceId); -export async function getUserTasks() { try { - return await _getUserTasks(); + const userTasks = await _getUserTasks(spaceId); + const users = await getSpaceUsers(spaceId, isOrganization); + const reachableEngines = await getAllAvailableEngines(spaceId, undefined, true); + + if (isUserErrorResponse(reachableEngines)) return reachableEngines; + + // map the ids in the actualOwner array to the users of the current space so the frontend can + // show richer information about who is working on the task + return userTasks.map((uT) => ({ + ...uT, + offline: + uT.machineId === 'ms-local' || reachableEngines.some((e) => e.id === uT.machineId) + ? false + : true, + actualOwner: uT.actualOwner + .map((id) => { + const user = users.find((u) => u.id === id); + if (user) { + return { id, name: user.firstName + ' ' + user.lastName, username: user.username }; + } else { + return { id, name: '' }; + } + }) + .filter(truthyFilter), + })) satisfies ExtendedTaskListEntry[]; } catch (err) { if (err instanceof UnauthorizedError) return userError('Permission denied', UserErrorType.PermissionError); diff --git a/src/management-system-v2/lib/executions/deployment-server-actions.ts b/src/management-system-v2/lib/executions/deployment-server-actions.ts index bb53ad12f..453672566 100644 --- a/src/management-system-v2/lib/executions/deployment-server-actions.ts +++ b/src/management-system-v2/lib/executions/deployment-server-actions.ts @@ -27,6 +27,7 @@ import { getCurrentEnvironment, getCurrentUser } from '@/components/auth'; import { addDeployment, getProcessDeployments, updateDeployment } from '../data/deployment'; import { savedEnginesToEngines } from '../engines/saved-engines-helpers'; import { getMSConfig } from '../ms-config/ms-config'; +import { updateTaskInfo } from '../tasks/server-actions'; export async function deployProcess( definitionId: string, @@ -316,6 +317,9 @@ export async function refetchDeployments() { const newInstances: { id: string; + processId: string; + environmentId: string; + versionId: string; deploymentId: string; initiatorId: null; engineIds: string[]; @@ -351,6 +355,9 @@ export async function refetchDeployments() { if (!existingInstance) { newInstances.push({ id: i.processInstanceId, + processId: p.definitionId, + environmentId: deployment.version.process.environmentId, + versionId: deployment.version.id, deploymentId: deployment.id, initiatorId: null, engineIds: [e.id], @@ -398,9 +405,42 @@ export async function refetchDeployments() { data: { state }, }); }), - newInstances.length && tx.processInstance.createMany({ data: newInstances }), + newInstances.length && + tx.processInstance.createMany({ + data: newInstances.map((i) => ({ + ...i, + processId: undefined, + versionId: undefined, + environmentId: undefined, + })), + }), ]); }); + + const knownInstances = Object.fromEntries( + res + .flatMap((d) => + d.instances.map((i) => ({ + ...i, + processId: d.version.processId, + environmentId: d.version.process.environmentId, + versionId: d.version.id, + })), + ) + .concat(newInstances) + .map((i) => [ + i.id, + { + instanceId: i.id, + processId: i.processId, + state: i.state as InstanceInfo, + environmentId: i.environmentId, + versionId: i.versionId, + }, + ]), + ); + + await updateTaskInfo(engines, reachableWithDeployments, knownInstances); } catch (err) { console.error('Error fetching deployment information: ', err); } diff --git a/src/management-system-v2/lib/tasks/server-actions.ts b/src/management-system-v2/lib/tasks/server-actions.ts index 41c32cb13..f40a06113 100644 --- a/src/management-system-v2/lib/tasks/server-actions.ts +++ b/src/management-system-v2/lib/tasks/server-actions.ts @@ -17,19 +17,10 @@ import { UserFacingError, getErrorMessage, isUserErrorResponse, userError } from import { getDataObject, isErrorResponse } from '@/app/api/spaces/[spaceId]/data/helper'; import db from '@/lib/data/db'; -import { getUserById } from '@/lib/data/db/iam/users'; -import { - addUserTasks, - deleteUserTask, - getUserTaskById, - getUserTasks, - updateUserTask, -} from '@/lib/data/user-tasks'; - -import { ExtendedTaskListEntry, UserTask } from '@/lib/user-task-schema'; +import { getUserTaskById, updateUserTask } from '@/lib/data/user-tasks'; import { Engine } from '@/lib/engines/types'; -import { getDeployment as fetchDeployment } from '@/lib/engines/deployment'; +import { InstanceInfo, getDeployment as fetchDeployment } from '@/lib/engines/deployment'; import { submitFileToMachine } from '@/lib/engines/instances'; import { activateUserTask, @@ -41,128 +32,11 @@ import { setTasklistEntryVariableValuesOnMachine, } from '@/lib/engines/tasklist'; import { getInstance } from '@/lib/data/instance'; - -export async function getAvailableTaskListEntries(spaceId: string, engines: Engine[]) { - try { - let stored = await getUserTasks(); - - if (isUserErrorResponse(stored)) { - throw stored.error; - } - - const removedTasks = [] as string[]; - - const fetched = ( - await asyncMap(engines, async (engine) => { - try { - const taskList = await getTaskListFromMachine(engine); - - // check if we have stored user tasks for this machine which have been removed from the - // machine - const removedFromMachine = Object.fromEntries( - (stored as UserTask[]) - .filter((task) => task.machineId === engine.id) - .map((task) => [task.id, task]), - ); - taskList.forEach( - (task) => delete removedFromMachine[`${task.id}|${task.instanceID}|${task.startTime}`], - ); - removedTasks.push(...Object.keys(removedFromMachine)); - - return taskList.map((task) => ({ - ...task, - machineId: engine.id, - potentialOwners: task.performers, - })); - } catch (e) { - return null; - } - }) - ) - .filter(truthyFilter) - .flat() - .map( - (task) => - ({ - ...task, - id: `${task.id}|${task.instanceID}|${task.startTime}`, - taskId: task.id, - fileName: task.attrs['proceed:fileName'], - milestones: undefined, - }) as UserTask, - ); - - const newTasks: UserTask[] = []; - const changedTasks: UserTask[] = []; - - fetched.forEach((task) => { - const alreadyStored = (stored as UserTask[]).some((t) => t.id === task.id); - if (alreadyStored) { - // TODO: maybe check if the task actually changed - changedTasks.push(task); - } else { - newTasks.push(task); - } - }); - - if (newTasks.length) await addUserTasks(newTasks); - await asyncForEach(changedTasks, async (task) => { - delete task.milestones; - await updateUserTask(task.id, task); - }); - await asyncForEach(removedTasks, async (id) => { - await deleteUserTask(id); - stored = (stored as UserTask[]).filter((task) => task.id !== id); - }); - - const userTasks = [ - ...fetched, - ...stored - .filter((task) => !fetched.some((t) => t.id === task.id)) - .map( - (task) => - ({ - ...task, - startTime: +task.startTime, - endTime: task.endTime && +task.endTime, - }) as UserTask, - ), - ]; - - return await asyncMap(userTasks, async (task) => { - const actualOwner = await db.$transaction(async (tx) => { - let users: { - id: string; - username?: string | null; - firstName?: string | null; - lastName?: string | null; - }[] = ( - await asyncMap(task.actualOwner, async (owner) => { - return getUserById(owner, undefined, tx) || owner; - }) - ).filter(truthyFilter); - - return users.map((user) => - typeof user === 'string' - ? { id: user, name: '' } - : { id: user.id, name: user.firstName + ' ' + user.lastName, username: user?.username }, - ); - }); - - return { - ...task, - actualOwner, - } satisfies ExtendedTaskListEntry; - }); - } catch (e) { - const message = getErrorMessage(e); - return userError(message); - } -} +import { getProcessBPMN, getProcessHtmlFormHTML } from '../data/processes'; export async function getGlobalVariablesForHTML( spaceId: string, - initiatorId: string, + initiatorId: string | null, html: string, ) { return await getGlobalVariables(html, async (varPath) => { @@ -171,6 +45,10 @@ export async function getGlobalVariablesForHTML( let userId: string | undefined; if (segments[0] === '@process-initiator') { + if (!initiatorId) + throw new UserFacingError( + 'Invalid selector for global initiator data in execution that does not have an initiator.', + ); userId = initiatorId; } else if (segments[0] === '@worker' || !segments[0].startsWith('@')) { ({ userId } = await getCurrentUser()); @@ -306,7 +184,6 @@ export async function getTasklistEntryHTML( const instance = await getInstance(spaceId, storedUserTask.instanceID); if (isUserErrorResponse(instance)) return instance; if (!instance) throw new Error('Cannot retrieve the instance initiator information.'); - if (!instance.initiatorId) throw new Error('Missing initiator information'); globalVars = await getGlobalVariablesForHTML(spaceId, instance.initiatorId, html); } @@ -481,3 +358,156 @@ export async function submitFile(engine: Engine | null, userTaskId: string, form return userError(message); } } + +export async function updateTaskInfo( + reachableEngines: Engine[], + reachableWithDeployments: Engine[], + knownInstances: Record< + string, + { + instanceId: string; + processId: string; + environmentId: string; + versionId: string; + state: InstanceInfo; + } + >, +) { + // get all users tasks that belong to instances (they were not created in the task editor) + const knownUserTasks = await db.userTask.findMany({ where: { NOT: { instanceID: null } } }); + + const reachableWithUserTasks = reachableEngines.filter((e) => + knownUserTasks.some((uT) => uT.machineId === e.id), + ); + + const reachableWithDeploymentsAndUserTasks = Object.values( + Object.fromEntries( + reachableWithDeployments.concat(reachableWithUserTasks).map((e) => [e.id, e]), + ), + ); + + const fetchedUserTasks = ( + await asyncMap(reachableWithDeploymentsAndUserTasks, async (e) => { + const tasklist = await getTaskListFromMachine(e); + + return tasklist.map((entry) => ({ ...entry, machineId: e.id })); + }) + ) + .flat() + .filter((uT) => !!knownInstances[uT.instanceID]) + .map((uT) => [`${uT.id}|${uT.instanceID}|${uT.startTime}`, uT] as const); + + const newUserTasks = fetchedUserTasks.filter(([id]) => { + return !knownUserTasks.some((kUT) => kUT.id === id); + }); + + const addedUserTasks = ( + await asyncMap(newUserTasks, async ([id, task]) => { + const relatedInstanceInfo = knownInstances[task.instanceID]; + + const machine = reachableEngines.find((e) => e.id === task.machineId); + if (!machine) return; + + try { + const definitionId = relatedInstanceInfo.processId; + const spaceId = relatedInstanceInfo.environmentId; + const bpmn = await getProcessBPMN( + definitionId, + spaceId, + relatedInstanceInfo.versionId, + undefined, + true, + ); + + if (isUserErrorResponse(bpmn)) return; + + const initialVariables = getCorrectVariableState(task, relatedInstanceInfo.state); + const milestones = await getCorrectMilestoneState(bpmn, task, relatedInstanceInfo.state); + + const fileName = task.attrs['proceed:fileName']; + const htmlForm = await getProcessHtmlFormHTML( + definitionId, + fileName, + spaceId, + undefined, + true, + ); + + if (isUserErrorResponse(htmlForm)) return; + + let html = htmlForm.replace(/\/resources\/process[^"]*/g, (match) => { + const path = match.split('/'); + return `/api/private/${spaceId}/engine/resources/process/${task.instanceID}/images/${path.pop()}`; + }); + + const processIds = await getProcessIds(bpmn); + let variableDefinitions: undefined | Variable[]; + if (processIds.length) { + const [processId] = processIds; + variableDefinitions = await getVariablesFromElementById(bpmn, processId); + } + + html = inlineScript(html, task.instanceID, id, variableDefinitions); + + return { + ...task, + attrs: undefined, + performers: undefined, + id, + taskId: task.id, + fileName, + potentialOwners: task.performers, + startTime: task.startTime, + environmentId: relatedInstanceInfo.environmentId, + initialVariables, + variableChanges: {}, + milestones, + milestonesChanges: {}, + html, + }; + } catch (err) { + console.error('Error', err); + } + }) + ).filter(truthyFilter); + + const updatedUserTasks = fetchedUserTasks + .filter(([id]) => { + return knownUserTasks.some((kUT) => kUT.id === id); + }) + .map( + ([id, data]) => + [ + id, + { + actualOwner: data.actualOwner, + state: data.state, + status: data.status, + priority: data.priority, + progress: data.progress, + endTime: data.endTime, + machineId: data.machineId, + }, + ] as const, + ); + + await db.$transaction(async (tx) => { + await tx.userTask.createMany({ + data: addedUserTasks.map((uT) => ({ + ...uT, + startTime: new Date(uT.startTime), + endTime: new Date(uT.endTime), + })), + }); + + await asyncForEach(updatedUserTasks, async ([id, data]) => { + await tx.userTask.update({ + where: { id }, + data: { + ...data, + endTime: new Date(data.endTime), + }, + }); + }); + }); +} diff --git a/src/management-system-v2/lib/use-user-tasks.ts b/src/management-system-v2/lib/use-user-tasks.ts index ec740c161..2f3991570 100644 --- a/src/management-system-v2/lib/use-user-tasks.ts +++ b/src/management-system-v2/lib/use-user-tasks.ts @@ -3,7 +3,6 @@ import useEngines from '@/lib/engines/use-engines'; import { useQuery } from '@tanstack/react-query'; import { useCallback } from 'react'; import { - getAvailableTaskListEntries, getTasklistEntryHTML, addOwnerToTaskListEntry, setTasklistEntryVariableValues, @@ -13,10 +12,11 @@ import { } from './tasks/server-actions'; import { getUserRoles } from './data/roles'; import { isUserErrorResponse } from './user-error'; +import { getUserTasks } from './data/user-tasks'; function useUserTasks( space: { spaceId: string; isOrganization: boolean }, - fetchInterval = 10000, + fetchInterval = 1000, filter?: { allowedStates?: string[]; hideUnassignedTasks?: boolean; @@ -30,7 +30,7 @@ function useUserTasks( const queryFn = useCallback(async () => { if (engines) { - let result = await getAvailableTaskListEntries(space.spaceId, engines); + let result = await getUserTasks(space.spaceId); if (isUserErrorResponse(result)) return []; From bcc8f8ae6846b64b5b8cd95612930ba62e37f45f Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Thu, 28 May 2026 12:50:08 +0200 Subject: [PATCH 03/13] Changed the user task update loop to only push updates to the database when a task actually changed --- .../user-task-helper/index.d.ts | 4 +- src/helper-modules/user-task-helper/index.js | 2 +- .../lib/engines/tasklist.ts | 2 +- .../lib/helpers/javascriptHelpers.ts | 4 +- .../lib/tasks/server-actions.ts | 37 +++++++++---------- 5 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/helper-modules/user-task-helper/index.d.ts b/src/helper-modules/user-task-helper/index.d.ts index 6140dc57e..544290cff 100644 --- a/src/helper-modules/user-task-helper/index.d.ts +++ b/src/helper-modules/user-task-helper/index.d.ts @@ -13,7 +13,7 @@ export type UserTaskInfo = { /** * - the time at which execution of the element ended */ - endTime?: number; + endTime?: number | null; /** * - the current execution state of the user task */ @@ -143,7 +143,7 @@ export type InstanceInfo = { * @type {object} * @property {string} id - the id of the user task * @property {number} startTime - the time at which execution of the element started - * @property {number} [endTime] - the time at which execution of the element ended + * @property {number | null} [endTime] - the time at which execution of the element ended * @property {string} state - the current execution state of the user task * @property {{ [key: string]: number }} [milestones] - the values of the milestones of the user task * @property {{ [key: string]: any }} [variableChanges] - the variables that were changed by the user task diff --git a/src/helper-modules/user-task-helper/index.js b/src/helper-modules/user-task-helper/index.js index d1048920e..eb3906bdf 100644 --- a/src/helper-modules/user-task-helper/index.js +++ b/src/helper-modules/user-task-helper/index.js @@ -13,7 +13,7 @@ const { getMilestonesFromElementById } = require('@proceed/bpmn-helper/src/gette * @type {object} * @property {string} id - the id of the user task * @property {number} startTime - the time at which execution of the element started - * @property {number} [endTime] - the time at which execution of the element ended + * @property {number | null} [endTime] - the time at which execution of the element ended * @property {string} state - the current execution state of the user task * @property {{ [key: string]: number }} [milestones] - the values of the milestones of the user task * @property {{ [key: string]: any }} [variableChanges] - the variables that were changed by the user task diff --git a/src/management-system-v2/lib/engines/tasklist.ts b/src/management-system-v2/lib/engines/tasklist.ts index 235398dae..480511f83 100644 --- a/src/management-system-v2/lib/engines/tasklist.ts +++ b/src/management-system-v2/lib/engines/tasklist.ts @@ -19,7 +19,7 @@ export type TaskListEntry = { performers: { user?: string[]; roles: string[] }; progress: 0; startTime: number; - endTime: number; + endTime: number | null; }; export async function getTaskListFromMachine(machine: Engine) { diff --git a/src/management-system-v2/lib/helpers/javascriptHelpers.ts b/src/management-system-v2/lib/helpers/javascriptHelpers.ts index a912253a4..a7da764fc 100644 --- a/src/management-system-v2/lib/helpers/javascriptHelpers.ts +++ b/src/management-system-v2/lib/helpers/javascriptHelpers.ts @@ -28,7 +28,7 @@ export async function asyncFilter(array: Array, cb: (entry: Type) => ).filter((entry) => entry) as Array; } -export function pick( +export function pick>( obj: T, keys: PickKeys, ): Prettify> { @@ -37,7 +37,7 @@ export function pick( ) as Prettify>; } -export function omit( +export function omit>( obj: T, keys: OmitKeys, ): Prettify> { diff --git a/src/management-system-v2/lib/tasks/server-actions.ts b/src/management-system-v2/lib/tasks/server-actions.ts index f40a06113..3717578aa 100644 --- a/src/management-system-v2/lib/tasks/server-actions.ts +++ b/src/management-system-v2/lib/tasks/server-actions.ts @@ -10,7 +10,7 @@ import { inlineUserTaskData, } from '@proceed/user-task-helper'; -import { asyncForEach, asyncMap } from '@/lib/helpers/javascriptHelpers'; +import { asyncForEach, asyncMap, deepEquals, pick } from '@/lib/helpers/javascriptHelpers'; import { truthyFilter } from '@/lib/typescript-utils'; import { getCurrentUser } from '@/components/auth'; import { UserFacingError, getErrorMessage, isUserErrorResponse, userError } from '@/lib/user-error'; @@ -471,32 +471,29 @@ export async function updateTaskInfo( }) ).filter(truthyFilter); + const changeableEntries = ['actualOwner', 'state', 'priority', 'progress', 'endTime'] as const; + const updatedUserTasks = fetchedUserTasks - .filter(([id]) => { - return knownUserTasks.some((kUT) => kUT.id === id); + .filter(([id, data]) => { + const knownUserTask = knownUserTasks.find((kUT) => kUT.id === id); + if (!knownUserTask) return false; + + const potentiallyChanged = pick( + { ...knownUserTask, endTime: knownUserTask.endTime && knownUserTask.endTime.getTime() }, + changeableEntries, + ); + const newInfo = pick(data, changeableEntries); + + return !deepEquals(potentiallyChanged, newInfo); }) - .map( - ([id, data]) => - [ - id, - { - actualOwner: data.actualOwner, - state: data.state, - status: data.status, - priority: data.priority, - progress: data.progress, - endTime: data.endTime, - machineId: data.machineId, - }, - ] as const, - ); + .map(([id, data]) => [id, pick(data, changeableEntries)] as const); await db.$transaction(async (tx) => { await tx.userTask.createMany({ data: addedUserTasks.map((uT) => ({ ...uT, startTime: new Date(uT.startTime), - endTime: new Date(uT.endTime), + endTime: uT.endTime === null ? null : new Date(uT.endTime), })), }); @@ -505,7 +502,7 @@ export async function updateTaskInfo( where: { id }, data: { ...data, - endTime: new Date(data.endTime), + endTime: data.endTime === null ? null : new Date(data.endTime), }, }); }); From 1a69ef2ad5d52d86232126213b29dc526d877190 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Thu, 28 May 2026 13:47:07 +0200 Subject: [PATCH 04/13] Fixed: user tasks are initialized with outdated variable information --- .../executions/deployment-server-actions.ts | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/src/management-system-v2/lib/executions/deployment-server-actions.ts b/src/management-system-v2/lib/executions/deployment-server-actions.ts index 453672566..a3d72897c 100644 --- a/src/management-system-v2/lib/executions/deployment-server-actions.ts +++ b/src/management-system-v2/lib/executions/deployment-server-actions.ts @@ -325,7 +325,13 @@ export async function refetchDeployments() { engineIds: string[]; state: InstanceInfo; }[] = []; - const instanceUpdates: { id: string; state: InstanceInfo }[] = []; + const instanceUpdates: { + id: string; + processId: string; + environmentId: string; + versionId: string; + state: InstanceInfo; + }[] = []; // fetch deployment data from the engines const engineDeployments = Object.fromEntries( @@ -364,7 +370,13 @@ export async function refetchDeployments() { state: i, }); } else if (!deepEquals(i, existingInstance.state)) { - instanceUpdates.push({ id: i.processInstanceId, state: i }); + instanceUpdates.push({ + id: i.processInstanceId, + processId: p.definitionId, + environmentId: deployment.version.process.environmentId, + versionId: deployment.version.id, + state: i, + }); } }); }); @@ -418,26 +430,7 @@ export async function refetchDeployments() { }); const knownInstances = Object.fromEntries( - res - .flatMap((d) => - d.instances.map((i) => ({ - ...i, - processId: d.version.processId, - environmentId: d.version.process.environmentId, - versionId: d.version.id, - })), - ) - .concat(newInstances) - .map((i) => [ - i.id, - { - instanceId: i.id, - processId: i.processId, - state: i.state as InstanceInfo, - environmentId: i.environmentId, - versionId: i.versionId, - }, - ]), + instanceUpdates.concat(newInstances).map((i) => [i.id, { ...i, instanceId: i.id }]), ); await updateTaskInfo(engines, reachableWithDeployments, knownInstances); From 216571a63203492b7333da31ffb781a63a4cf3ce Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Thu, 28 May 2026 14:06:27 +0200 Subject: [PATCH 05/13] The function for getting the user task html is now fetching the html from the database instead of getting it from the engine --- .../tasklist/user-task-view.tsx | 13 +- .../lib/tasks/server-actions.ts | 155 ++++++------------ .../lib/use-user-tasks.ts | 10 -- 3 files changed, 57 insertions(+), 121 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/user-task-view.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/user-task-view.tsx index f590efc15..955023beb 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/user-task-view.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/user-task-view.tsx @@ -14,6 +14,7 @@ import styles from './user-task-view.module.scss'; import { App, Skeleton } from 'antd'; import { ExtendedTaskListEntry } from '@/lib/user-task-schema'; import useUserTasks from '@/lib/use-user-tasks'; +import { getTasklistEntryHTML } from '@/lib/tasks/server-actions'; type UserTaskFormProps = { html?: string; @@ -98,21 +99,15 @@ const TaskListUserTaskForm: React.FC = ({ task, userI const { message } = App.useApp(); - const { - completeEntry, - setMilestoneValues, - setVariableValues, - addOwner, - getTaskListEntryHtml, - submitFile, - } = useUserTasks(space, Infinity); + const { completeEntry, setMilestoneValues, setVariableValues, addOwner, submitFile } = + useUserTasks(space, Infinity); const { data: html } = useQuery({ queryFn: async () => { if (!task) return null; const html = await wrapServerCall({ fn: async () => { - const html = await getTaskListEntryHtml(task.id, task.fileName); + const html = await getTasklistEntryHTML(space.spaceId, task.id); return html || null; }, diff --git a/src/management-system-v2/lib/tasks/server-actions.ts b/src/management-system-v2/lib/tasks/server-actions.ts index 3717578aa..70789c1b0 100644 --- a/src/management-system-v2/lib/tasks/server-actions.ts +++ b/src/management-system-v2/lib/tasks/server-actions.ts @@ -20,19 +20,19 @@ import db from '@/lib/data/db'; import { getUserTaskById, updateUserTask } from '@/lib/data/user-tasks'; import { Engine } from '@/lib/engines/types'; -import { InstanceInfo, getDeployment as fetchDeployment } from '@/lib/engines/deployment'; +import { InstanceInfo } from '@/lib/engines/deployment'; import { submitFileToMachine } from '@/lib/engines/instances'; import { activateUserTask, addOwnerToTaskListEntryOnMachine, completeTasklistEntryOnMachine, getTaskListFromMachine, - getUserTaskFileFromMachine, setTasklistEntryMilestoneValuesOnMachine, setTasklistEntryVariableValuesOnMachine, } from '@/lib/engines/tasklist'; import { getInstance } from '@/lib/data/instance'; import { getProcessBPMN, getProcessHtmlFormHTML } from '../data/processes'; +import { getEngineIfAvailable } from '../data/engines'; export async function getGlobalVariablesForHTML( spaceId: string, @@ -70,12 +70,7 @@ export async function getGlobalVariablesForHTML( }); } -export async function getTasklistEntryHTML( - spaceId: string, - userTaskId: string, - filename: string, - engine: Engine | null, -) { +export async function getTasklistEntryHTML(spaceId: string, userTaskId: string) { try { const storedUserTask = await getUserTaskById(userTaskId); @@ -83,114 +78,70 @@ export async function getTasklistEntryHTML( throw new Error('Failed to get stored user task data.'); } - let { - initialVariables, - variableChanges, - milestones, - milestonesChanges, - html, - state: storedState, - } = storedUserTask; - - if (engine && (!html || !milestones || !initialVariables)) { - const [taskId, instanceId, startTimeString] = userTaskId.split('|'); - - const [definitionId] = instanceId.split('-_'); - - const startTime = parseInt(startTimeString); - - const userTasks = await getTaskListFromMachine(engine); - const userTask = userTasks.find( - (uT) => uT.instanceID === instanceId && uT.id === taskId && uT.startTime == startTime, - ); - - if (!userTask) throw new Error('Could not fetch user task data!'); - - const deployment = await fetchDeployment(engine, definitionId); - const instance = deployment.instances.find((i) => i.processInstanceId === instanceId); - - if (!instance) - throw new Error( - 'Could not get instance information for the instance that started the user task', - ); - - const version = deployment.versions.find((v) => v.versionId === instance.processVersion)!; - - initialVariables = getCorrectVariableState(userTask, instance); - milestones = await getCorrectMilestoneState(version.bpmn, userTask, instance); - - html = await getUserTaskFileFromMachine(engine, definitionId, filename); - - variableChanges = initialVariables; - - html = html.replace(/\/resources\/process[^"]*/g, (match) => { - const path = match.split('/'); - return `/api/private/${spaceId}/engine/resources/process/${definitionId}/images/${path.pop()}`; - }); - - const processIds = await getProcessIds(version.bpmn); - let variableDefinitions: undefined | Variable[]; - if (processIds.length) { - const [processId] = processIds; - variableDefinitions = await getVariablesFromElementById(version.bpmn, processId); - } - - html = inlineScript(html, instanceId, taskId, variableDefinitions); - - if (storedState === 'READY') { - await activateUserTask(engine, instanceId, taskId, startTime); - storedState = 'ACTIVE'; + let { initialVariables, variableChanges, milestones, milestonesChanges, html } = storedUserTask; + + if (storedUserTask.state === 'READY') { + let activated = true; + if (storedUserTask.instanceID && storedUserTask.machineId) { + try { + const engine = await getEngineIfAvailable(spaceId, storedUserTask.machineId); + if (engine && !isUserErrorResponse(engine)) { + await activateUserTask( + engine, + storedUserTask.instanceID, + storedUserTask.taskId, + storedUserTask.startTime.getTime(), + ); + } else { + activated = false; + } + } catch (err) { + activated = false; + } } - updateUserTask(userTaskId, { html, initialVariables, milestones, state: storedState }); - } else { - variableChanges = { ...initialVariables, ...(variableChanges || {}) }; - - if (milestonesChanges) { - milestones = (milestones || []).map((milestone) => ({ - ...milestone, - value: milestonesChanges![milestone.id] ?? milestone.value, - })); - } + if (activated) await updateUserTask(userTaskId, { state: 'ACTIVE' }); } - // maps relative urls used to get resources on the engine to the MS api to allow them to work here as well - function mapResourceUrls(variables: Record) { - if (!variables || !engine) return variables; - - return Object.fromEntries( - Object.entries(variables).map(([key, value]) => { - const [_, instanceId] = userTaskId.split('|'); - const [definitionId] = instanceId.split('-_'); - - if ( - typeof value === 'string' && - value.includes(`resources/process/${definitionId}/instance/${instanceId}/file/`) - ) { - return [key, `api/private/${spaceId}/engine/` + value]; - } + variableChanges = { ...initialVariables, ...(variableChanges || {}) }; - return [key, value]; - }), - ); + if (milestonesChanges) { + milestones = (milestones || []).map((milestone) => ({ + ...milestone, + value: milestonesChanges![milestone.id] ?? milestone.value, + })); } - if (!html) throw new Error('Failed to get the html for the user task'); - if (!milestones) throw new Error('Failed to get the milestones for the user task'); - - let globalVars: Record = {}; - if (storedUserTask.instanceID) { const instance = await getInstance(spaceId, storedUserTask.instanceID); if (isUserErrorResponse(instance)) return instance; if (!instance) throw new Error('Cannot retrieve the instance initiator information.'); - globalVars = await getGlobalVariablesForHTML(spaceId, instance.initiatorId, html); - } + const globalVars = await getGlobalVariablesForHTML(spaceId, instance.initiatorId, html); + + // maps relative urls used to get resources on the engine to the MS api to allow them to work here as well + function mapResourceUrls(variables: Record) { + return Object.fromEntries( + Object.entries(variables).map(([key, value]) => { + const [_, instanceId] = userTaskId.split('|'); + const [definitionId] = instanceId.split('-_'); - variableChanges = { ...variableChanges, ...globalVars }; + if ( + typeof value === 'string' && + value.includes(`resources/process/${definitionId}/instance/${instanceId}/file/`) + ) { + return [key, `api/private/${spaceId}/engine/` + value]; + } + + return [key, value]; + }), + ); + } + + variableChanges = { ...mapResourceUrls(variableChanges), ...globalVars }; + } - return inlineUserTaskData(html, mapResourceUrls(variableChanges), milestones); + return inlineUserTaskData(html, variableChanges, milestones); } catch (e) { const message = getErrorMessage(e); return userError(message); diff --git a/src/management-system-v2/lib/use-user-tasks.ts b/src/management-system-v2/lib/use-user-tasks.ts index 2f3991570..b436c5e08 100644 --- a/src/management-system-v2/lib/use-user-tasks.ts +++ b/src/management-system-v2/lib/use-user-tasks.ts @@ -136,15 +136,6 @@ function useUserTasks( return await addOwnerToTaskListEntry(taskId, owner, machine); } - async function getTaskListEntryHtml(taskId: string, fileName: string) { - const machine = getTaskEngine(taskId); - - if (machine === undefined) - return { error: 'Could not find the machine the task is running on' }; - - return await getTasklistEntryHTML(space.spaceId, taskId, fileName, machine); - } - async function submitFile(taskId: string, file: File) { const machine = getTaskEngine(taskId); @@ -165,7 +156,6 @@ function useUserTasks( setMilestoneValues, setVariableValues, addOwner, - getTaskListEntryHtml, submitFile, }; } From 60e3ea56e9fa7382af41acb29e71e21ef4c57842 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Thu, 28 May 2026 14:45:36 +0200 Subject: [PATCH 06/13] Moved user task related actions into the backend --- .../tasklist/user-task-view.tsx | 31 ++++--- .../lib/engines/use-engines.tsx | 33 ------- .../lib/tasks/server-actions.ts | 68 ++++++++++++--- .../lib/use-user-tasks.ts | 85 +------------------ 4 files changed, 78 insertions(+), 139 deletions(-) delete mode 100644 src/management-system-v2/lib/engines/use-engines.tsx diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/user-task-view.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/user-task-view.tsx index 955023beb..9f10feab1 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/user-task-view.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/user-task-view.tsx @@ -13,8 +13,14 @@ import styles from './user-task-view.module.scss'; import { App, Skeleton } from 'antd'; import { ExtendedTaskListEntry } from '@/lib/user-task-schema'; -import useUserTasks from '@/lib/use-user-tasks'; -import { getTasklistEntryHTML } from '@/lib/tasks/server-actions'; +import { + addOwnerToTaskListEntry, + completeTasklistEntry, + getTasklistEntryHTML, + setTasklistEntryVariableValues, + setTasklistMilestoneValues, + submitFile, +} from '@/lib/tasks/server-actions'; type UserTaskFormProps = { html?: string; @@ -99,9 +105,6 @@ const TaskListUserTaskForm: React.FC = ({ task, userI const { message } = App.useApp(); - const { completeEntry, setMilestoneValues, setVariableValues, addOwner, submitFile } = - useUserTasks(space, Infinity); - const { data: html } = useQuery({ queryFn: async () => { if (!task) return null; @@ -142,10 +145,10 @@ const TaskListUserTaskForm: React.FC = ({ task, userI await wrapServerCall({ fn: async () => { if (!task?.actualOwner.some((owner) => owner.id === userId)) { - const updatedOwners = await addOwner(task.id, userId); + const updatedOwners = await addOwnerToTaskListEntry(space.spaceId, task.id, userId); if ('error' in updatedOwners) return updatedOwners; } - return await completeEntry(task.id, variables); + return await completeTasklistEntry(space.spaceId, task.id, variables); }, }); }} @@ -153,10 +156,10 @@ const TaskListUserTaskForm: React.FC = ({ task, userI await wrapServerCall({ fn: async () => { if (!task?.actualOwner.some((owner) => owner.id === userId)) { - const updatedOwners = await addOwner(task.id, userId); + const updatedOwners = await addOwnerToTaskListEntry(space.spaceId, task.id, userId); if ('error' in updatedOwners) return updatedOwners; } - return await setMilestoneValues(task.id, newValues); + return await setTasklistMilestoneValues(space.spaceId, task.id, newValues); }, onSuccess: () => {}, }); @@ -165,17 +168,21 @@ const TaskListUserTaskForm: React.FC = ({ task, userI wrapServerCall({ fn: async () => { if (!task?.actualOwner.some((owner) => owner.id === userId)) { - const updatedOwners = await addOwner(task.id, userId); + const updatedOwners = await addOwnerToTaskListEntry(space.spaceId, task.id, userId); if ('error' in updatedOwners) return updatedOwners; } - return await setVariableValues(task.id, newValues); + return await setTasklistEntryVariableValues(space.spaceId, task.id, newValues); }, onSuccess: () => {}, }); }} onFileSubmit={async (file) => { const path = await wrapServerCall({ - fn: async () => submitFile(task.id, file), + fn: async () => { + const formData = new FormData(); + formData.append('file', file); + return submitFile(space.spaceId, task.id, formData); + }, onSuccess: false, onError: () => message.error( diff --git a/src/management-system-v2/lib/engines/use-engines.tsx b/src/management-system-v2/lib/engines/use-engines.tsx deleted file mode 100644 index db4f4f1e2..000000000 --- a/src/management-system-v2/lib/engines/use-engines.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Engine } from './types'; -import { useCallback } from 'react'; -import { useQuery } from '@tanstack/react-query'; -import { asyncFilter } from '../helpers/javascriptHelpers'; -import { getAllAvailableEngines } from '../data/engines'; -import { isUserErrorResponse } from '../user-error'; - -function useEngines( - space: { spaceId: string; isOrganization: boolean }, - filter: { key: any[]; fn: (engine: Engine) => Promise } = { - key: [], - fn: async () => true, - }, -) { - const queryFn = useCallback(async () => { - if (space.spaceId) { - let res = await getAllAvailableEngines(space.spaceId); - - if (isUserErrorResponse(res)) return []; - - return await asyncFilter(res, filter.fn); - } - return null; - }, [space.spaceId, filter]); - - return useQuery({ - queryFn, - queryKey: ['engines', space.spaceId, ...filter.key], - refetchInterval: 5000, - }); -} - -export default useEngines; diff --git a/src/management-system-v2/lib/tasks/server-actions.ts b/src/management-system-v2/lib/tasks/server-actions.ts index 70789c1b0..f0c2fd673 100644 --- a/src/management-system-v2/lib/tasks/server-actions.ts +++ b/src/management-system-v2/lib/tasks/server-actions.ts @@ -148,11 +148,7 @@ export async function getTasklistEntryHTML(spaceId: string, userTaskId: string) } } -export async function addOwnerToTaskListEntry( - userTaskId: string, - owner: string, - engine: Engine | null, -) { +export async function addOwnerToTaskListEntry(spaceId: string, userTaskId: string, owner: string) { try { const storedUserTask = await getUserTaskById(userTaskId); @@ -167,7 +163,14 @@ export async function addOwnerToTaskListEntry( actualOwner: [...actualOwner, owner], }); - if (engine) { + if (storedUserTask.instanceID) { + const { machineId } = storedUserTask; + const engine = machineId && (await getEngineIfAvailable(spaceId, machineId)); + + if (!engine || isUserErrorResponse(engine)) { + return userError('Could not find the engine this user task is running on.'); + } + const [taskId, instanceId] = userTaskId.split('|'); return await addOwnerToTaskListEntryOnMachine(engine, instanceId, taskId, owner); @@ -182,9 +185,9 @@ export async function addOwnerToTaskListEntry( } export async function setTasklistEntryVariableValues( + spaceId: string, userTaskId: string, variables: { [key: string]: any }, - engine: Engine | null, ) { try { const storedUserTask = await getUserTaskById(userTaskId); @@ -197,7 +200,14 @@ export async function setTasklistEntryVariableValues( variableChanges: { ...storedUserTask.variableChanges, ...variables }, }); - if (engine) { + if (storedUserTask.instanceID) { + const { machineId } = storedUserTask; + const engine = machineId && (await getEngineIfAvailable(spaceId, machineId)); + + if (!engine || isUserErrorResponse(engine)) { + return userError('Could not find the engine this user task is running on.'); + } + const [taskId, instanceId] = userTaskId.split('|'); await setTasklistEntryVariableValuesOnMachine(engine, instanceId, taskId, variables); @@ -209,9 +219,9 @@ export async function setTasklistEntryVariableValues( } export async function setTasklistMilestoneValues( + spaceId: string, userTaskId: string, milestones: { [key: string]: any }, - engine: Engine | null, ) { try { const storedUserTask = await getUserTaskById(userTaskId); @@ -224,7 +234,14 @@ export async function setTasklistMilestoneValues( milestonesChanges: { ...storedUserTask.milestonesChanges, ...milestones }, }); - if (engine) { + if (storedUserTask.instanceID) { + const { machineId } = storedUserTask; + const engine = machineId && (await getEngineIfAvailable(spaceId, machineId)); + + if (!engine || isUserErrorResponse(engine)) { + return userError('Could not find the engine this user task is running on.'); + } + const [taskId, instanceId] = userTaskId.split('|'); await setTasklistEntryMilestoneValuesOnMachine(engine, instanceId, taskId, milestones); @@ -236,9 +253,9 @@ export async function setTasklistMilestoneValues( } export async function completeTasklistEntry( + spaceId: string, userTaskId: string, variables: { [key: string]: any }, - engine: Engine | null, ) { try { const storedUserTask = await getUserTaskById(userTaskId); @@ -249,7 +266,14 @@ export async function completeTasklistEntry( const { variableChanges, milestonesChanges } = storedUserTask; - if (engine) { + if (storedUserTask.instanceID) { + const { machineId } = storedUserTask; + const engine = machineId && (await getEngineIfAvailable(spaceId, machineId)); + + if (!engine || isUserErrorResponse(engine)) { + return userError('Could not find the engine this user task is running on.'); + } + const [taskId, instanceId] = userTaskId.split('|'); // push the values from the database to the engine so the instance state is correctly updated @@ -273,6 +297,7 @@ export async function completeTasklistEntry( await updateUserTask(userTaskId, { variableChanges: { ...variableChanges, ...variables }, state: 'COMPLETED', + endTime: !storedUserTask.instanceID ? new Date() : undefined, }); } catch (e) { const message = getErrorMessage(e); @@ -280,8 +305,25 @@ export async function completeTasklistEntry( } } -export async function submitFile(engine: Engine | null, userTaskId: string, formData: FormData) { +export async function submitFile(spaceId: string, userTaskId: string, formData: FormData) { try { + const storedUserTask = await getUserTaskById(userTaskId); + + if (!storedUserTask || 'error' in storedUserTask) { + throw new Error('Failed to get stored user task data.'); + } + + if (!storedUserTask.instanceID) { + return userError('We cannot save files for locally created user tasks'); + } + + const { machineId } = storedUserTask; + const engine = machineId && (await getEngineIfAvailable(spaceId, machineId)); + + if (!engine || isUserErrorResponse(engine)) { + return userError('Could not find the engine this user task is running on.'); + } + const file = formData.get('file') as File; const fileName = file.name; diff --git a/src/management-system-v2/lib/use-user-tasks.ts b/src/management-system-v2/lib/use-user-tasks.ts index b436c5e08..51db1746f 100644 --- a/src/management-system-v2/lib/use-user-tasks.ts +++ b/src/management-system-v2/lib/use-user-tasks.ts @@ -1,15 +1,6 @@ import { useSession } from '@/components/auth-can'; -import useEngines from '@/lib/engines/use-engines'; import { useQuery } from '@tanstack/react-query'; import { useCallback } from 'react'; -import { - getTasklistEntryHTML, - addOwnerToTaskListEntry, - setTasklistEntryVariableValues, - setTasklistMilestoneValues, - completeTasklistEntry, - submitFile as _submitFile, -} from './tasks/server-actions'; import { getUserRoles } from './data/roles'; import { isUserErrorResponse } from './user-error'; import { getUserTasks } from './data/user-tasks'; @@ -24,21 +15,15 @@ function useUserTasks( }, disabled?: boolean, ) { - const { data: engines } = useEngines(space); - const session = useSession(); const queryFn = useCallback(async () => { - if (engines) { - let result = await getUserTasks(space.spaceId); - - if (isUserErrorResponse(result)) return []; + let result = await getUserTasks(space.spaceId); - return result; - } + if (isUserErrorResponse(result)) return []; - return []; - }, [engines, space.spaceId]); + return result; + }, [space.spaceId]); const query = useQuery({ queryFn, @@ -92,71 +77,9 @@ function useUserTasks( } } - function getTaskEngine(taskId: string) { - const task = userTasks.find((t) => t.id === taskId); - - if (!task) return; - - if (task.machineId === 'ms-local') return null; - - return engines?.find((e) => e.id === task.machineId); - } - - async function completeEntry(taskId: string, variables: Record) { - const machine = getTaskEngine(taskId); - - if (machine === undefined) return; - - const res = await completeTasklistEntry(taskId, variables, machine); - return res; - } - - async function setMilestoneValues(taskId: string, milestones: Record) { - const machine = getTaskEngine(taskId); - - if (machine === undefined) return; - - return await setTasklistMilestoneValues(taskId, milestones, machine); - } - - async function setVariableValues(taskId: string, variables: Record) { - const machine = getTaskEngine(taskId); - - if (machine === undefined) return; - - return await setTasklistEntryVariableValues(taskId, variables, machine); - } - - async function addOwner(taskId: string, owner: string) { - const machine = getTaskEngine(taskId); - - if (machine === undefined) - return { error: 'Could not find the machine the task is running on' }; - - return await addOwnerToTaskListEntry(taskId, owner, machine); - } - - async function submitFile(taskId: string, file: File) { - const machine = getTaskEngine(taskId); - - if (machine === undefined) - return { error: 'Could not find the machine the task is running on' }; - - const formData = new FormData(); - formData.append('file', file); - - return await _submitFile(machine, taskId, formData); - } - return { - engines, userTasks, ...query, - completeEntry, - setMilestoneValues, - setVariableValues, - addOwner, - submitFile, }; } From 3b47f3c64f16f79cccde1ee5d18bdec8001e1f85 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Thu, 28 May 2026 14:46:02 +0200 Subject: [PATCH 07/13] Reduced some polling intervals since they will now fetch from the database instead of from the engines which took longer --- .../app/(dashboard)/[environmentId]/layout.tsx | 2 +- .../app/(dashboard)/[environmentId]/tasklist/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/layout.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/layout.tsx index be9339f92..2f6633ce3 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/layout.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/layout.tsx @@ -179,7 +179,7 @@ const DashboardLayout = async ( }); } - let pollingInterval = 10000; + let pollingInterval = 1000; if (Number.isInteger(automationSettings.taskPollingInterval)) { pollingInterval = automationSettings.taskPollingInterval; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/page.tsx index 3c9940a55..eae4e02b1 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/tasklist/page.tsx @@ -38,7 +38,7 @@ const TasklistPage = async (props: { params: Promise<{ environmentId: string }> return notFound(); } - let pollingInterval = 5000; + let pollingInterval = 1000; if (Number.isInteger(automationSettings.tasklist?.pollingInterval)) { pollingInterval = automationSettings.tasklist.pollingInterval; } From 81d5d89f57320fc2e8794e8fce3f15cdd0436b6e Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Thu, 28 May 2026 15:14:16 +0200 Subject: [PATCH 08/13] Removed unnecessary return value for use-user-task hook --- src/management-system-v2/lib/use-user-tasks.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/management-system-v2/lib/use-user-tasks.ts b/src/management-system-v2/lib/use-user-tasks.ts index 51db1746f..49f8cf268 100644 --- a/src/management-system-v2/lib/use-user-tasks.ts +++ b/src/management-system-v2/lib/use-user-tasks.ts @@ -77,10 +77,7 @@ function useUserTasks( } } - return { - userTasks, - ...query, - }; + return { userTasks }; } export default useUserTasks; From 3c1a1fcb3296ade671f2fa80572453ed860400ed Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Thu, 28 May 2026 15:14:55 +0200 Subject: [PATCH 09/13] Merged the user task data related files into one by merging the functions of both files --- .../lib/data/db/user-tasks.ts | 96 ------------------- .../lib/data/user-tasks.ts | 84 +++++++++------- 2 files changed, 50 insertions(+), 130 deletions(-) delete mode 100644 src/management-system-v2/lib/data/db/user-tasks.ts diff --git a/src/management-system-v2/lib/data/db/user-tasks.ts b/src/management-system-v2/lib/data/db/user-tasks.ts deleted file mode 100644 index f450e806f..000000000 --- a/src/management-system-v2/lib/data/db/user-tasks.ts +++ /dev/null @@ -1,96 +0,0 @@ -import db from '@/lib/data/db'; -import { z } from 'zod'; -import { - ExtendedTaskListEntry, - UserTask, - UserTaskInput, - UserTaskInputSchema, -} from '@/lib/user-task-schema'; -import { truthyFilter } from '@/lib/typescript-utils'; - -export async function getUserTasks(spaceId: string) { - return (await db.userTask.findMany({ - where: { - OR: [ - // get all user tasks that were created in the task editor of the MS - { instance: null }, - // and all user tasks that belong to instances of processes belonging to this space - { instance: { deployment: { version: { process: { environmentId: spaceId } } } } }, - ], - }, - })) as UserTask[]; -} - -export async function getUserTaskById(userTaskId: string) { - const userTask = await db.userTask.findUnique({ - where: { - id: userTaskId, - }, - }); - - if (!userTask) return undefined; - - // TODO: maybe handle view capability for specific user tasks - - return { ...userTask, offline: true } as unknown as UserTask; -} - -const UserTaskArraySchema = UserTaskInputSchema.array(); -export async function addUserTasks(userTaskInput: UserTaskInput[]) { - const newUserTasks = UserTaskArraySchema.parse(userTaskInput); - - // TODO: maybe check if the user can work on/add user tasks - - return await db.userTask.createMany({ - data: newUserTasks.map((task) => ({ - ...task, - startTime: new Date(task.startTime), - endTime: typeof task.endTime !== 'number' ? undefined : new Date(task.endTime), - })), - }); -} - -const PartialUserTaskInputSchema = UserTaskInputSchema.partial(); -type PartialDatabaseUserTaskInput = Omit< - z.infer, - 'startTime' | 'endTime' -> & { - startTime?: Date; - endTime?: Date; -}; -export async function updateUserTask(userTaskId: string, userTaskInput: Partial) { - const newUserTaskData = PartialUserTaskInputSchema.parse(userTaskInput); - - const updateData: PartialDatabaseUserTaskInput = { - ...newUserTaskData, - startTime: undefined, - endTime: undefined, - }; - - if (newUserTaskData.startTime) { - updateData.startTime = new Date(newUserTaskData.startTime); - } - - if ('endTime' in newUserTaskData) { - updateData.endTime = - typeof newUserTaskData.endTime !== 'number' ? undefined : new Date(newUserTaskData.endTime); - } - - // TODO: maybe check if a user is allowed to edit a user task - - return await db.userTask.update({ - data: updateData, - where: { - id: userTaskId, - }, - }); -} - -export async function deleteUserTask(userTaskId: string) { - // TODO: check if a user is allowed to delete a user task - return await db.userTask.delete({ - where: { - id: userTaskId, - }, - }); -} diff --git a/src/management-system-v2/lib/data/user-tasks.ts b/src/management-system-v2/lib/data/user-tasks.ts index 206a42828..32bfcf99d 100644 --- a/src/management-system-v2/lib/data/user-tasks.ts +++ b/src/management-system-v2/lib/data/user-tasks.ts @@ -1,16 +1,15 @@ 'use server'; -import { z } from 'zod'; -import { - getUserTasks as _getUserTasks, - getUserTaskById as _getUserTaskById, - addUserTasks as _addUserTasks, - updateUserTask as _updateUserTask, - deleteUserTask as _deleteUserTask, -} from './db/user-tasks'; +import db from '@/lib/data/db'; + import { UnauthorizedError } from '../ability/abilityHelper'; import { UserErrorType, isUserErrorResponse, userError } from '../user-error'; -import { ExtendedTaskListEntry, UserTaskInput } from '../user-task-schema'; +import { + ExtendedTaskListEntry, + UserTask, + UserTaskInput, + UserTaskInputSchema, +} from '../user-task-schema'; import { getCurrentEnvironment } from '@/components/auth'; import { getAllAvailableEngines } from './engines'; import { getSpaceUsers } from './db/iam/users'; @@ -22,7 +21,16 @@ export async function getUserTasks(spaceId: string) { } = await getCurrentEnvironment(spaceId); try { - const userTasks = await _getUserTasks(spaceId); + const userTasks = (await db.userTask.findMany({ + where: { + OR: [ + // get all user tasks that were created in the task editor of the MS + { instance: null }, + // and all user tasks that belong to instances of processes belonging to this space + { instance: { deployment: { version: { process: { environmentId: spaceId } } } } }, + ], + }, + })) as UserTask[]; const users = await getSpaceUsers(spaceId, isOrganization); const reachableEngines = await getAllAvailableEngines(spaceId, undefined, true); @@ -56,44 +64,52 @@ export async function getUserTasks(spaceId: string) { export async function getUserTaskById(userTaskId: string) { try { - return await _getUserTaskById(userTaskId); + const userTask = await db.userTask.findUnique({ + where: { + id: userTaskId, + }, + }); + + return userTask as UserTask; } catch (err) { - if (err instanceof UnauthorizedError) - return userError('Permission denied', UserErrorType.PermissionError); - else return userError('Error getting user tasks'); + return userError('Error getting user tasks'); } } export async function addUserTasks(userTasks: UserTaskInput[]) { try { - return await _addUserTasks(userTasks); - } catch (e) { - if (e instanceof UnauthorizedError) - return userError('Permission denied', UserErrorType.PermissionError); - else if (e instanceof z.ZodError) + const newUserTasks = UserTaskInputSchema.array().safeParse(userTasks); + + if (!newUserTasks.success) { return userError('Schema validation failed', UserErrorType.SchemaValidationError); - else return userError('Error getting user task'); + } + + // TODO: maybe check if the user can work on/add user tasks + return await db.userTask.createMany({ + data: newUserTasks.data, + }); + } catch (e) { + return userError('Error getting user task'); } } export async function updateUserTask(userTaskId: string, userTaskInput: Partial) { try { - return await _updateUserTask(userTaskId, userTaskInput); - } catch (e) { - if (e instanceof UnauthorizedError) - return userError('Permission denied', UserErrorType.PermissionError); - else if (e instanceof z.ZodError) + const newUserTaskData = UserTaskInputSchema.partial().safeParse(userTaskInput); + + if (!newUserTaskData.success) { return userError('Schema validation failed', UserErrorType.SchemaValidationError); - else return userError('Error getting updating user task'); - } -} + } -export async function deleteUserTask(userTaskId: string) { - try { - return await _deleteUserTask(userTaskId); + const res = await db.userTask.update({ + data: newUserTaskData.data, + where: { + id: userTaskId, + }, + }); + + return res; } catch (e) { - if (e instanceof UnauthorizedError) - return userError('Permission denied', UserErrorType.PermissionError); - else return userError('Error deleting user task'); + return userError('Error getting updating user task'); } } From e4b5f6e72167227d7d8700b7165e9290f85394ac Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 2 Jun 2026 17:14:30 +0200 Subject: [PATCH 10/13] Hide user tasks of deployments that were removed --- src/management-system-v2/lib/data/user-tasks.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/management-system-v2/lib/data/user-tasks.ts b/src/management-system-v2/lib/data/user-tasks.ts index 32bfcf99d..7dfcbe22b 100644 --- a/src/management-system-v2/lib/data/user-tasks.ts +++ b/src/management-system-v2/lib/data/user-tasks.ts @@ -27,7 +27,16 @@ export async function getUserTasks(spaceId: string) { // get all user tasks that were created in the task editor of the MS { instance: null }, // and all user tasks that belong to instances of processes belonging to this space - { instance: { deployment: { version: { process: { environmentId: spaceId } } } } }, + { + instance: { + deployment: { + AND: [ + { version: { process: { environmentId: spaceId } } }, + { removeTime: null, toRemove: false }, + ], + }, + }, + }, ], }, })) as UserTask[]; From d0a0110f7573924004c94a285abbec9e4e62ebe3 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Thu, 11 Jun 2026 17:12:12 +0200 Subject: [PATCH 11/13] Fixed: error in mapping for process image paths --- src/management-system-v2/lib/tasks/server-actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/management-system-v2/lib/tasks/server-actions.ts b/src/management-system-v2/lib/tasks/server-actions.ts index f0c2fd673..1d84958cf 100644 --- a/src/management-system-v2/lib/tasks/server-actions.ts +++ b/src/management-system-v2/lib/tasks/server-actions.ts @@ -430,7 +430,7 @@ export async function updateTaskInfo( let html = htmlForm.replace(/\/resources\/process[^"]*/g, (match) => { const path = match.split('/'); - return `/api/private/${spaceId}/engine/resources/process/${task.instanceID}/images/${path.pop()}`; + return `/api/private/${spaceId}/engine/resources/process/${relatedInstanceInfo.processId}/images/${path.pop()}`; }); const processIds = await getProcessIds(bpmn); From 2b0b070c6f99986d6ce954c741457f0d149fe83f Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Thu, 11 Jun 2026 17:16:41 +0200 Subject: [PATCH 12/13] Better error message if the task refetch logic fails to handle a task --- src/management-system-v2/lib/tasks/server-actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/management-system-v2/lib/tasks/server-actions.ts b/src/management-system-v2/lib/tasks/server-actions.ts index 1d84958cf..cbed8ae99 100644 --- a/src/management-system-v2/lib/tasks/server-actions.ts +++ b/src/management-system-v2/lib/tasks/server-actions.ts @@ -459,7 +459,7 @@ export async function updateTaskInfo( html, }; } catch (err) { - console.error('Error', err); + console.error(`Failed to process user task (id: ${id}): ${err}`); } }) ).filter(truthyFilter); From 1002094ff59e635245d94899c6ac0a2553a6e404 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Thu, 11 Jun 2026 17:18:02 +0200 Subject: [PATCH 13/13] Code improvement --- src/management-system-v2/lib/data/user-tasks.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/management-system-v2/lib/data/user-tasks.ts b/src/management-system-v2/lib/data/user-tasks.ts index 7dfcbe22b..c3f0e3659 100644 --- a/src/management-system-v2/lib/data/user-tasks.ts +++ b/src/management-system-v2/lib/data/user-tasks.ts @@ -49,10 +49,7 @@ export async function getUserTasks(spaceId: string) { // show richer information about who is working on the task return userTasks.map((uT) => ({ ...uT, - offline: - uT.machineId === 'ms-local' || reachableEngines.some((e) => e.id === uT.machineId) - ? false - : true, + offline: uT.machineId !== 'ms-local' && !reachableEngines.some((e) => e.id === uT.machineId), actualOwner: uT.actualOwner .map((id) => { const user = users.find((u) => u.id === id);