From 149a933a78eb85605c1ca6ad356d3861b9820065 Mon Sep 17 00:00:00 2001 From: delchev Date: Sun, 9 Aug 2026 10:35:30 +0300 Subject: [PATCH 1/2] fix(intent): the slot picker is an additional page too, and a view no longer double-registers its perspective Follow-up to the calendar change, closing the same hole for `view: slots`. `view: slots` was left replacing the layout on the argument that a picker is an authoring surface rather than a second way to browse. That does not survive contact with a booking document: the picker is how a booking is CREATED, the document page is how it is worked with afterwards, and an author needs both - exactly the choice #6547 was about. So the picker gets the calendar's treatment: `EdmIntentGenerator` emits `slotsView` instead of `layoutType: MANAGE_SLOTS`, `uiSlotsModels` keys on it, `slots.js` stops emitting the shared manage form (the layout owns the editor, so slot-click on a document master now lands on the DOCUMENT create page prefilled with the chosen datetime), the picker takes over / while the layout browses at //list, and the two carry a Slots/List switch. `MANAGE_SLOTS` is gone from every consumer; like MANAGE_CALENDAR it was intent-only and never offered by the entity editor. It also fixes a defect the calendar commit introduced, which extending the rule to slots made obvious: `navigation.js`'s PERSPECTIVE_COLLECTIONS listed `uiCalendarModels`/`uiSlotsModels` alongside the LAYOUT collections. Now that a view entity keeps its layout it is a member of both, so its perspective was generated TWICE - the same rename path written twice and a duplicate application-perspectives contribution. The two view collections are dropped from that list; a view entity is always in exactly one layout collection. Covered by EdmIntentGeneratorTest (a slots document master keeps MANAGE_DOCUMENT and carries slotsView + the slot* metadata) and the emission oracle, whose fixture gained a slots document: the /Visit picker landing route, /Visit/list, /Visit/create resolving to the DOCUMENT editor, and both toggles. Co-Authored-By: Claude Opus 5 --- components/engine/engine-intent/CLAUDE.md | 2 +- components/engine/engine-intent/README.md | 17 +++++---- .../apptest/AppTestIntentGenerator.java | 17 +++++---- .../generator/edm/EdmIntentGenerator.java | 23 +++++++----- .../main/resources/intent-assistant-guide.md | 15 ++++---- .../generator/edm/EdmIntentGeneratorTest.java | 33 +++++++++++++++++ .../README.md | 2 +- .../template/ui/navigation.js | 9 ++++- .../template/ui/slots.js | 37 ++++++++----------- .../ui/perspective/list/page.js.template | 5 +++ .../ui/perspective/list/view.html.template | 8 ++++ .../perspective/manage/list-page.js.template | 4 ++ .../manage/list-view.html.template | 8 ++++ .../master/master-page.js.template | 4 ++ .../master/master-view.html.template | 5 +++ .../perspective/slots/slots-page.js.template | 6 ++- .../slots/slots-view.html.template | 6 +++ .../ui/shell/index.html.template | 34 +++++++++-------- .../ui/translations.json.template | 1 + .../template/generateUtils.js | 7 +++- .../tests/api/IntentEmissionCoverageIT.java | 32 ++++++++++++++++ 21 files changed, 197 insertions(+), 78 deletions(-) diff --git a/components/engine/engine-intent/CLAUDE.md b/components/engine/engine-intent/CLAUDE.md index 7a68472dbc0..7e42f7bfdaa 100644 --- a/components/engine/engine-intent/CLAUDE.md +++ b/components/engine/engine-intent/CLAUDE.md @@ -330,7 +330,7 @@ Semantics worth knowing: - **`ageing(, [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: { : now | }` 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: `/` = the calendar, **`//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/` calendar, `/my//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: `/` = the calendar, **`//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/` calendar, `/my//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 `.edm` / `.model` / `.roles` and the table prefix is its upper-snake. - **Physical table names are intent-prefixed**: `_` 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`. diff --git a/components/engine/engine-intent/README.md b/components/engine/engine-intent/README.md index 9785ba59a9b..96382fd43f5 100644 --- a/components/engine/engine-intent/README.md +++ b/components/engine/engine-intent/README.md @@ -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 -(`/`), the layout's own browse page moves to `//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/` is the calendar, `/my//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 (`/`), the layout's own browse +page moves to `//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/` is the calendar, `/my//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 @@ -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 diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/apptest/AppTestIntentGenerator.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/apptest/AppTestIntentGenerator.java index a08659ef1fd..002a3098589 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/apptest/AppTestIntentGenerator.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/apptest/AppTestIntentGenerator.java @@ -145,7 +145,8 @@ private static Map entityManifest(EntityIntent entity, Map> 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 //list}), so - * the token the runner drives at {@code #/} 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 //list}), so the token the runner drives at {@code #/} 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"; }; } diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGenerator.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGenerator.java index 8529074f6de..421d9f765c5 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGenerator.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGenerator.java @@ -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 + // //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())); @@ -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); @@ -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"); } diff --git a/components/engine/engine-intent/src/main/resources/intent-assistant-guide.md b/components/engine/engine-intent/src/main/resources/intent-assistant-guide.md index 7114876fe76..a2cd9bd7645 100644 --- a/components/engine/engine-intent/src/main/resources/intent-assistant-guide.md +++ b/components/engine/engine-intent/src/main/resources/intent-assistant-guide.md @@ -630,14 +630,13 @@ bookings, day allocations, anything keyed by a date. Set `view:` on the entity ( the role alias for `view: calendar`) and add the matching config block. The generated REST controller and form are reused unchanged; only the presentation differs. -**A calendar is an ADDITIONAL page, never a replacement.** `view: calendar` / `view: range` leave the -entity's own layout (list / master / document) fully in place: the calendar becomes the landing browse -page `/`, the layout's own browse page moves to `//list`, both offer a switch to the -other, and create / edit / preview stay the layout's own routes. So `function: Document` + `view: -calendar` is a valid, useful combination - the documents are browsed on a calendar and still edited on -the document page, with their line items, Print and inline process tasks. (`view: slots` is the one -exception: a slot picker is an authoring surface, not a second way to browse, so it does replace the -layout.) +**A view is an ADDITIONAL page, never a replacement.** `view: calendar` / `range` / `slots` leave the +entity's own layout (list / master / document) fully in place: the view becomes the landing browse page +`/`, the layout's own browse page moves to `//list`, both offer a switch to the other, +and create / edit / preview stay the layout's own routes. So `function: Document` + `view: calendar` +(or `view: slots`) is a valid, useful combination - the documents are browsed on a calendar, or booked +from a slot picker, and still edited on the document page with their line items, Print and inline +process tasks. - **`view: calendar`** (or `function: Calendar`) + a `calendar:` block renders the records as events on the Harmonia calendar. **`view: range`** uses the same block for start/end spans. diff --git a/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGeneratorTest.java b/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGeneratorTest.java index 99e52d03f04..8042fe95350 100644 --- a/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGeneratorTest.java +++ b/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGeneratorTest.java @@ -1113,6 +1113,39 @@ void dependentCalendarChildKeepsTheDetailLayoutAndCarriesTheCalendarMeta() { assertEquals(null, primary.get("detailCalendar")); } + @Test + void documentMasterWithASlotsViewKeepsTheDocumentLayoutAndAddsThePicker() { + // #6547, same rule as the calendar: a slot picker is how a booking is CREATED, the document page + // is how it is worked with afterwards - so the picker rides alongside the layout instead of + // replacing it, and slot-click lands on the document editor. + String yaml = """ + name: clinic + entities: + - name: Appointment + function: Document + view: slots + slots: { start: startsAt, open: "09:00", close: "17:00", step: 15 } + fields: + - { name: id, type: integer, primaryKey: true, generated: true } + - { name: startsAt, type: timestamp, required: true } + - name: AppointmentItem + function: DocumentItem + fields: + - { name: id, type: integer, primaryKey: true, generated: true } + - { name: name, type: string, length: 100 } + relations: + - { name: Appointment, kind: manyToOne, to: Appointment, composition: true, required: true } + """; + Map model = EdmIntentGenerator.buildModelJsonForTest(IntentParser.parse(yaml), "clinic"); + Map appointment = entityByName(entities(model), "Appointment"); + assertEquals("MANAGE_DOCUMENT", appointment.get("layoutType"), "view: slots no longer overrides the document layout"); + assertEquals("true", appointment.get("slotsView"), "the picker rides alongside the layout as an additional page"); + assertEquals("StartsAt", appointment.get("slotStartProperty")); + assertEquals("09:00", appointment.get("slotOpen")); + assertEquals("17:00", appointment.get("slotClose")); + assertEquals("15", appointment.get("slotStep")); + } + @SuppressWarnings("unchecked") @Test void documentWhoseItemsChildIsACalendarRendersTheItemsPaneAsACalendar() { diff --git a/components/template/template-application-ui-harmonia-java/README.md b/components/template/template-application-ui-harmonia-java/README.md index 9a7f025d564..a9f0bf7ce6d 100644 --- a/components/template/template-application-ui-harmonia-java/README.md +++ b/components/template/template-application-ui-harmonia-java/README.md @@ -113,7 +113,7 @@ currently 1.8.0); an unknown name renders blank. | **calendar** | `uiCalendarModels` | ✅ a PRIMARY entity with intent `view: calendar` (+ a `calendar:` block: start/end/title/color/initialView) → the entity attribute `calendarView`; full-page `x-h-calendar` (month/week/day/year) whose events are the entity's records positioned by the date/datetime field. **An ADDITIONAL page, not a layout (#6547):** the entity keeps its own `layoutType` (MANAGE / MANAGE_MASTER / MANAGE_DOCUMENT) and every page it brings; the calendar owns the landing route `/`, that layout's browse page moves to `//list`, and both carry a switch to the other. So **date-click → /create** and **event-click → /:id/edit** land on whatever editor the layout owns - a document master browsed on a calendar edits on its document page. Colour keyed categorically by `color`. Optional `calendar.scope: ` scopes the calendar to a parent: when opened as `/?=` it filters events (controller `/search` EQ) and presets that FK on create - so it shows/creates only one parent's records (e.g. day allocations of one timesheet). | | **range** | `uiCalendarModels` | ✅ `view: range` - a span entity (`calendar.start` + `calendar.end`) on the same calendar renderer; events render as all-day multi-day bars (`calendarRange`). For vacation/booking spans. | | **document items on a calendar** | (within document) | ✅ the document's **line-items child** declaring `view: calendar` makes the items PANE an `x-h-calendar` instead of the row grid (`documentItemsLayout: calendar`, #6482) - for a day-grained line (booked days, allocated hours). Same rows, same line dialog: event-click edits, empty-day click adds with the date preset, Delete moves into the dialog. Rendered on the power, personal and partner document surfaces; the calendar's configuration is read at runtime from the child's detail registration, never baked in. Mutually exclusive with `documentItemsLayout: chat`. | -| **slots** | `uiSlotsModels` | ✅ `view: slots` (+ a `slots:` block: start/open/close/step/disabledDays) → `layoutType MANAGE_SLOTS`; a Harmonia `x-h-slot-picker` (3-day time-slot grid). Free slots are bookable; already-booked datetimes are crossed off (built from the entity's own records); slot-click opens the shared create form prefilled with the chosen datetime (`?=`). For appointment booking. | +| **slots** | `uiSlotsModels` | ✅ `view: slots` (+ a `slots:` block: start/open/close/step/disabledDays) → the entity attribute `slotsView`; a Harmonia `x-h-slot-picker` (3-day time-slot grid). Free slots are bookable; already-booked datetimes are crossed off (built from the entity's own records); slot-click opens **the layout's own create route** prefilled with the chosen datetime (`?=`), so a booking document is created as a document. **An ADDITIONAL page like the calendar (#6547):** the picker owns the landing route `/`, the layout browses at `//list`, and both carry a switch to the other. For appointment booking. | | main-details | (within master) | ⬜ stub | | setting | `uiSettingModels` | ✅ reuses the manage CRUD templates, grouped under a "Settings" sidebar section | | report / report-chart / report-table | `uiReportTableModels` / `uiReportChartModels` | ✅ in-SPA table page (data table + CSV export) + chart page (native Harmonia `x-h-chart-*` bar/line/pie/doughnut/polar-area/radar - no external chart library) against the Java report controller | diff --git a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/template/ui/navigation.js b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/template/ui/navigation.js index d734af52224..b90f34954c0 100644 --- a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/template/ui/navigation.js +++ b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/template/ui/navigation.js @@ -19,14 +19,19 @@ * One perspective per top-level entity across every primary layout plus settings, so reference data * (settings) can sit as first-class grouped entries (e.g. UoM/Country/Currency under "Master Data"). */ +/* + * One entry per LAYOUT collection - deliberately NOT uiCalendarModels / uiSlotsModels. Those two are + * additional-page collections: since #6547 a calendar / slots entity keeps its own layout, so it is + * already a member of exactly one of the collections below. Listing them here as well emitted that + * entity's perspective TWICE (the same rename path generated twice, and a duplicate contribution to + * the application-perspectives extension point). + */ const PERSPECTIVE_COLLECTIONS = [ "uiListModels", "uiManageModels", "uiManageMasterModels", "uiListMasterModels", "uiDocumentModels", - "uiCalendarModels", - "uiSlotsModels", "uiSettingModels" ]; diff --git a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/template/ui/slots.js b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/template/ui/slots.js index 55553bbd681..0a1d4ac7ef9 100644 --- a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/template/ui/slots.js +++ b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/template/ui/slots.js @@ -7,17 +7,24 @@ /* * Slots view type — Harmonia SPA variant. * - * A PRIMARY entity declared with `view: slots` is emitted by EdmIntentGenerator with layoutType - * MANAGE_SLOTS plus the slot* metadata (slotStartProperty / slotOpen / slotClose / slotStep / - * slotDisabledDays). It renders as a Harmonia x-h-slot-picker (a 3-day grid of selectable time slots): - * free slots are bookable, already-booked slots (from the entity's own records) are shown taken, and - * picking a slot opens the shared create form prefilled with the chosen datetime (?=…). + * A PRIMARY entity declared with `view: slots` is emitted by EdmIntentGenerator with slotsView="true" + * plus the slot* metadata (slotStartProperty / slotOpen / slotClose / slotStep / slotDisabledDays). It + * renders as a Harmonia x-h-slot-picker (a 3-day grid of selectable time slots): free slots are + * bookable, already-booked slots (from the entity's own records) are shown taken, and picking a slot + * opens the create form prefilled with the chosen datetime (?=…). + * + * The picker is an ADDITIONAL page, not a replacement (#6547). The entity keeps its own layout + * (MANAGE / MANAGE_MASTER / MANAGE_DOCUMENT) and every page that layout generates - so this descriptor + * emits ONLY the picker, and the create/edit surface it opens is whatever that layout owns. A picker is + * how a booking is CREATED; the list or document page is how it is worked with afterwards, and an + * author needs both. The picker takes over the landing route: * * / -> the slot-picker page - * //create -> the shared form (create; opened on slot-click) - * //{id}/edit -> the shared form (edit) + * //list -> the layout's own browse page (list / master / document list) + * //create -> the layout's editor (create; opened on slot-click) + * //{id}/edit -> the layout's editor (edit) * - * Collection: uiSlotsModels = layoutType === "MANAGE_SLOTS" && type === "PRIMARY". + * Collection: uiSlotsModels = slotsView === "true" && type === "PRIMARY". */ export function getSources(parameters) { const collection = "uiSlotsModels"; @@ -35,20 +42,6 @@ export function getSources(parameters) { engine: "velocity", rename: "gen/{{genFolderName}}/views/{{perspectiveName}}/{{name}}-slots.html", collection - }, - { - location: "/template-application-ui-harmonia-java/ui/perspective/manage/form-page.js.template", - action: "generate", - engine: "velocity", - rename: "gen/{{genFolderName}}/js/components/pages/{{perspectiveName}}/{{name}}FormPage.js", - collection - }, - { - location: "/template-application-ui-harmonia-java/ui/perspective/manage/form-view.html.template", - action: "generate", - engine: "velocity", - rename: "gen/{{genFolderName}}/views/{{perspectiveName}}/{{name}}-form.html", - collection } ]; }; diff --git a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/list/page.js.template b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/list/page.js.template index b3a9bfeaba2..e214f115b43 100644 --- a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/list/page.js.template +++ b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/list/page.js.template @@ -191,6 +191,11 @@ document.addEventListener('alpine:init', () => { // Switch to the additional calendar page (intent `view: calendar|range`), which owns /${name}. goCalendar() { window.PineconeRouter.navigate('/${name}'); }, +#end +#if($slotsView) + + // Switch to the additional slot-picker page (intent `view: slots`), which owns /${name}. + goSlots() { window.PineconeRouter.navigate('/${name}'); }, #end })); }, { once: true }); diff --git a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/list/view.html.template b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/list/view.html.template index af574b41981..585a329504f 100644 --- a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/list/view.html.template +++ b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/list/view.html.template @@ -21,6 +21,14 @@ +#end +#if($slotsView) + + #end +#end +#if($slotsView) + + #end +#end +#if($slotsView) + + #end diff --git a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/slots/slots-page.js.template b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/slots/slots-page.js.template index 2d8955bdc11..0842ff70754 100644 --- a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/slots/slots-page.js.template +++ b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/slots/slots-page.js.template @@ -97,6 +97,10 @@ document.addEventListener('alpine:init', () => { const val = slot.date + 'T' + slot.start; window.PineconeRouter.navigate('/${name}/create?${slotStartProperty}=' + encodeURIComponent(val)); }, - newEntity() { window.PineconeRouter.navigate('/${name}/create'); } + newEntity() { window.PineconeRouter.navigate('/${name}/create'); }, + + // Switch to the entity's own browse page (list / master / document list). The picker took over + // /${name}, so that layout browses at /${name}/list. + goList() { window.PineconeRouter.navigate('/${name}/list'); } })); }, { once: true }); diff --git a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/slots/slots-view.html.template b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/slots/slots-view.html.template index 2cc56d2b1aa..2ca6bc9ab13 100644 --- a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/slots/slots-view.html.template +++ b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/slots/slots-view.html.template @@ -14,6 +14,12 @@ + +