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
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ window.IntentDiagrams = (() => {
terminal: '#708090', // slate - start / end events
edge: '#7a8896', // mid-gray - relations and sequence flows, visible on both themes
output: '#9141ac', // purple - authoring outputs (forms, reports)
glue: '#c64600', // rust - declarative glue (notifications, schedules, integrations, inbound, outbound, rollups)
glue: '#c64600', // rust - declarative glue (notifications, schedules, integrations, inbound, outbound, rollups, register lookups, create-froms)
label: '#ffffff' // white - on-shape text
};

Expand All @@ -57,7 +57,9 @@ window.IntentDiagrams = (() => {
integration: 'sap-icon--chain-link',
inbound: 'sap-icon--inbox',
outbound: 'sap-icon--outbox',
rollup: 'sap-icon--sum'
rollup: 'sap-icon--sum',
resolve: 'sap-icon--search',
generate: 'sap-icon--create-form'
};

const escapeHtml = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
Expand Down Expand Up @@ -409,10 +411,46 @@ window.IntentDiagrams = (() => {
return relation ? relation.to : null;
};

// Card label: an icon-badged name over a one-line binding detail (escaped; the detail is optional).
// What a register lookup does, in the record's own vocabulary: the moment it runs on and the to-one
// it fills. The register it reads is not repeated here - it is the dashed edge coming into the card.
const resolveDetail = (resolve) => eventVerb(resolve.event) + ' \u2022 sets ' + (resolve.set || '?');

// What a create-from turns into what. The source is normally the entity the card is edged to, so it
// is named only when it is FOREIGN - then there is no local node to edge to and the detail line is
// all the reader has (the same shape a cross-model schedule or roll-up card uses).
const generatesFlow = (generates) => (generates.fromUses ? generates.fromUses + '.' + (generates.from || '?') + ' ' : '')
+ '\u2192 ' + (generates.uses ? generates.uses + '.' : '') + (generates.to || '?');

// The moment a create-from fires, as it reads on the card. `onTransition` is this construct's own
// axis - no other glue binds it - so it is spelled out here rather than in the shared `eventVerb`,
// and it carries its `when` guard: two rules bound to the same transition are told apart by their
// guards, which is precisely what the picture has to make visible.
const generatesTrigger = (event) => {
const guard = event.when ? ' ' + event.when : '';
if (event.onTransition) return 'on transition' + guard;
if (event.onCreate) return 'on create' + guard;
if (event.onPhase) return 'on phase ' + (event.phase || '');
return eventVerb(event);
};

// A create-from's trigger over its CARDINALITY. `once` is the DSL's default - the target's
// back-reference is checked first, so a redelivery is a no-op - while `append` drops that guard and
// mints a row per delivery. The badge is the point of drawing these at all: two appending rules on
// one transition, side by side, is a defect that is obvious in a picture and invisible in YAML.
// With no event at all the create-from is a button, which has no cardinality to state.
const generatesDetail = (generates) => {
const event = generates.event || {};
const trigger = generatesTrigger(event);
return [generatesFlow(generates), trigger ? trigger + ' \u2022 ' + (event.mode || 'once') : 'button'];
};

// Card label: an icon-badged name over its binding detail (escaped). The detail is optional and may
// be an array, which renders one line per entry - a card whose binding needs two.
const cardLabel = (icon, name, detail) =>
`<div style="font-weight:bold"><i class="${icon}" style="margin-right:6px"></i>${escapeHtml(name)}</div>`
+ (detail ? `<div style="font-size:11px;opacity:.9">${escapeHtml(detail)}</div>` : '');
+ (Array.isArray(detail) ? detail : [detail]).filter(Boolean)
.map((line) => `<div style="font-size:11px;opacity:.9">${escapeHtml(line)}</div>`)
.join('');

// One section diagramming the artifacts that hang off the entities - authoring outputs (forms,
// reports) and the declarative glue (notifications, schedules, integrations, inbound arrivals,
Expand All @@ -429,7 +467,14 @@ window.IntentDiagrams = (() => {
{ list: model.outbound, icon: ICON.outbound, color: COLOR.glue, entity: o => eventEntity(model, o.event), detail: outboundDetail },
// A cross-model child has no node here, so the card anchors to the LOCAL parent it feeds and
// names the foreign child in its detail line (the same shape a cross-model schedule uses).
{ list: model.rollups, icon: ICON.rollup, color: COLOR.glue, entity: r => r.model ? r.parent : r.entity, detail: r => (r.model ? r.model + '.' + (r.entity || '') + ' ' : '') + '→ ' + (rollupParent(model, r) || '?') + '.' + (r.field || '') }
{ list: model.rollups, icon: ICON.rollup, color: COLOR.glue, entity: r => r.model ? r.parent : r.entity, detail: r => (r.model ? r.model + '.' + (r.entity || '') + ' ' : '') + '→ ' + (rollupParent(model, r) || '?') + '.' + (r.field || '') },
// A register lookup reads one entity to fill a relation on another, so it is the one card with
// two edges: the solid binding edge from the record it runs on, and a dashed `reads` edge from
// the register - the read direction on the picture rather than only in the detail line.
{ list: model.resolves, icon: ICON.resolve, color: COLOR.glue, entity: r => eventEntity(model, r.event), reads: r => r.from, detail: resolveDetail },
// A create-from with a FOREIGN source has no local node to hang off, so it anchors to the local
// target it mints instead (its detail line names the foreign source).
{ list: model.generates, icon: ICON.generate, color: COLOR.glue, entity: g => g.fromUses ? (g.uses ? null : g.to) : g.from, detail: generatesDetail }
];

const items = [];
Expand Down Expand Up @@ -458,6 +503,8 @@ window.IntentDiagrams = (() => {
const card = graph.insertVertex(parent, null, cardLabel(category.icon, item.name, category.detail(item)), 0, 0, 190, 48, nodeStyle(category.color));
const entity = anchor(category.entity(item));
if (entity) graph.insertEdge(parent, null, '', entity, card, edgeStyle(false));
const read = category.reads ? anchor(category.reads(item)) : null;
if (read) graph.insertEdge(parent, null, 'reads', read, card, edgeStyle(true));
}
const layout = new mxHierarchicalLayout(graph, mxConstants.DIRECTION_WEST);
layout.intraCellSpacing = 30;
Expand All @@ -476,7 +523,7 @@ window.IntentDiagrams = (() => {
const normalize = (model) => {
model = model || {};
for (const key of ['entities', 'processes', 'forms', 'reports', 'permissions', 'seeds',
'notifications', 'schedules', 'integrations', 'inbound', 'outbound', 'rollups']) {
'notifications', 'schedules', 'integrations', 'inbound', 'outbound', 'rollups', 'resolves', 'generates']) {
model[key] = model[key] || [];
}
return model;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* Copyright (c) 2010-2026 Eclipse Dirigible contributors
*
* All rights reserved. This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.dirigible.integration.tests.ui.tests;

import java.nio.charset.StandardCharsets;
import java.time.Duration;

import org.eclipse.dirigible.repository.api.IRepository;
import org.eclipse.dirigible.repository.api.IRepositoryStructure;
import org.eclipse.dirigible.tests.base.UserInterfaceIntegrationTest;
import org.eclipse.dirigible.tests.framework.ide.Workbench;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.springframework.beans.factory.annotation.Autowired;

import com.codeborne.selenide.Condition;
import com.codeborne.selenide.Selenide;

/**
* Browser test for the Intent Editor's "Glue &amp; Outputs" diagram, over the two constructs that
* used to be drawn by nothing at all: {@code resolves} (the effective-dated register lookup) and
* {@code generates} (the create-from). A model whose whole automation lives in those two rendered
* an ER diagram and nothing else, so the picture said the model did almost nothing - and the defect
* class the picture exists to expose (two appending rules on one transition) stayed invisible.
*
* <p>
* The fixture is written straight into the workspace rather than cloned from a sample repository:
* the assertions are about the renderer, so the intent that feeds it belongs next to them, and the
* test needs no external repository to be merged first.
*
* <p>
* Only a browser can catch this. {@code intent-diagrams.js} is framework-free JavaScript rendering
* through mxGraph, so every service behind it stays green whether or not a card is drawn.
*/
public class IntentDiagramGlueIT extends UserInterfaceIntegrationTest {

private static final String PROJECT = "intent-diagram-glue-test";
private static final String INTENT_FILE = "app.intent";
private static final String PROJECT_PATH = IRepositoryStructure.PATH_USERS + "/admin/workspace/" + PROJECT;

/**
* The diagram container of the "Glue &amp; Outputs" section, so the ER section cannot satisfy an
* assertion.
*/
private static final String GLUE_DIAGRAM =
"//h4[contains(@class, 'intent-section-title') and contains(text(), 'Glue')]/following-sibling::div[1]";

/**
* A fine identifies its driver from the vehicle-assignment register valid on the violation date,
* then mints a declaration per identification and offers a notice on demand - one register lookup
* and both create-from triggers (event-driven appending, and button-only) in one small model.
*/
private static final String INTENT = """
name: fines
description: Traffic fines with driver identification
entities:
- name: FineStatus
function: Setting
fields:
- { name: id, type: integer, primaryKey: true, generated: true }
- { name: name, type: string }
- name: Vehicle
fields:
- { name: id, type: integer, primaryKey: true, generated: true }
- { name: plate, type: string }
- name: Driver
fields:
- { name: id, type: integer, primaryKey: true, generated: true }
- { name: name, type: string }
- name: VehicleAssignment
fields:
- { name: id, type: integer, primaryKey: true, generated: true }
- { name: validFrom, type: date }
- { name: validTo, type: date }
relations:
- { name: vehicle, kind: manyToOne, to: Vehicle }
- { name: driver, kind: manyToOne, to: Driver }
- name: Fine
fields:
- { name: id, type: integer, primaryKey: true, generated: true }
- { name: note, type: string }
- { name: violationAt, type: timestamp }
- { name: resolution, type: string, readOnly: true }
relations:
- { name: vehicle, kind: manyToOne, to: Vehicle }
- { name: driver, kind: manyToOne, to: Driver }
- { name: Status, kind: manyToOne, to: FineStatus, function: EntityStatus, init: 1 }
- name: Declaration
fields:
- { name: id, type: integer, primaryKey: true, generated: true }
- { name: note, type: string }
relations:
- { name: Fine, kind: manyToOne, to: Fine }
- name: Notice
fields:
- { name: id, type: integer, primaryKey: true, generated: true }
- { name: note, type: string }
relations:
- { name: Fine, kind: manyToOne, to: Fine }
seeds:
- name: fineStatuses
entity: FineStatus
rows:
- { id: 1, name: NEW }
- { id: 2, name: IDENTIFIED }
- { id: 3, name: UNRESOLVED }
resolves:
- name: identifyDriver
event: { onCreate: Fine }
set: driver
from: VehicleAssignment
match: { vehicle: vehicle }
between: { start: validFrom, end: validTo, value: violationAt }
outcome: resolution
found: { setStatus: IDENTIFIED }
notFound: { setStatus: UNRESOLVED }
ambiguous: { setStatus: UNRESOLVED }
generates:
- name: declarationFromFine
from: Fine
to: Declaration
event: { onTransition: Fine, when: "Status == IDENTIFIED", mode: append }
map: { Fine: id, Note: note }
- name: noticeFromFine
from: Fine
to: Notice
label: "Create Notice"
map: { Fine: id, Note: note }
""";

@Autowired
private IRepository repository;

@Test
void theGlueDiagramDrawsRegisterLookupsAndCreateFroms() {
repository.createResource(PROJECT_PATH + "/" + INTENT_FILE, INTENT.getBytes(StandardCharsets.UTF_8));

ide.openHomePage();
Workbench workbench = ide.openWorkbench();
workbench.openFile(PROJECT, INTENT_FILE);

// The frame sweep enters the editor's iframe; the Monaco source pane visible = editor loaded. It
// is deliberately the sweep's target rather than the diagram: the sweep is a single pass, while
// the diagram only appears after the debounced /parse round-trip, which Selenide polls for below.
browser.findElementInAllFrames(By.cssSelector(".intent-monaco .monaco-editor"), Condition.visible);

// The editor parsed the buffer and drew the section this test is about - its container exists only
// once renderGlue has run, so this is the wait for the debounced /parse round-trip. (Deliberately
// not an `//svg` step: XPath is namespace-aware, so it never matches an SVG element in an HTML
// document - the CSS descendant selector the sibling tests use does.)
Selenide.$(By.xpath(GLUE_DIAGRAM))
.shouldBe(Condition.visible, Duration.ofSeconds(30));

// The register lookup: its own card, the relation it fills, and the register it reads - which is
// drawn as a node of its own plus the dashed `reads` edge, so the read direction is on the
// picture. Nothing else in this section mentions the register, so both can only come from here.
glueDiagramDrew("identifyDriver");
glueDiagramDrew("sets driver");
glueDiagramDrew("VehicleAssignment");
glueDiagramDrew("reads");

// The event-driven create-from: its target, its trigger WITH the status guard, and its
// cardinality. The guard and the `append` badge are what make two rules on one transition
// distinguishable at a glance - the whole reason for drawing these.
glueDiagramDrew("declarationFromFine");
glueDiagramDrew("\u2192 Declaration");
glueDiagramDrew("on transition Status == 2");
glueDiagramDrew("append");

// ...and the create-from nobody triggers automatically says so, rather than claiming a
// cardinality it does not have.
glueDiagramDrew("noticeFromFine");
glueDiagramDrew("button");
}

/**
* Assert the "Glue &amp; Outputs" diagram drew a label carrying the given text.
*
* <p>
* Deliberately {@code exist} rather than {@code visible}: the diagram pane scrolls, so whether a
* given card is inside the viewport is a fact about the pane's scroll position and the window size,
* not about the renderer this test is exercising. The section container itself is asserted visible.
*
* @param text the label text, matched as a substring of one text node
*/
private void glueDiagramDrew(String text) {
Selenide.$(By.xpath(GLUE_DIAGRAM + "//*[contains(text(), '" + text + "')]"))
.should(Condition.exist);
}
}
Loading