From a7fb53013899e64fe968a2348b757de929cc4ef4 Mon Sep 17 00:00:00 2001 From: delchev Date: Tue, 4 Aug 2026 20:07:39 +0300 Subject: [PATCH] feat(intent): print scaffold emits a document-shaped layout, not a field dump The generated standard.print now reads like a printed business document: - header row - the humanized title left, the document number right; - header data in two weighted columns (plain fields left, relation labels right) instead of one long field list; - totals as label/value COLUMNS (a weighted row - 2* spacer, labels, values, all right-aligned) so the figures align under each other - a right-aligned label:value line per total leaves the values ragged; - the emphasized total after the row, carrying its label. The scaffold/feeder placeholder contract is unchanged (same keys, new layout); the emitted template still parses with the document DSL (asserted in the test). Layout constructs are proven: a row renders as a fixed fo:table and width weights work on any row child. Co-authored-by: Claude Fable 5 --- .../generator/print/PrintIntentGenerator.java | 96 +++++++++++++++---- .../print/PrintIntentGeneratorTest.java | 11 ++- 2 files changed, 85 insertions(+), 22 deletions(-) diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/print/PrintIntentGenerator.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/print/PrintIntentGenerator.java index c35f219868..70384e80c9 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/print/PrintIntentGenerator.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/print/PrintIntentGenerator.java @@ -177,28 +177,41 @@ public static String buildTemplate(EntityIntent master, EntityIntent items, Map< .append("-print\">\n"); template.append(" \n\n"); - // header: humanized title + the document number (when a documentTitle field exists) - template.append("
\n"); - template.append(" ") - .append(escape(label)) - .append("\n"); + // Header: the humanized title on the left, the document number (when a documentTitle field + // exists) on the right - the classic printed-document header line. FieldIntent number = documentTitleField(master); + template.append("
\n"); if (number != null) { - template.append(" {{document.") + template.append(" \n"); + template.append(" \n"); + template.append(" ") + .append(escape(label)) + .append("\n"); + template.append(" \n"); + template.append(" \n"); + template.append(" {{document.") .append(IntentNaming.pascalCase(number.getName())) .append("}}\n"); + template.append(" \n"); + template.append(" \n"); + } else { + template.append(" ") + .append(escape(label)) + .append("\n"); } template.append(" \n"); template.append("
\n\n"); - // header fields: plain non-aggregate fields + to-one relations (labels resolved client-side) - template.append("
\n"); + // Header data in two columns: the plain non-aggregate fields on the left, the to-one + // relation labels on the right - two weighted stacks instead of one long field list. + List plainFields = new ArrayList<>(); for (FieldIntent field : master.getFields()) { if (field.isPrimaryKey() || field.isAggregate() || field == number) { continue; } - appendField(template, " ", field.getName()); + plainFields.add(field.getName()); } + List relationFields = new ArrayList<>(); for (RelationIntent relation : master.getRelations()) { if (!isToOne(relation) || relation.getName() == null) { continue; @@ -211,10 +224,32 @@ public static String buildTemplate(EntityIntent master, EntityIntent items, Map< .isBlank(); if (crossModel || !IntentEntities.labelFieldOf(byName.get(relation.getTo())) .isEmpty()) { - appendField(template, " ", relation.getName()); + relationFields.add(relation.getName()); + } + } + if (!plainFields.isEmpty() && !relationFields.isEmpty()) { + template.append(" \n"); + template.append(" \n"); + for (String name : plainFields) { + appendField(template, " ", name); + } + template.append(" \n"); + template.append(" \n"); + for (String name : relationFields) { + appendField(template, " ", name); } + template.append(" \n"); + template.append(" \n\n"); + } else { + template.append("
\n"); + for (String name : plainFields) { + appendField(template, " ", name); + } + for (String name : relationFields) { + appendField(template, " ", name); + } + template.append("
\n\n"); } - template.append("
\n\n"); // line items template.append(" \n"); @@ -243,18 +278,41 @@ public static String buildTemplate(EntityIntent master, EntityIntent items, Map< } } if (!aggregates.isEmpty()) { - template.append("
\n"); FieldIntent total = totalField(aggregates); - for (FieldIntent aggregate : aggregates) { - if (aggregate == total) { - continue; + // Totals as label/value COLUMNS (a row of weighted stacks - 2* spacer, labels, values) + // so the figures align under each other; a right-aligned label:value line per total + // leaves the values ragged. + if (aggregates.size() > 1) { + template.append(" \n"); + template.append(" \n"); + template.append(" \n"); + template.append(" \n"); + for (FieldIntent aggregate : aggregates) { + if (aggregate == total) { + continue; + } + template.append(" ") + .append(escape(IntentNaming.humanize(aggregate.getName()))) + .append(":\n"); } - appendField(template, " ", aggregate.getName()); + template.append(" \n"); + template.append(" \n"); + for (FieldIntent aggregate : aggregates) { + if (aggregate == total) { + continue; + } + template.append(" {{document.") + .append(IntentNaming.pascalCase(aggregate.getName())) + .append("}}\n"); + } + template.append(" \n"); + template.append(" \n"); } - template.append(" {{document.") + template.append(" ") + .append(escape(IntentNaming.humanize(total.getName()))) + .append(": {{document.") .append(IntentNaming.pascalCase(total.getName())) - .append("}}\n"); - template.append("
\n\n"); + .append("}}\n\n"); } // Footer: the label plus the document number when one exists. NEVER the primary key - the diff --git a/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/print/PrintIntentGeneratorTest.java b/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/print/PrintIntentGeneratorTest.java index d3c3a64f53..fb3a45dd86 100644 --- a/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/print/PrintIntentGeneratorTest.java +++ b/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/print/PrintIntentGeneratorTest.java @@ -94,9 +94,14 @@ void buildsAParseableStandardTemplate() { assertTrue(template.contains("
")); assertTrue(template.contains("{{Name}}")); assertTrue(template.contains("{{Quantity}}")); - // totals: net as a field, total emphasized - assertTrue(template.contains("{{document.Net}}")); - assertTrue(template.contains("{{document.Total}}")); + // header data in two columns: plain fields left, relation labels right + assertTrue(template.contains("")); + // totals: label/value columns (a weighted row with a 2* spacer) so the figures align under + // each other; the total emphasized after the row, carrying its label + assertTrue(template.contains("")); + assertTrue(template.contains("Net:")); + assertTrue(template.contains("{{document.Net}}")); + assertTrue(template.contains("Total: {{document.Total}}")); } @Test