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
24 changes: 24 additions & 0 deletions .github/workflows/check_docs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: check_docs

on:
pull_request:
push:
branches: [standard]

permissions:
contents: read

concurrency:
group: check_docs-${{ github.ref }}
cancel-in-progress: true

jobs:
agents_structure:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version: '22'
- run: node scripts/check-agents-structure.js
177 changes: 74 additions & 103 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ ABAP code style / app-structure conventions.
**This entire project is in English.** All code, comments, commit messages, PR
titles, PR descriptions, and any other text must be written in English.

## Pull requests

- **The PR title becomes the squash-merge commit subject — make it describe
the change.** Before merging, replace any auto-generated title (e.g. a
branch name like `Claude/...-abc123`) with a short descriptive English
title that states what actually changed.
- **One topic per PR.** A structural change (moving, adding, or renaming
subpackages) must not ride along in a PR titled for an unrelated sample —
split it into its own PR so the history stays searchable.

---

## 1. Repository layout
Expand All @@ -25,32 +35,33 @@ src/
├── 01/ "basic" cloud-ready & downportable — survives every build
│ ├── 01/ framework - basics
│ ├── 02/ framework - action
│ ├── 05/ controls - extended
│ └── 08/ controls 1:1 rebuilds of UI5 demo kit samples, split by library
│ ├── 05/ framework - extended Controls (CC and Action)
│ ├── 06/ framework - use cases
│ └── 08/ controls - UI5 Demo Kit 1:1 rebuilds of UI5 demo kit samples, split by library
│ ├── 00/ controls - sap.m
│ ├── 01/ controls - sap.uxap
│ ├── 02/ controls - sap.f
│ ├── 03/ controls - sap.ui.core
│ ├── 04/ controls - sap.ui.layout
│ ├── 05/ controls - sap.tnt
│ └── 06/ controls - sap.ui.codeeditor
│ ├── 06/ controls - sap.ui.codeeditor
│ └── 07/ controls - sap.ui.unified
└── 00/ "extended" restricted / special-purpose — STRIPPED from cloud & 702 builds
├── 01/ only non-abap-cloud on-premise-only ABAP (not ABAP Cloud ready)
├── 02/ only non-openui5 SAPUI5-only controls (sap.suite.*, sap.ui.comp.*, VizFrame, …)
├── 02/ only non-openui5 or higher UI5 1.71 SAPUI5-only controls (sap.suite.*, sap.ui.comp.*, VizFrame, …) or a control/property introduced after UI5 1.71
├── 03/ only with launchpad runs only inside the Fiori Launchpad
├── 04/ only higher UI5 1.71 uses a control/property introduced after UI5 1.71
├── 04/ use of z2ui5 needs the z2ui5/Util frontend helper module in the view
├── 05/ only with javascript and css and html needs native JS / CSS / HTML
├── 06/ only testing test / scaffolding apps, not demos
├── 07/ experimental work-in-progress / not finished
├── 08/ demos complete showcase apps (multi-feature)
├── 09/ generic xml view built on z2ui5_cl_util_xml
├── 10/ only non-openui5-with-cc SAPUI5-only control that also needs a custom control
├── 11/ uncategorized not yet triaged into a category
├── 12/ controls - custom own control demos without a UI5 demo kit original
├── 13/ deprecated sample built on a deprecated UI5 control (ActionSheet, P13n*, UploadSet, …)
└── 99/ obsolete superseded, or uses a deprecated control
├── 07/ experimental, TODO work-in-progress / not finished
└── 99/ obsolete superseded, or built on a deprecated UI5 control
```

This tree is machine-checked: `node scripts/check-agents-structure.js` compares
it against the actual `package.devc.xml` `<CTEXT>` values and fails on any
drift (runs in CI). **Whenever a subpackage is added, removed, or renamed,
update this tree in the same change.**

Each subpackage's `package.devc.xml` `<CTEXT>` is the human-readable name shown
above (e.g. `only non-abap-cloud`). **That CTEXT string is also the overview
group name — keep the two identical** (see §4).
Expand All @@ -71,14 +82,14 @@ the library's `demokit/docuindex.json` in openui5 (HTML markup stripped,
truncated to the 60-character DESCRIPT limit). The **full, untruncated**
description is kept as additional ABAP Doc lines below the URL line; the
overview generator prefers those lines as the tile `sub` (§4).
Demos that have no demo kit original belong in `00/12` (controls - custom)
instead.
Demos that have no demo kit original do not belong in `01/08` — file them in
the framework packages (`01/05` for custom-control/action demos, `01/06` for
use cases) or, when a restriction applies, in the matching `src/00` category.

Machine-generated ports that have not been manually reviewed yet live in
`01/08/07` (controls - generated) and carry the marker line
`"! Generated port of a UI5 demo kit sample - not yet manually reviewed`
directly above the `"! Rebuild ...` line. After review, move a sample into
the library subpackage it belongs to and drop the marker line.
Machine-generated demo kit ports that have not been manually reviewed do not
live in this repository — they are collected in the separate api repository
of the abap2UI5 organization. Everything under `01/08` here is manually
reviewed.

---

Expand All @@ -103,22 +114,19 @@ The split is driven directly by the CI builds:
applies:

1. Needs on-premise-only ABAP (not Cloud) → `00/01`
2. Uses SAPUI5-only controls → `00/02` (and if it *also* needs a custom control → `00/10`)
2. Uses a SAPUI5-only control, **or** a control/property introduced after UI5 1.71 → `00/02`
3. Runs only inside the Launchpad → `00/03`
4. Uses a control/property introduced after UI5 1.71 → `00/04`
4. Needs the `z2ui5/Util` frontend helper module in the view (`core:require {Helper:'z2ui5/Util'}`) → `00/04`
5. Needs native JavaScript / CSS / HTML → `00/05`
6. Test / scaffolding app → `00/06`
7. Experimental / work-in-progress → `00/07`
8. Complete multi-feature showcase demo → `00/08`
9. Built on the generic XML view (`z2ui5_cl_util_xml`) → `00/09`
10. Deprecated control/property, or superseded → `00/99`
11. Not yet triaged → `00/11` (temporary parking; triage into 01–99 later)
8. Deprecated control/property, or superseded → `00/99`

A sample qualifies for `src/01` **only if none** of the above restrictions
apply: OpenUI5-compatible, ABAP-Cloud-ready, standalone, every control **and**
property available since UI5 1.71 (16 Jan 2020) **and** not deprecated, no native
JS, not a test, finished and clean. "Old" is not enough (deprecated → `00/99`);
"non-deprecated" is not enough (post-1.71 → `00/04`).
"non-deprecated" is not enough (post-1.71 → `00/02`).

---

Expand Down Expand Up @@ -224,20 +232,21 @@ from the old catalog.
every tile's `group` to match. A tile's group must equal the CTEXT of the
folder the class physically lives in — never a neighbouring category.
4. **Group blocks follow folder order.** Emit groups in ascending folder number
(`00/01` → `00/12` → `00/99`; `01/01` → `01/08/00` → `01/08/06`) so the
(`00/01` → `00/07` → `00/99`; `01/01` → `01/08/00` → `01/08/07`) so the
on-screen order mirrors the tree; a nested subpackage forms its own group
directly after its parent slot. When inserting a new group, place it at its
numeric slot (e.g. `uncategorized` = `00/11` goes **after**
`only non-openui5-with-cc` (`00/10`) and **before** `obsolete` (`00/99`)).
numeric slot (e.g. `use of z2ui5` = `00/04` goes **after**
`only with launchpad` (`00/03`) and **before**
`only with javascript and css and html` (`00/05`)).
5. **Within a group, sort tiles alphabetically (case-insensitive) by `header`,
then by `sub`.** Sorting by `header` first keeps numbered series together and
in order (`Binding I`, `Binding II`, `Binding III`, … underneath each other;
likewise `Popover I…IV`, `Popup I…III`). The group order from rule 4 is
untouched; only the tiles inside each group are ordered.
6. **Moving a subpackage = moving its whole tile group** between the two
catalogs, inserted at the correct numeric slot (e.g. the uncategorized move
`src/01/07` → `src/00/11` lifted the entire `uncategorized` group out of
`sample_app_001` and into `sample_app_000`).
catalogs, inserted at the correct numeric slot (moving a subpackage from
`src/01` to `src/00` lifts its entire tile group out of `sample_app_001`
and into `sample_app_000`).
7. After every change, verify: `get_catalog( )` and the folder tree agree —
same apps, same group names (== CTEXT), same grouping, no app in the wrong
overview, none missing. The safest way to regenerate is to rebuild each
Expand All @@ -256,18 +265,20 @@ newline). **Run `abaplint` — 0 issues — before committing.**

**Adding a sample**
1. Create the class; place it in the correct folder per §2.
2. Add one tile to the matching overview catalog (§4), in the right group and
numeric position, sorted by `sub`.
2. Regenerate the overview catalogs: `npm run launchpad` (§4).
3. `abaplint` → 0 issues → commit (English message).

**Moving a sample / subpackage**
1. `git mv` the files (no rename needed — `FOLDER_LOGIC=PREFIX`).
2. Update the catalog(s): change the tile's `group`, and if it crossed between
`src/00` and `src/01`, move the tile to the other overview app.
3. `abaplint` → 0 issues → commit.
2. Regenerate the overview catalogs: `npm run launchpad` (§4).
3. If a subpackage was added/removed/renamed: update the §1 tree and run
`node scripts/check-agents-structure.js`.
4. `abaplint` → 0 issues → commit.

**Before every commit**
- `abaplint` reports 0 issues (config `abaplint.jsonc`).
- `node scripts/check-agents-structure.js` reports no drift between the §1
tree and the actual `package.devc.xml` CTEXTs.
- abapGit file format for all file types: UTF-8, LF only, final newline,
2-space indent (§6).
- Overview catalogs still mirror the folder tree (§4).
Expand Down Expand Up @@ -481,7 +492,9 @@ ENDMETHOD.

## 10. Building Views

Views are XML strings passed to `client->view_display()`. There are two ways to build them:
Views are XML strings passed to `client->view_display()`, built with the
`z2ui5_cl_xml_view` fluent API — typed methods where they exist, `_generic( )`
for the rest.

### 1. `z2ui5_cl_xml_view` — typed fluent API
Pre-built methods for common UI5 controls (`shell`, `page`, `simple_form`, `input`, `button`, etc.). Use this for standard layouts.
Expand Down Expand Up @@ -542,75 +555,33 @@ ENDMETHOD.
The hierarchy above is: `shell` → `page` → `simple_form` → `content` → (leaf elements).
`label`, `input`, `button` are siblings inside `content`, so they stay at the same indent level as `)->content(`.

### 2. `z2ui5_cl_util_xml` — generic XML builder
Builds any XML structure directly from element names, namespaces and attributes. **Look up the control in the [UI5 API Reference](https://ui5.sap.com/#/api) and translate 1:1 to ABAP** — no wrapper, no abstraction layer.

**UI5 XML:**
```xml
<form:SimpleForm title="T" editable="true">
<form:content>
<Label text="qty"/>
<Input value="{...}"/>
</form:content>
</form:SimpleForm>
```

**ABAP:**
```abap
->_( n = `SimpleForm` ns = `form` p = VALUE #(
( n = `title` v = `T` )
( n = `editable` v = abap_true ) )
)->_( n = `content` ns = `form`
)->__( n = `Label` a = `text` v = `qty`
)->__( n = `Input` a = `value` v = client->_bind_edit( qty ) )
```

Key rules for `z2ui5_cl_util_xml`:
- `_( n, ns, p )` — add child element, navigate **into** it
- `__( n, ns, a, v, p )` — add child element, **stay** at current level
- `p( n, v )` — add a single attribute to current element
- Single attribute: use `a = ... v = ...` parameters directly
- Multiple attributes: use `p = VALUE #( ( n = ... v = ... ) ... )`
- Namespace declarations go on the root `mvc:View` element as regular attributes (`xmlns:form`, etc.)
- Only declare namespaces that are actually used in the view
- `stringify()` on the factory root produces the complete XML string

#### Method chaining

Each call in a chain must start on its own line — never place two `->_()` / `->__()` calls on the same line:
### 2. `_generic( )` — controls without a typed method

```abap
" Wrong
DATA(page) = root->__( `Shell` )->__( n = `Page`

" Correct
DATA(page) = root->__( `Shell`
)->__( n = `Page`
```

Chain continuations (`)->`) are indented 3 spaces relative to the statement's base indentation.

#### Parameter alignment

When `p` appears on a continuation line, align it directly under `n` — one space after the opening `(`:
When a control or aggregation has no typed wrapper method (yet), stay inside
the `z2ui5_cl_xml_view` chain and add the element generically. **Look up the
control in the [UI5 API Reference](https://ui5.sap.com/#/api) and translate
1:1** — element name, namespace and properties exactly as documented:

```abap
" Wrong — p is one space too far right
)->_( n = `Input`
p = VALUE #( ... ) ).

" Correct — p directly under n
)->_( n = `Input`
p = VALUE #( ... ) ).
header_title->_generic(
name = `breadcrumbs`
ns = `f`
)->breadcrumbs(
)->link( text = `Home` ).
```

Continuation lines inside `VALUE #( )` align with the first tuple `(`:

```abap
)->_( n = `Input`
p = VALUE #( ( n = `type` v = `Number` )
( n = `value` v = client->_bind_edit( qty ) ) ) ).
```
Key rules for `_generic( )`:
- `_generic( name = ... ns = ... t_prop = ... )` adds one element and
navigates **into** it; typed methods continue the chain below it.
- Attributes go into `t_prop = VALUE #( ( n = `...` v = `...` ) ... )`.
- `ns` is registered on the view automatically — only pass namespaces that
are actually used.
- Raw embedded content (e.g. an inline `<style>` body) goes through
`_cc_plain_xml( )`.

> The former standalone XML builder `z2ui5_cl_util_xml` is retired in the
> framework (obsolete package, no new consumers) and is no longer used by any
> sample — do not use it in new samples.

#### VALUE #( ) formatting

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"description": "Learn how to code with abap2UI5.",
"scripts": {
"launchpad": "node scripts/generate-launchpad.js",
"check:agents": "node scripts/check-agents-structure.js",
"syfixes": "find . -type f -name '*.abap' -exec sed -i -e 's/ RAISE EXCEPTION TYPE cx_sy_itab_line_not_found/ ASSERT 1 = 0/g' {} + ",
"abaplintpathfix": "sed -i 's|\"files\": \"/\\.\\.\\/\\.\\.\\/src\\/\\*\\*\\/\\*\\.\\*\"|\"files\": \"/src/**/*.*\"|g' abaplint.jsonc",
"downport": "rm -rf src/00 && abaplint --fix .github/abaplint/abap_702.jsonc && npm run syfixes && cp -f .github/abaplint/abap_702.jsonc abaplint.jsonc && npm run abaplintpathfix"
Expand Down
83 changes: 83 additions & 0 deletions scripts/check-agents-structure.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env node
// Verifies that the folder tree documented in AGENTS.md §1 matches the actual
// package structure: every subpackage in the tree must exist with exactly the
// documented CTEXT (package.devc.xml), and every actual subpackage must appear
// in the tree. Exits non-zero on any drift.
"use strict";

const fs = require("fs");
const path = require("path");

const root = path.join(__dirname, "..");

function actualPackages() {
const map = new Map(); // "src/00/01" -> CTEXT
const walk = (dir) => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(p);
} else if (entry.name === "package.devc.xml") {
const rel = path.relative(root, dir).split(path.sep).join("/");
if (rel === "src") continue; // root package is not part of the documented tree
const match = fs.readFileSync(p, "utf8").match(/<CTEXT>([^<]*)<\/CTEXT>/);
map.set(rel, match ? match[1].trim() : "");
}
}
};
walk(path.join(root, "src"));
return map;
}

function documentedPackages() {
const md = fs.readFileSync(path.join(root, "AGENTS.md"), "utf8");
const section = md.split(/^## 1\. Repository layout$/m)[1];
if (!section) throw new Error("AGENTS.md: section '## 1. Repository layout' not found");
const fence = section.match(/```\n([\s\S]*?)```/);
if (!fence) throw new Error("AGENTS.md: no fenced tree block in section 1");

const map = new Map(); // "src/00/01" -> CTEXT
const stack = [];
for (const line of fence[1].split("\n")) {
const m = line.match(/^([│ ]*)(?:├──|└──) (\d+)\/ {2}(.*)$/);
if (!m) continue;
const depth = m[1].length / 4;
if (!Number.isInteger(depth)) throw new Error(`AGENTS.md: unexpected tree indentation in line: ${line}`);
stack[depth] = m[2];
stack.length = depth + 1;

// CTEXT = quoted label ("basic") or the text before the first run of 2+ spaces
let rest = m[3].trim();
const quoted = rest.match(/^"([^"]*)"/);
const ctext = quoted ? quoted[1] : rest.split(/ {2,}/)[0].trim();
map.set("src/" + stack.join("/"), ctext);
}
if (map.size === 0) throw new Error("AGENTS.md: no package lines found in the tree block");
return map;
}

const actual = actualPackages();
const documented = documentedPackages();
const errors = [];

for (const [pkg, ctext] of documented) {
if (!actual.has(pkg)) {
errors.push(`documented in AGENTS.md but missing on disk: ${pkg} ("${ctext}")`);
} else if (actual.get(pkg) !== ctext) {
errors.push(`CTEXT mismatch for ${pkg}: AGENTS.md says "${ctext}", package.devc.xml says "${actual.get(pkg)}"`);
}
}
for (const [pkg, ctext] of actual) {
if (!documented.has(pkg)) {
errors.push(`exists on disk but not documented in AGENTS.md §1: ${pkg} ("${ctext}")`);
}
}

if (errors.length > 0) {
console.error("AGENTS.md §1 tree and src/ package structure have drifted apart:\n");
for (const error of errors) console.error(" - " + error);
console.error("\nUpdate the tree in AGENTS.md §1 (or fix the package) so both agree.");
process.exit(1);
}

console.log(`AGENTS.md §1 tree matches src/ package structure (${actual.size} subpackages).`);