diff --git a/.github/workflows/check-page-titles.yaml b/.github/workflows/check-page-titles.yaml new file mode 100644 index 0000000..2894277 --- /dev/null +++ b/.github/workflows/check-page-titles.yaml @@ -0,0 +1,31 @@ +name: check-page-titles + +# The page a sample opens with says it is abap2UI5: its main page title starts +# with `abap2UI5 - `. Nothing else fails when a title drifts - three had, before +# this gate existed - so a renamed or newly written sample would carry its own +# private naming scheme for as long as nobody happened to look. +# +# Plain node, no dependencies, so it stays a few seconds. + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: check-page-titles-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-page-titles: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '22' + - run: node scripts/check-page-titles.mjs diff --git a/.github/workflows/check-released-api.yaml b/.github/workflows/check-released-api.yaml new file mode 100644 index 0000000..e957348 --- /dev/null +++ b/.github/workflows/check-released-api.yaml @@ -0,0 +1,35 @@ +name: check-released-api + +# Does a package whose "Runs on" says Cloud name an API that ABAP Cloud does +# not release? The Cloud lint of create-package-branches judges the LANGUAGE +# version only - an unknown non-Z class sits outside errorNamespace and stays +# silent there. This run lints every cloud-capable package with errorNamespace +# matching every name, so whatever the steampunk-2305-api dependency (the list +# of released objects) does not carry is an error. The documented exceptions - +# src/10/01 and src/10/03 - and any open finding are listed in the script and +# printed on every run. See scripts/check-released-api.mjs. + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: check-released-api-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-released-api: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '22' + cache: 'npm' + - run: npm ci + - run: node scripts/check-released-api.mjs diff --git a/.github/workflows/create-package-branches.yaml b/.github/workflows/create-package-branches.yaml index 4cab1fe..147b71e 100644 --- a/.github/workflows/create-package-branches.yaml +++ b/.github/workflows/create-package-branches.yaml @@ -15,7 +15,8 @@ name: create-package-branches # Every branch is verified before it is pushed: abaplint at the release that # package declares, which is what keeps the "Runs on" column of the README # honest and what catches a package that has quietly grown a dependency on one -# of the other nine. On a pull request the whole thing runs except the push, +# of the other nine - and, for a package whose "Runs on" says Cloud, abaplint +# in the ABAP Cloud language version as well. On a pull request the whole thing runs except the push, # so a change that would break a branch fails while it can still be fixed. on: @@ -71,6 +72,29 @@ jobs: # the package alone, at the release it says it needs - run: npx abaplint ./abaplint.jsonc + + # ...and, where its "Runs on" says Cloud, once more in the ABAP Cloud + # language version. A release check reads statements ABAP Cloud refuses + # (a read of sy-datum, a statement outside the Cloud language scope) as + # fine - this is the run that found the sy-datum reads in src/03 and + # src/04. It does NOT judge API release state: a non-Z class absent from + # the steampunk dependency is outside errorNamespace and stays silent - + # check-released-api.yaml is the run that does. "Cloud-capable" is decided + # exactly as scripts/lib/read-packages.mjs decides it for catalogue.json: + # /cloud/i on runsOn. The config is a throwaway copy; the branch keeps the + # release config. + - name: abaplint in ABAP Cloud language version (cloud-capable packages) + run: | + if jq -e --arg b '${{ matrix.branch }}' \ + '.[] | select(.branch == $b) | .runsOn | test("cloud"; "i")' \ + .github/packages.json > /dev/null; then + sed -E 's/"version": "[^"]*"/"version": "Cloud"/' abaplint.jsonc > abaplint-cloud.jsonc + npx abaplint ./abaplint-cloud.jsonc + rm abaplint-cloud.jsonc + else + echo '${{ matrix.branch }} runs on Standard only - no Cloud run' + fi + - run: node scripts/check-overview.mjs - name: Commit and push diff --git a/AGENTS.md b/AGENTS.md index 736fe22..f556eff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -87,22 +87,40 @@ What that costs you when you edit: BY NAME and resolves them at runtime for exactly that reason — and it carries its own url helper rather than calling one, because `src/00` travels only with the two packages that name it in `shared`. `check:overview` fails on a - static `Z2UI5_CL_SMPS_*` reference that would not survive every branch. + static `Z2UI5_*_SMPS_*` reference — a class, an interface, a table, a CDS + entity — that would not survive every branch. - Each branch is linted at **its own** release before it is pushed, which is what makes the "Runs on" column in the README true rather than aspirational. + A branch whose "Runs on" says Cloud is linted a second time at + `"version": "Cloud"` — the release run reads a `sy-datum` as fine, the Cloud + run does not. It does not judge API release state — an unknown non-Z class is + outside `errorNamespace` either way. That is `npm run check:released-api` + (`scripts/check-released-api.mjs`, its own workflow): every cloud-capable + package once more at Cloud with `errorNamespace` matching every name, so + whatever the `steampunk-2305-api` dependency — the released objects — does + not carry is an error. Its exception list names `src/10/01` and `src/10/03` + (Standard-only and SDK-only by design, see `src/10/README.md`) and one open + finding in `src/05`; both kinds are printed on every run. +- `SAMPLES.md` and `catalogue.json` are **regenerated** on each branch, by the + same generators over the reduced tree, so they list that package alone and + link to nothing the branch does not carry. The generators scan the tree they + run in; that is the only branch-specific behaviour, and on `main` it drops + nothing. ## 4. Build & verify ```sh npm ci -npm run check # abaplint + abap2UI5-linter + overview + keywords + abapdoc + SAMPLES.md + catalogue.json + app-rules +npm run check # pin + abaplint + abap2UI5-linter + overview + keywords + titles + abapdoc + SAMPLES.md + # + catalogue.json + app-rules + atc + prose + catalogue-derived.json + released-api ``` Individually: `npm run lint` (abaplint), `npm run check:abap2ui5` (the app class and the view it builds, including a headless render of every view), -`npm run check:overview` (the five consistency directions between the overview +`npm run check:overview` (the six consistency directions between the overview app, the tree, `packages.json` and the two README tables — the package table -and the *Which package do I need?* decision table). +and the *Which package do I need?* decision table — plus the branch names the +overview tells a reader to pull when a package is not on the system). `npm run fmt:chains` applies the house chain layout. It rewrites whitespace between chain segments only — but it needs the ABAP to be *balanced* to know @@ -215,6 +233,13 @@ gone. "validations run at COMMIT" would otherwise surprise a reader. - The class description in `.clas.xml` (``) is what the overview app shows. Keep it in Title Case and specific. +- **The main page title starts with `abap2UI5 - `** — the first `Page` after + each `Shell`, as a literal: `abap2UI5 - Smart Controls - SmartTable`. What + follows is the sample's own name and is not prescribed (the RAP steps number + themselves, the session samples say `Sample: …`). A page with `showHeader` + false has no title and is exempt — the launchpad samples leave it to the + shell. Checked by `npm run check:titles` (`scripts/check-page-titles.mjs`, + its own workflow): three titles had drifted before it existed. - **Every app carries three lines about itself, and they are the only place each fact lives** — checked by `npm run check:keywords`: diff --git a/README.md b/README.md index 7f48553..5a74a3b 100644 --- a/README.md +++ b/README.md @@ -177,8 +177,9 @@ repository you are in. Always name a sample by its class. You do not have to look a number up. `?app_start=z2ui5_cl_smps_app_000` lists **every sample of this repository**, one collapsible section per package, and starts each one in a new browser tab — so the overview stays where it is and -several samples can run side by side. Its header button fills the demo data of -both RAP packages. +several samples can run side by side. Its header button, *Regenerate Demo Data*, +deletes every travel of both RAP packages — yours included — and creates the demo +set again. It is also the honest answer to *what does my system actually support*: the overview looks every sample up at runtime instead of referencing it statically, so @@ -267,7 +268,7 @@ check:overview`. `npm run fmt:chains` applies the house chain layout. Every `check-*` workflow below is a step of `npm run check`, and every step has its workflow — which is what makes a green run here mean a green run -there. The last two rows are build-and-publish jobs, not checks, and run only +there. The last row is a build-and-publish job, not a check, and runs only in CI. The node checks carry no dependencies, so they take seconds. | Workflow | What it does | @@ -276,13 +277,15 @@ in CI. The node checks carry no dependencies, so they take seconds. | `check-abap2UI5` | [`abap2ui5lint`](https://github.com/abap2UI5/linter) — the app class and the view it produces, together; also writes the two badges above | | `check-overview` | the hand-kept indexes: every sample is listed in the overview app, the package table matches `.github/packages.json`, and the *Which package do I need?* table routes to every package | | `check-samples-md` | [`SAMPLES.md`](SAMPLES.md) still is what the generator would write — and every app that exists is in an entry | -| `check-catalogue` | [`catalogue.json`](catalogue.json) still is what the generator would write — the same catalogue as data, committed for tooling that fetches one file instead of scanning the tree | | `check-keywords` | every app carries `@keywords` and `@summary`, and the overview's detail line still is the class's `@summary` | +| `check-page-titles` | every sample's main page title starts with `abap2UI5 - ` | | `check-abapdoc` | every `"!` block documents the declaration below it, rather than attaching to nothing | +| `check-atc` | the extended-check (SLIN/ATC) findings a script can decide — a `SELECT` without `WHERE` that does not say so, `sy-subrc` after a dynamic `ASSIGN`, a text symbol passed as a `string` | | `check-app-rules` | the shared abaplint rule block still matches its source in [abap2UI5](https://github.com/abap2UI5/abap2UI5) | | `check-prose-names` | every class name written in prose exists — including the sibling repositories' | | `check-framework-pin` | the abaplint config pins abap2UI5 to a release tag, never to whatever is on `main` | -| `check-catalogue` | `catalogue.json` and `catalogue-derived.json` are in sync with the tree — and with them every package has a README row that parses, every app sits in a package, and none is missing its `@summary` or `@keywords` | +| `check-released-api` | every package whose *Runs on* says Cloud, linted at Cloud against the released objects — an API ABAP Cloud does not release is an error | +| `check-catalogue` | [`catalogue.json`](catalogue.json) and `catalogue-derived.json` still are what the generators would write — the same catalogue as data, committed for tooling that fetches one file instead of scanning the tree — and with them every package has a README row that parses, every app sits in a package, and none is missing its `@summary` or `@keywords` | | `create-package-branches` | rebuilds the ten per-package branches, each verified with abaplint at its own release before it is pushed | `check-overview` exists because the overview app names its samples as strings and diff --git a/SAMPLES.md b/SAMPLES.md index 42e8578..bdb496f 100644 --- a/SAMPLES.md +++ b/SAMPLES.md @@ -97,7 +97,7 @@ type who does not know it exists. `Ctrl+F` on this page uses them. | Sample | Class | |---|---| -| **MIME** — Audio and Play Sound
a success and an error tone, addressed by their ICF path
mime audio sound play_audio wav follow_up_action | [`Z2UI5_CL_SMPS_APP_487`](src/08/z2ui5_cl_smps_app_487.clas.abap) | +| **MIME** — Audio and Play Sound
a success and an error tone, addressed by their ICF path
mime audio sound play_audio mp3 follow_up_action | [`Z2UI5_CL_SMPS_APP_487`](src/08/z2ui5_cl_smps_app_487.clas.abap) | ### Launchpad — `src/09` @@ -136,7 +136,7 @@ able to say they exist. | [`Z2UI5_CL_SMPS_DATA_TRD`](src/04/01/z2ui5_cl_smps_data_trd.clas.abap) | abap2UI5 EML sample - demo data (draft) | | [`Z2UI5_CL_SMPS_BP_TCK`](src/05/01/z2ui5_cl_smps_bp_tck.clas.abap) | RAP Events Demo - Ticket Behavior Pool | | [`Z2UI5_CL_SMPS_EVT_TCK`](src/05/01/z2ui5_cl_smps_evt_tck.clas.abap) | RAP Events Demo - Local Event Consumer (writes log) | -| [`Z2UI5_CL_SMPS_APP_489_WS`](src/07/z2ui5_cl_smps_app_489_ws.clas.abap) | Generated APC WebSocket protocol impementation class | +| [`Z2UI5_CL_SMPS_APP_489_WS`](src/07/z2ui5_cl_smps_app_489_ws.clas.abap) | Generated APC WebSocket protocol implementation class | | [`Z2UI5_CL_SMPS_LLM_CLAUDE`](src/10/z2ui5_cl_smps_llm_claude.clas.abap) | LLM - Anthropic Messages API | | [`Z2UI5_CL_SMPS_LLM_FACTORY`](src/10/z2ui5_cl_smps_llm_factory.clas.abap) | LLM - Configuration and Provider Factory | | [`Z2UI5_CL_SMPS_LLM_JSON`](src/10/z2ui5_cl_smps_llm_json.clas.abap) | LLM - JSON Written and Read by Hand | diff --git a/catalogue-derived.json b/catalogue-derived.json index 12e853b..46631b2 100644 --- a/catalogue-derived.json +++ b/catalogue-derived.json @@ -69,18 +69,18 @@ "noChain": 0 }, "samples": [ - {"class":"Z2UI5_CL_SMPS_APP_000","minUi5":"1.71","needs":[],"controls":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17],"controlCount":238}, + {"class":"Z2UI5_CL_SMPS_APP_000","minUi5":"1.71","needs":[],"controls":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17],"controlCount":248}, {"class":"Z2UI5_CL_SMPS_APP_001","minUi5":"1.71","needs":[],"controls":[1,22,37,7,10,17,38],"controlCount":21}, {"class":"Z2UI5_CL_SMPS_APP_002","minUi5":"1.71","needs":[],"controls":[1,39,22,37,7,10,17,38],"controlCount":22}, {"class":"Z2UI5_CL_SMPS_APP_003","minUi5":"1.71","needs":[],"controls":[1,2,3,22,7,10,11,12,13,18,17],"controlCount":19}, {"class":"Z2UI5_CL_SMPS_APP_004","minUi5":"1.71","needs":[],"controls":[1,2,3,7,10,11,12,13,18,17],"controlCount":19}, {"class":"Z2UI5_CL_SMPS_APP_005","minUi5":"1.71","needs":[],"controls":[1,2,3,39,40,31,22,37,6,7,10,11,12,13,18,41,15,17,38],"controlCount":58}, {"class":"Z2UI5_CL_SMPS_APP_006","minUi5":"1.71","needs":[],"controls":[1,2,3,6,7,10,11,12,13,18,41,17],"controlCount":25}, - {"class":"Z2UI5_CL_SMPS_APP_007","minUi5":"1.71","needs":[],"controls":[1,2,3,7,10,11,12,13,18,17],"controlCount":19}, - {"class":"Z2UI5_CL_SMPS_APP_008","minUi5":"1.71","needs":[],"controls":[1,2,3,22,5,7,10,11,12,13,18,17],"controlCount":17}, - {"class":"Z2UI5_CL_SMPS_APP_009","minUi5":"1.71","needs":[],"controls":[1,2,3,31,5,7,10,11,12,13,18,17],"controlCount":19}, + {"class":"Z2UI5_CL_SMPS_APP_007","minUi5":"1.71","needs":[],"controls":[1,2,3,7,10,11,12,13,18,41,17],"controlCount":21}, + {"class":"Z2UI5_CL_SMPS_APP_008","minUi5":"1.71","needs":[],"controls":[1,2,3,22,5,7,10,11,12,13,18,41,17],"controlCount":19}, + {"class":"Z2UI5_CL_SMPS_APP_009","minUi5":"1.71","needs":[],"controls":[1,2,3,31,5,7,10,11,12,13,18,41,17],"controlCount":21}, {"class":"Z2UI5_CL_SMPS_APP_010","minUi5":"1.71","needs":[],"controls":[1,2,3,39,40,22,37,6,7,10,11,12,13,18,41,15,17,38],"controlCount":55}, - {"class":"Z2UI5_CL_SMPS_APP_011","minUi5":"1.71","needs":[],"controls":[1,2,3,22,37,7,10,11,12,13,18,41,17,38],"controlCount":29}, + {"class":"Z2UI5_CL_SMPS_APP_011","minUi5":"1.71","needs":[],"controls":[1,2,3,22,37,5,7,10,11,12,13,18,41,17,38],"controlCount":33}, {"class":"Z2UI5_CL_SMPS_APP_012","minUi5":"1.71","needs":[],"controls":[1,2,3,7,10,11,12,13,18,41,17],"controlCount":18}, {"class":"Z2UI5_CL_SMPS_APP_013","minUi5":"1.71","needs":[],"controls":[1,50,22,37,5,23,7,51,10,41,52,17,38],"controlCount":28}, {"class":"Z2UI5_CL_SMPS_APP_014","minUi5":"1.71","needs":[],"controls":[1,43,44,22,45,5,7,10,14,17],"controlCount":11}, @@ -98,11 +98,11 @@ {"class":"Z2UI5_CL_SMPS_APP_482","minUi5":"1.71","needs":[],"controls":[1,22,37,5,7,10,17,38],"controlCount":10}, {"class":"Z2UI5_CL_SMPS_APP_483","minUi5":"1.71","needs":[],"controls":[1,22,37,5,7,10,17,38],"controlCount":11}, {"class":"Z2UI5_CL_SMPS_APP_484","minUi5":"1.71","needs":[],"controls":[1,22,37,5,7,10,17,38],"controlCount":13}, - {"class":"Z2UI5_CL_SMPS_APP_485","minUi5":"1.71","needs":[],"controls":[1,31,37,5,7,10,14,42,17],"controlCount":17}, + {"class":"Z2UI5_CL_SMPS_APP_485","minUi5":"1.71","needs":[],"controls":[1,31,37,5,7,10,14,42,17],"controlCount":18}, {"class":"Z2UI5_CL_SMPS_APP_486","minUi5":"1.71","needs":[],"controls":[1,31,37,7,10,14,42,17],"controlCount":11}, - {"class":"Z2UI5_CL_SMPS_APP_487","minUi5":"1.71","needs":[],"controls":[1,22,5,7,12,14,17],"controlCount":8}, + {"class":"Z2UI5_CL_SMPS_APP_487","minUi5":"1.71","needs":[],"controls":[1,22,5,7,10,12,14,17],"controlCount":9}, {"class":"Z2UI5_CL_SMPS_APP_489","minUi5":"1.71","needs":[],"controls":[1,43,44,22,37,45,5,23,7,46,47,10,48,41,42,15,17,38,49],"controlCount":21}, - {"class":"Z2UI5_CL_SMPS_APP_490","minUi5":"1.71","needs":[],"controls":[1,31,22,7,10,13,14,17,38],"controlCount":13}, + {"class":"Z2UI5_CL_SMPS_APP_490","minUi5":"1.71","needs":[],"controls":[1,31,22,5,7,10,13,14,17,38],"controlCount":14}, {"class":"Z2UI5_CL_SMPS_APP_493","minUi5":"1.71","needs":[],"controls":[2,3,31,22,5,7,10,11,12,35,36,32,17],"controlCount":27} ] } diff --git a/catalogue.json b/catalogue.json index 16f9d64..6c07690 100644 --- a/catalogue.json +++ b/catalogue.json @@ -737,7 +737,7 @@ "audio", "sound", "play_audio", - "wav", + "mp3", "follow_up_action" ], "runsOn": "Standard only, ≥ 7.50", diff --git a/package-lock.json b/package-lock.json index d5f263f..1abe428 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "devDependencies": { "@abap2ui5/linter": "^0.8.5", "@abap2ui5/linter-render": "^0.8.5", - "@abaplint/cli": "^2.120.60" + "@abaplint/cli": "^2.120.71" }, "engines": { "node": ">=22" @@ -216,9 +216,9 @@ } }, "node_modules/@abaplint/cli": { - "version": "2.120.60", - "resolved": "https://registry.npmjs.org/@abaplint/cli/-/cli-2.120.60.tgz", - "integrity": "sha512-GohYyKD6BTIiGuFQGCYgiDTt/1Uw39E4OtwXNiDSt/U+9mrT4Cfy3tvqfe3cHJPStrYjourHm9GW99arVtvDDw==", + "version": "2.120.71", + "resolved": "https://registry.npmjs.org/@abaplint/cli/-/cli-2.120.71.tgz", + "integrity": "sha512-FHLQJsUpK5aHVICZfu1mMueOsYo6ekLndqal3bDbDJh56R4N8URErvN7TnkKOJBf3ctiegqAM5TZI7Rbw1rSJA==", "dev": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index 2149919..a589f67 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,9 @@ "check:abap2ui5": "abap2ui5lint", "fmt:chains": "abap2ui5lint --fix", "check:overview": "node scripts/check-overview.mjs", - "check": "npm run check:pin && npm run lint && npm run check:abap2ui5 && npm run check:overview && npm run check:keywords && npm run check:abapdoc && npm run check:samples-md && npm run check:catalogue && npm run check:app-rules && npm run check:atc && npm run check:prose && npm run check:derived", + "check": "npm run check:pin && npm run lint && npm run check:abap2ui5 && npm run check:overview && npm run check:keywords && npm run check:titles && npm run check:abapdoc && npm run check:samples-md && npm run check:catalogue && npm run check:app-rules && npm run check:atc && npm run check:prose && npm run check:derived && npm run check:released-api", "check:keywords": "node scripts/check-keywords.mjs", + "check:titles": "node scripts/check-page-titles.mjs", "check:abapdoc": "node scripts/check-abapdoc.mjs", "samples:md": "node scripts/generate-samples-md.mjs", "check:samples-md": "node scripts/generate-samples-md.mjs --check", @@ -21,7 +22,8 @@ "check:pin": "node scripts/check-framework-pin.mjs", "check:prose": "node scripts/check-prose-names.mjs", "derived": "node scripts/generate-derived.mjs", - "check:derived": "node scripts/generate-derived.mjs --check" + "check:derived": "node scripts/generate-derived.mjs --check", + "check:released-api": "node scripts/check-released-api.mjs" }, "repository": { "type": "git", @@ -35,7 +37,7 @@ "devDependencies": { "@abap2ui5/linter": "^0.8.5", "@abap2ui5/linter-render": "^0.8.5", - "@abaplint/cli": "^2.120.60" + "@abaplint/cli": "^2.120.71" }, "engines": { "node": ">=22" diff --git a/scripts/build-package-branch.mjs b/scripts/build-package-branch.mjs index 40ffaa7..06d74d5 100644 --- a/scripts/build-package-branch.mjs +++ b/scripts/build-package-branch.mjs @@ -14,6 +14,7 @@ // DESTRUCTIVE - it rewrites the working tree in place. Run it on a throwaway // CI checkout, never on a tree you still want. +import { execFileSync } from 'node:child_process'; import { readFileSync, writeFileSync, readdirSync, rmSync, statSync } from 'node:fs'; import { basename, join } from 'node:path'; @@ -65,6 +66,17 @@ for (const entry of readdirSync(SRC)) { rmSync(join(SRC, entry), { recursive: true, force: true }); } +// 1b. SAMPLES.md and catalogue.json, regenerated for what is left. Copied +// over from main they list every package and link to classes and READMEs +// this branch does not carry. The generators stay the one source: they +// scan the tree they run in, which is now this package alone. Before step +// 5, because the catalogue reads the package table of the root README, +// which that step replaces. catalogue-derived.json is left as it is - it +// is keyed by class, links nowhere, and needs a linter pass to rebuild. +for (const generator of ['generate-samples-md.mjs', 'generate-catalogue.mjs']) { + execFileSync(process.execPath, [join('scripts', generator)], { stdio: 'inherit' }); +} + // 2. the workflows. A generated branch runs no CI of its own: pushes made with // GITHUB_TOKEN do not trigger workflow runs anyway, and the branch is // already verified by the job that builds it. `scripts/` and @@ -150,7 +162,7 @@ merged anywhere useful. carries all ${packages.length} packages and their READMEs. - Built by [\`create-package-branches.yaml\`](${MAIN}/.github/workflows/create-package-branches.yaml) from [\`.github/packages.json\`](${MAIN}/.github/packages.json); abaplint checked - this tree at \`${pkg.syntax}\` before it was pushed. + this tree at \`${pkg.syntax}\`${/cloud/i.test(pkg.runsOn) ? ' and in the ABAP Cloud language version' : ''} before it was pushed. ## License diff --git a/scripts/check-abapdoc.mjs b/scripts/check-abapdoc.mjs index 786077e..adb7b9b 100644 --- a/scripts/check-abapdoc.mjs +++ b/scripts/check-abapdoc.mjs @@ -25,6 +25,9 @@ * Move it INTO the chain, directly before the member it documents. * - a `"!` block whose next line is `END OF`, `ENDCLASS`, `ENDINTERFACE` or * a section start documents nothing. + * - a `"!` block followed by a blank line or a plain `"` comment documents + * nothing either: "directly before" means with nothing in between (a Code + * Inspector run on a 7.58 system reported exactly that shape, 2026-10-04). * - a `"!` block inside a running statement (the code line above it ends in * neither `.` nor `:` nor `,`) sits in a parameter list. A parameter is * documented from the method's own block: `"! @parameter | `. @@ -66,18 +69,30 @@ for (const file of walk(SRC)) { // only the FIRST line of a doc block speaks for the block if (/^\s*"!/.test(src[i - 1] || '')) return; - // the code line above: skip blanks and plain `"` comments + // the code line above: skip blanks and comments - plain ones, and a "! + // block a blank line cut off above this one (reported on its own) let p = i - 1; - while (p >= 0 && (!src[p].trim() || /^\s*"[^!]/.test(src[p]) || src[p].trim() === '"')) p -= 1; + while (p >= 0 && (!src[p].trim() || /^\s*"/.test(src[p]))) p -= 1; const prev = p >= 0 ? src[p].trim() : ''; - // the statement below: skip blanks and the rest of the doc block itself + // the line right below the block - "directly before" means NOTHING in + // between: a blank line or a plain `"` comment detaches the block exactly + // like a misplaced one (SLIN, measured on a 7.58 system) + let end = i + 1; + while (end < src.length && /^\s*"!/.test(src[end])) end += 1; + const gap = end < src.length && (!src[end].trim() || /^\s*"(?!!)/.test(src[end])); + + // the statement below: skip blanks, plain comments and the rest of the block let n = i + 1; - while (n < src.length && (/^\s*"!/.test(src[n]) || !src[n].trim())) n += 1; + while (n < src.length && (/^\s*"/.test(src[n]) || !src[n].trim())) n += 1; const next = n < src.length ? src[n].trim() : ''; const at = `${rel}:${i + 1}`; - if (prev && !/[.:,]$/.test(prev)) { + if (gap) { + findings.push( + `${at} — a blank line or a plain " comment between the "! block and \`${next}\` detaches it; the block has to sit directly on the declaration` + ); + } else if (prev && !/[.:,]$/.test(prev)) { findings.push( `${at} — "! inside a parameter list documents nothing; use "! @parameter | in the method's own block` ); diff --git a/scripts/check-overview.mjs b/scripts/check-overview.mjs index ea30b09..d065a8f 100644 --- a/scripts/check-overview.mjs +++ b/scripts/check-overview.mjs @@ -12,10 +12,12 @@ // 2. every class the overview names exists in the tree (full tree only) // 3. every package of .github/packages.json is in the README // table with the release it declares (full tree only) -// 4. every class the overview references STATICALLY survives on +// 4. every object the overview references STATICALLY survives on // every generated package branch (full tree only) // 5. the README's "Which package do I need?" table routes to // every package exactly once (full tree only) +// 6. every package's branch name in the overview is the one +// .github/packages.json builds (always) // // (3) is the second index this repository keeps by hand: packages.json drives // the generated per-package branches and the release each one is checked at, @@ -93,9 +95,31 @@ for (const name of new Set(listed)) { } } +// (6) the overview tells a reader which abapGit branch carries a package +// that is not on the system - a renamed branch there would send them to a +// branch that does not exist. Both files travel to every generated branch, +// so this one is checked on a partial checkout too. +const branchLiterals = [...overview.matchAll(/branch\s+=\s+`([^`]*)`/g)].map((match) => match[1]); +for (const entry of packages) { + if (!branchLiterals.includes(entry.branch)) { + errors.push( + `${OVERVIEW} does not name the branch "${entry.branch}" of src/${entry.dir} - ` + + `pass it as branch = \`${entry.branch}\` to render_package( ) in view_display( )`, + ); + } +} +for (const name of new Set(branchLiterals)) { + if (!packages.some((entry) => entry.branch === name)) { + errors.push( + `${OVERVIEW} names the branch "${name}", which .github/packages.json does not build - ` + + `known: ${packages.map((entry) => entry.branch).join(', ')}`, + ); + } +} + if (complete) { // the overview with its comments and its string literals taken out. What is - // left is ABAP the compiler resolves, so a Z2UI5_CL_SMPS_* name in there is + // left is ABAP the compiler resolves, so a Z2UI5_*_SMPS_* name in there is // a STATIC reference - the by-name lookups all sit inside backticks and are // gone by now. Template literals keep their embedded { ... } expressions, // which are code as well; only their literal text is dropped. @@ -107,15 +131,17 @@ if (complete) { .map((line) => (line.trimStart().startsWith('*') ? '' : line.replace(/".*$/, ''))) .join('\n'); - // where each class of the tree lives, as the top level entry under src/ that - // build-package-branch.mjs keeps or deletes as a whole + // where each object of the tree lives, as the top level entry under src/ + // that build-package-branch.mjs keeps or deletes as a whole. Every object + // type counts, not classes alone: a TYPE REF TO an interface, a TYPE of a + // table, data element or CDS entity of another package takes the branch's + // overview down exactly like a class reference does. The object name is the + // file name up to its first dot (abapGit's naming), the MIME and namespace + // files under src/ never carry a Z2UI5_..._SMPS_ name and drop out below. const home = new Map( files - .filter((path) => path.endsWith('.clas.abap')) - .map((path) => [ - basename(path).replace('.clas.abap', '').toLowerCase(), - path.split(/[\\/]/)[1], - ]), + .filter((path) => !basename(path).startsWith('package.')) + .map((path) => [basename(path).split('.')[0].toLowerCase(), path.split(/[\\/]/)[1]]), ); // what every branch keeps out of src/ on top of its own package - the same @@ -123,7 +149,12 @@ if (complete) { // is not reported here const always = /^(package\.devc\.xml|z2ui5_cl_smps_app_000\.clas\..*)$/; - for (const name of new Set([...code.matchAll(/z2ui5_c[lx]_smps_[a-z0-9_]+/g)].map((m) => m[0]))) { + // ABAP is case-insensitive, so the scan is too: Z2UI5_CL_SMPS_X=>y( ) is + // the same static reference as z2ui5_cl_smps_x=>y( ). Classes, exception + // classes, interfaces, tables, data elements and CDS entities - every + // Z2UI5__SMPS_ object type the naming rule hands out. + const staticRefs = [...code.matchAll(/\bz2ui5_[a-z]{1,2}_smps_[a-z0-9_]+/gi)].map((m) => m[0].toLowerCase()); + for (const name of new Set(staticRefs)) { const dir = home.get(name); if (dir === undefined || always.test(dir)) continue; diff --git a/scripts/check-page-titles.mjs b/scripts/check-page-titles.mjs new file mode 100644 index 0000000..054c526 --- /dev/null +++ b/scripts/check-page-titles.mjs @@ -0,0 +1,131 @@ +#!/usr/bin/env node +/* + * check-page-titles - the page a sample opens with says it is abap2UI5. + * + * The rule (AGENTS.md section 6): the title of a sample's main page is a + * literal that starts with `abap2UI5 - `, followed by the sample's own name. + * + * )->ele( `Page` + * )->a( n = `title` v = `abap2UI5 - Smart Controls - SmartTable` + * + * Measured on the tree when the gate was written (2026-10): 30 of 33 main + * pages already opened that way, three did not (`RAP Events Demo - Tickets + * (abap2UI5)`, `SmartMultiInput - conditions to ABAP SELECT-OPTIONS`) - the + * same drift abap2UI5/samples had before it gated its titles. What follows the + * prefix is NOT one rule here and is not judged: the RAP packages number their + * steps (`EML - 01 Read Travel`), the Smart Controls, Launchpad and AI ones + * repeat the package (`Smart Controls - SmartChart`), the session samples say + * `Sample: …`. abap2UI5/samples holds the title to `abap2UI5 - `; + * that would rewrite every title in this repository, and the overview app + * shows its own curated title, not the DESCRIPT, so it would not even make the + * tile and the page agree. + * + * What is read: the first `Page` built after each `Shell` - the page the user + * sees first. A class that builds two main views (Z2UI5_CL_SMPS_APP_490) is + * held to it for both. A page with `showHeader` false renders no title at all + * and is skipped: the launchpad samples (Z2UI5_CL_SMPS_APP_481, _482) leave + * the header to the launchpad shell, whose title is what _482 sets. Dialogs, + * further pages and form titles are the sample's own business. + * + * Who is held to it: every app (a class implementing z2ui5_if_app, the same + * scan as check-keywords) except the overview app, whose title sits in its + * custom header. Works on a one-package branch: it scans the tree it runs in. + * + * node scripts/check-page-titles.mjs (npm run check:titles) + */ +import path from 'path'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; +import { scanSamples, OVERVIEW_CLASS } from './lib/scan-samples.mjs'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const PREFIX = 'abap2UI5 - '; + +/* Source without comments: `*` in column 1, `"` outside a literal. */ +function stripComments(source) { + return source.split('\n').map((line) => { + if (line.startsWith('*')) return ''; + let quote = null; + for (let i = 0; i < line.length; i += 1) { + const c = line[i]; + if (quote) { + if (c === quote) quote = null; + continue; + } + if (c === '"') return line.slice(0, i); + if (c === '`' || c === "'" || c === '|') quote = c; + } + return line; + }).join('\n'); +} + +const SHELL = /(?:ele|tag)\(\s*(?:n\s*=\s*)?`Shell`/g; +const PAGE = /(?:ele|tag)\(\s*(?:n\s*=\s*)?`Page`/; +const ATTRIBUTE = /^\)->a\(\s*n\s*=\s*`([^`]*)`\s+([vtb])\s*=\s*(.*)$/; + +/** + * The Page's own attributes: the run of `)->a(` lines right after it (the + * house chain layout puts one call per line), up to the first other call or + * the end of the statement. + */ +function pageAttributes(code, from) { + const attrs = new Map(); + for (const raw of code.slice(from).split('\n').slice(1)) { + const line = raw.trim(); + const m = ATTRIBUTE.exec(line); + if (!m) break; + attrs.set(m[1], { kind: m[2], value: m[3].replace(/\s*\)?\s*\.?$/, '').trim() }); + if (/\)\s*\.$/.test(line)) break; + } + return attrs; +} + +const problems = []; +let pages = 0; +let headerless = 0; +const apps = scanSamples(ROOT).filter((s) => s.isApp && s.cls !== OVERVIEW_CLASS); + +for (const app of apps) { + const code = stripComments(fs.readFileSync(app.file, 'utf8')); + const shells = [...code.matchAll(SHELL)]; + if (!shells.length) { + problems.push(`${app.rel}: no Shell -> Page main view found, so its title cannot be checked`); + continue; + } + for (const shell of shells) { + const after = shell.index + shell[0].length; + const m = PAGE.exec(code.slice(after)); + if (!m) { + problems.push(`${app.rel}: a Shell without a Page after it, so its title cannot be checked`); + continue; + } + const at = after + m.index; + const line = code.slice(0, at).split('\n').length; + const attrs = pageAttributes(code, at); + const header = attrs.get('showHeader'); + if (header && /^(abap_false|`false`|space)$/.test(header.value)) { + headerless += 1; + continue; + } + pages += 1; + const title = attrs.get('title'); + const literal = title && /^`([^`]*)`$/.exec(title.value); + if (!title) { + problems.push(`${app.rel}:${line}: the main Page carries no title - want \`${PREFIX}…\``); + } else if (!literal) { + problems.push(`${app.rel}:${line}: the page title is computed (${title.value}) - want a literal \`${PREFIX}…\``); + } else if (!literal[1].startsWith(PREFIX) || !literal[1].slice(PREFIX.length).trim()) { + problems.push(`${app.rel}:${line}: page title \`${literal[1]}\` - want \`${PREFIX}\``); + } + } +} + +console.log(`check-page-titles: ${apps.length} app(s), ${pages} main page title(s) read, ${headerless} headerless page(s) skipped`); + +if (problems.length) { + console.error(`\n${problems.length} problem(s):`); + for (const p of problems) console.error(` ${p}`); + console.error(`\nSee AGENTS.md section 6: a sample's main page title starts with \`${PREFIX}\`.`); + process.exit(1); +} +console.log(`every main page title starts with \`${PREFIX}\` - OK`); diff --git a/scripts/check-released-api.mjs b/scripts/check-released-api.mjs new file mode 100644 index 0000000..18d5ab4 --- /dev/null +++ b/scripts/check-released-api.mjs @@ -0,0 +1,154 @@ +#!/usr/bin/env node +/* + * check-released-api — does a cloud-capable package name an API that ABAP + * Cloud does not release? + * + * The Cloud lint of the branch build (create-package-branches.yaml) runs at + * `"version": "Cloud"` and so judges the LANGUAGE: a read of sy-datum, a + * statement outside the Cloud scope. It does not judge the API, and could not: + * `errorNamespace` is `^Z2UI5`, so a class it cannot resolve - cl_http_client, + * say - is somebody else's and stays silent. + * + * The answer is already in the repository. abaplint resolves against the + * `steampunk-2305-api` dependency, which IS the list of objects SAP released + * for ABAP Cloud (abapedia's snapshot of release 2305). So this check lints + * every cloud-capable package once more with `errorNamespace` set to match + * EVERY name: an object that is neither in the tree, nor in abap2UI5, nor + * released, is now an error - "Class cl_http_client not found". That is the + * released-API check, with no allow-list of SAP classes to maintain by hand. + * + * "Cloud-capable" is decided exactly as scripts/lib/read-packages.mjs decides + * it for catalogue.json: /cloud/i on the package's runsOn. The package's + * `shared` directories come along, and so does the overview app, which ships + * on every branch. On a generated branch the same script checks what is left. + * + * Only four rules run - the ones that resolve names (check_syntax, + * unknown_types, check_ddic) and cloud_types (object types ABAP Cloud does not + * have). Everything else is the ordinary lint's business. + * + * Function modules come with it: at the Cloud language version check_syntax + * reports a static `CALL FUNCTION 'NAME'` the snapshot does not release + * ("Function module ... not found/released") - measured with a probe. + * + * EXPECTED below is the honest exception list: code that names an unreleased + * API ON PURPOSE because the package documents it as not Cloud. Each entry + * says why. An `open` entry is a finding nobody has fixed yet - it does not + * fail the run, but it is printed every time, so it cannot go quiet. + * + * Needs the network, like `npm run lint`: abaplint clones its dependencies. + * Usage: node scripts/check-released-api.mjs (from the repository root) + */ +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; + +const ROOT = process.cwd(); +const CONFIG = '.abaplint-released-api.json'; + +const EXPECTED = [ + { + path: /^src\/10\/01\//, + why: 'src/10/01 is the Standard half of the LLM transport (cl_http_client on an SM59 destination). ' + + 'src/10/README.md documents it as activating on Standard only; the samples create the transport by name.', + }, + { + path: /^src\/10\/03\//, + why: 'src/10/03 is the ABAP AI SDK transport. The SDK (cl_aic_islm_compl_api_factory) is younger than the ' + + 'steampunk-2305 snapshot and exists only where SAP ships it - src/10/README.md says so.', + }, + { + path: /^src\/05\//, + message: /SYSUUID_X16, lookupDomain/, + open: true, + why: 'OPEN: the data elements Z2UI5_E_SMPS_TCK_UUID and Z2UI5_E_SMPS_LOG_UUID take the DOMAIN SYSUUID_X16, ' + + 'which is not in the released list (the released DATA ELEMENT sysuuid_x16 is). Everything else reported ' + + 'for src/05 follows from those two. Needs a check on an ABAP Cloud system and, if confirmed, the two ' + + 'data elements re-exported with the predefined type RAW 16 - not a hand edit of the sidecars.', + }, +]; + +const RULES = ['check_syntax', 'unknown_types', 'check_ddic', 'cloud_types']; + +function parseJsonc(text) { + let out = ''; + let inString = false; + for (let i = 0; i < text.length; i += 1) { + const c = text[i]; + const next = text[i + 1]; + if (inString) { + out += c; + if (c === '\\') { out += next; i += 1; continue; } + if (c === '"') inString = false; + continue; + } + if (c === '"') { inString = true; out += c; continue; } + if (c === '/' && next === '/') { while (i < text.length && text[i] !== '\n') i += 1; out += '\n'; continue; } + if (c === '/' && next === '*') { i += 2; while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i += 1; i += 1; continue; } + out += c; + } + return JSON.parse(out.replace(/,(\s*[}\]])/g, '$1')); +} + +const packages = JSON.parse(fs.readFileSync(path.join(ROOT, '.github/packages.json'), 'utf8')); +const dirs = new Set(); +for (const pkg of packages.filter((p) => /cloud/i.test(p.runsOn))) { + for (const dir of [pkg.dir, ...pkg.shared]) { + if (fs.existsSync(path.join(ROOT, 'src', dir))) dirs.add(dir); + } +} +const files = [...[...dirs].sort().map((d) => `/src/${d}/**/*.*`), '/src/package.devc.xml', '/src/z2ui5_cl_smps_app_000.clas.*']; + +const base = parseJsonc(fs.readFileSync(path.join(ROOT, 'abaplint.jsonc'), 'utf8')); +const rules = Object.fromEntries(RULES.map((r) => [r, base.rules[r] ?? true])); +const config = { + global: { ...base.global, files }, + dependencies: base.dependencies, + syntax: { version: 'Cloud', errorNamespace: '.' }, + rules, +}; + +fs.writeFileSync(path.join(ROOT, CONFIG), JSON.stringify(config, null, 2)); +let issues; +try { + let out; + try { + out = execFileSync(process.execPath, [path.join(ROOT, 'node_modules/@abaplint/cli/abaplint'), CONFIG, '-f', 'json'], + { cwd: ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] }); + } catch (e) { + out = e.stdout || ''; // abaplint exits 1 when it finds anything + if (!out.includes('[')) { + console.error(e.stderr || e.message); + console.error('\ncheck-released-api: abaplint stopped before it produced a result, so nothing was checked. ' + + 'It clones its dependencies (steampunk-2305-api, abap2UI5) first - an unreachable github.com ends here ' + + 'as well as a broken abaplint.jsonc. Does `npm run lint` run?'); + process.exit(2); + } + } + issues = JSON.parse(out.slice(out.indexOf('['))); +} finally { + fs.rmSync(path.join(ROOT, CONFIG), { force: true }); +} + +const bad = []; +const open = new Map(); +const expected = new Map(); +const rel = (file) => (path.isAbsolute(file) ? path.relative(ROOT, file) : file.replace(/^\.\//, '')); +for (const issue of issues) { + const file = rel(issue.file); + const hit = EXPECTED.find((e) => e.path.test(file) && (!e.message || e.message.test(issue.description))); + if (!hit) bad.push(issue); + else (hit.open ? open : expected).set(hit, ((hit.open ? open : expected).get(hit) || 0) + 1); +} + +console.log(`check-released-api: ${[...dirs].sort().map((d) => `src/${d}`).join(', ')} and the overview app, ` + + 'at ABAP Cloud against the released-API snapshot'); +for (const [entry, n] of expected) console.log(` expected (${n} finding(s)): ${entry.why}`); +for (const [entry, n] of open) console.log(` WARNING (${n} finding(s)): ${entry.why}`); + +if (bad.length) { + for (const i of bad) console.log(`ERROR ${rel(i.file)}:${i.start.row} ${i.description} (${i.key})`); + console.log(`\n${bad.length} reference(s) to an object ABAP Cloud does not release, in a package whose runsOn says Cloud.`); + console.log('Either the code moves to a released API, or the package is not cloud-capable and .github/packages.json has to say so.'); + process.exit(1); +} +console.log('every object a cloud-capable package names is released - OK'); diff --git a/scripts/generate-catalogue.mjs b/scripts/generate-catalogue.mjs index 685d3d2..579931d 100644 --- a/scripts/generate-catalogue.mjs +++ b/scripts/generate-catalogue.mjs @@ -55,7 +55,14 @@ const die = (message) => { /* -------------------------------------------------------------------- build */ -const allPackages = packages(ROOT); +/* The packages THIS TREE carries. On main that is every entry of + * packages.json - check-overview.mjs fails on a declared package without its + * directory, so the filter drops nothing there. On a generated one-package + * branch (build-package-branch.mjs regenerates this file after it has removed + * the other packages) it is that package alone: an entry naming a README or a + * class the branch does not have is a dead link for whoever pulled it. */ +const allPackages = packages(ROOT) + .filter((p) => p.dir === '.' || fs.existsSync(path.join(ROOT, 'src', p.dir))); const byDir = new Map(allPackages.map((p) => [p.dir, p])); const apps = scanSamples(ROOT).filter((s) => s.isApp); diff --git a/scripts/generate-samples-md.mjs b/scripts/generate-samples-md.mjs index 3621b4c..9f4acdc 100644 --- a/scripts/generate-samples-md.mjs +++ b/scripts/generate-samples-md.mjs @@ -88,6 +88,25 @@ const body = sections.map((sec) => { return `### ${sec.title} — \`${where}\`\n\n${table(sec.items, sec.title)}`; }).join('\n\n'); +/* The helpers, when the tree has any. A one-package branch can carry none + * (build-package-branch.mjs regenerates this page there), and "0 classes are + * not apps" over an empty table says nothing. */ +const notSamples = helpers.length === 0 ? '' : ` +--- + +## Not samples + +${helpers.length} classes here are not apps and carry no tile: behavior pools, +demo data, an event consumer, the generated APC protocol class and the +language-model layer the AI samples call. They are +reached **by** a sample rather than looked up, which is why they are exempt from +the keyword rule — but a catalogue that claims to account for the tree has to be +able to say they exist. + +${['| Class | What it is |', '|---|---|', + ...helpers.map((s) => `| [\`${s.cls.toUpperCase()}\`](${s.rel}) | ${cell(s.descript)} |`)].join('\n')} +`; + const page = ` @@ -117,21 +136,7 @@ type who does not know it exists. \`Ctrl+F\` on this page uses them. --- ${body} - ---- - -## Not samples - -${helpers.length} classes here are not apps and carry no tile: behavior pools, -demo data, an event consumer, the generated APC protocol class and the -language-model layer the AI samples call. They are -reached **by** a sample rather than looked up, which is why they are exempt from -the keyword rule — but a catalogue that claims to account for the tree has to be -able to say they exist. - -${['| Class | What it is |', '|---|---|', - ...helpers.map((s) => `| [\`${s.cls.toUpperCase()}\`](${s.rel}) | ${cell(s.descript)} |`)].join('\n')} -`; +${notSamples}`; /* The loop the other way round. check-overview.mjs already refuses a catalogue * entry naming a class that does not exist; this refuses an app that exists and diff --git a/src/01/README.md b/src/01/README.md index 7c90628..f1c3f99 100644 --- a/src/01/README.md +++ b/src/01/README.md @@ -25,8 +25,14 @@ reference scenario: /sap/opu/odata/DMO/ui_flight_r_v2/ ``` +They come with the [flight reference scenario](https://github.com/SAP-samples/abap-platform-refscen-flight) +(`/DMO/`, ABAP Platform ≥ 1909, installed with abapGit): publish its two OData V2 +service bindings `/DMO/API_TRAVEL_U_V2` and `/DMO/UI_FLIGHT_R_V2` in ADT, and the +paths above answer. + Any two OData V2 services of your own system do just as well — swap the paths and -the sample keeps working. +the sample keeps working. That is also the way on a release below 1909, where the +flight scenario does not install. ## The sample diff --git a/src/01/z2ui5_cl_smps_app_315.clas.abap b/src/01/z2ui5_cl_smps_app_315.clas.abap index b74f22b..c4dcdde 100644 --- a/src/01/z2ui5_cl_smps_app_315.clas.abap +++ b/src/01/z2ui5_cl_smps_app_315.clas.abap @@ -24,7 +24,7 @@ CLASS z2ui5_cl_smps_app_315 IMPLEMENTATION. )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc` ). DATA(page) = view->ele( `Shell` )->ele( `Page` - )->a( n = `title` v = `abap2UI5 - Table with odata source` + )->a( n = `title` v = `abap2UI5 - OData - Two Models in One View` )->a( n = `showNavButton` b = client->check_app_prev_stack( ) )->a( n = `navButtonPress` v = client->_event_nav_app_leave( ) ). diff --git a/src/02/README.md b/src/02/README.md index 3b61e43..ed3a77e 100644 --- a/src/02/README.md +++ b/src/02/README.md @@ -23,6 +23,12 @@ written in — what they need is the service, not the platform. `GWSAMPLE_BASIC`, which ships with every on-premise system and only has to be activated once in `/IWFND/MAINT_SERVICE`. Where a sample uses a different service, it says so at the `switch_default_model_path` — adjust it to your system. +- **`UI_PRODUCTLIST`** for `Z2UI5_CL_SMPS_APP_313` and `Z2UI5_CL_SMPS_APP_319`, a + standard service that not every system carries. Look for it in + `/IWFND/MAINT_SERVICE` and activate it like `GWSAMPLE_BASIC`; + `Z2UI5_CL_SMPS_APP_319` also reads its value-list annotation + `UI_PRODUCTLIST_VAN` through the Gateway catalog service. Where it does not + exist, those two have no metadata to build from; the other seven are unaffected. ## The samples diff --git a/src/02/z2ui5_cl_smps_app_313.clas.abap b/src/02/z2ui5_cl_smps_app_313.clas.abap index 8239325..817bc88 100644 --- a/src/02/z2ui5_cl_smps_app_313.clas.abap +++ b/src/02/z2ui5_cl_smps_app_313.clas.abap @@ -49,22 +49,25 @@ CLASS z2ui5_cl_smps_app_313 IMPLEMENTATION. )->tag( n = `ControlConfiguration` ns = `smartFilterBar` )->a( n = `key` v = `ProductType` )->a( n = `visibleInAdvancedArea` b = abap_true - )->a( n = `preventInitialDataFetchInValueHelpDialog` b = abap_false - )->end( - )->ele( n = `SmartTable` ns = `smartTable` - )->a( n = `id` v = `smartFiltertable` - )->a( n = `smartFilterId` v = `smartFilterBar` - )->a( n = `tableType` v = `ResponsiveTable` - )->a( n = `editable` b = abap_false - )->a( n = `initiallyVisibleFields` v = `ProductType,ProductType_Text` - )->a( n = `entitySet` v = `ProductType_2` - )->a( n = `useVariantManagement` b = abap_true - )->a( n = `useExportToExcel` b = abap_true - )->a( n = `useTablePersonalisation` b = abap_true - )->a( n = `header` v = `Test` - )->a( n = `showRowCount` b = abap_true - )->a( n = `enableExport` b = abap_false - )->a( n = `enableAutoBinding` b = abap_false ). + )->a( n = `preventInitialDataFetchInValueHelpDialog` b = abap_false ). + + " a statement of its own, so the table is the filter bar's sibling on + " the page - chained behind an end( ) it landed INSIDE the filter bar, + " in the content aggregation of the Grid the FilterBar extends + page->ele( n = `SmartTable` ns = `smartTable` + )->a( n = `id` v = `smartFiltertable` + )->a( n = `smartFilterId` v = `smartFilterBar` + )->a( n = `tableType` v = `ResponsiveTable` + )->a( n = `editable` b = abap_false + )->a( n = `initiallyVisibleFields` v = `ProductType,ProductType_Text` + )->a( n = `entitySet` v = `ProductType_2` + )->a( n = `useVariantManagement` b = abap_true + )->a( n = `useExportToExcel` b = abap_true + )->a( n = `useTablePersonalisation` b = abap_true + )->a( n = `header` v = `Test` + )->a( n = `showRowCount` b = abap_true + )->a( n = `enableExport` b = abap_false + )->a( n = `enableAutoBinding` b = abap_false ). client->view_display( val = view->stringify( ) switch_default_model_path = `/sap/opu/odata/sap/UI_PRODUCTLIST/` ). diff --git a/src/02/z2ui5_cl_smps_app_319.clas.abap b/src/02/z2ui5_cl_smps_app_319.clas.abap index 1bd3294..cd29198 100644 --- a/src/02/z2ui5_cl_smps_app_319.clas.abap +++ b/src/02/z2ui5_cl_smps_app_319.clas.abap @@ -136,7 +136,7 @@ CLASS z2ui5_cl_smps_app_319 IMPLEMENTATION. DATA(page) = view->ele( `Shell` )->ele( `Page` - )->a( n = `title` v = `SmartMultiInput - conditions to ABAP SELECT-OPTIONS` + )->a( n = `title` v = `abap2UI5 - Smart Controls - SmartMultiInput to SELECT-OPTIONS` )->a( n = `showNavButton` b = m_client->check_app_prev_stack( ) )->a( n = `navButtonPress` v = m_client->_event_nav_app_leave( ) ). diff --git a/src/03/01/z2ui5_cl_smps_data_trv.clas.abap b/src/03/01/z2ui5_cl_smps_data_trv.clas.abap index 8f91bb9..70b7692 100644 --- a/src/03/01/z2ui5_cl_smps_data_trv.clas.abap +++ b/src/03/01/z2ui5_cl_smps_data_trv.clas.abap @@ -17,7 +17,8 @@ CLASS z2ui5_cl_smps_data_trv DEFINITION PUBLIC FINAL CREATE PUBLIC. "! "! Deleting first is what makes the keys predictable: early numbering "! continues behind MAX( travel_id ), so on an empty table the demo - "! travels always come out as 1, 2, 3. + "! travels always come out as 1, 2, 3. When the business object refuses + "! the delete, nothing is created - the result says so instead. CLASS-METHODS data_reset RETURNING VALUE(result) TYPE string. @@ -48,13 +49,30 @@ CLASS z2ui5_cl_smps_data_trv IMPLEMENTATION. METHOD data_reset. - result = |{ data_delete( ) } { data_generate( ) }|. + result = data_delete( ). + + " a refused delete leaves travels behind, and the demo set would number + " on behind them - stop here rather than create travels 4, 5, 6 + SELECT FROM z2ui5_r_smps_trv "#EC CI_NOWHERE + FIELDS COUNT( * ) + INTO @DATA(left_over). + + IF left_over > 0. + result = |{ result } No demo data created, { left_over } travel(s) are still there.|. + RETURN. + ENDIF. + + result = |{ result } { data_generate( ) }|. ENDMETHOD. METHOD data_generate. + " the system date through the released API: ABAP Cloud refuses a read of + " sy-datum, and the package says Cloud + Standard + DATA(today) = cl_abap_context_info=>get_system_date( ). + MODIFY ENTITIES OF z2ui5_r_smps_trv ENTITY travel CREATE FIELDS ( agencyid customerid begindate enddate bookingfee currencycode description ) @@ -62,22 +80,22 @@ CLASS z2ui5_cl_smps_data_trv IMPLEMENTATION. ( %cid = `DEMO_1` agencyid = '070001' customerid = '000001' - begindate = sy-datum - enddate = sy-datum + 14 + begindate = today + enddate = today + 14 bookingfee = '20.00' description = 'Demo travel - sightseeing' ) ( %cid = `DEMO_2` agencyid = '070002' customerid = '000002' - begindate = sy-datum + 30 - enddate = sy-datum + 37 + begindate = today + 30 + enddate = today + 37 bookingfee = '35.50' description = 'Demo travel - business trip' ) ( %cid = `DEMO_3` agencyid = '070003' customerid = '000003' - begindate = sy-datum + 60 - enddate = sy-datum + 74 + begindate = today + 60 + enddate = today + 74 bookingfee = '12.75' description = 'Demo travel - city break' ) ) FAILED DATA(s_failed). @@ -94,6 +112,7 @@ CLASS z2ui5_cl_smps_data_trv IMPLEMENTATION. FAILED DATA(s_failed_commit). IF s_failed_commit IS NOT INITIAL. + ROLLBACK ENTITIES. result = `Demo data rejected by the business object on commit.`. RETURN. ENDIF. @@ -126,7 +145,14 @@ CLASS z2ui5_cl_smps_data_trv IMPLEMENTATION. RETURN. ENDIF. - COMMIT ENTITIES. + COMMIT ENTITIES RESPONSE OF z2ui5_r_smps_trv + FAILED DATA(s_failed_commit). + + IF s_failed_commit IS NOT INITIAL. + ROLLBACK ENTITIES. + result = `Deletion refused by the business object on commit.`. + RETURN. + ENDIF. result = |{ lines( t_keys ) } travel(s) deleted.|. diff --git a/src/03/README.md b/src/03/README.md index 869798b..afcc5d7 100644 --- a/src/03/README.md +++ b/src/03/README.md @@ -40,9 +40,13 @@ fills both business objects. Fill the tables before the first run: execute `Z2UI5_CL_SMPS_DATA_TRV` (and `Z2UI5_CL_SMPS_DATA_TRD` for the draft package) with F9 in ADT, or press -*Regenerate Demo Data* in the overview — *Generate Demo Data* in a single sample -does the same for its own business object. Both offer `data_generate( )`, -`data_delete( )` and `data_reset( )`. +*Regenerate Demo Data* in the overview — *Reset Demo Data* in +`Z2UI5_CL_SMPS_APP_005` does the same for this business object alone. Every one +of them runs `data_reset( )`, which **deletes every travel first**, the ones you +created yourself included, so the demo travels come out as 1, 2, 3 again. If the +business object refuses a delete, it stops there and creates nothing — the message +says how many travels are left. Both classes also offer `data_generate( )` and +`data_delete( )` on their own. Demo data is created through the business object, not with an `INSERT` — otherwise the determinations would not run and the rows would be data the BO could never @@ -160,8 +164,9 @@ z2ui5_cl_smps_context=>msg_display( client = client val = s_reported-travel ). ``` -Every sample in this package calls it, which is why none of them formats a message -itself. +Every sample in this package that changes data calls it, which is why none of them +formats a message itself. (`001` only reads: a key that does not exist lands in +`FAILED`, which carries no message, so it says so in a message box of its own.) ## Where to go next diff --git a/src/03/z2ui5_cl_smps_app_001.clas.abap b/src/03/z2ui5_cl_smps_app_001.clas.abap index 7edd812..4bf5e93 100644 --- a/src/03/z2ui5_cl_smps_app_001.clas.abap +++ b/src/03/z2ui5_cl_smps_app_001.clas.abap @@ -81,7 +81,9 @@ CLASS z2ui5_cl_smps_app_001 IMPLEMENTATION. RESULT DATA(t_result) FAILED DATA(s_failed). - IF s_failed-travel IS NOT INITIAL. + " FAILED names a key RAP could not read; an empty RESULT is checked as + " well, so the read of the first row below can never dump + IF s_failed-travel IS NOT INITIAL OR t_result IS INITIAL. s_travel = VALUE #( ). client->message_box_display( text = |Travel { travel_id } does not exist| type = `error` ). diff --git a/src/03/z2ui5_cl_smps_app_002.clas.abap b/src/03/z2ui5_cl_smps_app_002.clas.abap index 9fc6154..c29deb9 100644 --- a/src/03/z2ui5_cl_smps_app_002.clas.abap +++ b/src/03/z2ui5_cl_smps_app_002.clas.abap @@ -59,14 +59,15 @@ CLASS z2ui5_cl_smps_app_002 IMPLEMENTATION. " object - CustomerId is filled and EndDate is not before BeginDate - " so pressing Create right away produces a travel. Change a value and " the same button shows what the validations answer instead. - " CONV d( ) is what turns the sum back into a date: sy-datum + 14 is + " CONV d( ) is what turns the sum back into a date: today + 14 is " calculated as a day number, and a string template renders that number " instead of a date - the field showed 739853 - DATA(end_date) = CONV d( sy-datum + 14 ). + DATA(today) = cl_abap_context_info=>get_system_date( ). + DATA(end_date) = CONV d( today + 14 ). s_travel = VALUE #( agency_id = `070001` customer_id = `000001` - begin_date = |{ sy-datum }| + begin_date = |{ today }| end_date = |{ end_date }| booking_fee = `20.00` currency = `EUR` @@ -117,14 +118,21 @@ CLASS z2ui5_cl_smps_app_002 IMPLEMENTATION. IF s_failed_commit IS NOT INITIAL. + " a failed save keeps the changes in the transactional buffer - discard + " them, or the next EML statement and abap2UI5's own COMMIT WORK at the + " end of the roundtrip run into them + ROLLBACK ENTITIES. z2ui5_cl_smps_context=>msg_display( client = client val = s_reported_commit-travel ). RETURN. ENDIF. " thanks to early numbering the key assigned by the business object is - " available in MAPPED, addressed by the %cid sent above - created_id = |{ s_mapped-travel[ %cid = `CREATE_1` ]-travelid ALPHA = OUT }|. + " available in MAPPED, addressed by the %cid sent above. OPTIONAL: the + " travel is saved by now, and a MAPPED without the row must cost the id + " in the toast, not a CX_SY_ITAB_LINE_NOT_FOUND dump after the save + DATA(s_new) = VALUE #( s_mapped-travel[ %cid = `CREATE_1` ] OPTIONAL ). + created_id = |{ s_new-travelid ALPHA = OUT }|. client->message_toast_display( |Travel { created_id } created| ). ENDMETHOD. diff --git a/src/03/z2ui5_cl_smps_app_003.clas.abap b/src/03/z2ui5_cl_smps_app_003.clas.abap index b492da9..e4b3446 100644 --- a/src/03/z2ui5_cl_smps_app_003.clas.abap +++ b/src/03/z2ui5_cl_smps_app_003.clas.abap @@ -77,8 +77,15 @@ CLASS z2ui5_cl_smps_app_003 IMPLEMENTATION. METHOD data_update. + " OPTIONAL: the id comes from the client, and the row it names may be + " gone from the list by now - a table expression without it raises + " CX_SY_ITAB_LINE_NOT_FOUND and dumps DATA(travel_id) = client->get_event_arg( ). - DATA(s_travel) = t_travels[ travel_id = travel_id ]. + DATA(s_travel) = VALUE #( t_travels[ travel_id = travel_id ] OPTIONAL ). + IF s_travel IS INITIAL. + client->message_toast_display( |Travel { travel_id } is not in the list| ). + RETURN. + ENDIF. " UPDATE FIELDS names exactly the fields that are changed - everything " else on the instance stays untouched, which is why no read is needed @@ -105,6 +112,7 @@ CLASS z2ui5_cl_smps_app_003 IMPLEMENTATION. IF s_failed_commit IS NOT INITIAL. + ROLLBACK ENTITIES. z2ui5_cl_smps_context=>msg_display( client = client val = s_reported_commit-travel ). RETURN. diff --git a/src/03/z2ui5_cl_smps_app_004.clas.abap b/src/03/z2ui5_cl_smps_app_004.clas.abap index 1bf5e4c..6c2bc87 100644 --- a/src/03/z2ui5_cl_smps_app_004.clas.abap +++ b/src/03/z2ui5_cl_smps_app_004.clas.abap @@ -96,6 +96,7 @@ CLASS z2ui5_cl_smps_app_004 IMPLEMENTATION. IF s_failed_commit IS NOT INITIAL. + ROLLBACK ENTITIES. z2ui5_cl_smps_context=>msg_display( client = client val = s_reported_commit-travel ). RETURN. diff --git a/src/03/z2ui5_cl_smps_app_005.clas.abap b/src/03/z2ui5_cl_smps_app_005.clas.abap index 71b0054..a4d0f9c 100644 --- a/src/03/z2ui5_cl_smps_app_005.clas.abap +++ b/src/03/z2ui5_cl_smps_app_005.clas.abap @@ -105,11 +105,12 @@ CLASS z2ui5_cl_smps_app_005 IMPLEMENTATION. " the popup opens on a set that passes both validations, so Create " goes through on the first press - see z2ui5_cl_smps_app_002, which " also explains why the end date needs the CONV d( ) - DATA(end_date) = CONV d( sy-datum + 14 ). + DATA(today) = cl_abap_context_info=>get_system_date( ). + DATA(end_date) = CONV d( today + 14 ). s_create = VALUE #( agency_id = `070001` customer_id = `000001` - begin_date = |{ sy-datum }| + begin_date = |{ today }| end_date = |{ end_date }| booking_fee = `20.00` currency = `EUR` @@ -169,9 +170,15 @@ CLASS z2ui5_cl_smps_app_005 IMPLEMENTATION. IF data_save( ). + " the travel is saved by now - read the key early numbering drew with + " OPTIONAL, so a MAPPED without the row costs the id in the toast and + " not a CX_SY_ITAB_LINE_NOT_FOUND dump after a successful save + DATA(s_new) = VALUE #( s_mapped-travel[ 1 ] OPTIONAL ). client->popup_destroy( ). data_read( ). - client->message_toast_display( |Travel { s_mapped-travel[ 1 ]-travelid ALPHA = OUT } created| ). + client->message_toast_display( COND #( WHEN s_new IS INITIAL + THEN `Travel created` + ELSE |Travel { s_new-travelid ALPHA = OUT } created| ) ). ENDIF. @@ -180,8 +187,15 @@ CLASS z2ui5_cl_smps_app_005 IMPLEMENTATION. METHOD on_event_save. + " OPTIONAL: the id comes from the client, and the row it names may be + " gone from the list by now - a table expression without it raises + " CX_SY_ITAB_LINE_NOT_FOUND and dumps DATA(travel_id) = client->get_event_arg( ). - DATA(s_travel) = t_travels[ travel_id = travel_id ]. + DATA(s_travel) = VALUE #( t_travels[ travel_id = travel_id ] OPTIONAL ). + IF s_travel IS INITIAL. + client->message_toast_display( `Travel not found - press refresh` ). + RETURN. + ENDIF. MODIFY ENTITIES OF z2ui5_r_smps_trv ENTITY travel @@ -330,6 +344,7 @@ CLASS z2ui5_cl_smps_app_005 IMPLEMENTATION. result = abap_true. ELSE. + ROLLBACK ENTITIES. z2ui5_cl_smps_context=>msg_display( client = client val = s_reported-travel ). ENDIF. @@ -351,7 +366,8 @@ CLASS z2ui5_cl_smps_app_005 IMPLEMENTATION. )->a( n = `navButtonPress` v = client->_event_nav_app_leave( ) ). DATA(table) = page->ele( `Table` - )->a( n = `items` v = client->_bind( t_travels ) ). + )->a( n = `items` v = client->_bind( t_travels ) + )->a( n = `noDataText` v = `No travels yet - press Reset Demo Data for three, or Create your own` ). table->ele( `headerToolbar` )->ele( `Toolbar` )->tag( `Title` @@ -363,8 +379,9 @@ CLASS z2ui5_cl_smps_app_005 IMPLEMENTATION. )->a( n = `icon` v = `sap-icon://add` )->a( n = `type` v = `Emphasized` )->tag( `Button` - )->a( n = `press` v = client->_event( `GENERATE` ) - )->a( n = `text` v = `Generate Demo Data` + )->a( n = `press` v = client->_event( `GENERATE` ) + )->a( n = `text` v = `Reset Demo Data` + )->a( n = `tooltip` v = `Deletes every travel, then creates the three demo travels again` )->tag( `Button` )->a( n = `press` v = client->_event( `REFRESH` ) )->a( n = `icon` v = `sap-icon://refresh` diff --git a/src/04/01/z2ui5_cl_smps_data_trd.clas.abap b/src/04/01/z2ui5_cl_smps_data_trd.clas.abap index afc322a..a071501 100644 --- a/src/04/01/z2ui5_cl_smps_data_trd.clas.abap +++ b/src/04/01/z2ui5_cl_smps_data_trd.clas.abap @@ -14,6 +14,8 @@ CLASS z2ui5_cl_smps_data_trd DEFINITION PUBLIC FINAL CREATE PUBLIC. INTERFACES if_oo_adt_classrun. "! Deletes everything, then creates the demo set. This is what F9 runs. + "! When the business object refuses a discard or a delete, nothing is + "! created - the result says so instead. CLASS-METHODS data_reset RETURNING VALUE(result) TYPE string. @@ -45,13 +47,34 @@ CLASS z2ui5_cl_smps_data_trd IMPLEMENTATION. METHOD data_reset. - result = |{ data_delete( ) } { data_generate( ) }|. + result = data_delete( ). + + " a refused discard or delete leaves rows behind, and a reset that keeps + " old travels and drafts next to the new ones is no reset - stop here + SELECT FROM z2ui5_r_smps_trd "#EC CI_NOWHERE + FIELDS COUNT( * ) + INTO @DATA(left_over). + + SELECT FROM z2ui5_d_smps_trd "#EC CI_NOWHERE + FIELDS COUNT( * ) + INTO @DATA(drafts_left_over). + + IF left_over > 0 OR drafts_left_over > 0. + result = |{ result } No demo data created, { left_over } travel(s) and { drafts_left_over } draft(s) are still there.|. + RETURN. + ENDIF. + + result = |{ result } { data_generate( ) }|. ENDMETHOD. METHOD data_generate. + " the system date through the released API: ABAP Cloud refuses a read of + " sy-datum, and the package says Cloud + Standard + DATA(today) = cl_abap_context_info=>get_system_date( ). + " a new instance of a draft enabled business object is born as a draft MODIFY ENTITIES OF z2ui5_r_smps_trd ENTITY travel @@ -61,22 +84,22 @@ CLASS z2ui5_cl_smps_data_trd IMPLEMENTATION. ( %cid = `DEMO_1` agencyid = '070001' customerid = '000001' - begindate = sy-datum - enddate = sy-datum + 14 + begindate = today + enddate = today + 14 bookingfee = '20.00' description = 'Demo travel - sightseeing' ) ( %cid = `DEMO_2` agencyid = '070002' customerid = '000002' - begindate = sy-datum + 30 - enddate = sy-datum + 37 + begindate = today + 30 + enddate = today + 37 bookingfee = '35.50' description = 'Demo travel - business trip' ) ( %cid = `DEMO_3` agencyid = '070003' customerid = '000003' - begindate = sy-datum + 60 - enddate = sy-datum + 74 + begindate = today + 60 + enddate = today + 74 bookingfee = '12.75' description = 'Demo travel - city break' ) ) MAPPED DATA(s_mapped) @@ -88,7 +111,14 @@ CLASS z2ui5_cl_smps_data_trd IMPLEMENTATION. RETURN. ENDIF. - COMMIT ENTITIES. + COMMIT ENTITIES RESPONSE OF z2ui5_r_smps_trd + FAILED DATA(s_failed_draft). + + IF s_failed_draft IS NOT INITIAL. + ROLLBACK ENTITIES. + result = `The demo drafts were refused on commit.`. + RETURN. + ENDIF. " Activate runs the validations, so anything wrong surfaces here MODIFY ENTITIES OF z2ui5_r_smps_trd @@ -107,6 +137,7 @@ CLASS z2ui5_cl_smps_data_trd IMPLEMENTATION. FAILED DATA(s_failed_commit). IF s_failed_commit IS NOT INITIAL. + ROLLBACK ENTITIES. result = `Demo data rejected by the business object on commit.`. RETURN. ENDIF. @@ -123,24 +154,34 @@ CLASS z2ui5_cl_smps_data_trd IMPLEMENTATION. ORDER BY TravelUuid INTO TABLE @DATA(t_keys). - IF t_keys IS INITIAL. + " The CDS entity shows active instances only. A draft that was never + " activated - a new instance is born as a draft, and data_generate( ) + " leaves its drafts behind when Activate refuses them - exists in the + " draft table alone, so the draft keys are read from there. Reading the + " draft table is fine; writing it is what EML is for. + SELECT FROM z2ui5_d_smps_trd "#EC CI_NOWHERE + FIELDS traveluuid + ORDER BY traveluuid + INTO TABLE @DATA(t_draft_keys). + + IF t_keys IS INITIAL AND t_draft_keys IS INITIAL. result = `Nothing to delete.`. RETURN. ENDIF. - " An active instance may carry a draft, and that draft has to go first. - " Ask which ones actually have one instead of discarding blindly: a - " Discard on an instance without a draft lands in FAILED, and an EML - " failure that is neither rolled back nor evaluated leaves the RAP - " transaction marked for abortion. Every later statement of the same LUW - " then aborts - which is how this method used to end the whole request in - " a CX_SADL_DUMP_APPL_MODEL_ERROR instead of deleting anything. + " Every draft has to go before its active instance can. Ask which ones EML + " actually returns instead of discarding blindly: a Discard on an instance + " without a draft lands in FAILED, and an EML failure that is neither + " rolled back nor evaluated leaves the RAP transaction marked for abortion. + " Every later statement of the same LUW then aborts - which is how this + " method used to end the whole request in a CX_SADL_DUMP_APPL_MODEL_ERROR + " instead of deleting anything. " " Reading the keys with %is_draft = mk-on is the same trick sample 06 " uses: what comes back in RESULT has a draft. READ ENTITIES OF z2ui5_r_smps_trd ENTITY travel - FIELDS ( travelid ) WITH VALUE #( FOR s_row IN t_keys + FIELDS ( travelid ) WITH VALUE #( FOR s_row IN t_draft_keys ( %tky = VALUE #( traveluuid = s_row-traveluuid %is_draft = if_abap_behv=>mk-on ) ) ) RESULT DATA(t_drafts). @@ -159,8 +200,20 @@ CLASS z2ui5_cl_smps_data_trd IMPLEMENTATION. RETURN. ENDIF. - COMMIT ENTITIES. + COMMIT ENTITIES RESPONSE OF z2ui5_r_smps_trd + FAILED DATA(s_failed_commit_discard). + IF s_failed_commit_discard IS NOT INITIAL. + ROLLBACK ENTITIES. + result = `Existing drafts could not be discarded on commit.`. + RETURN. + ENDIF. + + ENDIF. + + IF t_keys IS INITIAL. + result = |{ lines( t_drafts ) } draft(s) discarded.|. + RETURN. ENDIF. MODIFY ENTITIES OF z2ui5_r_smps_trd @@ -176,9 +229,16 @@ CLASS z2ui5_cl_smps_data_trd IMPLEMENTATION. RETURN. ENDIF. - COMMIT ENTITIES. + COMMIT ENTITIES RESPONSE OF z2ui5_r_smps_trd + FAILED DATA(s_failed_commit). + + IF s_failed_commit IS NOT INITIAL. + ROLLBACK ENTITIES. + result = `Deletion refused by the business object on commit.`. + RETURN. + ENDIF. - result = |{ lines( t_keys ) } travel(s) deleted.|. + result = |{ lines( t_drafts ) } draft(s) discarded, { lines( t_keys ) } travel(s) deleted.|. ENDMETHOD. diff --git a/src/04/README.md b/src/04/README.md index 5405b47..4863236 100644 --- a/src/04/README.md +++ b/src/04/README.md @@ -22,7 +22,11 @@ ABAP Platform >= 1909 or a BTP ABAP Environment. The draft enabled business obje and its two tables come with this package ([`src/04/01`](01)). Fill the table with `Z2UI5_CL_SMPS_DATA_TRD` (F9 in ADT) or press *Regenerate Demo -Data* in the overview app `?app_start=z2ui5_cl_smps_app_000`. +Data* in the overview app `?app_start=z2ui5_cl_smps_app_000` — or *Reset Demo +Data* in `Z2UI5_CL_SMPS_APP_006` or `Z2UI5_CL_SMPS_APP_010`. All three discard every +draft and delete every travel before they create the demo set, so a draft you +left open goes with them. If the business object refuses one of those, nothing is +created and the message says what is left. ## What changes with draft @@ -46,6 +50,16 @@ first. Start at `06` — it carries the one trick the other three reuse. +**Try it** with the four side by side, each opened from the overview in its own +tab. In `Z2UI5_CL_SMPS_APP_007` press *Edit* on a travel: both description +columns now show the same text. Refresh `Z2UI5_CL_SMPS_APP_006` and the travel is +marked *Draft*. Refresh `Z2UI5_CL_SMPS_APP_008`, change the description and press +*Save Draft* — refresh `Z2UI5_CL_SMPS_APP_007` and only its draft column has the +new text. Refresh `Z2UI5_CL_SMPS_APP_009` and *Activate* to carry the change into +the active instance, or *Discard* to drop it. Each tab reads its list when it +starts and when you press its own buttons, so a change made in another tab shows +up after the refresh button in the table toolbar. + **The complete app** puts all four together in one screen with popups, message handling and a refresh — roughly three times the size, and close to what a real app looks like: diff --git a/src/04/z2ui5_cl_smps_app_006.clas.abap b/src/04/z2ui5_cl_smps_app_006.clas.abap index 817d1e2..55cdf79 100644 --- a/src/04/z2ui5_cl_smps_app_006.clas.abap +++ b/src/04/z2ui5_cl_smps_app_006.clas.abap @@ -117,7 +117,8 @@ CLASS z2ui5_cl_smps_app_006 IMPLEMENTATION. )->a( n = `showNavButton` b = client->check_app_prev_stack( ) )->a( n = `navButtonPress` v = client->_event_nav_app_leave( ) )->ele( `Table` - )->a( n = `items` v = client->_bind( t_travels ) ). + )->a( n = `items` v = client->_bind( t_travels ) + )->a( n = `noDataText` v = `No travels yet - press Reset Demo Data` ). table->ele( `headerToolbar` )->ele( `Toolbar` @@ -125,8 +126,9 @@ CLASS z2ui5_cl_smps_app_006 IMPLEMENTATION. )->a( n = `text` v = `READ ENTITIES ... WITH %is_draft = mk-on` )->tag( `ToolbarSpacer` )->tag( `Button` - )->a( n = `press` v = client->_event( `GENERATE` ) - )->a( n = `text` v = `Generate Demo Data` + )->a( n = `press` v = client->_event( `GENERATE` ) + )->a( n = `text` v = `Reset Demo Data` + )->a( n = `tooltip` v = `Discards every draft and deletes every travel, then creates the three demo travels again` )->tag( `Button` )->a( n = `press` v = client->_event( `REFRESH` ) )->a( n = `icon` v = `sap-icon://refresh` diff --git a/src/04/z2ui5_cl_smps_app_007.clas.abap b/src/04/z2ui5_cl_smps_app_007.clas.abap index 6536166..f529da8 100644 --- a/src/04/z2ui5_cl_smps_app_007.clas.abap +++ b/src/04/z2ui5_cl_smps_app_007.clas.abap @@ -63,6 +63,9 @@ CLASS z2ui5_cl_smps_app_007 IMPLEMENTATION. view_display( ). ELSEIF client->check_on_event( `OPEN` ). draft_open( ). + ELSEIF client->check_on_event( `REFRESH` ). + " a draft the other samples created or ended in a tab of their own + data_read( ). ENDIF. ENDMETHOD. @@ -128,6 +131,7 @@ CLASS z2ui5_cl_smps_app_007 IMPLEMENTATION. IF s_failed_commit IS NOT INITIAL. + ROLLBACK ENTITIES. z2ui5_cl_smps_context=>msg_display( client = client val = s_reported_commit-travel ). RETURN. @@ -184,12 +188,18 @@ CLASS z2ui5_cl_smps_app_007 IMPLEMENTATION. )->a( n = `showNavButton` b = client->check_app_prev_stack( ) )->a( n = `navButtonPress` v = client->_event_nav_app_leave( ) )->ele( `Table` - )->a( n = `items` v = client->_bind( t_travels ) ). + )->a( n = `items` v = client->_bind( t_travels ) + )->a( n = `noDataText` v = `No travels yet - press Regenerate Demo Data in the overview app` ). table->ele( `headerToolbar` )->ele( `Toolbar` )->tag( `Title` - )->a( n = `text` v = `EXECUTE Edit / EXECUTE Resume` ). + )->a( n = `text` v = `EXECUTE Edit / EXECUTE Resume` + )->tag( `ToolbarSpacer` + )->tag( `Button` + )->a( n = `press` v = client->_event( `REFRESH` ) + )->a( n = `icon` v = `sap-icon://refresh` + )->a( n = `tooltip` v = `Refresh` ). table->ele( `columns` )->ele( `Column` diff --git a/src/04/z2ui5_cl_smps_app_008.clas.abap b/src/04/z2ui5_cl_smps_app_008.clas.abap index 29e3e30..7d187be 100644 --- a/src/04/z2ui5_cl_smps_app_008.clas.abap +++ b/src/04/z2ui5_cl_smps_app_008.clas.abap @@ -53,6 +53,9 @@ CLASS z2ui5_cl_smps_app_008 IMPLEMENTATION. view_display( ). ELSEIF client->check_on_event( `SAVE` ). draft_save( ). + ELSEIF client->check_on_event( `REFRESH` ). + " a draft the Enter Draft Mode sample created in a tab of its own + data_read( ). ENDIF. ENDMETHOD. @@ -60,8 +63,15 @@ CLASS z2ui5_cl_smps_app_008 IMPLEMENTATION. METHOD draft_save. + " OPTIONAL: the uuid comes from the client, and the row it names may be + " gone from the list by now - a table expression without it raises + " CX_SY_ITAB_LINE_NOT_FOUND and dumps DATA(uuid) = client->get_event_arg( ). - DATA(s_draft) = t_drafts[ travel_uuid = uuid ]. + DATA(s_draft) = VALUE #( t_drafts[ travel_uuid = uuid ] OPTIONAL ). + IF s_draft IS INITIAL. + client->message_toast_display( `Draft not found - it may have been activated or discarded meanwhile` ). + RETURN. + ENDIF. " An ordinary UPDATE - the only thing that makes it a draft update is " %is_draft = mk-on in the key. The active instance stays untouched. @@ -92,6 +102,7 @@ CLASS z2ui5_cl_smps_app_008 IMPLEMENTATION. IF s_failed_commit IS NOT INITIAL. + ROLLBACK ENTITIES. z2ui5_cl_smps_context=>msg_display( client = client val = s_reported_commit-travel ). RETURN. @@ -146,12 +157,18 @@ CLASS z2ui5_cl_smps_app_008 IMPLEMENTATION. )->a( n = `type` v = `Information` ). DATA(table) = page->ele( `Table` - )->a( n = `items` v = client->_bind( t_drafts ) ). + )->a( n = `items` v = client->_bind( t_drafts ) + )->a( n = `noDataText` v = `No drafts yet - press Edit in the Enter Draft Mode app, then press refresh here` ). table->ele( `headerToolbar` )->ele( `Toolbar` )->tag( `Title` - )->a( n = `text` v = `UPDATE ... WITH %is_draft = mk-on` ). + )->a( n = `text` v = `UPDATE ... WITH %is_draft = mk-on` + )->tag( `ToolbarSpacer` + )->tag( `Button` + )->a( n = `press` v = client->_event( `REFRESH` ) + )->a( n = `icon` v = `sap-icon://refresh` + )->a( n = `tooltip` v = `Refresh` ). table->ele( `columns` )->ele( `Column` diff --git a/src/04/z2ui5_cl_smps_app_009.clas.abap b/src/04/z2ui5_cl_smps_app_009.clas.abap index 3b42615..7eca11d 100644 --- a/src/04/z2ui5_cl_smps_app_009.clas.abap +++ b/src/04/z2ui5_cl_smps_app_009.clas.abap @@ -58,6 +58,9 @@ CLASS z2ui5_cl_smps_app_009 IMPLEMENTATION. draft_activate( ). WHEN `DISCARD`. draft_discard( ). + WHEN `REFRESH`. + " a draft the Enter Draft Mode sample created in a tab of its own + data_read( ). ENDCASE. ENDIF. @@ -138,6 +141,7 @@ CLASS z2ui5_cl_smps_app_009 IMPLEMENTATION. result = abap_true. ELSE. + ROLLBACK ENTITIES. z2ui5_cl_smps_context=>msg_display( client = client val = s_reported-travel ). ENDIF. @@ -185,12 +189,18 @@ CLASS z2ui5_cl_smps_app_009 IMPLEMENTATION. )->a( n = `type` v = `Information` ). DATA(table) = page->ele( `Table` - )->a( n = `items` v = client->_bind( t_drafts ) ). + )->a( n = `items` v = client->_bind( t_drafts ) + )->a( n = `noDataText` v = `No drafts yet - press Edit in the Enter Draft Mode app, then press refresh here` ). table->ele( `headerToolbar` )->ele( `Toolbar` )->tag( `Title` - )->a( n = `text` v = `EXECUTE Activate / EXECUTE Discard` ). + )->a( n = `text` v = `EXECUTE Activate / EXECUTE Discard` + )->tag( `ToolbarSpacer` + )->tag( `Button` + )->a( n = `press` v = client->_event( `REFRESH` ) + )->a( n = `icon` v = `sap-icon://refresh` + )->a( n = `tooltip` v = `Refresh` ). table->ele( `columns` )->ele( `Column` diff --git a/src/04/z2ui5_cl_smps_app_010.clas.abap b/src/04/z2ui5_cl_smps_app_010.clas.abap index b24a0c7..b097b44 100644 --- a/src/04/z2ui5_cl_smps_app_010.clas.abap +++ b/src/04/z2ui5_cl_smps_app_010.clas.abap @@ -55,9 +55,19 @@ CLASS z2ui5_cl_smps_app_010 DEFINITION PUBLIC. METHODS popup_edit_display. METHODS data_read. + "! abap_false when the draft is gone - discarded or activated in + "! another session between the Edit and this read METHODS draft_read IMPORTING - uuid TYPE string. + uuid TYPE string + RETURNING + VALUE(result) TYPE abap_bool. + + "! writes the popup's fields into the draft - Save Draft and Activate both + "! start here, so what the user typed is never lost on the way + METHODS draft_update + RETURNING + VALUE(result) TYPE abap_bool. METHODS data_save RETURNING @@ -165,9 +175,11 @@ CLASS z2ui5_cl_smps_app_010 IMPLEMENTATION. IF data_save( ). - draft_read( uuid ). + DATA(draft_found) = draft_read( uuid ). data_read( ). - popup_edit_display( ). + IF draft_found = abap_true. + popup_edit_display( ). + ENDIF. ENDIF. @@ -176,27 +188,8 @@ CLASS z2ui5_cl_smps_app_010 IMPLEMENTATION. METHOD on_event_save_draft. - MODIFY ENTITIES OF z2ui5_r_smps_trd - ENTITY travel - UPDATE FIELDS ( agencyid customerid begindate enddate bookingfee currencycode description ) - WITH VALUE #( ( %tky = VALUE #( traveluuid = s_draft-travel_uuid - %is_draft = if_abap_behv=>mk-on ) - agencyid = s_draft-agency_id - customerid = s_draft-customer_id - begindate = s_draft-begin_date - enddate = s_draft-end_date - bookingfee = s_draft-booking_fee - currencycode = s_draft-currency - description = s_draft-description ) ) - FAILED DATA(s_failed) - REPORTED DATA(s_reported). - - IF s_failed-travel IS NOT INITIAL. - - ROLLBACK ENTITIES. - z2ui5_cl_smps_context=>msg_display( client = client val = s_reported-travel ). + IF draft_update( ) = abap_false. RETURN. - ENDIF. IF data_save( ). @@ -211,6 +204,12 @@ CLASS z2ui5_cl_smps_app_010 IMPLEMENTATION. METHOD on_event_activate. + " what the popup shows is what gets activated - fields changed since the + " last Save Draft go into the draft first, in the same transaction + IF draft_update( ) = abap_false. + RETURN. + ENDIF. + " the validations of the business object run during activation - " an invalid draft stays a draft and the messages are displayed MODIFY ENTITIES OF z2ui5_r_smps_trd @@ -265,6 +264,36 @@ CLASS z2ui5_cl_smps_app_010 IMPLEMENTATION. ENDMETHOD. + METHOD draft_update. + + MODIFY ENTITIES OF z2ui5_r_smps_trd + ENTITY travel + UPDATE FIELDS ( agencyid customerid begindate enddate bookingfee currencycode description ) + WITH VALUE #( ( %tky = VALUE #( traveluuid = s_draft-travel_uuid + %is_draft = if_abap_behv=>mk-on ) + agencyid = s_draft-agency_id + customerid = s_draft-customer_id + begindate = s_draft-begin_date + enddate = s_draft-end_date + bookingfee = s_draft-booking_fee + currencycode = s_draft-currency + description = s_draft-description ) ) + FAILED DATA(s_failed) + REPORTED DATA(s_reported). + + IF s_failed-travel IS NOT INITIAL. + + ROLLBACK ENTITIES. + z2ui5_cl_smps_context=>msg_display( client = client val = s_reported-travel ). + RETURN. + + ENDIF. + + result = abap_true. + + ENDMETHOD. + + METHOD draft_read. READ ENTITIES OF z2ui5_r_smps_trd @@ -273,7 +302,15 @@ CLASS z2ui5_cl_smps_app_010 IMPLEMENTATION. %is_draft = if_abap_behv=>mk-on ) ) ) RESULT DATA(t_result). - DATA(s_result) = t_result[ 1 ]. + " READ ENTITIES returns no row, and no exception, for a key that is not + " there - a table expression without a guard would dump on it + READ TABLE t_result INTO DATA(s_result) INDEX 1. + IF sy-subrc <> 0. + client->message_box_display( text = `The draft could not be read - it was discarded or activated in the meantime` + type = `error` ). + RETURN. + ENDIF. + s_draft = VALUE #( travel_uuid = uuid travel_id = |{ s_result-travelid ALPHA = OUT }| @@ -284,6 +321,7 @@ CLASS z2ui5_cl_smps_app_010 IMPLEMENTATION. booking_fee = |{ s_result-bookingfee }| currency = |{ s_result-currencycode }| description = |{ s_result-description }| ). + result = abap_true. ENDMETHOD. @@ -338,6 +376,7 @@ CLASS z2ui5_cl_smps_app_010 IMPLEMENTATION. result = abap_true. ELSE. + ROLLBACK ENTITIES. z2ui5_cl_smps_context=>msg_display( client = client val = s_reported-travel ). ENDIF. @@ -359,16 +398,18 @@ CLASS z2ui5_cl_smps_app_010 IMPLEMENTATION. )->a( n = `navButtonPress` v = client->_event_nav_app_leave( ) ). DATA(table) = page->ele( `Table` - )->a( n = `items` v = client->_bind( t_travels ) ). + )->a( n = `items` v = client->_bind( t_travels ) + )->a( n = `noDataText` v = `No travels yet - press Reset Demo Data` ). table->ele( `headerToolbar` )->ele( `Toolbar` )->tag( `Title` )->a( n = `text` v = `Travels (Z2UI5_R_SMPS_TRD)` )->tag( `ToolbarSpacer` )->tag( `Button` - )->a( n = `press` v = client->_event( `GENERATE` ) - )->a( n = `text` v = `Generate Demo Data` - )->a( n = `icon` v = `sap-icon://add` + )->a( n = `press` v = client->_event( `GENERATE` ) + )->a( n = `text` v = `Reset Demo Data` + )->a( n = `tooltip` v = `Discards every draft and deletes every travel, then creates the three demo travels again` + )->a( n = `icon` v = `sap-icon://reset` )->tag( `Button` )->a( n = `press` v = client->_event( `REFRESH` ) )->a( n = `icon` v = `sap-icon://refresh` diff --git a/src/05/README.md b/src/05/README.md index 92b66d8..c62d7db 100644 --- a/src/05/README.md +++ b/src/05/README.md @@ -44,7 +44,9 @@ Start them with `?app_start=z2ui5_cl_smps_app_011` and `?app_start=z2ui5_cl_smps_app_012`, or from the overview app `?app_start=z2ui5_cl_smps_app_000`, whose Open button puts each in its own tab. Open both in two browser tabs, create a ticket in the first, press refresh in the -second — the log entry the handler wrote is there. +second — the log entry the handler wrote is there. Then change a ticket's status +in the first tab's table and press *Update Status*: that is an update, so the +data event `StatusChanged` arrives, with its payload in the log text. Events are raised in the save sequence and consumed **afterwards**, so the log entry appears once the transaction is through, not during the roundtrip that @@ -95,12 +97,16 @@ the instances as a table, and writes them into the log: ```abap METHODS on_ticket_created FOR ENTITY EVENT - ticketcreated FOR z2ui5_r_smps_tck~TicketCreated. + ticketcreated FOR ticket~TicketCreated. METHODS on_status_changed FOR ENTITY EVENT - statuschanged FOR z2ui5_r_smps_tck~StatusChanged. + statuschanged FOR ticket~StatusChanged. ``` +`ticket` is the entity's alias from the behavior definition (`alias Ticket`) — +name the entity by it, not by `z2ui5_r_smps_tck`, or the extended check reports +that the alias should be used instead. + Nothing registers this class anywhere — the `FOR ENTITY EVENT` declaration *is* the subscription. Add a second handler and it runs too; delete this one and the BO still works. That is the property worth taking away: consumers come and go without @@ -134,6 +140,16 @@ they are not part of the repository, so the first publish creates them fresh. parser errors this package reports are about the linter, not about the code: it activates fine in an ABAP system. +**On ABAP Cloud, one open question.** The released-API check +(`npm run check:released-api`) reports the two data elements +`Z2UI5_E_SMPS_TCK_UUID` and `Z2UI5_E_SMPS_LOG_UUID`: they take the domain +`SYSUUID_X16`, and the released-object list of ABAP Cloud carries the data element +of that name, not the domain. Nobody has imported the package into a BTP ABAP +Environment yet to see whether that matters. If the two do not activate there, +that is the cause — and everything that types a key with them (both tables, the +CDS entities, the handler) fails after them. Recreate the two data elements in +your system with the predefined type `RAW` length 16, and the rest activates. + ## Where to go next - [`07` AMC/APC](../07/README.md) — the other half of the story: pushing what diff --git a/src/05/z2ui5_cl_smps_app_011.clas.abap b/src/05/z2ui5_cl_smps_app_011.clas.abap index 00dc963..e643dbd 100644 --- a/src/05/z2ui5_cl_smps_app_011.clas.abap +++ b/src/05/z2ui5_cl_smps_app_011.clas.abap @@ -7,10 +7,11 @@ CLASS z2ui5_cl_smps_app_011 DEFINITION PUBLIC CREATE PUBLIC. TYPES: BEGIN OF ty_s_ticket, - title TYPE z2ui5_e_smps_title, - priority TYPE z2ui5_e_smps_priority, - status TYPE z2ui5_e_smps_status, - created_by TYPE syuname, + ticket_uuid TYPE string, + title TYPE z2ui5_e_smps_title, + priority TYPE z2ui5_e_smps_priority, + status TYPE z2ui5_e_smps_status, + created_by TYPE syuname, END OF ty_s_ticket. DATA mt_tickets TYPE STANDARD TABLE OF ty_s_ticket WITH EMPTY KEY. @@ -28,9 +29,23 @@ CLASS z2ui5_cl_smps_app_011 DEFINITION PUBLIC CREATE PUBLIC. METHODS on_init. METHODS on_event. METHODS on_event_create. + METHODS on_event_update. METHODS data_read. METHODS view_display. + "! what to say when the business object refused a create or an update: + "! its own message where it sent one - a lock held by a draft of another + "! user, say - rather than a bare "failed" + "! @parameter action | Create or Update, the start of the text + "! @parameter msg | the first message of REPORTED, unbound when it is empty + "! @parameter result | the text for the message box + METHODS failure_text + IMPORTING + action TYPE string + msg TYPE REF TO if_abap_behv_message + RETURNING + VALUE(result) TYPE string. + PRIVATE SECTION. ENDCLASS. @@ -58,6 +73,8 @@ CLASS z2ui5_cl_smps_app_011 IMPLEMENTATION. CASE client->get_event( ). WHEN `CREATE`. on_event_create( ). + WHEN `UPDATE`. + on_event_update( ). WHEN `REFRESH`. data_read( ). view_display( ). @@ -78,16 +95,20 @@ CLASS z2ui5_cl_smps_app_011 IMPLEMENTATION. title = ms_create-title priority = ms_create-priority status = ms_create-status ) ) - FAILED DATA(failed). + FAILED DATA(failed) + REPORTED DATA(reported). IF failed-ticket IS NOT INITIAL. + DATA(text) = failure_text( action = `Create` + msg = VALUE #( reported-ticket[ 1 ]-%msg OPTIONAL ) ). ROLLBACK ENTITIES. - client->message_toast_display( `Create failed` ). + client->message_box_display( text = text type = `error` ). RETURN. ENDIF. COMMIT ENTITIES RESPONSE OF z2ui5_r_smps_tck - FAILED DATA(commit_failed). + FAILED DATA(commit_failed) + REPORTED DATA(commit_reported). IF commit_failed IS INITIAL. client->message_toast_display( |Ticket '{ ms_create-title }' created - business event fired| ). @@ -95,16 +116,83 @@ CLASS z2ui5_cl_smps_app_011 IMPLEMENTATION. data_read( ). view_display( ). ELSE. - client->message_toast_display( `Save failed` ). + text = failure_text( action = `Create` + msg = VALUE #( commit_reported-ticket[ 1 ]-%msg OPTIONAL ) ). + ROLLBACK ENTITIES. + client->message_box_display( text = text type = `error` ). + ENDIF. + ENDMETHOD. + + METHOD on_event_update. + " OPTIONAL: the uuid comes from the client, and the row it names may be + " gone by now - deleted in another session, or out of the top 50 + DATA(uuid) = client->get_event_arg( ). + DATA(s_ticket) = VALUE #( mt_tickets[ ticket_uuid = uuid ] OPTIONAL ). + IF s_ticket IS INITIAL. + client->message_toast_display( `Ticket not found - press refresh` ). + RETURN. + ENDIF. + + " Update the status via the RAP business object -> the additional save + " sees the update and raises the data event StatusChanged with its payload + MODIFY ENTITIES OF z2ui5_r_smps_tck + ENTITY Ticket + UPDATE FIELDS ( status ) + WITH VALUE #( ( ticketuuid = s_ticket-ticket_uuid + status = s_ticket-status ) ) + FAILED DATA(failed) + REPORTED DATA(reported). + + IF failed-ticket IS NOT INITIAL. + DATA(text) = failure_text( action = `Update` + msg = VALUE #( reported-ticket[ 1 ]-%msg OPTIONAL ) ). + ROLLBACK ENTITIES. + client->message_box_display( text = text type = `error` ). + RETURN. + ENDIF. + + COMMIT ENTITIES RESPONSE OF z2ui5_r_smps_tck + FAILED DATA(commit_failed) + REPORTED DATA(commit_reported). + + IF commit_failed IS INITIAL. + client->message_toast_display( |Ticket '{ s_ticket-title }' set to { s_ticket-status } - business event fired| ). + data_read( ). + view_display( ). + ELSE. + text = failure_text( action = `Update` + msg = VALUE #( commit_reported-ticket[ 1 ]-%msg OPTIONAL ) ). + ROLLBACK ENTITIES. + client->message_box_display( text = text type = `error` ). ENDIF. ENDMETHOD. METHOD data_read. SELECT FROM z2ui5_t_smps_tck "#EC CI_NOWHERE - FIELDS title, priority, status, created_by + FIELDS ticket_uuid, title, priority, status, created_by ORDER BY created_at DESCENDING - INTO CORRESPONDING FIELDS OF TABLE @mt_tickets + INTO TABLE @DATA(t_result) UP TO 50 ROWS. + + " the key travels to the browser as text - a RAW16 has no JSON form + mt_tickets = VALUE #( FOR s_result IN t_result + ( ticket_uuid = |{ s_result-ticket_uuid }| + title = s_result-title + priority = s_result-priority + status = s_result-status + created_by = s_result-created_by ) ). + ENDMETHOD. + + METHOD failure_text. + + IF msg IS BOUND. + result = |{ action } refused by the business object: { msg->if_message~get_text( ) }|. + ELSE. + " FAILED without a message in REPORTED - say that much rather than + " leave the reader with a bare "failed" + result = |{ action } refused by the business object, which sent no message with it|. + ENDIF. + ENDMETHOD. METHOD view_display. @@ -117,10 +205,19 @@ CLASS z2ui5_cl_smps_app_011 IMPLEMENTATION. )->a( n = `xmlns:form` v = `sap.ui.layout.form` ). DATA(page) = view->ele( `Shell` )->ele( `Page` - )->a( n = `title` v = `RAP Events Demo - Tickets (abap2UI5)` + )->a( n = `title` v = `abap2UI5 - Business Events - Tickets` )->a( n = `showNavButton` b = client->check_app_prev_stack( ) )->a( n = `navButtonPress` v = client->_event_nav_app_leave( ) ). + page->tag( `MessageStrip` + )->a( n = `text` v = `Create a ticket and the business object raises the notification event TicketCreated. ` && + `Change a status in the table and press Update Status, and it raises the data event ` && + `StatusChanged with the new values. What the handler made of both is in the event log ` && + `app - open it in a second tab and press refresh there.` + )->a( n = `type` v = `Information` + )->a( n = `showIcon` v = `true` + )->a( n = `class` v = `sapUiSmallMargin` ). + " --- create form --- page->ele( n = `SimpleForm` ns = `form` )->a( n = `editable` b = abap_true @@ -144,7 +241,8 @@ CLASS z2ui5_cl_smps_app_011 IMPLEMENTATION. " --- tickets table --- DATA(table) = page->ele( `Table` - )->a( n = `items` v = client->_bind( mt_tickets ) ). + )->a( n = `items` v = client->_bind( mt_tickets ) + )->a( n = `noDataText` v = `No tickets yet - create one above` ). table->ele( `headerToolbar` )->ele( `Toolbar` )->tag( `Title` @@ -170,7 +268,11 @@ CLASS z2ui5_cl_smps_app_011 IMPLEMENTATION. )->end( )->ele( `Column` )->tag( `Text` - )->a( n = `text` v = `Created By` ). + )->a( n = `text` v = `Created By` + )->end( + )->ele( `Column` + )->tag( `Text` + )->a( n = `text` v = `` ). table->ele( `items` )->ele( `ColumnListItem` @@ -179,10 +281,14 @@ CLASS z2ui5_cl_smps_app_011 IMPLEMENTATION. )->a( n = `text` v = `{TITLE}` )->tag( `Text` )->a( n = `text` v = `{PRIORITY}` + )->tag( `Input` + )->a( n = `value` v = `{STATUS}` )->tag( `Text` - )->a( n = `text` v = `{STATUS}` - )->tag( `Text` - )->a( n = `text` v = `{CREATED_BY}` ). + )->a( n = `text` v = `{CREATED_BY}` + )->tag( `Button` + )->a( n = `press` v = client->_event( val = `UPDATE` + arg = `${TICKET_UUID}` ) + )->a( n = `text` v = `Update Status` ). client->view_display( view->stringify( ) ). ENDMETHOD. diff --git a/src/05/z2ui5_cl_smps_app_012.clas.abap b/src/05/z2ui5_cl_smps_app_012.clas.abap index c45d9a1..90aebed 100644 --- a/src/05/z2ui5_cl_smps_app_012.clas.abap +++ b/src/05/z2ui5_cl_smps_app_012.clas.abap @@ -55,12 +55,13 @@ CLASS z2ui5_cl_smps_app_012 IMPLEMENTATION. )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc` ). DATA(page) = view->ele( `Shell` )->ele( `Page` - )->a( n = `title` v = `RAP Events Demo - Event Log (abap2UI5)` + )->a( n = `title` v = `abap2UI5 - Business Events - Event Log` )->a( n = `showNavButton` b = client->check_app_prev_stack( ) )->a( n = `navButtonPress` v = client->_event_nav_app_leave( ) ). DATA(table) = page->ele( `Table` - )->a( n = `items` v = client->_bind( mt_log ) ). + )->a( n = `items` v = client->_bind( mt_log ) + )->a( n = `noDataText` v = `No events yet - create a ticket in the Tickets app, then press refresh here. The handler runs after the commit, so an entry can take a moment to arrive` ). table->ele( `headerToolbar` )->ele( `Toolbar` )->tag( `Title` diff --git a/src/06/README.md b/src/06/README.md index 468f377..039b786 100644 --- a/src/06/README.md +++ b/src/06/README.md @@ -21,8 +21,8 @@ is on-premise by design, not by omission. — this package alone, without the other nine on your system. ABAP Standard (on-premise). The locks go through the function modules -`ENQUEUE_E_TABLE` and `ENQUEUE_READ`, which are available there — `485`'s own page -title points this out. +`ENQUEUE_E_TABLE`, `DEQUEUE_E_TABLE` and `ENQUEUE_READ`, which are available +there — the page title of `Z2UI5_CL_SMPS_APP_485` points this out. The lock table `Z2UI5_T_SMPS_01` comes with this package ([`src/06/01`](01)); after the import it only has to be activated, it is never diff --git a/src/06/z2ui5_cl_smps_app_485.clas.abap b/src/06/z2ui5_cl_smps_app_485.clas.abap index 6005501..a58e19c 100644 --- a/src/06/z2ui5_cl_smps_app_485.clas.abap +++ b/src/06/z2ui5_cl_smps_app_485.clas.abap @@ -40,8 +40,6 @@ CLASS z2ui5_cl_smps_app_485 IMPLEMENTATION. METHOD initialize_view. - set_session_stateful( client = client stateful = abap_true ). - DATA(view) = z2ui5_cl_ui5_view_builder=>factory( )->ele( n = `View` ns = `mvc` )->a( n = `displayBlock` v = `true` @@ -52,7 +50,7 @@ CLASS z2ui5_cl_smps_app_485 IMPLEMENTATION. DATA(page) = view->ele( `Shell` )->ele( `Page` - )->a( n = `title` v = `abap2UI5 - Sample: Sticky Session with locks - (ABAP Standard Only)` + )->a( n = `title` v = `abap2UI5 - Sample: Sticky Session with Locks (ABAP Standard Only)` )->a( n = `showNavButton` b = client->check_app_prev_stack( ) )->a( n = `navButtonPress` v = client->_event( `BACK` ) ). @@ -61,6 +59,14 @@ CLASS z2ui5_cl_smps_app_485 IMPLEMENTATION. )->a( n = `type` v = `Error` )->a( n = `visible` v = client->_bind( error-flag ) ). + page->tag( `MessageStrip` + )->a( n = `text` v = `Press Lock, then Refresh lock status - SM12 holds the lock now. End session and refresh ` && + `again: the lock went away with the session that held it, so start the session again ` && + `before the next Lock. Rollback Work releases it while the session goes on.` + )->a( n = `type` v = `Information` + )->a( n = `showIcon` v = `true` + )->a( n = `class` v = `sapUiSmallMarginBottom` ). + DATA(vbox) = page->ele( `VBox` ). DATA(hbox) = vbox->ele( `HBox` @@ -91,7 +97,7 @@ CLASS z2ui5_cl_smps_app_485 IMPLEMENTATION. hbox->tag( `Button` )->a( n = `press` v = client->_event( `REFRESH` ) - )->a( n = `text` v = `Refresh lock counter` ). + )->a( n = `text` v = `Refresh lock status` ). hbox->tag( `Button` )->a( n = `press` v = client->_event( `ROLLBACK` ) @@ -114,7 +120,7 @@ CLASS z2ui5_cl_smps_app_485 IMPLEMENTATION. client->nav_app_leave( ). WHEN `LOCK`. lcl_locking=>acquire_lock( ). - client->message_toast_display( `Lock acquired. Press 'Refresh lock counter'` ). + client->message_toast_display( `Lock acquired. Press 'Refresh lock status'` ). WHEN `END_SESSION`. set_session_stateful( client = client stateful = abap_false ). WHEN `START_SESSION`. @@ -122,8 +128,16 @@ CLASS z2ui5_cl_smps_app_485 IMPLEMENTATION. WHEN `REFRESH`. update_lock_counter( ). WHEN `ROLLBACK`. + " read before and after, so the toast says what the ROLLBACK + " released and not what the last Refresh happened to show + DATA(locks_before) = lcl_locking=>get_lock_counter( ). ROLLBACK WORK. - client->message_toast_display( |ROLLBACK WORK done, { lock_counter } locks released. Press 'Refresh lock counter'| ). + update_lock_counter( ). + client->message_toast_display( COND #( WHEN locks_before > 0 AND lock_counter = 0 + THEN `ROLLBACK WORK done, the lock is released` + WHEN lock_counter > 0 + THEN `ROLLBACK WORK done, the lock is still held` + ELSE `ROLLBACK WORK done, there was no lock to release` ) ). ENDCASE. ENDMETHOD. @@ -151,22 +165,30 @@ CLASS z2ui5_cl_smps_app_485 IMPLEMENTATION. error = VALUE #( ). IF client->check_on_init( ). - update_lock_counter( ). + " the view first: the counter reads the lock table through a + " function module that can fail, and the bound text reaches the + " browser either way - after the view, a failure leaves a screen + " under the error box instead of none + set_session_stateful( client = client stateful = abap_true ). initialize_view( client ). + update_lock_counter( ). ELSEIF client->check_on_navigated( ). + " the session as the user left it - switching it back on here + " would undo an "End session" on every navigation + set_session_stateful( client = client stateful = session_is_stateful ). initialize_view( client ). + ELSEIF client->check_on_event( ). + TRY. + on_event( client ). + " a lock that could not be taken is the outcome this sample is + " about, so it is shown in the MessageStrip of the view rather + " than in a popup - see lcx_error in the local implementations + CATCH lcx_error INTO DATA(x_error). + error-text = x_error->get_text( ). + error-flag = abap_true. + ENDTRY. ENDIF. - TRY. - on_event( client ). - " a lock that could not be taken is the outcome this sample is about, - " so it is shown in the MessageStrip of the view rather than in a - " popup - see lcx_error in the local implementations - CATCH lcx_error INTO DATA(x_error). - error-text = x_error->get_text( ). - error-flag = abap_true. - ENDTRY. - CATCH cx_root INTO DATA(lx). client->message_box_display( lx->get_text( ) ). ENDTRY. @@ -176,8 +198,13 @@ CLASS z2ui5_cl_smps_app_485 IMPLEMENTATION. METHOD update_lock_counter. + " the counter belongs to the ONE lock entry of this sample: every Lock + " the session presses again on the key it already holds counts it up, + " SM12 still shows a single entry lock_counter = lcl_locking=>get_lock_counter( ). - lock_text = |There are { lock_counter } SM12 locks|. + lock_text = COND #( WHEN lock_counter = 0 + THEN |No lock on { lcl_locking=>lock_key } in SM12| + ELSE |SM12 holds the lock on { lcl_locking=>lock_key } - requested { lock_counter } time(s)| ). ENDMETHOD. diff --git a/src/06/z2ui5_cl_smps_app_485.clas.locals_imp.abap b/src/06/z2ui5_cl_smps_app_485.clas.locals_imp.abap index e0622d7..f6829d4 100644 --- a/src/06/z2ui5_cl_smps_app_485.clas.locals_imp.abap +++ b/src/06/z2ui5_cl_smps_app_485.clas.locals_imp.abap @@ -93,6 +93,11 @@ CLASS lcl_locking DEFINITION CREATE PRIVATE. gusevbt TYPE n LENGTH 10, END OF ty_seqg3. + " the key this sample locks. Z2UI5_T_SMPS_01 is client-dependent, and + " ENQUEUE_E_TABLE takes the table key as one string, client included - + " see lock_argument + CONSTANTS lock_key TYPE c LENGTH 4 VALUE 'Z100'. + CLASS-METHODS acquire_lock. CLASS-METHODS get_lock_counter @@ -101,24 +106,37 @@ CLASS lcl_locking DEFINITION CREATE PRIVATE. PROTECTED SECTION. PRIVATE SECTION. + CLASS-METHODS lock_argument + RETURNING + VALUE(result) TYPE char120. + + "! the message a failed function module left in sy-msg* + "! @parameter fallback | the text when it left none + "! @parameter result | the message text, else the fallback + CLASS-METHODS message_text + IMPORTING + fallback TYPE string + RETURNING + VALUE(result) TYPE string. + ENDCLASS. CLASS lcl_locking IMPLEMENTATION. METHOD acquire_lock. + DATA(lv_varkey) = lock_argument( ). DATA(lv_fm) = 'ENQUEUE_E_TABLE'. CALL FUNCTION lv_fm EXPORTING tabname = 'Z2UI5_T_SMPS_01' - varkey = 'Z100' + varkey = lv_varkey EXCEPTIONS foreign_lock = 1 system_failure = 2 OTHERS = 3. IF sy-subrc <> 0. - MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4 INTO DATA(error_text). - RAISE EXCEPTION TYPE lcx_error EXPORTING val = error_text. + RAISE EXCEPTION TYPE lcx_error EXPORTING val = message_text( |Lock on { lock_key } could not be set (sy-subrc { sy-subrc })| ). ENDIF. ENDMETHOD. @@ -127,13 +145,14 @@ CLASS lcl_locking IMPLEMENTATION. METHOD get_lock_counter. DATA enqueue_table TYPE STANDARD TABLE OF ty_seqg3 WITH EMPTY KEY. - DATA argument TYPE c LENGTH 150. - argument = |Z2UI5_T_SMPS_01 Z100*|. - + " no GARG filter: the lock argument of E_TABLE is the table name padded to + " the length of RSTABLE-TABNAME followed by the key, and a filter string + " that gets that padding wrong matches nothing. All locks of this user are + " read and the one of this sample is picked out below - by lock object, + " table name and key, independent of the column layout DATA(lv_fm) = 'ENQUEUE_READ'. CALL FUNCTION lv_fm EXPORTING - garg = argument guname = sy-uname TABLES enq = enqueue_table @@ -142,11 +161,39 @@ CLASS lcl_locking IMPLEMENTATION. system_failure = 2 OTHERS = 3. IF sy-subrc <> 0. - MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4 INTO DATA(error_text). - RAISE EXCEPTION TYPE lcx_error EXPORTING val = error_text. + RAISE EXCEPTION TYPE lcx_error EXPORTING val = message_text( |Lock entries could not be read (sy-subrc { sy-subrc })| ). ENDIF. - result = VALUE #( enqueue_table[ 1 ]-gusevb OPTIONAL ). + DATA(lv_pattern) = |Z2UI5_T_SMPS_01*{ lock_argument( ) }*|. + LOOP AT enqueue_table INTO DATA(ls_enqueue). + IF ls_enqueue-gobj = 'E_TABLE' AND ls_enqueue-garg CP lv_pattern. + " the cumulative counter of the update task owner - ENQUEUE_E_TABLE + " locks with the default _SCOPE 2, which hands the lock to it + result = ls_enqueue-gusevb. + RETURN. + ENDIF. + ENDLOOP. + + ENDMETHOD. + + + METHOD message_text. + + " a function module that fails without a message leaves sy-msgid empty - + " and a MESSAGE statement on an empty message is no text to show + IF sy-msgid IS NOT INITIAL. + MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4 INTO result. + ENDIF. + IF result IS INITIAL. + result = fallback. + ENDIF. + + ENDMETHOD. + + + METHOD lock_argument. + + result = |{ sy-mandt }{ lock_key }|. ENDMETHOD. diff --git a/src/06/z2ui5_cl_smps_app_486.clas.abap b/src/06/z2ui5_cl_smps_app_486.clas.abap index 1d3d80d..fe74594 100644 --- a/src/06/z2ui5_cl_smps_app_486.clas.abap +++ b/src/06/z2ui5_cl_smps_app_486.clas.abap @@ -34,12 +34,18 @@ CLASS z2ui5_cl_smps_app_486 IMPLEMENTATION. TRY. - IF client->check_on_navigated( ). + IF client->check_on_init( ). + set_session_stateful( client = client stateful = abap_true ). initialize_view( client ). + ELSEIF client->check_on_navigated( ). + " the session as the user left it - switching it back on here + " would undo an "End session" on every navigation + set_session_stateful( client = client stateful = session_is_stateful ). + initialize_view( client ). + ELSEIF client->check_on_event( ). + on_event( client ). ENDIF. - on_event( client ). - CATCH cx_root INTO DATA(lx). client->message_box_display( lx->get_text( ) ). ENDTRY. @@ -49,8 +55,6 @@ CLASS z2ui5_cl_smps_app_486 IMPLEMENTATION. METHOD initialize_view. - set_session_stateful( client = client stateful = abap_true ). - DATA(view) = z2ui5_cl_ui5_view_builder=>factory( )->ele( n = `View` ns = `mvc` )->a( n = `displayBlock` v = `true` diff --git a/src/06/z2ui5_cl_smps_app_490.clas.abap b/src/06/z2ui5_cl_smps_app_490.clas.abap index d194cff..c4f0d32 100644 --- a/src/06/z2ui5_cl_smps_app_490.clas.abap +++ b/src/06/z2ui5_cl_smps_app_490.clas.abap @@ -5,7 +5,7 @@ CLASS z2ui5_cl_smps_app_490 DEFINITION PUBLIC. PUBLIC SECTION. INTERFACES z2ui5_if_app. - DATA text TYPE string VALUE `call booking mask`. + DATA text TYPE string VALUE `Open the first lock view`. DATA varkey TYPE char120. METHODS initialize_view2 @@ -15,6 +15,16 @@ CLASS z2ui5_cl_smps_app_490 DEFINITION PUBLIC. PROTECTED SECTION. DATA view_id TYPE i. + "! ENQUEUE_E_TABLE (abap_true) or DEQUEUE_E_TABLE (abap_false) on this + "! view's VARKEY in Z2UI5_T_SMPS_01 + "! @parameter acquire | abap_true sets the lock, abap_false releases it + "! @parameter result | the message of a lock that could not be set, else empty + METHODS lock + IMPORTING + acquire TYPE abap_bool + RETURNING + VALUE(result) TYPE string. + PRIVATE SECTION. ENDCLASS. @@ -29,6 +39,9 @@ CLASS z2ui5_cl_smps_app_490 IMPLEMENTATION. view_id = 1. TRY. IF client->check_on_navigated( ). + " the last lock view has left (or the app just started) - nothing + " is locked any more, so the session does not need to stay + client->set_session_stateful( abap_false ). DATA(view) = z2ui5_cl_ui5_view_builder=>factory( )->ele( n = `View` ns = `mvc` )->a( n = `displayBlock` v = `true` @@ -38,7 +51,16 @@ CLASS z2ui5_cl_smps_app_490 IMPLEMENTATION. )->a( n = `xmlns:form` v = `sap.ui.layout.form` ). DATA(page) = view->ele( `Shell` )->ele( `Page` - )->a( n = `title` v = `Startview` ). + )->a( n = `title` v = `abap2UI5 - Sample: Sticky Session with Navigation and Locks` + )->a( n = `showNavButton` b = client->check_app_prev_stack( ) + )->a( n = `navButtonPress` v = client->_event_nav_app_leave( ) ). + page->tag( `MessageStrip` + )->a( n = `text` v = `Every lock view takes the next key of table Z2UI5_T_SMPS_01 and holds an ENQUEUE ` && + `lock on it while the session is stateful - keep SM12 open and watch them line up. ` && + `Back releases the lock of the view you leave, and only that one.` + )->a( n = `type` v = `Information` + )->a( n = `showIcon` v = `true` + )->a( n = `class` v = `sapUiSmallMargin` ). page->ele( n = `SimpleForm` ns = `form` )->ele( n = `content` ns = `form` )->tag( `Button` @@ -52,7 +74,9 @@ CLASS z2ui5_cl_smps_app_490 IMPLEMENTATION. IF client->check_on_event( `CALL_BOOKING_MASK` ). DATA(lr_view2) = NEW z2ui5_cl_smps_app_490( ). lr_view2->view_id = 2. - lr_view2->varkey = `001`. + " four digits, the width NEXT_LOCK counts up in - so the first + " lock reads 0001 and the next one 0002, not 001 and 0002 + lr_view2->varkey = `0001`. client->nav_app_call( lr_view2 ). RETURN. ENDIF. @@ -63,42 +87,30 @@ CLASS z2ui5_cl_smps_app_490 IMPLEMENTATION. ELSEIF view_id = 2. TRY. + " every lock view holds its own lock in the one stateful session the + " stack shares: the first one switches the session on, the start view + " switches it off again once the last lock view has left IF client->check_on_init( ). - - DATA(lv_fm) = `ENQUEUE_E_TABLE`. - CALL FUNCTION lv_fm - EXPORTING - tabname = `Z2UI5_T_SMPS_01` - varkey = varkey - EXCEPTIONS - foreign_lock = 1 - system_failure = 2 - OTHERS = 3. - - IF sy-subrc <> 0. - client->set_session_stateful( abap_false ). + DATA(lv_error) = lock( abap_true ). + IF lv_error IS NOT INITIAL. + client->message_toast_display( lv_error ). client->nav_app_leave( ). - - ELSE. - - client->set_session_stateful( ). - initialize_view2( client ). + RETURN. ENDIF. + client->set_session_stateful( ). + initialize_view2( client ). RETURN. ENDIF. + " back from the lock view above: its lock is released, this one's is + " still held - the screen comes back as it was IF client->check_on_navigated( ). - client->set_session_stateful( abap_false ). - TRY. - client->nav_app_leave( ). - RETURN. - CATCH cx_sy_move_cast_error ##NO_HANDLER ##CATCH_ALL. - ENDTRY. + initialize_view2( client ). + RETURN. ENDIF. CASE client->get_event( ). WHEN `NEXT_LOCK`. - client->set_session_stateful( abap_false ). lr_view2 = NEW z2ui5_cl_smps_app_490( ). lr_view2->view_id = 2. lf_new_varkey = varkey+0(4). @@ -107,7 +119,8 @@ CLASS z2ui5_cl_smps_app_490 IMPLEMENTATION. client->nav_app_call( lr_view2 ). RETURN. WHEN `BACK`. - client->set_session_stateful( abap_false ). + " going back releases this view's lock and no other + lock( abap_false ). client->nav_app_leave( ). RETURN. ENDCASE. @@ -120,6 +133,46 @@ CLASS z2ui5_cl_smps_app_490 IMPLEMENTATION. ENDMETHOD. + METHOD lock. + + " the table is client-dependent, so its key - and with it the lock + " argument E_TABLE takes - starts with the client + DATA lv_varkey TYPE char120. + DATA lv_fm TYPE string. + + lv_varkey = |{ sy-mandt }{ varkey }|. + + IF acquire = abap_false. + lv_fm = `DEQUEUE_E_TABLE`. + CALL FUNCTION lv_fm + EXPORTING + tabname = 'Z2UI5_T_SMPS_01' + varkey = lv_varkey. + RETURN. + ENDIF. + + lv_fm = `ENQUEUE_E_TABLE`. + CALL FUNCTION lv_fm + EXPORTING + tabname = 'Z2UI5_T_SMPS_01' + varkey = lv_varkey + EXCEPTIONS + foreign_lock = 1 + system_failure = 2 + OTHERS = 3. + IF sy-subrc <> 0. + IF sy-msgid IS NOT INITIAL. + MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4 INTO result. + ENDIF. + " an empty result reads as success to the caller - never on a failure + IF result IS INITIAL. + result = |Lock on { varkey } could not be set (sy-subrc { sy-subrc })|. + ENDIF. + ENDIF. + + ENDMETHOD. + + METHOD initialize_view2. DATA(view) = z2ui5_cl_ui5_view_builder=>factory( @@ -130,14 +183,14 @@ CLASS z2ui5_cl_smps_app_490 IMPLEMENTATION. )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc` ). DATA(page) = view->ele( `Shell` )->ele( `Page` - )->a( n = `title` v = `Stateful Application with lock` + )->a( n = `title` v = `abap2UI5 - Sample: Lock View` )->a( n = `showNavButton` b = client->check_app_prev_stack( ) )->a( n = `navButtonPress` v = client->_event( `BACK` ) ). DATA(vbox) = page->ele( `VBox` ). DATA(hbox) = vbox->ele( `HBox` )->a( n = `alignItems` v = `Center` ). hbox->tag( `Title` - )->a( n = `text` v = `Current Lock Value in Table ZTEST` ). + )->a( n = `text` v = `Current Lock Value in Table Z2UI5_T_SMPS_01` ). hbox->tag( `Input` )->a( n = `editable` b = abap_false )->a( n = `value` v = client->_bind( varkey ) ). diff --git a/src/07/z2ui5_cl_smps_app_489.clas.abap b/src/07/z2ui5_cl_smps_app_489.clas.abap index 1060d34..041e9e2 100644 --- a/src/07/z2ui5_cl_smps_app_489.clas.abap +++ b/src/07/z2ui5_cl_smps_app_489.clas.abap @@ -298,7 +298,7 @@ CLASS z2ui5_cl_smps_app_489 IMPLEMENTATION. SELECT SINGLE FROM icfservloc FIELDS icfactive - WHERE icf_name = `Z2UI5_APC_SMP_2` + WHERE icf_name = @( `Z2UI5_APC_SMP_2` ) INTO @DATA(icfactive). DATA(view) = z2ui5_cl_ui5_view_builder=>factory( diff --git a/src/07/z2ui5_cl_smps_app_489_ws.clas.abap b/src/07/z2ui5_cl_smps_app_489_ws.clas.abap index ee1b34f..afa2152 100644 --- a/src/07/z2ui5_cl_smps_app_489_ws.clas.abap +++ b/src/07/z2ui5_cl_smps_app_489_ws.clas.abap @@ -45,10 +45,13 @@ CLASS z2ui5_cl_smps_app_489_ws IMPLEMENTATION. METHOD if_apc_wsp_extension~on_message. + " MESSAGE ... TYPE 'X' rather than RAISE SHORTDUMP: both end the request + " in a short dump that names the exception's text, but RAISE SHORTDUMP + " only exists from 7.53 on, and this package runs from 7.50 TRY. send( i_message->get_text( ) ). CATCH cx_root INTO DATA(error). - RAISE SHORTDUMP error. + MESSAGE error TYPE 'X'. ENDTRY. ENDMETHOD. @@ -65,7 +68,7 @@ CLASS z2ui5_cl_smps_app_489_ws IMPLEMENTATION. get_producer( )->send( c_msg-__new_connection__ ). CATCH cx_root INTO DATA(error). - RAISE SHORTDUMP error. + MESSAGE error TYPE 'X'. ENDTRY. ENDMETHOD. @@ -77,7 +80,7 @@ CLASS z2ui5_cl_smps_app_489_ws IMPLEMENTATION. get_producer( )->send( c_msg-__closed__ ). CATCH cx_root INTO DATA(error). - RAISE SHORTDUMP error. + MESSAGE error TYPE 'X'. ENDTRY. ENDMETHOD. diff --git a/src/07/z2ui5_cl_smps_app_489_ws.clas.xml b/src/07/z2ui5_cl_smps_app_489_ws.clas.xml index 00f957a..da19fa0 100644 --- a/src/07/z2ui5_cl_smps_app_489_ws.clas.xml +++ b/src/07/z2ui5_cl_smps_app_489_ws.clas.xml @@ -5,7 +5,7 @@ Z2UI5_CL_SMPS_APP_489_WS E - Generated APC WebSocket protocol impementation class + Generated APC WebSocket protocol implementation class 1 X X diff --git a/src/08/z2ui5_cl_smps_app_487.clas.abap b/src/08/z2ui5_cl_smps_app_487.clas.abap index 2ababee..911a15f 100644 --- a/src/08/z2ui5_cl_smps_app_487.clas.abap +++ b/src/08/z2ui5_cl_smps_app_487.clas.abap @@ -1,4 +1,4 @@ -" @keywords mime audio sound play_audio wav follow_up_action +" @keywords mime audio sound play_audio mp3 follow_up_action " @summary a success and an error tone, addressed by their ICF path CLASS z2ui5_cl_smps_app_487 DEFINITION PUBLIC. @@ -29,10 +29,10 @@ CLASS z2ui5_cl_smps_app_487 IMPLEMENTATION. IF client->check_on_navigated( ). view_display( ). + ELSEIF client->check_on_event( ). + on_event( ). ENDIF. - on_event( ). - ENDMETHOD. @@ -48,15 +48,18 @@ CLASS z2ui5_cl_smps_app_487 IMPLEMENTATION. SELECT SINGLE FROM icfservloc FIELDS icfactive - WHERE icf_name = `MIME_DEMO` + WHERE icf_name = @( `MIME_DEMO` ) INTO @DATA(icfactive). " Note, these are demo sounds and are part of the abap2UI5 sample repo. " They are NOT meant to use in production. - DATA(vbox) = view->ele( `Page` - )->a( n = `title` v = `Play success and error sounds` - )->ele( `VBox` - )->a( n = `class` v = `sapUiSmallMargin` ). + DATA(vbox) = view->ele( `Shell` + )->ele( `Page` + )->a( n = `title` v = `abap2UI5 - Sample: Play Success and Error Sounds` + )->a( n = `showNavButton` b = client->check_app_prev_stack( ) + )->a( n = `navButtonPress` v = client->_event_nav_app_leave( ) + )->ele( `VBox` + )->a( n = `class` v = `sapUiSmallMargin` ). IF icfactive = abap_false. vbox->tag( `MessageStrip` @@ -78,7 +81,7 @@ CLASS z2ui5_cl_smps_app_487 IMPLEMENTATION. )->a( n = `submit` v = client->_event( `enter` ) ). vbox->tag( `Button` )->a( n = `press` v = client->_event( `enter` ) - )->a( n = `text` v = `submit` + )->a( n = `text` v = `Submit` )->a( n = `type` v = `Accept` ). client->view_display( view->stringify( ) ). diff --git a/src/09/README.md b/src/09/README.md index e382046..25dd007 100644 --- a/src/09/README.md +++ b/src/09/README.md @@ -31,8 +31,10 @@ The target mapping's URL is the abap2UI5 ICF node plus the app to start: The framework detects the launchpad from that context (`scenario=LAUNCHPAD` in the query, or `/ui2/flp` / `test/flpSandbox` in the path) and sets -`check_launchpad_active`. Every sample here checks the flag and tells you with a -message box when it was started standalone — where it then has no shell to talk to. +`check_launchpad_active`. `481` to `483` check the flag and tell you with a message +box when they were started standalone — where they then have no shell to talk to; +`484`, the receiver, shows the flag in its form and hides its two navigation +buttons instead. ## The samples @@ -47,7 +49,7 @@ Start any of them with `?app_start=z2ui5_cl_smps_app_` — from a tile, that what the target mapping's URL carries. The overview app `?app_start=z2ui5_cl_smps_app_000` lists them too, but its Open button starts them standalone, and standalone is exactly the case where they have no shell to talk to -and say so in a message box. +and say so. ## The one pair worth configuring diff --git a/src/09/z2ui5_cl_smps_app_481.clas.abap b/src/09/z2ui5_cl_smps_app_481.clas.abap index 6a576b2..d4685aa 100644 --- a/src/09/z2ui5_cl_smps_app_481.clas.abap +++ b/src/09/z2ui5_cl_smps_app_481.clas.abap @@ -72,8 +72,9 @@ CLASS z2ui5_cl_smps_app_481 IMPLEMENTATION. )->tag( `Label` )->a( n = `text` v = `` )->tag( `Button` - )->a( n = `press` v = client->_event_nav_app_leave( ) - )->a( n = `text` v = `Go Back` ). + )->a( n = `press` v = client->_event_nav_app_leave( ) + )->a( n = `text` v = `Go Back` + )->a( n = `visible` b = client->check_app_prev_stack( ) ). client->view_display( view->stringify( ) ). diff --git a/src/09/z2ui5_cl_smps_app_482.clas.abap b/src/09/z2ui5_cl_smps_app_482.clas.abap index 75376bf..2a2367a 100644 --- a/src/09/z2ui5_cl_smps_app_482.clas.abap +++ b/src/09/z2ui5_cl_smps_app_482.clas.abap @@ -75,8 +75,9 @@ CLASS z2ui5_cl_smps_app_482 IMPLEMENTATION. )->a( n = `press` v = client->_event( `SET_TITLE` ) )->a( n = `text` v = `Set Title` )->tag( `Button` - )->a( n = `press` v = client->_event_nav_app_leave( ) - )->a( n = `text` v = `Go Back` ). + )->a( n = `press` v = client->_event_nav_app_leave( ) + )->a( n = `text` v = `Go Back` + )->a( n = `visible` b = client->check_app_prev_stack( ) ). client->view_display( view->stringify( ) ). diff --git a/src/10/01/z2ui5_cl_smps_llm_sm59.clas.abap b/src/10/01/z2ui5_cl_smps_llm_sm59.clas.abap index a13e624..798a7be 100644 --- a/src/10/01/z2ui5_cl_smps_llm_sm59.clas.abap +++ b/src/10/01/z2ui5_cl_smps_llm_sm59.clas.abap @@ -102,9 +102,22 @@ CLASS z2ui5_cl_smps_llm_sm59 IMPLEMENTATION. ENDIF. client->response->get_status( IMPORTING code = result-status ). - result-body = cl_abap_codepage=>convert_from( client->response->get_data( ) ). + DATA(raw) = client->response->get_data( ). client->close( EXCEPTIONS OTHERS = 1 ). + " a body that is not UTF-8 - typically the HTML error page of a proxy or + " gateway in its own code page - raises a dynamic check exception the + " callers would not catch; turned into the one exception they do, as the + " cloud transport does + TRY. + result-body = cl_abap_codepage=>convert_from( raw ). + CATCH cx_sy_conversion_codepage cx_sy_codepage_converter_init cx_parameter_invalid_range + cx_parameter_invalid_type INTO DATA(error). + z2ui5_cx_smps_llm=>raise( text = |SM59 destination { destination }: HTTP { result-status }, | && + |the response is not UTF-8 - { error->get_text( ) }| + previous = error ). + ENDTRY. + ENDMETHOD. ENDCLASS. diff --git a/src/10/02/z2ui5_cl_smps_llm_cloud.clas.abap b/src/10/02/z2ui5_cl_smps_llm_cloud.clas.abap index 57a9d0f..a3dd179 100644 --- a/src/10/02/z2ui5_cl_smps_llm_cloud.clas.abap +++ b/src/10/02/z2ui5_cl_smps_llm_cloud.clas.abap @@ -35,6 +35,14 @@ CLASS z2ui5_cl_smps_llm_cloud DEFINITION PUBLIC PRIVATE SECTION. DATA destination TYPE string. + "! Closes the connection, on every path of post( ) - a timeout in + "! execute( ) or a response that is not UTF-8 would otherwise leave it + "! open until the session ends. + "! @parameter client | the client, unbound when it was never created + METHODS client_close + IMPORTING + client TYPE REF TO if_web_http_client. + ENDCLASS. @@ -49,6 +57,8 @@ CLASS z2ui5_cl_smps_llm_cloud IMPLEMENTATION. METHOD z2ui5_if_smps_llm_http~post. + DATA client TYPE REF TO if_web_http_client. + TRY. " service_specific: the destination authenticates as itself, not as " the user in front of the screen - an API key belongs to the system @@ -56,7 +66,7 @@ CLASS z2ui5_cl_smps_llm_cloud IMPLEMENTATION. i_name = destination i_authn_mode = if_a4c_cp_service=>service_specific ). - DATA(client) = cl_web_http_client_manager=>create_by_http_destination( http_destination ). + client = cl_web_http_client_manager=>create_by_http_destination( http_destination ). DATA(request) = client->get_http_request( ). request->set_uri_path( path ). @@ -74,14 +84,33 @@ CLASS z2ui5_cl_smps_llm_cloud IMPLEMENTATION. DATA(response) = client->execute( if_web_http_client=>post ). result-status = response->get_status( )-code. result-body = cl_abap_conv_codepage=>create_in( )->convert( response->get_binary( ) ). - client->close( ). CATCH cx_http_dest_provider_error cx_web_http_client_error cx_web_message_error cx_sy_conversion_codepage INTO DATA(error). + " closed here as well: a CLEANUP would not run, it runs only when an + " exception LEAVES the TRY, and this one catches its own + client_close( client ). z2ui5_cx_smps_llm=>raise( text = |Destination { destination }: { error->get_text( ) }| previous = error ). ENDTRY. + client_close( client ). + + ENDMETHOD. + + + METHOD client_close. + + IF client IS NOT BOUND. + RETURN. + ENDIF. + + TRY. + client->close( ). + CATCH cx_web_http_client_error ##NO_HANDLER. + " already closed - nothing left to release + ENDTRY. + ENDMETHOD. ENDCLASS. diff --git a/src/10/z2ui5_cl_smps_app_014.clas.abap b/src/10/z2ui5_cl_smps_app_014.clas.abap index 1a2e9ef..13f44fc 100644 --- a/src/10/z2ui5_cl_smps_app_014.clas.abap +++ b/src/10/z2ui5_cl_smps_app_014.clas.abap @@ -138,9 +138,11 @@ CLASS z2ui5_cl_smps_app_014 IMPLEMENTATION. busy = abap_true. status_visible = abap_false. - " the answer comes in a SECOND roundtrip, see the class documentation + " the answer comes in a SECOND roundtrip, see the class documentation - + " the X keeps the global busy overlay down for it, so the busy feed stays + " visible instead of disappearing behind it after a second client->follow_up_action( val = z2ui5_if_client=>cs_event-start_timer - t_arg = VALUE #( ( `ANSWER` ) ( `0` ) ) ). + t_arg = VALUE #( ( `ANSWER` ) ( `0` ) ( `X` ) ) ). ENDMETHOD. diff --git a/src/10/z2ui5_cl_smps_app_015.clas.abap b/src/10/z2ui5_cl_smps_app_015.clas.abap index fd6c22f..516bc0a 100644 --- a/src/10/z2ui5_cl_smps_app_015.clas.abap +++ b/src/10/z2ui5_cl_smps_app_015.clas.abap @@ -113,11 +113,12 @@ CLASS z2ui5_cl_smps_app_015 IMPLEMENTATION. RETURN. ENDIF. " the request goes out in a SECOND roundtrip - this one only puts - " the panel into its busy state, so the press shows at once + " the panel into its busy state, so the press shows at once - the X + " keeps the global busy overlay down, so that panel stays visible busy = abap_true. status_visible = abap_false. client->follow_up_action( val = z2ui5_if_client=>cs_event-start_timer - t_arg = VALUE #( ( `RUN` ) ( `0` ) ) ). + t_arg = VALUE #( ( `RUN` ) ( `0` ) ( `X` ) ) ). WHEN `RUN`. summary_get( ). diff --git a/src/10/z2ui5_cl_smps_llm_json.clas.abap b/src/10/z2ui5_cl_smps_llm_json.clas.abap index 2a1a31f..bd9bfed 100644 --- a/src/10/z2ui5_cl_smps_llm_json.clas.abap +++ b/src/10/z2ui5_cl_smps_llm_json.clas.abap @@ -5,7 +5,7 @@ "! the cloud releases - and this package runs on both stacks from 7.40 SP08 "! on. So: "! -"! - OUTBOUND, the request body is a string template; string_escape( ) is +"! - OUTBOUND, the request body is a string template; string_escape( ) "! makes free text - a chat message, a table cell - safe inside it. "! - INBOUND, get_string( ) reads the ONE string field a provider's answer "! is about (content -> text, choices -> message -> content, or error -> @@ -49,6 +49,8 @@ CLASS z2ui5_cl_smps_llm_json DEFINITION PUBLIC PROTECTED SECTION. PRIVATE SECTION. + TYPES ty_byte TYPE x LENGTH 1. + "! Reads the string token that starts at POS (the character after the "! opening quote) and leaves POS behind its closing quote. CLASS-METHODS string_read @@ -59,6 +61,17 @@ CLASS z2ui5_cl_smps_llm_json DEFINITION PUBLIC CHANGING pos TYPE i. + "! The code of a control character (below U+0020), for its \u00XX + "! escape. Read from the character's bytes, as ABAP has no function + "! from a character to its code that both stacks release. + "! @parameter char | one character below the blank + "! @parameter result | its code, 00 to 1F + CLASS-METHODS control_code + IMPORTING + char TYPE string + RETURNING + VALUE(result) TYPE ty_byte. + "! Moves POS past blanks, tabs and line breaks. CLASS-METHODS blanks_skip IMPORTING @@ -84,9 +97,11 @@ CLASS z2ui5_cl_smps_llm_json IMPLEMENTATION. METHOD string_escape. + DATA escaped TYPE string. + " The backslash goes FIRST - escaping it after the others would escape " the backslashes they just introduced. Tab, line feed and carriage - " return are the control characters business text actually contains. + " return are the control characters business text usually contains. result = val. result = replace( val = result sub = `\` with = `\\` occ = 0 ). result = replace( val = result sub = `"` with = `\"` occ = 0 ). @@ -94,6 +109,51 @@ CLASS z2ui5_cl_smps_llm_json IMPLEMENTATION. result = replace( val = result sub = |\r| with = `\r` occ = 0 ). result = replace( val = result sub = |\t| with = `\t` occ = 0 ). + " Every other character below the blank (U+0000 to U+001F) is just as + " illegal raw in a JSON string - the provider answers 400 - and text + " pasted from a PDF or a spreadsheet does carry form feeds and vertical + " tabs. JSON writes them \u00XX. A run without one is copied whole. + DATA(length) = strlen( result ). + DATA(pos) = 0. + DATA(start) = 0. + WHILE pos < length. + DATA(char) = substring( val = result off = pos len = 1 ). + IF char < ` `. + escaped = escaped && substring( val = result off = start len = pos - start ) + && |\\u00{ control_code( char ) }|. + start = pos + 1. + ENDIF. + pos = pos + 1. + ENDWHILE. + + IF start > 0. + result = escaped && substring( val = result off = start ). + ENDIF. + + ENDMETHOD. + + + METHOD control_code. + + " ABAP has no function from a character to its code that both stacks + " release, so the code is read from the character's bytes: two on a + " Unicode system (UTF-16), one of them 00 - which one depends on the + " byte order of the platform, so all of them are OR-ed together + DATA single TYPE c LENGTH 1. + DATA offset TYPE i. + FIELD-SYMBOLS TYPE x. + + single = char. + ASSIGN single TO CASTING. + IF IS NOT ASSIGNED. + RETURN. + ENDIF. + + DO xstrlen( ) TIMES. + result = result BIT-OR +offset(1). + offset = offset + 1. + ENDDO. + ENDMETHOD. diff --git a/src/10/z2ui5_cl_smps_llm_json.clas.testclasses.abap b/src/10/z2ui5_cl_smps_llm_json.clas.testclasses.abap new file mode 100644 index 0000000..f8ca89f --- /dev/null +++ b/src/10/z2ui5_cl_smps_llm_json.clas.testclasses.abap @@ -0,0 +1,73 @@ +CLASS ltcl_string_escape DEFINITION FINAL + FOR TESTING + RISK LEVEL HARMLESS + DURATION SHORT. + + PRIVATE SECTION. + METHODS quote_and_backslash FOR TESTING. + METHODS line_breaks_and_tab FOR TESTING. + METHODS other_control_characters FOR TESTING. + METHODS plain_text_unchanged FOR TESTING. + METHODS round_trip FOR TESTING. + +ENDCLASS. + + +CLASS ltcl_string_escape IMPLEMENTATION. + + METHOD quote_and_backslash. + + cl_abap_unit_assert=>assert_equals( + exp = `say \"hi\" to C:\\temp` + act = z2ui5_cl_smps_llm_json=>string_escape( `say "hi" to C:\temp` ) ). + + ENDMETHOD. + + + METHOD line_breaks_and_tab. + + cl_abap_unit_assert=>assert_equals( + exp = `a\nb\r\nc\td` + act = z2ui5_cl_smps_llm_json=>string_escape( |a\nb\r\nc\td| ) ). + + ENDMETHOD. + + + METHOD other_control_characters. + + " raw below U+0020 is a 400 from the provider - each one is written as + " \u00XX, and the text around it stays as it was + DATA(text) = `page` && cl_abap_char_utilities=>form_feed && + `next` && cl_abap_char_utilities=>vertical_tab && + `line` && cl_abap_char_utilities=>backspace && `end`. + + cl_abap_unit_assert=>assert_equals( + exp = `page\u000Cnext\u000Bline\u0008end` + act = z2ui5_cl_smps_llm_json=>string_escape( text ) ). + + ENDMETHOD. + + + METHOD plain_text_unchanged. + + cl_abap_unit_assert=>assert_equals( + exp = `Revenue 2025: 1,250.00 EUR - {region} / 100%` + act = z2ui5_cl_smps_llm_json=>string_escape( `Revenue 2025: 1,250.00 EUR - {region} / 100%` ) ). + + ENDMETHOD. + + + METHOD round_trip. + + " what string_escape( ) writes, get_string( ) reads back unchanged + DATA(text) = |a "quoted" C:\\path\nnext line|. + DATA(json) = |\{"text":"{ z2ui5_cl_smps_llm_json=>string_escape( text ) }"\}|. + + cl_abap_unit_assert=>assert_equals( + exp = text + act = z2ui5_cl_smps_llm_json=>get_string( json = json + path = VALUE #( ( `text` ) ) ) ). + + ENDMETHOD. + +ENDCLASS. diff --git a/src/10/z2ui5_cl_smps_llm_json.clas.xml b/src/10/z2ui5_cl_smps_llm_json.clas.xml index 77f3604..a292ab6 100644 --- a/src/10/z2ui5_cl_smps_llm_json.clas.xml +++ b/src/10/z2ui5_cl_smps_llm_json.clas.xml @@ -10,6 +10,7 @@ X X X + X diff --git a/src/z2ui5_cl_smps_app_000.clas.abap b/src/z2ui5_cl_smps_app_000.clas.abap index 3a9abe4..fe84077 100644 --- a/src/z2ui5_cl_smps_app_000.clas.abap +++ b/src/z2ui5_cl_smps_app_000.clas.abap @@ -187,12 +187,19 @@ CLASS z2ui5_cl_smps_app_000 DEFINITION PUBLIC. VALUE(result) TYPE string. "! one package each - same markup, different binding + "! @parameter branch | the generated abapGit branch that carries the + "! package alone - named in the panel once a row of it is not on this + "! system, so the reader knows what to pull; npm run check:overview + "! keeps the names in step with .github/packages.json + "! @parameter samples | the rows ITEMS binds, read for that decision METHODS render_package IMPORTING - page TYPE REF TO z2ui5_cl_ui5_view_builder - title TYPE string - hint TYPE string - items TYPE string. + page TYPE REF TO z2ui5_cl_ui5_view_builder + title TYPE string + hint TYPE string + branch TYPE string + samples TYPE ty_t_sample + items TYPE string. "! one row of a list, including the runtime lookup of CLASSNAME METHODS sample @@ -307,7 +314,15 @@ CLASS z2ui5_cl_smps_app_000 IMPLEMENTATION. " the one without draft, those of src/04 against the draft enabled " one, and an empty table is the most common reason a sample looks " broken. data_reset( ) deletes first, so the travel ids stay 1, 2, 3. - DATA(text) = condense( |{ data_reset( cs_class-data_trv ) } { data_reset( cs_class-data_trd ) }| ). + " Both answers read alike, so each one says which package it is from. + DATA(text) = data_reset( cs_class-data_trv ). + IF text IS NOT INITIAL. + text = |RAP: { text }|. + ENDIF. + DATA(text_draft) = data_reset( cs_class-data_trd ). + IF text_draft IS NOT INITIAL. + text = condense( |{ text } RAP with Draft: { text_draft }| ). + ENDIF. IF text IS INITIAL. text = `No demo data on this system - the two RAP packages are not installed`. ENDIF. @@ -371,55 +386,75 @@ CLASS z2ui5_cl_smps_app_000 IMPLEMENTATION. )->a( n = `showIcon` v = `true` )->a( n = `class` v = `sapUiSmallMarginBottom` ). - render_package( page = page - title = `01 - OData` - hint = `bind a table to an OData V2 model - needs an activated OData V2 service` - items = client->_bind( t_odata ) ). - - render_package( page = page - title = `02 - Smart Controls` - hint = `sap.ui.comp driven by OData metadata - needs SAPUI5 and an activated Gateway service` - items = client->_bind( t_smart ) ). - - render_package( page = page - title = `03 - RAP` - hint = `one EML statement per sample on Z2UI5_R_SMPS_TRV - the business object ships with the package` - items = client->_bind( t_rap ) ). - - render_package( page = page - title = `04 - RAP with Draft` - hint = `Z2UI5_R_SMPS_TRD - start at 06, it carries the trick the other three reuse` - items = client->_bind( t_draft ) ). - - render_package( page = page - title = `05 - Business Events` - hint = `needs a release that already carries RAP business events - open both samples side by side` - items = client->_bind( t_events ) ). - - render_package( page = page - title = `06 - Stateful Sessions / Locks` - hint = `ABAP Standard (on-premise) - keep SM12 open next to the browser and start with 486` - items = client->_bind( t_stateful ) ). - - render_package( page = page - title = `07 - AMC/APC` - hint = `on-premise WebSockets - activate the ICF node /sap/bc/apc/sap/z2ui5_apc_smp_2` - items = client->_bind( t_websocket ) ). - - render_package( page = page - title = `08 - MIME Play Audio` - hint = `activate the ICF service /SAP/PUBLIC/BC/ABAP/mime_demo` - items = client->_bind( t_mime ) ). - - render_package( page = page - title = `09 - Launchpad` - hint = `these four show what the shell adds - start them from a launchpad tile, not from here` - items = client->_bind( t_launchpad ) ). - - render_package( page = page - title = `10 - AI / LLM` - hint = `a language model over HTTPS - set up the provider in the settings first, then chat or summarize` - items = client->_bind( t_ai ) ). + render_package( page = page + title = `01 - OData` + hint = `bind a table to an OData V2 model - needs an activated OData V2 service` + branch = `01-odata` + samples = t_odata + items = client->_bind( t_odata ) ). + + render_package( page = page + title = `02 - Smart Controls` + hint = `sap.ui.comp driven by OData metadata - needs SAPUI5 and an activated Gateway service` + branch = `02-smart-controls` + samples = t_smart + items = client->_bind( t_smart ) ). + + render_package( page = page + title = `03 - RAP` + hint = `one EML statement per sample on Z2UI5_R_SMPS_TRV - the business object ships with the package` + branch = `03-rap` + samples = t_rap + items = client->_bind( t_rap ) ). + + render_package( page = page + title = `04 - RAP with Draft` + hint = `Z2UI5_R_SMPS_TRD - start at 06, it carries the trick the other three reuse` + branch = `04-rap-draft` + samples = t_draft + items = client->_bind( t_draft ) ). + + render_package( page = page + title = `05 - Business Events` + hint = `needs a release that already carries RAP business events - open both samples side by side` + branch = `05-business-events` + samples = t_events + items = client->_bind( t_events ) ). + + render_package( page = page + title = `06 - Stateful Sessions / Locks` + hint = `ABAP Standard (on-premise) - keep SM12 open next to the browser and start with 486` + branch = `06-stateful-locks` + samples = t_stateful + items = client->_bind( t_stateful ) ). + + render_package( page = page + title = `07 - AMC/APC` + hint = `on-premise WebSockets - activate the ICF node /sap/bc/apc/sap/z2ui5_apc_smp_2` + branch = `07-amc-apc` + samples = t_websocket + items = client->_bind( t_websocket ) ). + + render_package( page = page + title = `08 - MIME Play Audio` + hint = `activate the ICF service /SAP/PUBLIC/BC/ABAP/mime_demo` + branch = `08-mime` + samples = t_mime + items = client->_bind( t_mime ) ). + + render_package( page = page + title = `09 - Launchpad` + hint = `these four show what the shell adds - start them from a launchpad tile, not from here` + branch = `09-launchpad` + samples = t_launchpad + items = client->_bind( t_launchpad ) ). + + render_package( page = page + title = `10 - AI / LLM` + hint = `a language model over HTTPS - set up the provider in the settings first, then chat or summarize` + branch = `10-ai-llm` + samples = t_ai + items = client->_bind( t_ai ) ). client->view_display( view->stringify( ) ). @@ -462,6 +497,8 @@ CLASS z2ui5_cl_smps_app_000 IMPLEMENTATION. right->tag( `Button` )->a( n = `text` v = `Regenerate Demo Data` )->a( n = `icon` v = `sap-icon://refresh` + " it deletes before it creates - say so before the press, not after + )->a( n = `tooltip` v = `Deletes every travel of both RAP packages, drafts included, and creates the demo travels again` )->a( n = `type` v = `Transparent` )->a( n = `visible` b = demo_data_installed )->a( n = `press` v = client->_event( cs_backend_event-regenerate ) ). @@ -668,6 +705,21 @@ CLASS z2ui5_cl_smps_app_000 IMPLEMENTATION. )->a( n = `text` t = hint )->a( n = `class` v = `sapUiSmallMarginBottom` ). + " "not on this system" alone leaves the reader guessing what to install. + " A row is missing for one of two reasons, and the strip names both: the + " package never came in - each one has an abapGit branch of its own - or + " it came in and the release cannot activate it + IF line_exists( samples[ installed = abap_false ] ). + panel->tag( `MessageStrip` + )->a( n = `text` t = |Not on this system? Pull the abapGit branch { branch } of { cs_url-stack } | && + |- it carries this package alone, main carries all of them. Installed and | && + |still listed here: this release cannot activate it - src/{ substring( val = branch len = 2 ) }/README.md | && + |says what it needs.| + )->a( n = `type` v = `Warning` + )->a( n = `showIcon` v = `true` + )->a( n = `class` v = `sapUiSmallMarginBottom` ). + ENDIF. + DATA(table) = panel->ele( `Table` )->a( n = `items` v = items ).