Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
2887cd4
feat(template): Alpine.js + Harmonia runtime UI template (list + manage)
delchev Jun 23, 2026
c81f167
feat(template): Harmonia setting view type + fix index.html model dump
delchev Jun 23, 2026
f6ba905
feat(template): Harmonia master-detail view type + fix sanitized REST…
delchev Jun 24, 2026
08a7da0
feat(template): Harmonia process-task surfacing (entity-process-tasks…
delchev Jun 24, 2026
a00ac35
feat(template): Harmonia forms runtime (template-form-builder-harmonia)
delchev Jun 24, 2026
4e57d0a
feat(template): Harmonia report view types (table + chart)
delchev Jun 24, 2026
a3c81b2
feat(template): transfer Process Inbox + Documents to the Harmonia shell
delchev Jun 24, 2026
7715324
fix(template): intent glue handlers use self-describing @Component, n…
delchev Jun 24, 2026
ccc8925
fix(template): Harmonia forms convert date/time widgets to the backen…
delchev Jun 24, 2026
dc4167f
fix(template): master-detail panel doubled REST path + null-master fetch
delchev Jun 24, 2026
9a499ae
docs: Harmonia runtime UI — implementation status, CLAUDE.md guide, R…
delchev Jun 24, 2026
ac55820
test(intent): assert self-describing @Component glue handlers
delchev Jun 24, 2026
571b288
fix(intent,template): start process when ProcessId arrives empty, not…
delchev Jun 24, 2026
a5b3d5a
fix(intent): don't fire onUpdate when a process trigger stamps ProcessId
delchev Jun 24, 2026
36dfb7a
fix(template): Harmonia BPM task form runs standalone (self-contained…
delchev Jun 24, 2026
70a69b0
feat(template): Harmonia Process Inbox Outlook-style + fix Documents
delchev Jun 24, 2026
9d86298
feat(intent): default the intent generators to the Harmonia stack
delchev Jun 24, 2026
1f2673a
feat(template): Harmonia shell — light theme + switcher, single Setti…
delchev Jun 24, 2026
c204066
feat(template,intent): Harmonia standalone report-file template + mak…
delchev Jun 24, 2026
a6a579a
fix(template): Harmonia standalone pages follow the shell theme (no d…
delchev Jun 24, 2026
db95e4c
fix(template): Harmonia full-stack must include the database schema (…
delchev Jun 24, 2026
ddd5d9a
feat(template): full Document Storage in Harmonia (Outlook master-det…
delchev Jun 24, 2026
1710888
fix(template): Harmonia Documents — don't percent-encode the slash in…
delchev Jun 25, 2026
dfec444
docs(CLAUDE): Harmonia gotchas — schema-required, CMS path contract, …
delchev Jun 25, 2026
6e413a6
feat(template): Harmonia theme via Harmonia colour-scheme API + top-r…
delchev Jun 25, 2026
491c2a9
docs(CLAUDE): Harmonia theme via Harmonia colour-scheme API; settings…
delchev Jun 25, 2026
84fa108
fix(template): Harmonia report page renders columns from the data (wa…
delchev Jun 25, 2026
fea7041
feat(intent,template): humanized labels, app title/description from i…
delchev Jun 25, 2026
f8aec75
fix(template): populate entity edit forms (dates + combobox); add Har…
delchev Jun 25, 2026
505af13
docs: track the Harmonia/Alpine reference skills under the harmonia m…
delchev Jun 25, 2026
2bd512e
fix(template): drop x-h-select-clear from the combobox (value rendere…
delchev Jun 25, 2026
0e231cb
docs(CLAUDE): Harmonia edit-form value shapes + combobox contract got…
delchev Jun 25, 2026
33b526b
test(intent): assert the Harmonia default recipes (CI fix for PR #6078)
delchev Jun 25, 2026
3dad3ef
fix(template): preload BPM task form fields from the task's process v…
delchev Jun 25, 2026
d9337fb
Harmonia UI: width-aware forms, embedded reports/settings, layout fixes
delchev Jun 25, 2026
e63bff5
Harmonia entity master-detail + process tasks; auto-close stale editors
delchev Jun 25, 2026
c785eda
Harmonia sidebar: sections, per-report nav, model-driven nav labels
delchev Jun 26, 2026
e701795
Fix JDT.LS build break: track rolling snapshot instead of pinned mile…
delchev Jun 26, 2026
70f9da1
Harmonia master-detail: collapse the detail pane until a row is selected
delchev Jun 26, 2026
adb63fe
Harmonia shell: working breadcrumb derived from the route
delchev Jun 26, 2026
ffbe8b4
Harmonia: model-generated Dashboard with entity KPI tiles
delchev Jun 26, 2026
e52b17f
Harmonia dashboard: simplified report preview tiles (instead of a count)
delchev Jun 26, 2026
5517819
Dashboard: exclude flag for entities + model-driven report widgets
delchev Jun 26, 2026
ebb4f68
Fix Velocity parse error in dashboardPage template ($store escaping)
delchev Jun 26, 2026
1b911c1
Harmonia sidebar: add a Dashboard nav item
delchev Jun 26, 2026
7d6da7d
Harmonia: configurable brand icon + per-deployment branding in .settings
delchev Jun 26, 2026
42b8ee0
Harmonia shell: top-right user menu, notifications bell, visible them…
delchev Jun 26, 2026
1839369
Harmonia: fix embedded-iframe theme switching + surface BPM tasks as …
delchev Jun 26, 2026
0eedde9
Harmonia shell: theme store, corner bell badge, live task refresh
delchev Jun 26, 2026
a1697dd
Harmonia master-detail: null-safe property pane (selected?.)
delchev Jun 26, 2026
c306e50
Docs: record Harmonia shell + intent branding/dashboard gotchas in CL…
delchev Jun 26, 2026
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
30 changes: 30 additions & 0 deletions CLAUDE.md

Large diffs are not rendered by default.

421 changes: 421 additions & 0 deletions HARMONIA_RUNTIME_PLAN.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions components/engine/engine-intent/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,10 @@ Implemented and generating annotated client-Java off the shared `EventBinding` /

**Done:**

- **Harmonia dashboard + branding + per-widget exclude (model-driven).** The Harmonia shell now generates a home Dashboard with one KPI tile per entity (live record count via the entity controller's `/count`) and a preview tile per report; the model feeds it directly. Three intent-side levers were added (all flow through `EdmIntentGenerator`/`ReportIntentGenerator` into the `.model`/`.report`, read by the Harmonia templates):
- **Brand icon + per-deployment branding.** `IntentModel.icon` (optional top-level `icon:` — a Lucide name or an image URL) → `.model` `icon` → the shell header. **Precedence: `.settings` `branding` (`IntentSettings.Branding` = `{title, description, icon}`, scaffolded from the model, developer-owned, preserved across regenerations) > the intent's own name/description/icon > defaults.** This is the answer to "one Library model, many libraries" — rebrand per deployment by editing `.settings`, not the intent. The Harmonia full-stack template also exposes an `appIcon` generate-parameter that overrides at generate time. The shell renders a value with `/` or `.` as an `<img>` (custom SVG/PNG/data-URI), otherwise a Lucide icon.
- **Dashboard exclude flags.** `EntityIntent.dashboard: false` → `.model` entity `dashboardWidget="false"` → the Harmonia dashboard skips that entity's tile (SETTING entities are excluded anyway by type). `ReportIntent.dashboard: false` → `.report` `dashboard:false` → the reports store omits its dashboard tile (it still shows in the sidebar Reports section). Reports also carry `description` into the `.report`, shown on the dashboard tile; the Harmonia reports store reads the `.report` JSON directly (it's already valid JSON at the project root) to get label/description/dashboard — no separate widget file.
- **`widgetSize` is a plain integer column count (3/4/6/12), not a Fundamental CSS class.** `IntentNaming.pluralize` was also added (humanized + pluralized `menuLabel` so generated nav reads "Books"/"Sales Invoices"). The editor stores the integer; Harmonia maps it to a 12-col `grid-column: span N`, AngularJS to `fd-col-md--N`.
- **AI assistant (Claude chat + patch preview), third pane of the Intent Editor.** See the "AI assistant" section below. Server-side Anthropic bridge (`agent/IntentAgentService` + `IntentAgentEndpoint`, `POST /services/ide/intent/agent`); the key lives server-side in `DirigibleConfig` and never reaches the browser. The assistant proposes the **complete** updated `app.intent` via a `propose_intent` tool; the editor renders a Monaco diff and Accept replaces the buffer (still unsaved) - the agent never writes disk or runs the generators. Honors "edit shape, not file shape" via prompt discipline (change minimally, preserve key order + comments). `IntentEngineIT.agent_reports_when_not_configured` covers the network-free 412 path.
- **`kind: setting` entities** -> `type="SETTING"`, routed under the dashboard Settings menu (relations targeting them resolve to the `Settings` perspective). See the YAML-semantics bullet above.
- **Java report template** ("Application Report - Table - Java", `template-application-ui-angular-java/template/template-report-file.js` + `ui/reportFile.js`): a Java port of the working "Application Report - Table V2" (`template-application-ui-angular-v2/.../template-report-file.js`). Generates the report server-side as Java instead of TS - `template-application-dao-java/data/reportFileEntity.java.template` (a plain repository running the report SQL via `Database.queryNamed(...)`, query embedded as a pre-escaped Java string literal `queryJava`) + `template-application-rest-java/api/reportFileEntity.java.template` (`@Controller` with `GET /`, `GET/POST /count`, `POST /search`, `POST /export`) - reusing the existing Java report-file UI perspective (which already targets the Java controller URL). The intent `.settings` **report recipe now defaults to this Java template** (`IntentSettings.scaffold`). Added a Refresh button (`sap-icon--refresh`, before Export/Print) + `refresh()` to the report-file UI controller. The deprecated path (report from an EDM Report **Entity**) is NOT this - this is the standalone-`.report`-file template, confusingly named `report-file`/`reportFile` but it IS the working "Table V2" lineage.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
*/
package org.eclipse.dirigible.components.intent.generator;

import java.util.Locale;

/**
* Naming conventions shared by every intent generator. The physical table name in particular is
* referenced from three artefacts (the {@code .edm} entity {@code dataName}, the {@code .report}
Expand Down Expand Up @@ -132,4 +134,35 @@ public static String humanize(String name) {
}
return out.toString();
}

/**
* Pluralizes the last word of a (already humanized) label using simple English rules - used for
* navigation / menu labels so the sidebar reads naturally (e.g. {@code "Sales Invoice"} ->
* {@code "Sales Invoices"}, {@code "Category"} -> {@code "Categories"}, {@code "Book"} ->
* {@code "Books"}).
*
* @param label the label whose last word to pluralize (may be null)
* @return the label with its last word pluralized, empty for null/empty input
*/
public static String pluralize(String label) {
if (label == null || label.isEmpty()) {
return "";
}
int sp = label.lastIndexOf(' ');
String head = sp >= 0 ? label.substring(0, sp + 1) : "";
String last = sp >= 0 ? label.substring(sp + 1) : label;
if (last.isEmpty()) {
return label;
}
String lower = last.toLowerCase(Locale.ROOT);
String plural;
if (lower.length() > 1 && lower.endsWith("y") && "aeiou".indexOf(lower.charAt(lower.length() - 2)) < 0) {
plural = last.substring(0, last.length() - 1) + "ies";
} else if (lower.endsWith("s") || lower.endsWith("x") || lower.endsWith("z") || lower.endsWith("ch") || lower.endsWith("sh")) {
plural = last + "es";
} else {
plural = last + "s";
}
return head + plural;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,35 @@ public static final class UserTasks {
private List<String> candidateGroupsExtra = new ArrayList<>();
}

/**
* Per-deployment branding for the generated app's shell header (title, description tooltip, and a
* brand icon). Lives in {@code .settings} - which is developer-owned and preserved across
* regenerations - so one model (e.g. "Library") can be regenerated with different branding per
* deployment (e.g. each library) without editing the intent itself. The icon is a Lucide icon name
* (e.g. {@code book}) or an image URL (custom SVG/PNG).
*/
public static final class Branding {
private String title;
private String description;
private String icon;

public String getTitle() {
return title;
}

public String getDescription() {
return description;
}

public String getIcon() {
return icon;
}
}

private Map<String, Recipe> generation = new LinkedHashMap<>();
private Map<String, Map<String, ArtefactOverride>> overrides = new LinkedHashMap<>();
private UserTasks userTasks = new UserTasks();
private Branding branding = new Branding();

/** Parse a settings document; tolerant of missing sections. */
public static IntentSettings parse(String json) {
Expand All @@ -105,12 +131,20 @@ public static IntentSettings parse(String json) {
*/
public static IntentSettings scaffold(IntentModel model) {
IntentSettings settings = new IntentSettings();
settings.generation.put("model", new Recipe("template-application-angular-java/template/template.js",
// The full-stack UI template is named explicitly here (DAO + REST + UI). Default is now the
// Alpine.js + Harmonia SPA stack (a self-describing client-Java backend whose @Entity classes
// create the tables, plus the Harmonia SPA UI); to generate the AngularJS + BlimpKit stack
// instead, set this to "template-application-angular-java/template/template.js". The glue
// template is framework-neutral (annotated client-Java), shared by both stacks.
settings.generation.put("model", new Recipe("template-application-ui-harmonia-java/template/template.js",
orderedMap("tablePrefix", "", "dataSource", "DefaultDB")));
settings.generation.put("glue", new Recipe("template-application-events-java/template/template.js", new LinkedHashMap<>()));
settings.generation.put("form", new Recipe("template-form-builder-angularjs/template/template.js", new LinkedHashMap<>()));
settings.generation.put("form", new Recipe("template-form-builder-harmonia/template/template.js", new LinkedHashMap<>()));
// Standalone report-file UI: the Harmonia page (self-contained Alpine page over the same
// framework-neutral Java report backend). The AngularJS equivalent is
// "template-application-ui-angular-java/template/template-report-file.js".
settings.generation.put("report",
new Recipe("template-application-ui-angular-java/template/template-report-file.js", new LinkedHashMap<>()));
new Recipe("template-application-ui-harmonia-java/template/template-report-file.js", new LinkedHashMap<>()));

Map<String, ArtefactOverride> triggers = new LinkedHashMap<>();
for (ProcessIntent process : model.getProcesses()) {
Expand Down Expand Up @@ -138,9 +172,19 @@ public static IntentSettings scaffold(IntentModel model) {
settings.overrides.put("forms", forms);
}
settings.userTasks.candidateGroupsExtra.add("ADMINISTRATOR");
// Seed branding from the model so it is visible/editable in .settings; a developer rebrands
// per deployment by editing these (they win over the intent's own name/description/icon).
settings.branding.title = IntentNaming.humanize(model.getName());
settings.branding.description = model.getDescription();
settings.branding.icon = model.getIcon();
return settings;
}

/** Per-deployment branding (title / description / icon) for the shell header. Never null. */
public Branding getBranding() {
return branding == null ? new Branding() : branding;
}

/** Whether the generator should emit the named artefact in the given category (default true). */
public boolean shouldGenerate(String category, String name) {
Map<String, ArtefactOverride> categoryOverrides = overrides.get(category);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.eclipse.dirigible.components.base.helpers.JsonHelper;
import org.eclipse.dirigible.components.intent.generator.IntentGenerationContext;
import org.eclipse.dirigible.components.intent.generator.IntentNaming;
import org.eclipse.dirigible.components.intent.generator.IntentSettings;
import org.eclipse.dirigible.components.intent.generator.IntentTargetGenerator;
import org.eclipse.dirigible.components.intent.generator.TriggerSupport;
import org.eclipse.dirigible.components.intent.model.EntityIntent;
Expand Down Expand Up @@ -93,7 +94,10 @@ public void generate(IntentGenerationContext context) {
return;
}
String baseName = IntentNaming.baseName(context);
EdmDocument document = buildDocument(model, baseName);
IntentSettings.Branding branding = context.getSettings() != null ? context.getSettings()
.getBranding()
: new IntentSettings.Branding();
EdmDocument document = buildDocument(model, baseName, branding);
context.writeModelFile(baseName + ".model", JsonHelper.toJson(document.modelJson));
context.writeModelFile(baseName + ".edm", renderEdmXml(document));
}
Expand All @@ -106,7 +110,7 @@ private static final class EdmDocument {
final Map<String, List<Map<String, Object>>> relationsByEntity = new LinkedHashMap<>();
}

private static EdmDocument buildDocument(IntentModel model, String intentName) {
private static EdmDocument buildDocument(IntentModel model, String intentName, IntentSettings.Branding branding) {
List<EntityIntent> entities = model.getEntities();
Map<String, EntityIntent> byName = indexEntities(entities);
Map<String, String> compositionParents = computeCompositionParents(entities);
Expand All @@ -130,10 +134,15 @@ private static EdmDocument buildDocument(IntentModel model, String intentName) {
// A setting lives under the global Settings perspective (provided by the shell); it does not
// own a generated perspective.
String perspective = perspectiveFor(name, compositionParents, settingEntities);
Map<String, Object> entityMap =
entityDefaults(name, entity.getDescription(), dependent, setting, perspective, tablePrefix, perspectiveOrder);
Map<String, Object> entityMap = entityDefaults(name, entity.getDescription(), entity.getIcon(), dependent, setting, perspective,
tablePrefix, perspectiveOrder);
// dashboard: false excludes the entity from the home dashboard tiles (settings are excluded
// anyway by their type); carried on the .model entity, read by the Harmonia dashboard.
if (entity.isDashboardExcluded()) {
entityMap.put("dashboardWidget", "false");
}
if (!dependent && !setting) {
perspectiveList.add(perspectiveEntry(name, perspectiveOrder));
perspectiveList.add(perspectiveEntry(name, perspectiveOrder, iconUrl(entity.getIcon())));
perspectiveOrder++;
}

Expand Down Expand Up @@ -174,6 +183,24 @@ private static EdmDocument buildDocument(IntentModel model, String intentName) {
}

Map<String, Object> body = new LinkedHashMap<>();
// Model-level caption for the generated app (the Harmonia shell title / sidebar header). The
// intent's `name` (humanised) is more meaningful than the raw project name; the `description`
// rides along as a subtitle/tooltip. Both are ignored by tooling that only reads entities.
// Branding precedence: .settings branding (developer-owned, per-deployment) wins over the
// intent's own name/description/icon, which win over the defaults. So one model can be
// rebranded per deployment by editing .settings, without touching the intent.
String title = notBlank(branding.getTitle()) ? branding.getTitle() : IntentNaming.humanize(model.getName());
body.put("title", title);
String description = notBlank(branding.getDescription()) ? branding.getDescription() : model.getDescription();
if (notBlank(description)) {
body.put("description", description);
}
// Optional brand icon (Lucide name or image URL) for the shell header; the UI template
// defaults it when absent.
String icon = notBlank(branding.getIcon()) ? branding.getIcon() : model.getIcon();
if (notBlank(icon)) {
body.put("icon", icon);
}
body.put("entities", entityList);
body.put("perspectives", perspectiveList);
body.put("navigations", new ArrayList<>());
Expand Down Expand Up @@ -256,16 +283,39 @@ private static String resolvePerspective(String entityName, Map<String, String>
return current;
}

private static Map<String, Object> perspectiveEntry(String name, int order) {
private static final String UNICONS_BASE = "/services/web/resources/unicons/";

/**
* Resolve an intent icon name to a unicons SVG URL (for the AngularJS perspective); blank →
* default.
*/
private static String iconUrl(String icon) {
if (icon == null || icon.isBlank()) {
return DEFAULT_ICON;
}
String n = icon.trim();
return (n.startsWith("/") || n.startsWith("http")) ? n : UNICONS_BASE + n + ".svg";
}

/** The raw icon name (a Lucide icon name for the Harmonia sidebar); blank → a neutral default. */
private static String iconName(String icon) {
return (icon == null || icon.isBlank()) ? "list" : icon.trim();
}

private static boolean notBlank(String value) {
return value != null && !value.isBlank();
}

private static Map<String, Object> perspectiveEntry(String name, int order, String icon) {
Map<String, Object> perspective = new LinkedHashMap<>();
perspective.put("name", name);
perspective.put("label", name);
perspective.put("icon", DEFAULT_ICON);
perspective.put("icon", icon);
perspective.put("order", Integer.toString(order));
return perspective;
}

private static Map<String, Object> entityDefaults(String name, String description, boolean dependent, boolean setting,
private static Map<String, Object> entityDefaults(String name, String description, String icon, boolean dependent, boolean setting,
String perspective, String tablePrefix, int order) {
String dataName = tablePrefix + "_" + IntentNaming.upperSnake(name);
Map<String, Object> entity = new LinkedHashMap<>();
Expand All @@ -278,15 +328,19 @@ private static Map<String, Object> entityDefaults(String name, String descriptio
entity.put("caption", "Manage entity " + name);
entity.put("description", description != null && !description.isBlank() ? description : "Manage entity " + name);
entity.put("tooltip", name);
entity.put("icon", DEFAULT_ICON);
entity.put("icon", iconUrl(icon));
// Raw icon name for the Harmonia sidebar (Lucide). Defaults to "list" when unset.
entity.put("iconName", iconName(icon));
entity.put("menuKey", name.toLowerCase(Locale.ROOT));
entity.put("menuLabel", name);
// Navigation label: humanized + pluralized so the menu reads naturally
// (SalesInvoice -> "Sales Invoices", Book -> "Books").
entity.put("menuLabel", IntentNaming.pluralize(IntentNaming.humanize(name)));
entity.put("menuIndex", "100");
entity.put("layoutType", dependent ? "MANAGE_DETAILS" : "MANAGE_MASTER");
entity.put("perspectiveName", perspective);
entity.put("perspectiveLabel", perspective);
entity.put("perspectiveHeader", "");
entity.put("perspectiveIcon", DEFAULT_ICON);
entity.put("perspectiveIcon", iconUrl(icon));
entity.put("perspectiveOrder", Integer.toString(order));
entity.put("perspectiveNavId", "");
entity.put("perspectiveRole", "");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ private static Map<String, Object> build(IntentGenerationContext context, Report
.isBlank()) {
document.put("description", report.getDescription());
}
// dashboard: false excludes the report's tile from the home dashboard (it still shows in the
// sidebar). Carried on the .report; the Harmonia reports store reads it.
document.put("dashboard", report.isDashboardExcluded() ? Boolean.FALSE : Boolean.TRUE);
document.put("columns", columns);
document.put("query", query);
document.put("conditions", conditions(context, model, source, baseAlias, report.getFilter()));
Expand Down
Loading
Loading