Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,9 @@ A single `app.intent` YAML file at a project root is the source of truth one alt

## Harmonia runtime UI (`template-application-ui-harmonia-java` + `template-form-builder-harmonia`)

A second runtime UI stack, parallel to the AngularJS/BlimpKit one: generated apps render as a self-contained **Alpine.js + Harmonia SPA** (client-routed by Pinecone in hash mode, no iframes/`postMessage` hubs), served at `/services/web/<project>/gen/<model>/index.html`, talking to the **reused** generated Java REST controllers over a `fetch` client. The AngularJS IDE is untouched; the two stacks coexist by URL. `template-application-ui-harmonia-java` (registered on `platform-templates` as "Application - UI (Harmonia) - Java") mirrors `template-application-ui-angular-java` and emits the view types (list, manage, setting, master-detail, reports) + built-in **Process Inbox** (`/inbox`) and **Documents** (`/documents`) shell sections + inline process-task surfacing; `template-form-builder-harmonia` ("Harmonia Generator from Form Model", extension `form`) is the runtime form generator. The whole stack — Alpine 3.15.11, Harmonia 2.3.1, Lucide 1.8.0 — is embedded as **webjars** via `components/resources/application-core` (report charts use Harmonia's own native `x-h-chart-*` SVG charts; the `chart.js` webjar remains only for the legacy AngularJS report/dashboard stacks) (incl. Pinecone Router — `org.webjars.npm:pinecone-router`, served version-less at `/webjars/pinecone-router/dist/router.min.js`; it was vendored until the 7.5.2 webjar existed). Developed on PR [#6078](https://github.com/eclipse-dirigible/dirigible/pull/6078).
A second runtime UI stack, parallel to the AngularJS/BlimpKit one: generated apps render as a self-contained **Alpine.js + Harmonia SPA** (client-routed by Pinecone in hash mode, no iframes/`postMessage` hubs), served at `/services/web/<project>/gen/<model>/index.html`, talking to the **reused** generated Java REST controllers over a `fetch` client. The AngularJS IDE is untouched; the two stacks coexist by URL. `template-application-ui-harmonia-java` (registered on `platform-templates` as "Application - UI (Harmonia) - Java") mirrors `template-application-ui-angular-java` and emits the view types (list, manage, setting, master-detail, reports) + built-in **Process Inbox** (`/inbox`) and **Documents** (`/documents`) shell sections + inline process-task surfacing; `template-form-builder-harmonia` ("Harmonia Generator from Form Model", extension `form`) is the runtime form generator. The whole stack — Alpine 3.15.11, Harmonia 2.6.0, Lucide 1.8.0 — is embedded as **webjars** via `components/resources/application-core` (report charts use Harmonia's own native `x-h-chart-*` SVG charts; the `chart.js` webjar remains only for the legacy AngularJS report/dashboard stacks) (incl. Pinecone Router — `org.webjars.npm:pinecone-router`, served version-less at `/webjars/pinecone-router/dist/router.min.js`; it was vendored until the 7.5.2 webjar existed). Developed on PR [#6078](https://github.com/eclipse-dirigible/dirigible/pull/6078).

**Component reference:** [`components/template/template-application-ui-harmonia-java/reference/harmonia/SKILL.md`](components/template/template-application-ui-harmonia-java/reference/harmonia/SKILL.md) is the full codbex-harmonia directive catalog (all `x-h-*` components incl. the `x-h-select` combobox contract, theming, layout), and [`.../reference/alpinejs/`](components/template/template-application-ui-harmonia-java/reference/alpinejs/) covers the Alpine patterns (routing, page components) — both mirrored from `codbex-athena-app`, the reference app this stack was adopted from. Read them before changing Harmonia markup.
**Component reference:** the full codbex-harmonia directive catalog (all `x-h-*` components incl. the `x-h-select` combobox contract, theming, layout) lives **upstream** — the docs site <https://codbex.com/harmonia/> and the agent-readable skill <https://github.com/codbex/harmonia/blob/main/skills/harmonia/SKILL.md> (formerly mirrored in-repo under `reference/harmonia/`; that copy was removed to avoid drift — always consult the upstream, version-matched to `harmonia.version` in the root `pom.xml`). [`.../reference/alpinejs/`](components/template/template-application-ui-harmonia-java/reference/alpinejs/) still covers the Alpine patterns (routing, page components), mirrored from `codbex-athena-app`, the reference app this stack was adopted from. Read them before changing Harmonia markup.

**Detailed guides:** [`components/template/template-application-ui-harmonia-java/README.md`](components/template/template-application-ui-harmonia-java/README.md) (the SPA shell, view-type parity checklist, the master-detail detail registry, process-tasks store) and [`components/template/template-form-builder-harmonia/README.md`](components/template/template-form-builder-harmonia/README.md) (the neutral `formController(ctx)` contract). The repo-root [`HARMONIA_RUNTIME_PLAN.md`](HARMONIA_RUNTIME_PLAN.md) is the design doc + implementation status. **Gotchas that already burned someone — read before changing the templates:**

Expand All @@ -207,7 +207,7 @@ A second runtime UI stack, parallel to the AngularJS/BlimpKit one: generated app
- **Process trigger writes ProcessId via a targeted single-column update (no event, no full row).** Starting a process on `onCreate` writes the instance id back; doing it through the normal `update()` republishes `<entity>-updated` and spuriously fires every onUpdate reaction (e.g. the member-email notification fired the instant a loan was created) — and even the silent `updateWithoutEvent()` was a **full-row merge of the trigger's stale snapshot**, which raced concurrent writes (line items recalculating the header totals milliseconds after create, a start-step status set) and silently reverted them. The trigger now uses `repository.updateProperty(id, "ProcessId", processId)` — an SDK `JavaRepository`/`JavaEntityStore` HQL mutation touching only the named column (same for a minted `businessKeyStrategy` field); no audit stamping, no events, nothing else to clobber. `updateProperty` is the sanctioned workflow/system write-back primitive — reserve it for system columns; user data keeps going through the generated repository's normal write path. The trigger guard is `ProcessId != null && !isBlank()` (an empty string from a form must not count as "already started").
- **Documents/CMS path contract (`/services/js/documents/api/documents.js`).** List the **root with NO `?path=`** (a `?path=/` 400s "null has no such function getName"); navigate by path. The CMS query layer does **not** decode an encoded slash, so build `?path=` with **literal slashes** — encode each segment, not the whole path (`p.split('/').map(encodeURIComponent).join('/')`); a blanket `encodeURIComponent` turns `/`→`%2F` and every subfolder/file 400s. Rename is `PUT {path,name}`; delete is `DELETE` with a JSON body of absolute paths; preview/download are `GET /preview|/download?path=`. Matches the dashboard `documents/js/documents.js`.
- **Edit forms: match the value to the input shape (dates + comboboxes).** The Java REST controller serializes `java.time` via Jackson as **arrays** (`LocalDate` → `[y,m,d]`, `LocalDateTime` → `[y,m,d,h,mi,s,ns]`) and `Instant` as a **numeric epoch** — NOT ISO strings — so the form's `toDateInput()` handles arrays/numbers/strings (a naive `String(v).slice()` yields garbage → empty date controls on edit). And a relationship FK comes back as a **number** while an option's `data-value` is a string (HTML attribute), so the value must be **stringified on load** (`form.X = String(record.X)`) or the `x-h-select` matches no option and renders empty on edit — the same `String(...)` the codbex-athena-app edit pages use.
- **Combobox = the Harmonia `x-h-select` contract; mirror codbex-athena-app's forms, not the SKILL's toy example.** `x-model` on `x-h-select-input` holds the VALUE (the option `data-value`); the input displays the matched option's label. Use `data-filter="contains"` + an in-dropdown `x-h-select-search` for searchable pickers, options via `x-for` with `:data-value="String(opt.value)"`. **Do NOT add `x-h-select-clear`** — athena's forms omit it (it appears only in the SKILL's standalone example) and it pushes the selected value ~half its height below the field frame. The component reference is `template-application-ui-harmonia-java/reference/harmonia/SKILL.md`.
- **Combobox = the Harmonia `x-h-select` contract; mirror codbex-athena-app's forms, not the SKILL's toy example.** `x-model` on `x-h-select-input` holds the VALUE (the option `data-value`); the input displays the matched option's label. Use `data-filter="contains"` + an in-dropdown `x-h-select-search` for searchable pickers, options via `x-for` with `:data-value="String(opt.value)"`. **Do NOT add `x-h-select-clear`** — athena's forms omit it (it appears only in the SKILL's standalone example) and it pushes the selected value ~half its height below the field frame. The component reference is the upstream Harmonia skill (<https://github.com/codbex/harmonia/blob/main/skills/harmonia/SKILL.md>).
- **Theme goes through Harmonia's own colour-scheme API — do not hand-toggle `.dark`.** `harmonia.min.js` manages the `.dark` class itself from `window.Harmonia.get/setColorScheme('light'|'dark'|'auto')`, persisted to localStorage key `codbex.harmonia.colorMode`; its default is `auto` (follow the OS). The shell's top-right switch (`appShell.js toggleTheme()`) calls `Harmonia.setColorScheme`, and a head bootstrap defaults the key to `light` on first run. Because **every** page loads harmonia.min.js and reads the same key, the shell and the standalone form/report iframes all theme consistently — this is why the task form no longer renders dark on a light shell (an earlier custom `dirigible.harmonia.theme` key + manual `.dark` toggle only fixed the shell, not the iframes). Settings is pinned to the **sidebar footer** (bottom-left, like codbex-athena-app); Reports is a single entry **discovered at runtime** (a `reports` store walks the project registry tree for `*/reports/*/index.html`, since intent reports are standalone pages in a separate gen folder the shell can't see at generation time). The intent recipe defaults (`IntentSettings.scaffold`) point model/form/report at the Harmonia templates; glue stays the neutral client-Java template.

- **Velocity vs Alpine `$` is the #1 trap in the Harmonia shell templates.** The shell's `index.html.template` and `dashboardPage.js.template` are **generated via Velocity** (they use `$models`/`$entity`/`$appIcon`), and Velocity parses `$`-references **everywhere — including inside HTML comments and JS string bodies**. An Alpine magic like `$store`/`$notifications`/`$nextTick` is fine as a *bare reference* (undefined → Velocity passes it through literally), but the moment it's followed by a **method call with args or trailing statements** Velocity tries to parse it and the generation 500s with `Encountered "..."`. Three real burns: `$store.processTasks.openTask(n.task); n.unread=false` in an `@click` (fixed by moving the logic to an `appShell.openNotification(n)` method and calling that), `$notifications.add({ template:'toast', data:{ message, variant } })` **in an HTML comment** (Velocity choked on `{ message,…}` — reworded the comment), and `$store...filter(r => r.dashboard !== false)` in a getter (escaped via `#set($dollar='$')` + `${dollar}store`, the same trick `report.js.template` uses for `$limit`/`$nextTick`). Rule: in a Velocity-generated file keep `$store.x` to bare refs / no-arg calls, escape with `${dollar}` for anything richer, or move it into a copied (non-Velocity) JS file / an appShell method.
Expand Down
4 changes: 3 additions & 1 deletion HARMONIA_2_MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ phases land.
Harmonia were fixed: `upload` -> built-in `import`; `inbox`/`eye` -> Lucide.
(`x-h-info-page-media.icon` still exists in 2.x - left as is.)
3. Refreshed the vendored `template-application-ui-harmonia-java/reference/harmonia/`
skill from the 2.1.0-shipped `skills/harmonia/`.
skill from the 2.1.0-shipped `skills/harmonia/`. (Since removed - the skill is now
consulted upstream at <https://github.com/codbex/harmonia/blob/main/skills/harmonia/SKILL.md>
/ <https://codbex.com/harmonia/> to avoid drift.)
4. Synced version strings in `CLAUDE.md` and `HARMONIA_RUNTIME_PLAN.md`
(`data-size` split note re-verified - still accurate in 2.1.0).
5. Also fixed a pre-existing bug found during runtime verification: the
Expand Down
2 changes: 1 addition & 1 deletion components/engine/engine-intent/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ seeds:
- { id: 2, name: Albania, code2: AL, code3: ALB, numeric: "008" }
```

Logical field types (`FieldIntent.type`) are: `string`, `text`, `integer`, `int`, `long`, `decimal`, `double`, `boolean`, `date`, `timestamp`, `uuid`. Generators map them to JDBC + EDM types. **Primary keys must be an integer type (`integer`/`int`/`long`)** - the Dirigible model convention is integer auto-increment identifiers, and a non-integer auto-increment column is invalid SQL (a `uuid`/`VARCHAR` PK produced `AUTO_INCREMENT` on a `VARCHAR(36)` column, which H2 rejects); the parser enforces this and the EDM generator only emits `dataAutoIncrement` for integer columns. `uuid` remains valid for non-PK fields (maps to `VARCHAR(36)`). Relation kinds: `oneToMany`, `manyToOne`, `oneToOne`, `manyToMany`. Step kinds: `userTask`, `serviceTask`, `decision`, `script`, `end`.
Logical field types (`FieldIntent.type`) are: `string`, `text`, `integer`, `int`, `long`, `decimal`, `double`, `boolean`, `date`, `timestamp`, `uuid`, `month`, `week`. Generators map them to JDBC + EDM types. `month` (`YYYY-MM`) and `week` (`YYYY-Www`) are stored as VARCHAR strings and get the `MONTH`/`WEEK` widget types (the Harmonia month/week pickers, `x-h-month-picker`/`x-h-week-picker`); their widget is chosen from the logical type, not the JDBC dataType, since both are VARCHAR. **Primary keys must be an integer type (`integer`/`int`/`long`)** - the Dirigible model convention is integer auto-increment identifiers, and a non-integer auto-increment column is invalid SQL (a `uuid`/`VARCHAR` PK produced `AUTO_INCREMENT` on a `VARCHAR(36)` column, which H2 rejects); the parser enforces this and the EDM generator only emits `dataAutoIncrement` for integer columns. `uuid` remains valid for non-PK fields (maps to `VARCHAR(36)`). Relation kinds: `oneToMany`, `manyToOne`, `oneToOne`, `manyToMany`. Step kinds: `userTask`, `serviceTask`, `decision`, `script`, `end`.

Semantics worth knowing:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -899,7 +899,7 @@ private static Map<String, Object> propertyMap(String entityName, FieldIntent fi
}
p.put("auditType", "NONE");
// Document role: the number/title field renders in the document form's title, not as an input.
p.put("widgetType", field.isDocumentTitle() ? "DOCUMENT_NUMBER" : widgetForType(dataType));
p.put("widgetType", field.isDocumentTitle() ? "DOCUMENT_NUMBER" : widgetForField(field, dataType));
p.put("widgetSize", field.getSize() == null ? ""
: field.getSize()
.toString());
Expand Down Expand Up @@ -1568,13 +1568,26 @@ private static String labelFieldName(EntityIntent target) {
return keyFieldName(target);
}

/** Declared length, with type-derived defaults ({@code uuid} -> 36). */
/**
* Declared length, with type-derived defaults ({@code uuid} -> 36, {@code month} -> 7, {@code week}
* -> 8).
*/
private static Integer fieldLength(FieldIntent field) {
if (field.getLength() != null) {
return field.getLength();
}
if (field.getType() != null && "uuid".equalsIgnoreCase(field.getType())) {
return 36;
if (field.getType() != null) {
switch (field.getType()
.toLowerCase(Locale.ROOT)) {
case "uuid":
return 36;
case "month": // YYYY-MM
return 7;
case "week": // YYYY-Www
return 8;
default:
break;
}
}
return defaultLength(mapDataType(field.getType()));
}
Expand Down Expand Up @@ -1602,11 +1615,32 @@ private static String mapDataType(String type) {
return "CLOB";
case "uuid":
case "string":
case "month": // stored as the picker's YYYY-MM string
case "week": // stored as the picker's YYYY-Www ISO-week string
default:
return "VARCHAR";
}
}

/**
* Widget for a field. {@code month}/{@code week} are both stored as {@code VARCHAR}, so they are
* indistinguishable at the JDBC-type level - the picker widget is chosen from the logical type (the
* same reason {@code documentTitle} is special-cased at the call site).
*/
private static String widgetForField(FieldIntent field, String dataType) {
String type = field.getType() == null ? ""
: field.getType()
.toLowerCase(Locale.ROOT);
switch (type) {
case "month":
return "MONTH";
case "week":
return "WEEK";
default:
return widgetForType(dataType);
}
}

private static String widgetForType(String dataType) {
switch (dataType) {
case "INTEGER":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,10 @@ private static Control pickControl(FieldIntent field) {
return new Control("input-date", "date");
case "timestamp":
return new Control("input-datetime-local", "datetime-local");
case "month":
return new Control("input-month", "month");
case "week":
return new Control("input-week", "week");
case "uuid":
case "string":
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@

/**
* Single attribute on an {@link EntityIntent}. {@link #type} carries a logical type string
* ({@code string}, {@code integer}, {@code decimal}, {@code boolean}, {@code date}, {@code uuid},
* {@code text}) that the entity generator maps to JDBC and EDM types.
* ({@code string}, {@code integer}, {@code decimal}, {@code boolean}, {@code date},
* {@code timestamp}, {@code uuid}, {@code text}, {@code month}, {@code week}) that the entity
* generator maps to JDBC and EDM types. {@code month} ({@code YYYY-MM}) and {@code week}
* ({@code YYYY-Www}) are stored as strings and render as the Harmonia month / week pickers.
*/
public class FieldIntent {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@
*/
public final class IntentParser {

private static final Set<String> FIELD_TYPES =
Set.of("string", "text", "integer", "int", "long", "decimal", "double", "boolean", "date", "timestamp", "uuid");
private static final Set<String> FIELD_TYPES = Set.of("string", "text", "integer", "int", "long", "decimal", "double", "boolean",
"date", "timestamp", "uuid", "month", "week");
/**
* Primary keys must be an integer type - the Dirigible model convention is integer identifiers
* (auto-increment), and a non-integer auto-increment column is invalid SQL on most databases.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1179,7 +1179,7 @@ payment's unallocated balance; entity writes go only through the generated repos

| Where | Allowed |
|---|---|
| field `type` | `string`, `text`, `integer`, `int`, `long`, `decimal`, `double`, `boolean`, `date`, `timestamp`, `uuid` |
| field `type` | `string`, `text`, `integer`, `int`, `long`, `decimal`, `double`, `boolean`, `date`, `timestamp`, `uuid`, `month` (a `YYYY-MM` string, month picker), `week` (a `YYYY-Www` ISO-week string, week picker) |
| primary-key `type` | `integer`, `int`, `long` (integer only) |
| relation `kind` | `oneToMany`, `manyToOne`, `oneToOne`, `manyToMany` |
| step `kind` | `userTask`, `serviceTask`, `decision`, `script`, `end` |
Expand Down
Loading
Loading