diff --git a/CLAUDE.md b/CLAUDE.md index d14d0c1a2d..80367ae221 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -182,7 +182,7 @@ Client `.java` under `/registry/public//...` is synchronized by `JavaSy A single `app.intent` YAML file at a project root is the source of truth one altitude above the model files. **The intent is an authoring artifact, not a runtime artifact** — like the `.edm` it has an editor and an explicit Generate, and (like the `.edm`) it has **no synchronizer**. Double-clicking any `*.intent` file opens the Intent Editor (`components/ui/editor-intent`): editable YAML left, live read-only diagram right (mxGraph ER + per-process flowcharts, the same engine the EDM/schema/mapping modelers use), validation inline; the Generate button runs six generators that write `.edm`/`.model`, `.bpmn`, `.form`, `.report`, `.roles` and `.csvim`/`.csv` **into the developer's workspace project at the project root** (the layout of real-world Dirigible application projects) — nothing touches the registry until normal publish, after which the per-artefact synchronizers bring the runtime live as for any project. Services: `POST /services/ide/intent/parse` and `POST /services/ide/intent/generate`. A third editor pane is a **Claude AI assistant** (`POST /services/ide/intent/agent`): it proposes the complete updated `app.intent` via the Anthropic API (key server-side in `DirigibleConfig.INTENT_AI_*`, never sent to the browser), the editor shows a Monaco diff, and Accept merges it into the buffer — the agent never writes disk or runs Generate. Developed on PR [#6017](https://github.com/eclipse-dirigible/dirigible/pull/6017). -**Detailed guide:** [`components/engine/engine-intent/CLAUDE.md`](components/engine/engine-intent/CLAUDE.md). Read it before changing anything under that module — it covers the editor-first architecture and altitude contract (model files only, never code), the YAML schema and its semantics (integer-only primary keys, `composition: true` to-one = DEPENDENT master-detail while `required` alone is just a NOT NULL FK, PascalCase property names with UPPER_SNAKE columns, decision `then`/`else`, intent-prefixed table names via `IntentNaming`), the `writeModelFile`-only write surface with the stale-output scrub, the wrong turns already made (wrong altitude, template-output paths, registry-relative vs repository-absolute paths, the `JsonHelper` Gson pitfall, **and the synchronizer-based first incarnation — do not reintroduce it**), and the follow-up list (chaining model-to-code via `.gen` descriptors, `/custom/` escape hatch). Process triggers (`trigger: { onCreate: }`) are wired: the EDM adds a `ProcessId` field + a `triggers` collection to the `.model`, and the `template-application-events-java` template generates a `gen/events/Trigger.java` listener that starts the process on create. `IntentEngineIT` is the HTTP-only end-to-end test (~1 minute, no sync cycles). The editor's diagram pane is **mxGraph** (replacing Mermaid, which had unfixable light/dark theming bugs) with a fixed brand-colour palette that reads on both themes — see the module guide's "Intent Editor diagram = mxGraph" section before touching `editor-intent/js/editor.js`. +**Detailed guide:** [`components/engine/engine-intent/CLAUDE.md`](components/engine/engine-intent/CLAUDE.md). Read it before changing anything under that module — it covers the editor-first architecture and altitude contract (model files only, never code), the YAML schema and its semantics (integer-only primary keys, `composition: true` to-one = DEPENDENT master-detail while `required` alone is just a NOT NULL FK, PascalCase property names with UPPER_SNAKE columns, decision `then`/`else`, intent-prefixed table names via `IntentNaming`), the `writeModelFile`-only write surface with the stale-output scrub, the wrong turns already made (wrong altitude, template-output paths, registry-relative vs repository-absolute paths, the `JsonHelper` Gson pitfall, **and the synchronizer-based first incarnation — do not reintroduce it**), and the follow-up list (chaining model-to-code via `.gen` descriptors, `/custom/` escape hatch). Process triggers (`trigger: { onCreate: }`) are wired: the EDM adds a `ProcessId` field + a `triggers` collection to the `.model`, and the `template-application-events-java` template generates a `gen/events/Trigger.java` listener that starts the process on create. That persisted `ProcessId` is in turn **consumed by the generated entity-view UI**: a shared `ProcessTasks` module (`components/resources/resources-dashboard/.../dashboard/services/process-tasks.js`) surfaces the record's actionable BPM user tasks inline via an `` directive (correlating `entity.ProcessId === task.processInstanceId`), wired into every generated view gated on a `hasProcess` flag; the task form completes via the permission-checked `/services/inbox/tasks/{id}` and self-closes (#6074). `IntentEngineIT` is the HTTP-only end-to-end test (~1 minute, no sync cycles). The editor's diagram pane is **mxGraph** (replacing Mermaid, which had unfixable light/dark theming bugs) with a fixed brand-colour palette that reads on both themes — see the module guide's "Intent Editor diagram = mxGraph" section before touching `editor-intent/js/editor.js`. **The general platform line this enshrines:** authoring artifacts (`.edm`, `.model`, `.form`, `.report`, `.intent`) get **workspace editors + an explicit Generate**; only runtime artifacts (`.roles`, `.bpmn`, `.csvim`, `.table`, jobs, listeners, …) get **synchronizers**. Applying the synchronizer hammer to an authoring artifact generates into the registry where no modeler, Projects view, or template can use it — that mistake was made once and reverted; the inventory of synchronizers (grep `extends BaseSynchronizer`) deliberately contains no authoring formats. diff --git a/components/engine/engine-intent/CLAUDE.md b/components/engine/engine-intent/CLAUDE.md index a12ebb5f55..7b53dda23b 100644 --- a/components/engine/engine-intent/CLAUDE.md +++ b/components/engine/engine-intent/CLAUDE.md @@ -437,6 +437,7 @@ Implemented and generating annotated client-Java off the shared `EventBinding` / - Reports rewritten to the Dirigible `.report` shape with a materialised SQL `query` (was empty), `relation.field` -> `INNER JOIN`, `filter` -> qualified `WHERE`, and default-role `security`. Covered by `IntentEngineIT` (aggregate + join/filter reports). - Cross-artefact PascalCase: the `.form` control `model`/`id` bind to the PascalCase EDM property name; a bare to-one relation report dimension auto-joins and shows the target's `name`-like field instead of the raw FK id. - Process triggers (`trigger: { onCreate: }`) fully wired in Java: validated by the parser; the new `template-application-events-java` ("Application - Glue Code - Java") template generates a `gen/events/Trigger.java` self-describing `MessageHandler` that starts the process on the entity's create event; the Java DAO template publishes that event. The EDM keeps only the persisted `ProcessId` column (`EdmIntentGenerator`). Covered by `IntentEngineIT` end-to-end (verified live: create → trigger → process start → ProcessId written back). +- **`ProcessId` consumed by the generated entity-view UI** (in-context BPM task surfacing). The `ProcessId` the trigger writes back is read by the generated views: a shared `ProcessTasks` AngularJS module (`components/resources/resources-dashboard/.../dashboard/services/process-tasks.js` — service + `` directive) fetches the current user's Inbox tasks once, buckets them by `processInstanceId`, and a record shows its actionable tasks inline by matching `entity.ProcessId === task.processInstanceId`. Wired into every generated view (`list`, `manage`, `master-list`/`master-manage` `detail` and `main-details`) gated on a `hasProcess` flag that `parameterUtils.js` sets when an entity has a `ProcessId` property — so non-process entities generate unchanged. The generated task form (`FormIntentGenerator`) completes via the per-task **permission-checked** `/services/inbox/tasks/{id}` (not the role-guarded `/services/bpm/bpm-processes/tasks/{id}`, which blocks candidate-group users) and self-closes on completion via both `DialogHub.closeWindow()` (dialog/inbox) and `window.close()` (standalone window). (#6074 + refinements #6075.) - **Process glue externalized to `.glue`** (the precedent: `.report`/`.form` were lifted out of the EDM). The `triggers` + `resolvers` collections live in `.glue` (`GlueIntentGenerator`), NOT the `.model` - the EDM describes entities, the BPMN describes flow, neither owns "who starts a process / how its context is populated". The Glue-Code template binds to `extension: "glue"`; `generateUtils.js` has `triggers` + `resolvers` collection cases. (Supersedes the older "triggers in the .model" wiring.) - **Decision resolvers (`relation.field`):** a decision condition like `book.price > 500` referencing a one-hop to-one relation of the trigger entity gets a `${JavaTask}` resolver service task inserted before the gateway and the condition rewritten to the resolved variable (`book_price`); the `gen/events/Resolve.java` `JavaDelegate` (generated from `.glue`) loads the related entity at the decision and sets the variable. `ProcessResolverSupport` + `IntentEntities` (shared perspective/PK resolution). Rewrite happens on a copy of the step list so the glue generator still sees the original path. - **`.settings`** (`IntentSettings`, loaded/scaffolded by `IntentGenerationService.loadOrScaffoldSettings`): developer-owned, scaffolded once then preserved (not scrubbed). Holds the `generation` recipe (template id + parameters per model type), per-artefact `overrides` (`{triggers|resolvers|forms}..generate=false` -> skip and reuse a hand-written one), and `userTasks.candidateGroupsExtra` (defaults to `ADMINISTRATOR`, appended to every user-task `candidateGroups`). Loaded into `IntentGenerationContext` before generators run; honored by the Glue/Form/BPMN generators. The Generate endpoint returns a `codeGenerations` plan from the recipe + written files, and the **editor's Generate chains model->code** by replaying it through `generate.mjs`. Cross-module tenant-context fix: the Java `@Listener` dispatch (`ListenerClassConsumer`) now runs in the message's tenant context. diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/form/FormIntentGenerator.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/form/FormIntentGenerator.java index 117d1a16f8..d941b4faaa 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/form/FormIntentGenerator.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/form/FormIntentGenerator.java @@ -60,14 +60,16 @@ * carries a {@code callback} like {@code onApproveClicked()} wired in the {@code code} block to * complete the current BPM user task: the Inbox/Process perspective opens the form with * {@code ?taskId=&processInstanceId=}, and the handler POSTs {@code COMPLETE} to - * {@code /services/bpm/bpm-processes/tasks/} with the action name and the form model as - * process variables (so a downstream gateway can branch on the action). On success the handler - * closes its host via both {@code DialogHub.closeWindow()} and {@code window.close()} - the former - * closes the dialog when the form is opened from an entity view, the latter a standalone - * (script-opened) window; each is a harmless no-op where it does not apply, including the Inbox's - * inline iframe (which clears its own pane on its refresh cycle). Forms opened outside a task - * report the missing {@code taskId} instead of failing silently. Business logic beyond completing - * the task belongs in a hand-written form override under {@code custom/}. + * {@code /services/inbox/tasks/} (the per-task permission-checked Inbox endpoint, so a + * candidate-group user - not only ADMINISTRATOR/DEVELOPER/OPERATOR - can complete) with the action + * name and the form model as process variables (so a downstream gateway can branch on the action). + * On success the handler closes its host via both {@code DialogHub.closeWindow()} and + * {@code window.close()} - the former closes the dialog when the form is opened from an entity + * view, the latter a standalone (script-opened) window; each is a harmless no-op where it does not + * apply, including the Inbox's inline iframe (which clears its own pane on its refresh cycle). + * Forms opened outside a task report the missing {@code taskId} instead of failing silently. + * Business logic beyond completing the task belongs in a hand-written form override under + * {@code custom/}. * *

* Idempotent: identical input always produces byte-identical output. @@ -157,7 +159,7 @@ function __completeTask(action) { __notifications.show({ type: 'negative', title: 'Cannot submit', description: 'This form was not opened from a task (no taskId).' }); return; } - $http.post('/services/bpm/bpm-processes/tasks/' + __taskId, { + $http.post('/services/inbox/tasks/' + __taskId, { action: 'COMPLETE', data: Object.assign({ action: action }, $scope.model || {}) }).then(() => { diff --git a/components/resources/resources-dashboard/src/main/resources/META-INF/dirigible/dashboard/services/process-tasks.js b/components/resources/resources-dashboard/src/main/resources/META-INF/dirigible/dashboard/services/process-tasks.js index 664705d81b..a5983e3bf1 100644 --- a/components/resources/resources-dashboard/src/main/resources/META-INF/dirigible/dashboard/services/process-tasks.js +++ b/components/resources/resources-dashboard/src/main/resources/META-INF/dirigible/dashboard/services/process-tasks.js @@ -119,7 +119,7 @@ angular.module('ProcessTasks', ['platformLocale']) scope: { entity: '<' }, template: ` - + @@ -130,6 +130,12 @@ angular.module('ProcessTasks', ['platformLocale']) link: (scope) => { scope.ariaLabel = LocaleService.t('dashboard.processTasks.pending', {}, 'Pending tasks'); scope.tasks = () => ProcessTasks.getTasks(scope.entity); + // Surface the current step inline: a single actionable task shows its name (answers + // "why is there a task here?" at a glance), several collapse to a count. + scope.label = () => { + const open = scope.tasks(); + return open.length === 1 ? open[0].name : LocaleService.t('dashboard.processTasks.count', { count: open.length }, open.length + ' tasks'); + }; scope.openTask = (task) => ProcessTasks.openTask(task); ProcessTasks.ensureLoaded().then(() => scope.$applyAsync()); } diff --git a/components/template/template-application-ui-angular-java/src/main/resources/META-INF/dirigible/template-application-ui-angular-java/ui/perspective/master-list/main-details/controller.js.template b/components/template/template-application-ui-angular-java/src/main/resources/META-INF/dirigible/template-application-ui-angular-java/ui/perspective/master-list/main-details/controller.js.template index d68bb689b5..027edfddfe 100644 --- a/components/template/template-application-ui-angular-java/src/main/resources/META-INF/dirigible/template-application-ui-angular-java/ui/perspective/master-list/main-details/controller.js.template +++ b/components/template/template-application-ui-angular-java/src/main/resources/META-INF/dirigible/template-application-ui-angular-java/ui/perspective/master-list/main-details/controller.js.template @@ -1,5 +1,5 @@ #set($dollar = '$') -angular.module('page', ['blimpKit', 'platformView', 'platformLocale'#if($hasProcess), 'ProcessTasks'#end]).controller('PageController', ($scope, Extensions, LocaleService) => { +angular.module('page', ['blimpKit', 'platformView', 'platformLocale'#if($hasProcess), 'ProcessTasks'#end]).controller('PageController', ($scope, Extensions, LocaleService#if($hasProcess), ProcessTasks#end) => { const Dialogs = new DialogHub(); $scope.entity = {}; @@ -48,6 +48,9 @@ angular.module('page', ['blimpKit', 'platformView', 'platformLocale'#if($hasProc #end #end }); +#if($hasProcess) + ProcessTasks.refresh(); +#end }}); //-----------------Events-------------------// diff --git a/components/template/template-application-ui-angular-java/src/main/resources/META-INF/dirigible/template-application-ui-angular-java/ui/perspective/master-manage/main-details/controller.js.template b/components/template/template-application-ui-angular-java/src/main/resources/META-INF/dirigible/template-application-ui-angular-java/ui/perspective/master-manage/main-details/controller.js.template index 3bfd655054..5e83105a04 100644 --- a/components/template/template-application-ui-angular-java/src/main/resources/META-INF/dirigible/template-application-ui-angular-java/ui/perspective/master-manage/main-details/controller.js.template +++ b/components/template/template-application-ui-angular-java/src/main/resources/META-INF/dirigible/template-application-ui-angular-java/ui/perspective/master-manage/main-details/controller.js.template @@ -3,7 +3,7 @@ angular.module('page', ['blimpKit', 'platformView', 'platformLocale', 'EntitySer .config(["EntityServiceProvider", (EntityServiceProvider) => { EntityServiceProvider.baseUrl = '/services/java/${projectName}/gen/${javaGenFolderName}/api/${javaPerspectiveName}/${name}Controller'; }]) - .controller('PageController', ($scope, ${dollar}http, Extensions, LocaleService, EntityService) => { + .controller('PageController', ($scope, ${dollar}http, Extensions, LocaleService, EntityService#if($hasProcess), ProcessTasks#end) => { const Dialogs = new DialogHub(); const Notifications = new NotificationHub(); let description = 'Description'; @@ -76,6 +76,9 @@ angular.module('page', ['blimpKit', 'platformView', 'platformLocale', 'EntitySer #end $scope.action = 'select'; }); +#if($hasProcess) + ProcessTasks.refresh(); +#end }}); Dialogs.addMessageListener({ topic: '${projectName}.${perspectiveName}.${name}.createEntity', handler: (data) => { $scope.$evalAsync(() => { diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEngineIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEngineIT.java index 949d886114..71530f2e9d 100644 --- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEngineIT.java +++ b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEngineIT.java @@ -943,8 +943,12 @@ private void assertForm() { // HTML-escaped by Gson - ' becomes \\u0027, = becomes \\u003d - so match escape-free substrings; // the form-builder un-escapes the code when it injects it into the controller.) assertTrue(body.contains("__completeTask("), "the action buttons should complete the task"); - assertTrue(body.contains("/services/bpm/bpm-processes/tasks/") && body.contains("COMPLETE"), - "the form should complete the task via the platform BPM task API"); + assertTrue(body.contains("/services/inbox/tasks/") && body.contains("COMPLETE"), + "the form should complete the task via the per-task permission-checked Inbox endpoint"); + assertFalse(body.contains("/services/bpm/bpm-processes/tasks/"), + "the form must not use the role-guarded BPM endpoint, which would block candidate-group users"); + assertTrue(body.contains("closeWindow(") && body.contains("window.close("), + "on completion the form should close its host (dialog via closeWindow, standalone via window.close)"); assertFalse(body.contains("TODO: wire"), "the action handlers must no longer be TODO stubs"); }