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
2 changes: 1 addition & 1 deletion components/engine/engine-intent/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ Semantics worth knowing:
- **`ageing(<date field>, [30, 60, 90])` dimension = the receivables-ageing bucket column (#6357).** Buckets rows by how long ago a date fell, so the standard ageing family (`0-30` / `31-60` / `61-90` / `90+`) is a report definition instead of hand SQL; the field may be an own `date`/`timestamp` or a one-hop `relation.field`. **Emitted as a `CASE` over DATE BOUNDARIES** (`field > CURRENT_DATE - INTERVAL 'n' DAY`), deliberately NOT as the day-count arithmetic the issue sketched: `CURRENT_DATE - field` yields an **integer on PostgreSQL but an INTERVAL on H2** (verified), so comparing it to a number is not portable - and the `.report` `query` is a static string baked at generate time with no dialect to switch on. The interval form is standard SQL and was executed against H2 to confirm the emitted shape (including `GROUP BY` over the CASE) runs and buckets correctly. A **null date buckets as `n/a`**, never into the oldest bucket, which would misreport it as maximally overdue. Parser: thresholds must be ascending positive day counts and the field must be temporal (a non-temporal column would otherwise fail at query time instead of authoring time). **Caveat:** the bucket is a text label, and the query builder emits no `ORDER BY`, so buckets sort lexicographically - fine for equal-width thresholds (`[30,60,90]`), wrong for mixed widths (`[7,30,120]` sorts `0-7`, `120+`, `31-120`, `8-30`). Prefer equal-digit thresholds until an explicit bucket ordering lands.
- **`reports[].widget` = a dashboard KPI tile backed by the report.** The report supplies the data (source/dimensions/measures/filter → the generated SQL + controller); the widget only says which number the tile shows: `kind: count` (default — the report's record count via the controller's count endpoint), `kind: value` (`value:` names a declared measure; `at: { <dimension>: now | <literal> }` pins dimension columns as typed EQ conditions over the report output — the `now` token stays symbolic in the `.report` and is resolved client-side, type-aware: `month(x)` → current YYYYMM, `year(x)` → current year, date → today), or `kind: list` (`limit:` rows, default 5, rendered as a mini table from the report's own column metadata). `IntentParser.validateReportWidget` checks kind/value-measure/at-dimensions; `ReportIntentGenerator.widget(...)` resolves authored expressions to column aliases and emits the `widget` block on the `.report` (no SQL, no URLs — path-agnostic rule intact). At runtime the shared reports store (`application-core/shell/js/stores/reports.js`) reads the block off the `.report`, derives the report controller URL from the discovered page path (a `sanitizeJavaIdentifier` mirror — keep it in sync with `parameterUtils.js`), resolves the pins and fetches count/value/rows; a 403 hides the tile (role-guarded report) instead of erroring. A widget-bearing report shows the KPI tile INSTEAD of its iframe preview tile; `dashboard: false` hides both.
- **Top-level `widgets:` = custom dashboard widgets (the dashboard's escape hatch).** `kind: kpi` (default) is a number tile fed by a developer REST endpoint (`url` returns `{value, description?}` — typically a client-Java `@Controller` under `custom/`); `kind: page` embeds the developer's HTML page like a report preview tile. The kind implies how the URL is consumed — there is deliberately no separate source-type field. The parser (`validateWidgets`) checks name/kind and that `url` is a same-origin path (no scheme/host); `EdmIntentGenerator.buildCustomWidgets` bakes them onto the `.model` root (`widgets` array with defaults + `tId`), the model's translate action emits their labels into the catalog, and the Harmonia `dashboardPage.js.template` bakes and renders them (kpi tiles fetch via the shared client with `{ baseUrl: '' }`; page tiles iframe). Prefer `reports[].widget` when a report can supply the number; the value the endpoint returns may be a string (`"99.9%"`), rendered as-is. The `.report` widget block is also **authorable by hand in the Web IDE's Report Editor** (`editor-report`: a "Dashboard Widget" panel — enable, kind, label/icon, value-measure picker over the aggregate columns, `at` pins over the grouping columns, list limit — plus Description + "Show on the home dashboard" in General), so classic non-intent projects get KPI tiles too.
- **`view: calendar` / `view: range` = an ADDITIONAL page, never a replacement (#6547) — and on a document's line-items child it is the items PANE (#6482).** The calendar used to be emitted as `layoutType: MANAGE_CALENDAR`, which *replaced* whatever layout the entity had resolved: a `function: Document` master browsed on a calendar silently lost its whole document surface (line items, Print, inline process tasks — approvals fell back to the Inbox), so authors had to choose between the two. `EdmIntentGenerator` now emits the entity attribute **`calendarView="true"`** and leaves `layoutType` at the natural MANAGE / MANAGE_MASTER / MANAGE_DOCUMENT, so every page that layout generates still exists; the calendar rides alongside as an extra page. Routing keeps the landing route where it always was (no behaviour change for existing calendar apps) and moves the layout's own browse page down one segment: `/<Entity>` = the calendar, **`/<Entity>/list`** = the layout's list / master / document list, `/create` + `/:id/edit` + `/:id/preview` = the layout's own editor — which is the whole fix, since those now resolve to the document page for a document master. Both browse pages carry a switch to the other (`goCalendar()` / `goList()`); same on the personal surface (`/my/<Entity>` calendar, `/my/<Entity>/list` list, so `personalListModels` no longer excludes calendar roots). `MANAGE_CALENDAR` is gone from every consumer (`uiCalendarModels` / `personalCalendarModels` key on `calendarView`; the shell template, the AppTest manifest's layout token and `calendar.js` follow) — it was intent-only, never offered by the entity editor, so nothing hand-authored can still carry it. `calendar.js` no longer emits the shared manage form either: the layout owns it. **`view: slots` deliberately still replaces the layout** — a slot picker is an authoring surface (pick a free slot → create), not a second way to browse the same records. **The line-items case:** a document's items child declaring `view: calendar` used to emit its panel markup and then be filtered out of `secondaryDetails` by name (the items child has its own section), so the declaration produced nothing at all — the authored-but-unconsumed failure mode, green at every step. The master now carries **`documentItemsLayout: "calendar"`** (derived from the child, never authored on the master — the `calendar:` config belongs to the child) and the items pane renders as an `x-h-calendar` on all three document surfaces (power / personal / partner): the same rows and the same line dialog, event-click edits, empty-day click adds with the date preset, and Delete moves into the dialog (a calendar has no per-row menu). It is mutually exclusive with `documentItemsLayout: chat` (both claim that pane) — parser-rejected. The event mapping is the shared `application-core/shell/js/services/calendarEvents.js` (`window.HarmoniaCalendar`), and the calendar's configuration is read at RUNTIME from the child's detail registration, so the document page still never enumerates the child at generation time.
- **Every `view:` = an ADDITIONAL page, never a replacement (#6547) — and on a document's line-items child a calendar is the items PANE (#6482).** The calendar used to be emitted as `layoutType: MANAGE_CALENDAR`, which *replaced* whatever layout the entity had resolved: a `function: Document` master browsed on a calendar silently lost its whole document surface (line items, Print, inline process tasks — approvals fell back to the Inbox), so authors had to choose between the two. `EdmIntentGenerator` now emits the entity attribute **`calendarView="true"`** and leaves `layoutType` at the natural MANAGE / MANAGE_MASTER / MANAGE_DOCUMENT, so every page that layout generates still exists; the calendar rides alongside as an extra page. Routing keeps the landing route where it always was (no behaviour change for existing calendar apps) and moves the layout's own browse page down one segment: `/<Entity>` = the calendar, **`/<Entity>/list`** = the layout's list / master / document list, `/create` + `/:id/edit` + `/:id/preview` = the layout's own editor — which is the whole fix, since those now resolve to the document page for a document master. Both browse pages carry a switch to the other (`goCalendar()` / `goList()`); same on the personal surface (`/my/<Entity>` calendar, `/my/<Entity>/list` list, so `personalListModels` no longer excludes calendar roots). `MANAGE_CALENDAR` is gone from every consumer (`uiCalendarModels` / `personalCalendarModels` key on `calendarView`; the shell template, the AppTest manifest's layout token and `calendar.js` follow) — it was intent-only, never offered by the entity editor, so nothing hand-authored can still carry it. `calendar.js` no longer emits the shared manage form either: the layout owns it. **`view: slots` got the same treatment in the same PR** (`slotsView="true"`, `uiSlotsModels` keyed on it, `slots.js` no longer emitting the shared form, a `Slots`/`List` toggle pair): the picker is how a booking is CREATED — slot-click opens the LAYOUT's create route prefilled with the datetime, so a booking document is created as a document — and the list/document page is how it is worked with afterwards. An author needs both, so nothing about a view replaces a layout any more. **One consequence to keep in mind:** because a calendar/slots entity is now also a member of a LAYOUT collection, `navigation.js`'s `PERSPECTIVE_COLLECTIONS` must NOT list `uiCalendarModels`/`uiSlotsModels` — it did at first and emitted that entity's perspective twice (same rename path generated twice, duplicate `application-perspectives` contribution). **The line-items case:** a document's items child declaring `view: calendar` used to emit its panel markup and then be filtered out of `secondaryDetails` by name (the items child has its own section), so the declaration produced nothing at all — the authored-but-unconsumed failure mode, green at every step. The master now carries **`documentItemsLayout: "calendar"`** (derived from the child, never authored on the master — the `calendar:` config belongs to the child) and the items pane renders as an `x-h-calendar` on all three document surfaces (power / personal / partner): the same rows and the same line dialog, event-click edits, empty-day click adds with the date preset, and Delete moves into the dialog (a calendar has no per-row menu). It is mutually exclusive with `documentItemsLayout: chat` (both claim that pane) — parser-rejected. The event mapping is the shared `application-core/shell/js/services/calendarEvents.js` (`window.HarmoniaCalendar`), and the calendar's configuration is read at RUNTIME from the child's detail registration, so the document page still never enumerates the child at generation time.
- **The YAML `name:` field is the intent's identity for outputs.** `IntentNaming.baseName` prefers it over the artefact name derived from the file name (which is conventionally just `app` from `app.intent`); single-file outputs are `<name>.edm` / `<name>.model` / `<name>.roles` and the table prefix is its upper-snake.
- **Physical table names are intent-prefixed**: `<INTENT>_<ENTITY>` upper-snake (`ORDERS_ORDER`), via `IntentNaming.tableName`, consistently across `.edm` `dataName`, `.report` `table` and `.csvim` `table`. This avoids SQL reserved words (`ORDER`, `USER`, ...) and cross-project collisions in a shared schema. If the downstream "Generate from EDM" wizard asks for a table prefix, intent projects must leave it empty - the prefix is already part of `dataName`.

Expand Down
17 changes: 9 additions & 8 deletions components/engine/engine-intent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,12 +230,12 @@ included. For logic beyond an expression, a hand-written `CalculatedField` compo
slots: { start: startTime }
```

`view: calendar` / `view: range` on a **top-level entity** adds a calendar **alongside** that entity's
own layout - it never replaces it. The calendar becomes the entity's landing browse page
(`/<Entity>`), the layout's own browse page moves to `/<Entity>/list`, and both carry a switch to the
other; create / edit / preview stay the layout's own, so a **document master browsed on a calendar
still edits on its document page** (line items, Print, inline process tasks included). Same on the
personal surface: `/my/<Entity>` is the calendar, `/my/<Entity>/list` the list.
Every `view:` on a **top-level entity** adds a page **alongside** that entity's own layout - it never
replaces it. The view becomes the entity's landing browse page (`/<Entity>`), the layout's own browse
page moves to `/<Entity>/list`, and both carry a switch to the other; create / edit / preview stay the
layout's own, so a **document master browsed on a calendar (or booked on a slot picker) still edits on
its document page** (line items, Print, inline process tasks included). Same on the personal surface:
`/my/<Entity>` is the calendar, `/my/<Entity>/list` the list.

`view: calendar` on a **composition child** renders an **embedded calendar panel inside its master's
page** (and the master's edit form) instead of the detail table: the same master-filtered rows become
Expand All @@ -244,8 +244,9 @@ clicked date preset. The child keeps everything a detail has (registry, filtered
pages) - the calendar is just how its panel renders. When that child is the document's **line-items**
entity, the document's items **pane** is the calendar (see below); `range` works the same way.

`view: slots` is the exception that still REPLACES the layout: a slot picker is an authoring surface
(pick a free slot → create), not a second way to browse the same records.
`view: slots` works the same way: the picker is how a booking is CREATED (pick a free slot → the
layout's create page, prefilled with the chosen datetime), and the list / document page is how it is
worked with afterwards - an author needs both, so the picker is additional too.

## generate children - collection-driven scheduled generation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ private static Map<String, Object> entityManifest(EntityIntent entity, Map<Strin
out.put("name", name);
out.put("label", stringOr(edm.get("entityLabel"), IntentNaming.humanize(name)));
out.put("labelPlural", stringOr(edm.get("menuLabel"), IntentNaming.pluralize(IntentNaming.humanize(name))));
out.put("layout", layout(string(edm.get("layoutType")), "true".equals(string(edm.get("calendarView")))));
out.put("layout", layout(string(edm.get("layoutType")), "true".equals(string(edm.get("calendarView"))),
"true".equals(string(edm.get("slotsView")))));
out.put("route", "#/" + name);
out.put("navGroup", string(edm.get("perspectiveNavId")));
out.put("api", "/" + sanitizeJavaIdentifier(string(edm.get("perspectiveName"))) + "/" + name + "Controller");
Expand Down Expand Up @@ -501,18 +502,20 @@ private static String idProperty(Map<String, Map<String, Object>> edmEntities) {
}

/**
* The runner's layout token from the EDM layout type. A calendar view keeps the entity's layout
* intact but takes over its landing route (the layout's list moves to {@code /<Entity>/list}), so
* the token the runner drives at {@code #/<Entity>} is the calendar - it must not expect
* columns/rows there. Same for the slot picker, which does replace the layout.
* The runner's layout token from the EDM layout type. A calendar or slots view keeps the entity's
* layout intact but takes over its landing route (the layout's list moves to
* {@code /<Entity>/list}), so the token the runner drives at {@code #/<Entity>} is that view - it
* must not expect columns/rows there.
*/
private static String layout(String layoutType, boolean calendarView) {
private static String layout(String layoutType, boolean calendarView, boolean slotsView) {
if (calendarView) {
return "calendar";
}
if (slotsView) {
return "slots";
}
return switch (layoutType == null ? "" : layoutType) {
case "MANAGE_DOCUMENT" -> "document";
case "MANAGE_SLOTS" -> "slots";
default -> "manage-list";
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,12 +287,14 @@ private static EdmDocument buildDocument(IntentGenerationContext context, Intent
}
// A slots entity renders as a Harmonia x-h-slot-picker for appointment/booking: free time
// slots are bookable and open the create form prefilled with the chosen datetime. Reuses the
// generated controller (existing records mark their slots taken) and the shared form. Unlike
// the calendar above, the slot picker still REPLACES the layout - it is an authoring surface
// (pick a free slot -> create), not a second way to browse the same records.
// generated controller (existing records mark their slots taken) and the shared form. Like the
// calendar above it is an ADDITIONAL page - the entity keeps its own layout and every page
// that brings, the picker takes over the landing route, and the layout's browse page moves to
// /<Entity>/list. A picker is how you CREATE a booking; the document/list is how you work
// with one afterwards, and an author needs both.
if (entity.isSlots()) {
SlotsIntent slotsCfg = entity.getSlots();
entityMap.put("layoutType", "MANAGE_SLOTS");
entityMap.put("slotsView", "true");
if (slotsCfg != null) {
if (notBlank(slotsCfg.getStart())) {
entityMap.put("slotStartProperty", IntentNaming.pascalCase(slotsCfg.getStart()));
Expand Down Expand Up @@ -323,8 +325,9 @@ private static EdmDocument buildDocument(IntentGenerationContext context, Intent
}
// A document master keeps its own perspective/nav but swaps the master-detail layout for the
// document layout; it names its line-items entity so the document page renders that child as
// the inline table (and any other composition children as ordinary detail panels).
else if (documentItems.containsKey(name)) {
// the inline table (and any other composition children as ordinary detail panels). Resolved
// independently of the calendar / slots views above, which are additional pages now.
if (documentItems.containsKey(name)) {
String itemsEntity = documentItems.get(name);
entityMap.put("layoutType", "MANAGE_DOCUMENT");
entityMap.put("documentItemsEntity", itemsEntity);
Expand Down Expand Up @@ -375,10 +378,10 @@ else if (!extension && !dependent && !setting && !compositionParents.containsVal
}
// A document master (owns a *Item / DocumentItem composition child) gets a generated .print
// template + feeder, so its edit surface can offer a Print button. Flag it independently of
// the layout: the MANAGE_DOCUMENT layout renders Print in the document view already, but a
// document master whose UI is overridden to a slots view reuses the plain manage form for
// edit - the flag is what lets that shared form show Print too. (A calendar view no longer
// overrides the layout, so a calendar-browsed document edits on the document page.)
// the layout - the MANAGE_DOCUMENT layout renders Print in the document view, and the flag is
// what lets the shared manage form show Print too for a master whose edit surface is that
// form. (Neither the calendar nor the slots view overrides the layout any more, so a document
// browsed on either still edits on its document page.)
if (documentItems.containsKey(name)) {
entityMap.put("hasPrint", "true");
}
Expand Down
Loading
Loading