Skip to content

Commit 045c334

Browse files
committed
Merge branch 'main' of github.com:PROCEED-Labs/proceed into dynamic-task-list
2 parents 2ce89af + 4d4c665 commit 045c334

File tree

31 files changed

+1851
-1199
lines changed

31 files changed

+1851
-1199
lines changed

.github/copilot-instructions.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Copilot / AI Agent Instructions for PROCEED
2+
3+
Purpose: Give an AI coding agent the minimal, actionable context to be productive in this monorepo.
4+
5+
**Big Picture:**
6+
7+
- **Management System (MS):** Next.js frontend + server code in [src/management-system-v2](src/management-system-v2). Runs at `http://localhost:3000` in dev (first, start a local Postgres instance and then run `yarn dev-ms` from the repo root).
8+
- **Process Engine (PE / Engine):** Node/TS engine code in [src/engine](src/engine). Start the engine in dev from the repo root with `yarn dev`.
9+
- **Helpers:** Reusable helpers live under [src/helper-modules](src/helper-modules).
10+
- **Deprecated/Currrently not in development**: the code under [src/capabilities](src/capabilities) and [src/config-server](src/config-server) is not currently in active development and may be outdated. The focus is on the MS v2 and the engine.
11+
12+
**Monorepo facts & tooling:**
13+
14+
- Workspaces are defined in the root [package.json](package.json). Node >= 20.11 and Yarn v1.22 are expected (see `engines` and `packageManager`).
15+
- Scripts in the root `package.json` orchestrate common workflows (start MS, start engine, db setup, builds).
16+
17+
**Key developer workflows (commands you should use):**
18+
19+
- Install: `yarn install` at the repo root.
20+
- Initialize local Postgres for MS: `yarn dev-ms-db` (this uses [src/management-system-v2/docker-compose-dev.yml](src/management-system-v2/docker-compose-dev.yml) and runs `scripts/db-helper.mts`).
21+
- Run Management System (dev): `yarn dev-ms` (root) or `cd src/management-system-v2 && yarn dev`. (Requires the local Postgres from the previous step).
22+
- Create new DB structure during schema changes: `yarn dev-ms-db-new-structure --name "<desc>"`.
23+
- Build MS for production: `yarn build-ms` (root) or `cd src/management-system-v2 && yarn build`.
24+
- Run Engine in dev: `yarn dev` (root).
25+
- Tests: `yarn test-engine`, `yarn test-e2e`, or `cd src/management-system-v2 && yarn test:unit` for MS unit tests.
26+
27+
**Project-specific conventions & patterns:**
28+
29+
- MS v2 is a Next.js app using `prisma` and server-side code; database access and schema changes rely on `scripts/db-helper.mts` and prisma commands in [src/management-system-v2/package.json](src/management-system-v2/package.json).
30+
- Many repo scripts call `cd` into subfolders; prefer using the root scripts (e.g. `yarn dev-ms`) to ensure the correct environment.
31+
- Build/test scripts may use `ts-node` or `tsx` for short TS scripts (see `predev`, `prebuild`, `postinstall`).
32+
- The engine has a `universal` and `native` part under [src/engine/universal](src/engine/universal) and [src/engine/native](src/engine/native). The `universal` part contains pure Typescript/JavaScript and most business logic. The `native` part contains platform-specific code which is not standardized in JavaScript, e.g. Node.js code for Windows, Linux and Mac under [src/engine/native/node](src/engine/native/node). The universal part uses functionality in the native part via an abstraction layer.
33+
34+
**Integration points & external dependencies:**
35+
36+
- The MS talks to Postgres (local docker compose in dev) and expects environment variables from [src/management-system-v2/.env].
37+
- The MS uses `@proceed/bpmn-helper` and `bpmn-js` for BPMN editing and rendering.
38+
- The engine exposes runtime APIs used by the MS and E2E tests located under [src/engine/e2e_tests](src/engine/e2e_tests).
39+
40+
**Files to inspect for context when changing behavior:**
41+
42+
- Repo-level scripts and workflows: [package.json](package.json)
43+
- MS-specific: [src/management-system-v2/package.json](src/management-system-v2/package.json), [src/management-system-v2/docker-compose-dev.yml](src/management-system-v2/docker-compose-dev.yml), [src/management-system-v2/scripts/db-helper.mts](src/management-system-v2/scripts/db-helper.mts)
44+
- Engine: [src/engine/native](src/engine/native) and [src/engine/universal](src/engine/universal)
45+
46+
**When you change DB schema or Prisma models:**
47+
48+
- Run `yarn dev-ms-db-new-structure --name "<desc>"` to create a new DB instance and run migrations locally.
49+
- Run `yarn dev-ms-db-generate` to regenerate Prisma client if models changed.
50+
51+
If anything here is ambiguous or you want more details (run commands, CI hooks, or where to find specific APIs), tell me what area to expand and I will iterate.

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ services:
1313
- PROCEED_PUBLIC_GANTT_ACTIVE=true
1414
- PROCEED_PUBLIC_CONFIG_SERVER_ACTIVE=false
1515
- PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE=true
16+
- PROCEED_PUBLIC_PROCESS_AUTOMATION_TASK_EDITOR_ACTIVE=true
17+
- PROCEED_PUBLIC_COMPETENCE_MATCHING_ACTIVE=true
1618
ports:
1719
- '3002:33081'
1820
depends_on:

src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler-toolbar.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,11 @@ const ModelerToolbar = ({
306306
);
307307
}}
308308
options={(isListView ? [] : [LATEST_VERSION])
309-
.concat(process.versions ?? [])
309+
.concat(
310+
(process.versions ?? []).sort((a, b) => {
311+
return b.createdOn.getTime() - a.createdOn.getTime();
312+
}),
313+
)
310314
.map(({ id, name }) => ({
311315
value: id,
312316
label: name,

src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { debounce, spaceURL } from '@/lib/utils';
88
import VersionToolbar from './version-toolbar';
99
import useMobileModeler from '@/lib/useMobileModeler';
1010
import { updateProcess } from '@/lib/data/processes';
11-
import { App } from 'antd';
11+
import { Alert, App } from 'antd';
1212
import { is as bpmnIs, isAny as bpmnIsAny } from 'bpmn-js/lib/util/ModelUtil';
1313
import BPMNCanvas, { BPMNCanvasProps, BPMNCanvasRef } from '@/components/bpmn-canvas';
1414
import { useEnvironment } from '@/components/auth-can';
@@ -32,9 +32,10 @@ type ModelerProps = React.HTMLAttributes<HTMLDivElement> & {
3232
versionName?: string;
3333
process: Process;
3434
folder?: Folder;
35+
inEditing?: { userId: string; name: string; timestamp: number };
3536
};
3637

37-
const Modeler = ({ versionName, process, folder, ...divProps }: ModelerProps) => {
38+
const Modeler = ({ versionName, process, folder, inEditing, ...divProps }: ModelerProps) => {
3839
const pathname = usePathname();
3940
const environment = useEnvironment();
4041
const query = useSearchParams();
@@ -92,7 +93,21 @@ const Modeler = ({ versionName, process, folder, ...divProps }: ModelerProps) =>
9293

9394
const showMobileView = useMobileModeler();
9495

95-
const canEdit = !selectedVersionId && !showMobileView && !isListView;
96+
const canEdit = !selectedVersionId && !showMobileView && !isListView && !inEditing;
97+
98+
useEffect(() => {
99+
if (!canEdit) {
100+
return;
101+
}
102+
// Implement a ping to an endpoint to mark the editing session alive.
103+
const interval = setInterval(() => {
104+
fetch(`/api/private/${environment.spaceId}/processes/${process.id}/ping`, {
105+
method: 'POST',
106+
});
107+
}, 1000 * 5);
108+
109+
return () => clearInterval(interval);
110+
}, [canEdit, environment.spaceId, process.id]);
96111

97112
// We shouldn't get to a place where the event listener isn't removed, since the debounce will
98113
// always be fired, as it doesn't get cancelled when process.id changes
@@ -336,6 +351,17 @@ const Modeler = ({ versionName, process, folder, ...divProps }: ModelerProps) =>
336351
return (
337352
<CanEditContext.Provider value={canEdit}>
338353
<div className={styles.Modeler} style={{ height: '100%' }}>
354+
{inEditing && (
355+
<Alert
356+
message={
357+
'This process is currently being edited by ' +
358+
(inEditing.name ?? 'a user') +
359+
'. No changes can be made while it is being edited.'
360+
}
361+
type="warning"
362+
closable
363+
/>
364+
)}
339365
{!minimized && (
340366
<>
341367
{loaded && (

src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/page.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getCurrentEnvironment } from '@/components/auth';
1+
import { getCurrentEnvironment, getCurrentUser } from '@/components/auth';
22
import Wrapper from './wrapper';
33
import styles from './page.module.scss';
44
import Modeler from './modeler';
@@ -14,6 +14,7 @@ import type { Process } from '@/lib/data/process-schema';
1414
import { redirect } from 'next/navigation';
1515
import { spaceURL } from '@/lib/utils';
1616
import { getFolderById } from '@/lib/data/db/folders';
17+
import { getUserById } from '@/lib/data/db/iam/users';
1718

1819
type ProcessPageProps = {
1920
params: Promise<{ processId: string; environmentId: string; mode: string }>;
@@ -36,6 +37,7 @@ const ProcessComponent = async (props: ProcessComponentProps) => {
3637
//console.log('processId', processId);
3738
//console.log('query', searchParams);
3839
const { ability, activeEnvironment } = await getCurrentEnvironment(environmentId);
40+
const { userId } = await getCurrentUser();
3941

4042
const selectedVersionId = searchParams.version;
4143
// Only load BPMN if no version selected (for latest version)
@@ -78,9 +80,26 @@ const ProcessComponent = async (props: ProcessComponentProps) => {
7880
? await getProcessBPMN(processId, environmentId, selectedVersionId)
7981
: process.bpmn;
8082
const selectedVersion = selectedVersionId
81-
? process.versions.find((version) => version.id === selectedVersionId)
83+
? process.versions.find((version: any) => version.id === selectedVersionId)
8284
: undefined;
8385

86+
let inEditing = {
87+
...((process.inEditingBy as any)?.find(
88+
(e: any) => e.userId !== userId && (e.timestamp ?? 0) + 15000 > Date.now(),
89+
) as any),
90+
name: '',
91+
};
92+
93+
if (!inEditing.userId) {
94+
inEditing = undefined;
95+
}
96+
97+
// Get name of user who is editing
98+
if (inEditing?.userId) {
99+
const user = await getUserById(inEditing.userId, { throwIfNotFound: false });
100+
inEditing.name = user ? (Object.hasOwn(user, 'username') ? (user as any).username : '') : '';
101+
}
102+
84103
// Since the user is able to minimize and close the page, everything is in a
85104
// client component from here.
86105
return (
@@ -98,6 +117,7 @@ const ProcessComponent = async (props: ProcessComponentProps) => {
98117
process={{ ...process, bpmn: selectedVersionBpmn as string } as Process}
99118
folder={folder}
100119
versionName={selectedVersion?.name}
120+
inEditing={inEditing}
101121
/>
102122
}
103123
timelineComponent={

src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/planned-duration-input.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ type PlannedDurationModalProperties = {
2828
durationValues: DurationValues;
2929
show: boolean;
3030
close: (values?: DurationValues) => void;
31+
title?: string;
3132
};
3233
export const PlannedDurationModal: React.FC<PlannedDurationModalProperties> = ({
3334
durationValues,
3435
show,
3536
close,
37+
title,
3638
}) => {
3739
const [form] = Form.useForm();
3840

@@ -64,7 +66,7 @@ export const PlannedDurationModal: React.FC<PlannedDurationModalProperties> = ({
6466

6567
return (
6668
<Modal
67-
title="Edit Planned Duration"
69+
title={title || 'Edit Planned Duration'}
6870
className={styles.PlannedDurationModal}
6971
width={getModalWidth()}
7072
style={{ maxWidth: '400px' }}
@@ -113,12 +115,14 @@ type PlannedDurationInputProperties = {
113115
timePlannedDuration: string;
114116
onChange: (changedTimePlannedDuration: string) => void;
115117
readOnly?: boolean;
118+
title?: string;
116119
};
117120

118121
const PlannedDurationInput: React.FC<PlannedDurationInputProperties> = ({
119122
timePlannedDuration,
120123
onChange,
121124
readOnly = false,
125+
title,
122126
}) => {
123127
const [isPlannedDurationModalOpen, setIsPlannedDurationModalOpen] = useState(false);
124128

@@ -167,6 +171,7 @@ const PlannedDurationInput: React.FC<PlannedDurationInputProperties> = ({
167171
<PlannedDurationModal
168172
durationValues={durationValues}
169173
show={isPlannedDurationModalOpen}
174+
title={title}
170175
close={(values) => {
171176
if (values) {
172177
const timeFormalExpression = calculateTimeFormalExpression(values);
@@ -216,9 +221,15 @@ export const TimerEventButton: React.FC<TimerEventButtonProps> = ({ element }) =
216221
}
217222
}, [element]);
218223

224+
// Determine title based on event type
225+
const isStartEvent = element && bpmnIs(element, 'bpmn:StartEvent');
226+
const title = isStartEvent
227+
? 'Edit Trigger Interval for Timer Start Event'
228+
: 'Edit Wait Duration of Timer Event';
229+
219230
return (
220231
<>
221-
<Tooltip title="Edit Wait Duration of Timer Event">
232+
<Tooltip title={title}>
222233
<Button
223234
icon={<ClockCircleOutlined />}
224235
onClick={() => setIsPlannedDurationModalOpen(true)}
@@ -227,6 +238,7 @@ export const TimerEventButton: React.FC<TimerEventButtonProps> = ({ element }) =
227238
<PlannedDurationModal
228239
durationValues={durationValues}
229240
show={isPlannedDurationModalOpen}
241+
title={title}
230242
close={async (values) => {
231243
if (modeler && element && values) {
232244
const modeling = modeler.getModeling();

src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/properties-panel.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ const PropertiesPanelContent: React.FC<PropertiesPanelContentProperties> = ({
373373
}}
374374
timePlannedDuration={timePlannedDuration || ''}
375375
readOnly={readOnly}
376+
title="Edit Planned Execution Duration"
376377
></PlannedDurationInput>
377378
)}
378379
</Space>

src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/user-task-editor.tsx

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import HtmlFormEditor, { HtmlFormEditorRef } from '@/components/html-form-editor
22
import useEditorStateStore, {
33
EditorStoreProvider,
44
} from '@/components/html-form-editor/use-editor-state-store';
5-
import { Alert, App, Grid, Modal } from 'antd';
5+
import { Alert, App, Button, Grid, Modal } from 'antd';
66
import { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
77

88
import { LuImage, LuMilestone } from 'react-icons/lu';
@@ -142,7 +142,7 @@ const UserTaskEditorModal: React.FC<UserTaskEditorModalProps> = ({ processId, op
142142
const editingEnabled = useCanEdit();
143143

144144
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
145-
145+
const [modalKey, setModalKey] = useState(0);
146146
const app = App.useApp();
147147
const breakpoint = Grid.useBreakpoint();
148148

@@ -243,10 +243,31 @@ const UserTaskEditorModal: React.FC<UserTaskEditorModalProps> = ({ processId, op
243243
modalApi.confirm({
244244
title: 'You have unsaved changes!',
245245
content: 'Are you sure you want to close without saving?',
246+
okText: 'Discard Changes',
247+
okType: 'default',
248+
cancelText: 'Cancel',
246249
onOk: () => {
247250
setHasUnsavedChanges(false);
251+
// Increment key to force editor remount and discard changes
252+
setModalKey((prev) => prev + 1);
248253
onClose();
249254
},
255+
footer: (_, { OkBtn, CancelBtn }) => (
256+
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
257+
<CancelBtn />
258+
<OkBtn />
259+
260+
<Button
261+
type="primary"
262+
onClick={async () => {
263+
Modal.destroyAll();
264+
await handleSave();
265+
}}
266+
>
267+
Save
268+
</Button>
269+
</div>
270+
),
250271
});
251272
}
252273
};
@@ -275,7 +296,10 @@ const UserTaskEditorModal: React.FC<UserTaskEditorModalProps> = ({ processId, op
275296
let title = <div>Html Form</div>;
276297

277298
if (bpmnIs(affectedElement, 'bpmn:UserTask')) {
278-
title = <span>User Task Form</span>;
299+
const taskName = affectedElement?.businessObject.name
300+
? `: ${affectedElement?.businessObject.name}`
301+
: '';
302+
title = <span>User Task Form{taskName}</span>;
279303
} else if (bpmnIs(affectedElement, 'bpmn:Process')) {
280304
title = <span>Process Start Form</span>;
281305
}
@@ -310,7 +334,7 @@ const UserTaskEditorModal: React.FC<UserTaskEditorModalProps> = ({ processId, op
310334
onOk={handleSave}
311335
destroyOnHidden
312336
>
313-
<EditorStoreProvider>
337+
<EditorStoreProvider key={modalKey}>
314338
<UserTaskEditor json={json} onChange={() => setHasUnsavedChanges(true)} ref={builder} />
315339
</EditorStoreProvider>
316340
{modalElement}

src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/user-task-image.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { useEditor, useNode, UserComponent } from '@craftjs/core';
33
import { Button, Input, InputNumber, Select, Space, Upload } from 'antd';
44

55
import { useEffect, useRef, useState } from 'react';
6+
import { DeleteOutlined } from '@ant-design/icons';
67

78
import { useParams } from 'next/navigation';
89
import {
910
ContextMenu,
1011
Setting,
12+
useDeleteControl,
1113
VariableSelection,
1214
} from '@/components/html-form-editor/elements/utils';
1315
import { fallbackImage, useImageUpload } from '@/components/image-upload';
@@ -16,6 +18,7 @@ import { useFileManager } from '@/lib/useFileManager';
1618
import useEditorStateStore from '@/components/html-form-editor/use-editor-state-store';
1719

1820
import { tokenize } from '@proceed/user-task-helper/src/tokenize';
21+
import { DeleteButton } from '@/components/html-form-editor/DeleteButton';
1922

2023
type ImageProps = {
2124
src?: string;
@@ -53,7 +56,7 @@ export const EditImage: UserComponent<ImageProps> = ({ src, width, definitionId
5356
const imageRef = useRef<HTMLImageElement>(null);
5457

5558
const [showOverlay, setShowOverlay] = useState(false);
56-
59+
const { handleDelete } = useDeleteControl();
5760
const {
5861
connectors: { connect },
5962
actions: { setProp },
@@ -124,6 +127,7 @@ export const EditImage: UserComponent<ImageProps> = ({ src, width, definitionId
124127
onMouseOver={() => editingEnabled && setShowOverlay(true)}
125128
onMouseOut={() => editingEnabled && setShowOverlay(false)}
126129
>
130+
<DeleteButton show={editingEnabled && showOverlay} onClick={handleDelete} />
127131
<img
128132
ref={imageRef}
129133
style={{ width: width && `${width}%` }}

0 commit comments

Comments
 (0)