diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 7df67268..1ca8a777 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -58,7 +58,10 @@ jobs: ref: main path: .samples-controls fetch-depth: 1 - sparse-checkout: SAMPLES.md + sparse-checkout: | + SAMPLES.md + catalogue.json + src sparse-checkout-cone-mode: false - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 continue-on-error: true @@ -67,7 +70,10 @@ jobs: ref: main path: .samples-stack fetch-depth: 1 - sparse-checkout: SAMPLES.md + sparse-checkout: | + SAMPLES.md + catalogue.json + src sparse-checkout-cone-mode: false # The playground's stylesheet, for the shared-design gate, and the two diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 532415aa..515e866d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -83,7 +83,10 @@ jobs: ref: main path: .samples-controls fetch-depth: 1 - sparse-checkout: SAMPLES.md + sparse-checkout: | + SAMPLES.md + catalogue.json + src sparse-checkout-cone-mode: false # samples-stack was missing here while generate-llms.mjs counts three # catalogues into the llms.txt it publishes. The deploy therefore @@ -96,7 +99,10 @@ jobs: ref: main path: .samples-stack fetch-depth: 1 - sparse-checkout: SAMPLES.md + sparse-checkout: | + SAMPLES.md + catalogue.json + src sparse-checkout-cone-mode: false # The playground's stylesheet, for the shared-design gate, and the two diff --git a/AGENTS.md b/AGENTS.md index 4bd4336c..080bf539 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,10 +18,10 @@ person reads the page. Do not put "as an AI, …" prose back into `docs/`. | `docs/` | The pages. Sidebar and nav live in `docs/.vitepress/config.mjs` | | `docs/public/` | Static assets — **and** the generated `llms.txt`, `llms-full.txt` and per-page `.md`, which are gitignored | | `scripts/check-examples.mjs` | Extracts every fenced ABAP block that builds a view, compiles it against the real framework and lints the view it produces | -| `scripts/link-samples.mjs` | Generates the *Working Samples* block on a page from its `samples:` frontmatter plus `SAMPLES.md` in an `abap2UI5/samples` checkout, checks the link in both directions, and resolves the source links a page writes by hand against the same checkout | +| `scripts/link-samples.mjs` | Generates the *Working Samples* block on a page from its `samples:` frontmatter plus the catalogue of the repository each class belongs to — the prefix decides: `z2ui5_cl_smp_app_*` is `abap2UI5/samples` (read from its `SAMPLES.md` in a checkout), `z2ui5_cl_smpc_*` samples-controls and `z2ui5_cl_smps_*` samples-stack (read from their `catalogue.json`, a checkout or the published copy). Checks the link in both directions for the samples repository (the `" @docs` line), the class's existence for the other two, and resolves the source links a page writes by hand against whichever checkout is here | | `scripts/generate-llms.mjs` | Builds `llms.txt` / `llms-full.txt` / per-page markdown from the sidebar. Runs inside `docs:build`, so the deploy publishes them | | `scripts/generate-api-reference.mjs` | Generates the client API reference — the block in `docs/resources/api.md` and `docs/public/api/client-api.json` — from `z2ui5_if_client` on the framework branch this site tracks (`main`); `--check` is the freshness gate | -| `scripts/lib/client-interface.mjs` | Where `z2ui5_if_client` is fetched from (the ref comes from `lib/release.mjs`, shared with `check-api-names.mjs`) and the full parser `generate-api-reference.mjs` renders from | +| `scripts/lib/client-interface.mjs` | Where `z2ui5_if_client` is fetched from (the ref comes from `lib/release.mjs`, shared with `check-api-names.mjs`; `A2UI5_HOME` reads the same ref out of a local clone of the framework instead, with `git show`, never its working tree) and the full parser `generate-api-reference.mjs` renders from; pinned by `test/client-interface.test.mjs` | | `scripts/check-version.mjs` | The release number in the bar's menu, the deprecations page and the changelog, against the newest release tag of the framework | | `scripts/generate-search.mjs` | Builds `docs/public/search-index.json` — the pages of this site plus every entry in the three sample catalogues, which is what the box in the middle of the bar searches. Runs inside `docs:build`, so the deploy publishes it; `scripts/lib/search-index.mjs` is what goes in | | `scripts/lib/pages.mjs` | What a page of this site IS: the sidebar walk, its title, its opening sentences, its headings, its words. Shared by `build-site.mjs`, `generate-llms.mjs` and `generate-search.mjs` so a page added to the sidebar reaches all three; `summarise( )` — the description of 150 pages and the note in `llms.txt`, which nobody writes and nobody proofreads — is pinned by `test/summarise.test.mjs` | @@ -74,7 +74,7 @@ in it are decidable, and all thirteen are decided before a merge: **What it now guards is the second opinion, not the site.** Since the switch, the published pages link the playground's own `catalogue.css`, borrowed whole at build time — so the palette cannot drift from the playground's there, by construction. `style.css` is VitePress's, and VitePress is no longer served. Keep the gate: it is what says so when somebody edits `style.css` expecting the site to change | | `check:images` | every image under `docs/public`, against the three things a page can afford and the one it cannot: a screenshot is WebP (the PNG captures were 200 to 335 kB each, 2.9 MB across the manual, on pages of 20 kB of text; the same captures as WebP are a fifth of that), a deliverable is one of the PNGs the logo page hands out, nothing is over its budget, and the build can measure every one - an image it cannot size gets no width and height and moves the page when it lands | -| `check:samples` | the **Working Samples** blocks and the source links a page writes by hand, against [abap2UI5/samples](https://github.com/abap2UI5/samples) | +| `check:samples` | the **Working Samples** blocks and the source links a page writes by hand, against [abap2UI5/samples](https://github.com/abap2UI5/samples), [samples-controls](https://github.com/abap2UI5/samples-controls) and [samples-stack](https://github.com/abap2UI5/samples-stack) — a page may declare a class of any of the three | **All four walking gates carry a floor.** A gate that checked nothing reports the same shape as a gate that found nothing wrong — which is precisely how @@ -101,7 +101,12 @@ silence — went a release without CI, and how `check:conventions` sat in `check:samples` needs an `abap2UI5/samples` checkout — set `SAMPLES_HOME`, or clone it as a sibling. Without one it *skips* rather than fails, so verify the output says what you think it says. CI checks out `abap2UI5/samples@main` -explicitly for this reason. +explicitly for this reason. The other two repositories are found the same way +(`SAMPLES_CONTROLS_HOME` / `SAMPLES_STACK_HOME`, or `.samples-controls` / +`../samples-controls` and the stack equivalents) and fall back to the +`catalogue.json` each publishes; CI checks both out with `catalogue.json` and +`src`, so a class a page declares from them is held to the tree, not only to +the catalogue. There used to be one more, `check:counts`, holding four figures on a `resources/samples.md` page against the catalogues themselves. That page is @@ -625,7 +630,16 @@ Delete a stub when the old URL has stopped receiving traffic, not before. PROSE: `resources/deprecations.md` carries a *next release* column, and `check:version` keeps the release number in the bar's menu, the deprecations page and the changelog honest. `A2UI5_REF` still overrides the ref — now to - pin a run BACK to a release rather than forward to main. + pin a run BACK to a release rather than forward to main. `A2UI5_HOME` points + the same three at a local clone of the framework: the ref is then read out + of it with `git show` (so `A2UI5_HOME=../abap2UI5 npm run generate:api` + regenerates the reference on a machine that cannot reach + raw.githubusercontent.com, and `A2UI5_REF=my-branch` on top regenerates it + against a branch that has not merged). It is deliberately not a + sibling-checkout convenience like the sample catalogues have: the clone's + working tree is never read, because the branch a neighbouring checkout + happens to be on must not change a gate's verdict. Every run prints where + the interface came from — read the line before trusting an OK. - **A link to a neighbouring site needs `target="_self"`, and looks fine without it.** The playground, the catalogue and the linter's rule pages are @@ -708,7 +722,8 @@ Delete a stub when the old URL has stopped receiving traffic, not before. `npm run link:samples` after changing a page's `samples:` frontmatter; `check:samples` fails if a rewrite would change anything. It also resolves every source link a page writes **by hand** — `github.com/abap2UI5/samples/blob/main/…` - in the prose rather than in a generated block — against that same checkout. + in the prose rather than in a generated block, and the same for the two + sibling repositories — against that same checkout. The 37 essay pages under `advanced/insights/` link a sample that way on purpose (none of them carries a block: a "Full source:" sentence is the shape an essay wants, not a see-also list at the end), and the samples repository diff --git a/blog/teaser-posts.md b/blog/teaser-posts.md index 9fdcde05..e95edf0a 100644 --- a/blog/teaser-posts.md +++ b/blog/teaser-posts.md @@ -5,8 +5,14 @@ markdown. Kept here rather than on the published pages: they are publishing material, not documentation. The sections stand in the order the articles are numbered in -`docs/advanced/insights/`, so this file is also the publishing order. Move a -post here when an article moves there. +`docs/advanced/insights/`, one per article, so this file is also the +publishing order. Move a post here when an article moves there. A post whose +article does not exist yet waits under *Not yet published* at the end, and +moves up the day its article lands. + +The fifteen posts marked *draft* were written from the articles after the +fact, in the shape of the ones before them, and have not been posted - read +one before it goes out, the way the others were read. ## Somewhere on the Way to UI5, We Lost RTTS @@ -353,24 +359,257 @@ Plain text — LinkedIn renders no markdown. > > #ABAP #SAP #UI5 -## No Cache, No Deploy, Any IDE +## One Click, One Request -Plain text — LinkedIn renders no markdown. +Plain text — LinkedIn renders no markdown. Draft, 803 characters. -> The abap2UI5 loop is: change the class, activate, refresh the browser. +> Every click is one HTTP request, and every request is one call to main( ) on a +> fresh instance. > -> What is missing from that sentence is the point. No deployment — activating is -> the deployment. No cache to invalidate — the UI is built per request, so -> nothing can be stale. No IDE agreement, because it is ABAP. No context switch -> to debug: a breakpoint in the method that built the view stops there. +> That is PBO and PAI with the names filed off. A request arrives with the event +> and what the user changed, main( ) runs, decides, returns. Nothing runs between +> two clicks, because nothing is there to run - no timer, no controller, no +> half-finished promise. > -> Individually, conveniences. Together, the reason a screen gets tried at all — -> when an experiment costs a class and a refresh, "could we just show this?" -> stops being a project. +> So main( ) is a dispatcher over three questions: is this the first call, is the +> app coming back to the screen, did an event fire. Two of the three are the +> mistake everybody makes exactly once - the init branch has to stand first, and +> a return owes a view. > -> New article 🎉 +> New article 🎉 The mental model everything else in abap2UI5 rests on, in one +> sentence and one CASE statement. > -> What is your current edit-to-see-it time on a Fiori change? +> Which habit from dialog programming did you have to unlearn first? +> +> #ABAP #SAP #UI5 + +## What the Client Can Do + +Plain text — LinkedIn renders no markdown. Draft, 778 characters. + +> An app implements one interface with one method. Its single parameter is the +> other interface - and it is the whole of what an app can ask the framework for. +> +> Show a screen, put ABAP data on it, find out what the user did, talk to the +> user, move to another app and back, ask the browser for something. Eight rows +> in a table, and the two that carry the most weight are the smallest: _bind( ) +> takes an ABAP variable and returns the binding path, _event( ) names what the +> frontend sends when a control fires. Both are string generators, and that is +> all they are. +> +> New article 🎉 The API surface an abap2UI5 app ever touches - small enough to +> hold in your head, by design rather than by accident. +> +> How many methods of your UI framework could you name without looking? +> +> #ABAP #SAP #UI5 + +## CALL SCREEN, LEAVE SCREEN + +Plain text — LinkedIn renders no markdown. Draft, 714 characters. + +> Module pools had a call stack of screens. CALL SCREEN pushed one, LEAVE TO +> SCREEN 0 popped it, and the screen underneath came back with its fields intact. +> +> abap2UI5 has the same stack. Its elements are app instances: nav_app_call( ) +> pushes one, nav_app_leave( ) pops it, and the caller gets main( ) called again +> with its attributes still there. What it reads back is not a string in a +> parameter table - it is the sub-app instance itself, typed attributes and all. +> A luxury CALL SCREEN never had. +> +> New article 🎉 Screens within one class, screens between classes, popups as +> classes of their own - and the one navigation that is not on the stack. +> +> What did your CALL SCREEN chains hand back, and how? +> +> #ABAP #SAP #UI5 + +## Where F4 Went + +Plain text — LinkedIn renders no markdown. Draft, 749 characters. + +> PARAMETERS pa_arbgb TYPE t100-arbgb got a value help for free. The DDIC knew +> the search help, the screen knew the DDIC, and nobody wrote a line for it. +> +> An Input in a UI5 view is a box that holds text. It has no idea what type +> stands behind it. +> +> abap2UI5 does not put the automatic version back. What it gives you is the +> three shapes an F4 actually comes in: suggestions while typing (a bound table, +> no roundtrip), a selection popup (a SELECT, a list, an event with the picked +> row), and a reusable value help as a class of its own on the stack. +> +> New article 🎉 F4 is three lines and a SELECT now. No longer free - and no +> longer limited to the DDIC either. +> +> Which of your value helps would be better as a SELECT than as a search help? +> +> #ABAP #SAP #UI5 + +## MESSAGE Still Works + +Plain text — LinkedIn renders no markdown. Draft, 918 characters. + +> Thirty years of habits around messages, and most of them carry over unchanged. +> What changes is where the message ends up. +> +> A toast for what needs no acknowledgment, a box for what does. sy after +> MESSAGE ... INTO, a BAPIRET2 table, an exception object - the framework reads +> text, type and details out of each. Message classes, T100 texts and the +> translation tooling around them stay exactly as useful as they were: an app +> is an ABAP class, there is no i18n file beside it, nobody exports a properties +> file to anybody. +> +> What is different is the exception that gets away: one catch in the HTTP +> handler, HTTP 500, a restart overlay - and one setting every production +> system needs so the 500 body does not tell a browser more than it should. +> +> New article 🎉 The message classes, the texts and the translation stay. Only +> the dump looks different. +> +> Where do your messages land today - and who translates them? +> +> #ABAP #SAP #UI5 + +## The Lock Is Gone by the Next Click + +Plain text — LinkedIn renders no markdown. Draft, 915 characters. + +> VA02 holds its lock for as long as the dialog session lives. That works +> because the session lives. +> +> In abap2UI5 every click is a fresh session, so a lock set while the order +> opens is released before the user has finished reading the screen. The ABAP is +> still valid. The assumption underneath it is not - and this is the habit that +> most reliably surprises a developer coming from dialog programming. +> +> It is not a limitation to work around. It is the question every stateless web +> application has answered since roughly 1999: lock at save, an optimistic check +> on the timestamp, a soft lock for the "being edited by Müller" warning - and +> the GUI way, still there, for the few apps that should pin a work process. +> +> New article 🎉 A lock is a promise about a session. Where the session is one +> request long, the promise has to be, too. +> +> How does your app tell the second user that the first one was faster? +> +> #ABAP #SAP #UI5 + +## Who May Start Which App + +Plain text — LinkedIn renders no markdown. Draft, 840 characters. + +> One ICF node serves every abap2UI5 app, and the URL names the class. So +> anybody with the node can start any class? +> +> Only if nobody decided otherwise, and the deciding happens in two familiar +> places. Authentication is the ICF node's - logon, SSO, certificate, like any +> other UI5 app. Authorization is the app's, the way a report checks before it +> selects: an AUTHORITY-CHECK in the branch that shows the first view, one +> object, one field, the class as the value, roles in PFCG. The check sits in +> the class it protects, so a transport carries the app and its guard together. +> +> New article 🎉 One node, one class, one AUTHORITY-CHECK. The rest is PFCG - +> plus the CSRF token and the Content Security Policy a web app needs and a +> report never did. +> +> Where does the authorization check sit in your apps - on the node, or in the +> code? +> +> #ABAP #SAP #UI5 + +## 100 Rows, Four Hours, One Request + +Plain text — LinkedIn renders no markdown. Draft, 847 characters. + +> Three numbers decide whether an abap2UI5 app feels fast, and none of them is +> in your code. +> +> 100 rows: a UI5 JSON model shows a list binding at most 100 items by default. +> Bind a table with 300 entries and the rest are dropped - silently, no error +> anywhere. Raised per view from ABAP, and not the fix for fifty thousand rows. +> +> Four hours: the serialized instance lives in one table until it expires. Back +> after lunch, you continue; back tomorrow, you start fresh. +> +> One request: every click is one roundtrip plus one read and one write of the +> draft, so the cost of a click is the size of the instance plus the size of +> the model - which are the same attributes. +> +> New article 🎉 Small instance, bounded model, one request per click. +> Everything else the system already does well. +> +> Which of the three has cost you the most debugging time? +> +> #ABAP #SAP #UI5 + +## ABAP Unit for a Screen + +Plain text — LinkedIn renders no markdown. Draft, 817 characters. + +> Testing a screen is usually where enthusiasm goes to die. You drive a +> browser, or you mock a framework, or you decide - silently, in a meeting +> nobody minuted - that this part is tested by the users. +> +> An abap2UI5 app is a global class, and a global class has a Test Classes +> include. That is the whole story, and it stays that short because of one +> decision in the app: the logic does not touch the client. main( ) dispatches, +> the methods it dispatches to read data, decide and change attributes - and a +> test calls those methods directly and looks at the attributes afterwards. +> Nothing in the test is abap2UI5. +> +> New article 🎉 A screen under ABAP Unit, with no browser driven and no +> framework mocked - and the linter for the half a unit test cannot see. +> +> When did a screen of yours last have a unit test? +> +> #ABAP #SAP #UI5 + +## When One Class Is Not Enough + +Plain text — LinkedIn renders no markdown. Draft, 819 characters. + +> Every example in this series is one class, and for the screens it is about +> that is the right size. Real applications grow past it. +> +> A framework with no answer here produces one 4,000-line class and a +> maintainer who resigns. There are four seams, and each one is ordinary ABAP: +> one class per screen, connected by the stack. Popups and value helps as +> classes of their own. The logic in a class that has no screen - the service a +> second screen, a batch job and a test all call. Parts of one screen as nested +> views. +> +> What does not appear in that list is a single framework concept. No component, +> no manifest, no router, no controller hierarchy, no dependency injection +> container. +> +> New article 🎉 A larger app is more classes. The unit stayed the same size. +> +> Where do you cut a screen that has grown too big? +> +> #ABAP #SAP #UI5 + +## A Tile Like Any Other + +Plain text — LinkedIn renders no markdown. Draft, 802 characters. + +> An app that is reached by pasting a URL is a demo. It becomes an application +> when it has a tile. +> +> In the launchpad an abap2UI5 app is indistinguishable from the RAP and +> freestyle tiles beside it. One thing is installed once: the abap2UI5 shell in +> the UI5 repository, through abapGit, for the whole system. Everything else is +> the Fiori administration the system already does - a target mapping, a tile, +> a catalog, a role - with nothing abap2UI5-specific in it except one +> parameter: the app class. +> +> And when the tile is blank, because it will be, once: the app index after an +> abapGit import. One report, one cache, one hard reload. +> +> New article 🎉 One shell in the UI5 repository, one parameter per tile. No +> user can tell the difference. +> +> How long did your last "blank tile" take to find? > > #ABAP #SAP #UI5 @@ -396,68 +635,100 @@ Plain text — LinkedIn renders no markdown. > > #ABAP #SAP #UI5 -## 2,300 Lines +## Cloud-Ready Is a Property of Your App Plain text — LinkedIn renders no markdown. -> The communication core of abap2UI5 is one HTTP handler, two interfaces and one -> database table — originally around 2,300 lines of ABAP. +> abap2UI5 is ABAP Cloud compliant: written in ABAP for Cloud, released APIs +> only, no modifications, installs on BTP and S/4HANA Public Cloud. > -> It is small because of what it does not do. It does not build views, apps do. -> It does not decide flow, apps do. It does not wrap UI5 controls, so it does -> not grow when UI5 does. +> None of that says anything about the app you build with it. > -> Which matters past elegance: a framework in the request path sees every input, -> every response, every user. There is a difference between trusting a package -> because it is popular and being able to open it and find out. +> SELECT FROM i_salesorder, or SELECT FROM vbak. Same screen, same framework — +> one survives the next upgrade and can move to a cloud system, the other +> cannot. Nothing about the UI is involved in the difference. +> +> Clean core is not a property a dependency grants your application. It is +> decided in the SELECT. > > New article 🎉 > -> When did you last read a framework you depend on? +> Where does your team actually check this — review, ATC, or after the upgrade? > > #ABAP #SAP #UI5 -## What It Does Not Do +## When the API Is Not Released -Plain text — LinkedIn renders no markdown. +Plain text — LinkedIn renders no markdown. Draft, 923 characters. -> Eighteen articles is enough credit to say what abap2UI5 does not do. +> SAP grades an extension on four clean core levels now: A for released APIs +> only, B for classic APIs in Standard ABAP, C for SAP-internal objects, D for +> modifications. > -> Offline is out — every event asks the server what happens next; take the -> server away and there is no app left. Pushdown to HANA is indirect, and a -> frontend fuzzy search help is not available. Real-time and collaborative UIs -> are the wrong shape for a request-per-event model. Separate frontend and -> backend teams lose the contract between them. And where a Fiori Elements -> floorplan fits, it fits — that is less work, not more. +> abap2UI5 is Level A. Which says nothing about your app - the level an app +> reaches is decided by what the app calls. > -> What is left is still most business software: forms, tables, dashboards, -> approvals, the small screens nobody funds a project for. +> So the case that actually comes up on a Tuesday: the API you need is not +> released. The move is a wrapper - a class in Standard ABAP that calls the +> classic API and is itself released for ABAP for Cloud Development. Graded on +> its own, B, while the app calling it stays A. The value is not the grade. It +> is that the part of the system which is not upgrade-stable now has a name, a +> size and a boundary - one class to re-check when SAP changes the API +> underneath it. > -> New article 🎉 +> New article 🎉 A wrapper does not make the dependency clean. It makes it +> findable. > -> Where would you put the line? +> How many classic APIs does your newest app reach - and could you list them? > > #ABAP #SAP #UI5 -## Cloud-Ready Is a Property of Your App +## On-Stack or Side-by-Side -Plain text — LinkedIn renders no markdown. +Plain text — LinkedIn renders no markdown. Draft, 834 characters. -> abap2UI5 is ABAP Cloud compliant: written in ABAP for Cloud, released APIs -> only, no modifications, installs on BTP and S/4HANA Public Cloud. +> Two places to run an abap2UI5 app, and the choice is about lifecycles, not +> about code. The class is the same class either way. > -> None of that says anything about the app you build with it. +> On-stack: inside the SAP system. Direct reads, no remote call, the transport +> you already use - and that system's release, upgrade window and change freeze +> in December. > -> SELECT FROM i_salesorder, or SELECT FROM vbak. Same screen, same framework — -> one survives the next upgrade and can move to a cloud system, the other -> cannot. Nothing about the UI is involved in the difference. +> Side-by-side: on the SAP BTP ABAP Environment, calling S/4 through released +> remote APIs. Released, upgraded and restarted on its own schedule, and the +> S/4 system does not have to agree. The cost is equally plain: a remote call is +> slower than a local read, the released APIs are narrower, and there is a +> second system to run. > -> Clean core is not a property a dependency grants your application. It is -> decided in the SELECT. +> New article 🎉 Neither choice touches the app class. The same code renders in +> both places - only what it reads changes. > -> New article 🎉 +> Which constraint decides it for you - the data path, or the release cycle? > -> Where does your team actually check this — review, ATC, or after the upgrade? +> #ABAP #SAP #UI5 + +## One App, Many Systems + +Plain text — LinkedIn renders no markdown. Draft, 931 characters. + +> A side-by-side app already runs outside the system it serves. So: how many +> systems can it serve? +> +> More than one. The app lives on the SAP BTP ABAP Environment and reaches each +> S/4 system through its released APIs, so the connection is configuration +> rather than code. One codebase, deployed once, serving several tenants, each +> with its own system, its own data and its own release - nobody transports +> into a customer system, and a fix reaches every tenant at once. +> +> Nothing was added to make this possible. The frontend renders whatever +> arrives, the app is one class with nothing beside it, and the state travels +> with the request. The limits are the ones the shape implies - and the +> tenant's data boundary is now the app's to enforce. +> +> New article 🎉 A framework with nothing to install per system can serve +> systems it was never installed on. +> +> Would you run one app for several S/4 systems - and what would stop you? > > #ABAP #SAP #UI5 @@ -481,28 +752,6 @@ Plain text — LinkedIn renders no markdown. > > #ABAP #SAP #UI5 -## Where the View Lives - -Plain text — LinkedIn renders no markdown. - -> Three ways to get a UI5 screen in front of a user on an ABAP stack. All three -> render in the browser, same control library, same framework. What differs is -> where the view is defined — and therefore when it is fixed. -> -> UI5 Freestyle: a file in a frontend project, fixed at build time. -> RAP with Fiori Elements: UI annotations on CDS, fixed when they are activated. -> abap2UI5: an XML string an ABAP class produced for this request. -> -> Everything else follows from that axis — what gets deployed per app, what a -> screen costs in artifacts, what can still change at runtime. Not a ranking: -> fixed early standardises well, fixed late adapts well. -> -> New article 🎉 -> -> Where does the view live in the app you are working on today? -> -> #ABAP #SAP #UI5 - ## RAP or abap2UI5? Plain text — LinkedIn renders no markdown. @@ -570,3 +819,118 @@ Plain text — LinkedIn renders no markdown. > Is your bottleneck building the apps, or governing them afterwards? > > #ABAP #SAP #UI5 + +## Written for Agents + +Plain text — LinkedIn renders no markdown. Draft, 848 characters. + +> An app is one class, and the class is text. That property has a consequence +> nobody was designing for in 2023: it is the shape an AI coding agent is best +> at. +> +> An agent writing a Fiori Elements app keeps a CDS view, annotations, a +> behavior definition, a service binding and a generated frontend in step, and +> can verify none of it without a system. An agent writing abap2UI5 writes one +> file, in one language, and the thing it writes is the thing that runs. +> +> Three things turn that into a working setup: a map for the reader with no +> checkout, a linter that checks the view without a system, and several +> hundred worked examples - many of them ported by agents exactly that way. +> +> New article 🎉 Code-first was the design. Agent-friendly was the consequence. +> +> What does your assistant get wrong about your framework, and what would fix +> it? +> +> #ABAP #SAP #UI5 + +## Not yet published + +Posts whose article is not in `docs/advanced/insights/` yet. Each moves up +into the numbered order the day its article lands. + +### No Cache, No Deploy, Any IDE + +Plain text — LinkedIn renders no markdown. + +> The abap2UI5 loop is: change the class, activate, refresh the browser. +> +> What is missing from that sentence is the point. No deployment — activating is +> the deployment. No cache to invalidate — the UI is built per request, so +> nothing can be stale. No IDE agreement, because it is ABAP. No context switch +> to debug: a breakpoint in the method that built the view stops there. +> +> Individually, conveniences. Together, the reason a screen gets tried at all — +> when an experiment costs a class and a refresh, "could we just show this?" +> stops being a project. +> +> New article 🎉 +> +> What is your current edit-to-see-it time on a Fiori change? +> +> #ABAP #SAP #UI5 + +### 2,300 Lines + +Plain text — LinkedIn renders no markdown. + +> The communication core of abap2UI5 is one HTTP handler, two interfaces and one +> database table — originally around 2,300 lines of ABAP. +> +> It is small because of what it does not do. It does not build views, apps do. +> It does not decide flow, apps do. It does not wrap UI5 controls, so it does +> not grow when UI5 does. +> +> Which matters past elegance: a framework in the request path sees every input, +> every response, every user. There is a difference between trusting a package +> because it is popular and being able to open it and find out. +> +> New article 🎉 +> +> When did you last read a framework you depend on? +> +> #ABAP #SAP #UI5 + +### What It Does Not Do + +Plain text — LinkedIn renders no markdown. + +> Eighteen articles is enough credit to say what abap2UI5 does not do. +> +> Offline is out — every event asks the server what happens next; take the +> server away and there is no app left. Pushdown to HANA is indirect, and a +> frontend fuzzy search help is not available. Real-time and collaborative UIs +> are the wrong shape for a request-per-event model. Separate frontend and +> backend teams lose the contract between them. And where a Fiori Elements +> floorplan fits, it fits — that is less work, not more. +> +> What is left is still most business software: forms, tables, dashboards, +> approvals, the small screens nobody funds a project for. +> +> New article 🎉 +> +> Where would you put the line? +> +> #ABAP #SAP #UI5 + +### Where the View Lives + +Plain text — LinkedIn renders no markdown. + +> Three ways to get a UI5 screen in front of a user on an ABAP stack. All three +> render in the browser, same control library, same framework. What differs is +> where the view is defined — and therefore when it is fixed. +> +> UI5 Freestyle: a file in a frontend project, fixed at build time. +> RAP with Fiori Elements: UI annotations on CDS, fixed when they are activated. +> abap2UI5: an XML string an ABAP class produced for this request. +> +> Everything else follows from that axis — what gets deployed per app, what a +> screen costs in artifacts, what can still change at runtime. Not a ranking: +> fixed early standardises well, fixed late adapts well. +> +> New article 🎉 +> +> Where does the view live in the app you are working on today? +> +> #ABAP #SAP #UI5 diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index 5a1e55bf..bb9cc023 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -276,13 +276,17 @@ export default defineConfig({ ], }, { - // No overview page any more. This is a collection of concrete - // problem-and-solution chapters, and a map page in front of it was a - // stop between the reader and the recipe - it restated the sidebar - // they were already looking at. The section opens on the first - // chapter instead. + // An index page again, for DISCOVERY rather than lookup. It was + // taken out once, with reason: a reader looking a recipe up has this + // sidebar, and a map page in front of it restated the menu they were + // already reading. So the section opened on its first chapter, the + // Cheat Sheet - and a first visit landed on a page whose opening line + // says it is a checklist, not an introduction. The index is that + // introduction: one row per group with what it covers and where it + // starts, and the sheet named as the page to keep open. Lookup still + // goes straight to the rows below; the row above them opens the map. text: "Cookbook", - link: "/cookbook/cheat_sheet", + link: "/cookbook/", collapsed: true, items: [ // The sheet you keep open while working - first, because this is @@ -465,7 +469,10 @@ export default defineConfig({ link: "/configuration/installation", collapsed: true, items: [ - { text: "Installation", link: "/configuration/installation" }, + // The file keeps its name - the URL has been public for years - and + // the page says what it is about: not how to install the framework + // (that is the Quickstart) but where the endpoint lives per system. + { text: "Deployment Scenarios", link: "/configuration/installation" }, { text: "Setup", link: "/configuration/setup", @@ -502,7 +509,11 @@ export default defineConfig({ }, { text: "Advanced Topics", - link: "/advanced/extensibility/user_exits", + // The section's own page: one row per group below, with what it + // covers and its first page. It opened on User Exits, the first leaf + // of the first group, which told a reader nothing about the other + // four groups. + link: "/advanced/", collapsed: true, items: [ // Twelve entries in a flat list until now, of four different kinds: diff --git a/docs/advanced/downporting.md b/docs/advanced/downporting.md index f5bddacc..f4d69ec4 100644 --- a/docs/advanced/downporting.md +++ b/docs/advanced/downporting.md @@ -16,7 +16,7 @@ To install on an older system, use the `702` branch: Some sample projects and other repositories also ship a downported version. Check whether a `702` branch is available. ## Installation -The installation on an old release follows the normal [installation](/configuration/installation) flow with two adjustments: +The installation on an old release follows the normal [Quickstart](/get_started/quickstart) flow with two adjustments: 1. Pull the `702` branch with abapGit — abapGit itself is 7.02-compatible, so deployment to old ECC machines works the same way as everywhere else 2. Create the HTTP handler (SICF service) with the 7.02-compatible handler syntax instead of the modern snippet diff --git a/docs/advanced/fiori.md b/docs/advanced/fiori.md index b004bc8f..1cf79b66 100644 --- a/docs/advanced/fiori.md +++ b/docs/advanced/fiori.md @@ -11,14 +11,14 @@ Extend the object page of a Fiori list report app with an abap2UI5 app. See the 1. Register the FLP integration in the component.js: -```javascript +```js // Register abap2UI5 FLP integration for component container jQuery.sap.registerModulePath("z2ui5", "/sap/bc/ui5_ui5/sap/z2ui5"); ``` 2. Adapt the object page extension controller.js: -```javascript +```js sap.ui.core.Component.create({ name: "z2ui5", settings: { diff --git a/docs/advanced/http.md b/docs/advanced/http.md index 5e324da2..c7734fd0 100644 --- a/docs/advanced/http.md +++ b/docs/advanced/http.md @@ -3,7 +3,7 @@ outline: [2, 4] --- # HTTP Connector -The [abap2UI5 HTTP Connector](https://github.com/abap2UI5-addons/http-connector) calls abap2UI5 apps remotely over HTTP between two ABAP systems. It works similarly to the [RFC Connector](./rfc.md), but uses HTTP instead of RFC as the communication channel. +The [abap2UI5 HTTP Connector](https://github.com/abap2UI5-addons/http-connector) calls abap2UI5 apps remotely over HTTP between two ABAP systems. It works similarly to the [RFC Connector](/advanced/rfc), but uses HTTP instead of RFC as the communication channel. ## When to use The HTTP Connector enables several useful cases: diff --git a/docs/advanced/index.md b/docs/advanced/index.md new file mode 100644 index 00000000..d7632b10 --- /dev/null +++ b/docs/advanced/index.md @@ -0,0 +1,20 @@ +--- +outline: [2, 4] +description: What comes after the first app - extending the framework, connecting it to other systems, running it on older releases and other stacks, and the tools you develop with. +--- +# Advanced Topics + +Everything in this section is something you do to an app that already runs: +extend the framework underneath it, connect it to another system, take it to +a release or a stack it was not written on, or set up the machine you write +it on. None of it is needed for a first app — the +[Quickstart](/get_started/quickstart) and the [Cookbook](/cookbook/) come +first — and every group below stands on its own. + +| Group | What it covers | Start with | +|---|---|---| +| Extensibility | User exits on the HTTP handler, the frontend artifacts as an app of your own, custom UI5 controls | [User Exits](/advanced/extensibility/user_exits) | +| Integration | Calling abap2UI5 apps on another system over RFC or HTTP, and an abap2UI5 app inside a Fiori Elements object page | [RFC Connector](/advanced/rfc) | +| Releases, Stacks | The downport to NetWeaver 7.02, renaming the `Z2UI5_` prefix, developing in a repository off the stack, the legacy-free UI5 runtime | [Downporting](/advanced/downporting) | +| Developer Setup | The tooling around the framework, the linter, the MCP server, the VS Code extension, and the setup for an AI agent | [Tooling](/advanced/tooling) | +| Toolchain | The open-source projects the framework is built with — abapGit, ajson, S-RTTI, abaplint, open-abap, abap-cleaner, abapmerge | [Toolchain](/technical/tools/) | diff --git a/docs/advanced/insights/01-somewhere-on-the-way-to-ui5.md b/docs/advanced/insights/01-somewhere-on-the-way-to-ui5.md index ad19afdb..75493820 100644 --- a/docs/advanced/insights/01-somewhere-on-the-way-to-ui5.md +++ b/docs/advanced/insights/01-somewhere-on-the-way-to-ui5.md @@ -135,5 +135,3 @@ directly, so a structure that exists only at runtime stops being a special case. It is just data — the way it was in the old SALV and field-catalog days. So next time you reach for RTTS and need a screen, give abap2UI5 a try. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/02-the-cost-of-a-screen.md b/docs/advanced/insights/02-the-cost-of-a-screen.md index df05b812..05fccfae 100644 --- a/docs/advanced/insights/02-the-cost-of-a-screen.md +++ b/docs/advanced/insights/02-the-cost-of-a-screen.md @@ -127,8 +127,6 @@ Install it with abapGit and give it a try! A screen that costs one class is a screen that gets built. -Happy ABAPing! 🦖🦕🦣 - *The article before this one: [#1 Somewhere on the Way to UI5, We Lost RTTS](/advanced/insights/01-somewhere-on-the-way-to-ui5) — a table whose structure is only known at runtime, drawn in UI5.* diff --git a/docs/advanced/insights/03-your-favorite-programming-model.md b/docs/advanced/insights/03-your-favorite-programming-model.md index 6cbaca54..639165b6 100644 --- a/docs/advanced/insights/03-your-favorite-programming-model.md +++ b/docs/advanced/insights/03-your-favorite-programming-model.md @@ -193,5 +193,3 @@ launchpad as all your other UI5 and RAP apps. It reaches your business logic however you like, with the programming model of your choice. One interface, one method, and no opinion about what is behind the screen. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/04-no-annotation-in-between.md b/docs/advanced/insights/04-no-annotation-in-between.md index 36065ace..de23c314 100644 --- a/docs/advanced/insights/04-no-annotation-in-between.md +++ b/docs/advanced/insights/04-no-annotation-in-between.md @@ -119,5 +119,3 @@ control by control. Reaching every control is not the same as being handed one. A vocabulary is a promise about what will be needed. An API makes no such promise — and takes nothing off the table either. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/05-ui5-over-the-wire.md b/docs/advanced/insights/05-ui5-over-the-wire.md index 91bc96a2..8c8ed88a 100644 --- a/docs/advanced/insights/05-ui5-over-the-wire.md +++ b/docs/advanced/insights/05-ui5-over-the-wire.md @@ -56,5 +56,3 @@ design — so what travels is the layer directly above it. A view, not a page. The frontend stopped being an application and became a renderer, and most of what follows in this series comes out of that one move. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/06-the-frontend-knows-nothing.md b/docs/advanced/insights/06-the-frontend-knows-nothing.md index 0e129364..873418a9 100644 --- a/docs/advanced/insights/06-the-frontend-knows-nothing.md +++ b/docs/advanced/insights/06-the-frontend-knows-nothing.md @@ -43,5 +43,3 @@ ancient UI5 version knows what that is worth. A frontend that knows nothing about the app is a frontend that never needs to be redeployed when the app changes. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/07-one-handler-for-every-app.md b/docs/advanced/insights/07-one-handler-for-every-app.md index 8dd2e90e..92a22600 100644 --- a/docs/advanced/insights/07-one-handler-for-every-app.md +++ b/docs/advanced/insights/07-one-handler-for-every-app.md @@ -54,5 +54,3 @@ govern, and becomes a class. A handler that knows nothing about the application never has to be written again for the next one. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/08-only-the-changed-part.md b/docs/advanced/insights/08-only-the-changed-part.md index a7b80e22..591137b9 100644 --- a/docs/advanced/insights/08-only-the-changed-part.md +++ b/docs/advanced/insights/08-only-the-changed-part.md @@ -53,5 +53,3 @@ noticed. Data binding was doing this while the JavaScript world was still inventing names for it. Sending the whole view is the exception, not the rhythm. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/09-a-new-instance-on-every-request.md b/docs/advanced/insights/09-a-new-instance-on-every-request.md index e23f7de1..dcd2f4d5 100644 --- a/docs/advanced/insights/09-a-new-instance-on-every-request.md +++ b/docs/advanced/insights/09-a-new-instance-on-every-request.md @@ -57,5 +57,3 @@ about this app during a rolling restart. Take that as high praise. Stateful for the user, stateless for the system — and the price is keeping the instance small. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/10-swapping-the-view-at-runtime.md b/docs/advanced/insights/10-swapping-the-view-at-runtime.md index 678c4403..8f44efdf 100644 --- a/docs/advanced/insights/10-swapping-the-view-at-runtime.md +++ b/docs/advanced/insights/10-swapping-the-view-at-runtime.md @@ -66,5 +66,3 @@ request is running, then what a screen can adapt to is no longer fixed on the day it was designed. A view that is built is a view that can be built differently. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/11-no-build-no-deploy-no-cache.md b/docs/advanced/insights/11-no-build-no-deploy-no-cache.md index 99c9cd47..4e071067 100644 --- a/docs/advanced/insights/11-no-build-no-deploy-no-cache.md +++ b/docs/advanced/insights/11-no-build-no-deploy-no-cache.md @@ -56,5 +56,3 @@ show this on a screen?" stops being the opening of a project and becomes something you try before lunch. Iteration speed is not a nice-to-have. It decides which ideas get built at all. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/12-where-your-own-javascript-goes.md b/docs/advanced/insights/12-where-your-own-javascript-goes.md index c094e5c9..074e0a8f 100644 --- a/docs/advanced/insights/12-where-your-own-javascript-goes.md +++ b/docs/advanced/insights/12-where-your-own-javascript-goes.md @@ -53,5 +53,3 @@ for either. Extensibility is not the absence of a boundary. It is knowing exactly where the boundary is. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/13-four-verbs-every-control.md b/docs/advanced/insights/13-four-verbs-every-control.md index 850859d3..abda8ab1 100644 --- a/docs/advanced/insights/13-four-verbs-every-control.md +++ b/docs/advanced/insights/13-four-verbs-every-control.md @@ -40,5 +40,3 @@ The completion list was a way to avoid reading the SDK. Coverage of the whole API is worth more than a shortcut around part of it. Four verbs that name no control can build every control. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/14-a-classrun-for-the-browser.md b/docs/advanced/insights/14-a-classrun-for-the-browser.md index 5f24b808..7285781b 100644 --- a/docs/advanced/insights/14-a-classrun-for-the-browser.md +++ b/docs/advanced/insights/14-a-classrun-for-the-browser.md @@ -55,5 +55,3 @@ That holds for the small screens this series keeps coming back to. A larger app splits into several classes like any other ABAP program does, and how is [its own article](/advanced/insights/25-when-one-class-is-not-enough). The point is where it starts: one class, one method, and it runs. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/15-where-the-selection-screen-went.md b/docs/advanced/insights/15-where-the-selection-screen-went.md index 394a1cf0..56743565 100644 --- a/docs/advanced/insights/15-where-the-selection-screen-went.md +++ b/docs/advanced/insights/15-where-the-selection-screen-went.md @@ -46,5 +46,3 @@ field and the variable. The selection screen's best idea was never the screen. It was that the variable and the field were the same declaration. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/16-one-click-one-request.md b/docs/advanced/insights/16-one-click-one-request.md index 51d7a7e9..bcdd6e6c 100644 --- a/docs/advanced/insights/16-one-click-one-request.md +++ b/docs/advanced/insights/16-one-click-one-request.md @@ -61,5 +61,3 @@ Local variables, an open cursor, a lock: gone with the work process, every time, no exceptions. PBO builds, PAI decides, and the dialog step is a POST. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/17-what-the-client-can-do.md b/docs/advanced/insights/17-what-the-client-can-do.md index a2e00671..d2962aa7 100644 --- a/docs/advanced/insights/17-what-the-client-can-do.md +++ b/docs/advanced/insights/17-what-the-client-can-do.md @@ -56,5 +56,3 @@ interface itself, so it cannot drift from what the framework actually offers. One method in, one interface out. Everything an app can do is a method on `client`. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/18-call-screen-leave-screen.md b/docs/advanced/insights/18-call-screen-leave-screen.md index 6de1ab7d..db559810 100644 --- a/docs/advanced/insights/18-call-screen-leave-screen.md +++ b/docs/advanced/insights/18-call-screen-leave-screen.md @@ -62,5 +62,3 @@ than a `nav_app_call( )`, and [its own page](/cookbook/event_navigation/navigation/cross_app). The stack came along. Its elements are instances now, not screen numbers. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/19-where-f4-went.md b/docs/advanced/insights/19-where-f4-went.md index 60a04981..e33fb352 100644 --- a/docs/advanced/insights/19-where-f4-went.md +++ b/docs/advanced/insights/19-where-f4-went.md @@ -59,5 +59,3 @@ per user, or a table that did not exist a millisecond ago, which is where F4 is three lines and a SELECT now. It is no longer free, and it is no longer limited to the DDIC either. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/20-message-still-works.md b/docs/advanced/insights/20-message-still-works.md index d0ebb0eb..3247c7f0 100644 --- a/docs/advanced/insights/20-message-still-works.md +++ b/docs/advanced/insights/20-message-still-works.md @@ -60,5 +60,3 @@ developer wants and considerably more than a browser should get. The message classes, the texts and the translation stay. Only the dump looks different. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/21-the-lock-is-gone-by-the-next-click.md b/docs/advanced/insights/21-the-lock-is-gone-by-the-next-click.md index 1d0b16e2..5c665597 100644 --- a/docs/advanced/insights/21-the-lock-is-gone-by-the-next-click.md +++ b/docs/advanced/insights/21-the-lock-is-gone-by-the-next-click.md @@ -75,5 +75,3 @@ out of habit. A lock is a promise about a session. Where the session is one request long, the promise has to be, too. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/22-who-may-start-which-app.md b/docs/advanced/insights/22-who-may-start-which-app.md index 3e8afd87..fc05a6c2 100644 --- a/docs/advanced/insights/22-who-may-start-which-app.md +++ b/docs/advanced/insights/22-who-may-start-which-app.md @@ -62,5 +62,3 @@ it, and nothing else — there is no client-side rule to read out of a bundle, because there is no bundle. One node, one class, one AUTHORITY-CHECK. The rest is PFCG. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/23-100-rows-four-hours-one-request.md b/docs/advanced/insights/23-100-rows-four-hours-one-request.md index c0cc21c7..63a9f01c 100644 --- a/docs/advanced/insights/23-100-rows-four-hours-one-request.md +++ b/docs/advanced/insights/23-100-rows-four-hours-one-request.md @@ -48,5 +48,3 @@ read again. Small instance, bounded model, one request per click. Everything else the system already does well. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/24-abap-unit-for-a-screen.md b/docs/advanced/insights/24-abap-unit-for-a-screen.md index 0a95dfb9..a2323e05 100644 --- a/docs/advanced/insights/24-abap-unit-for-a-screen.md +++ b/docs/advanced/insights/24-abap-unit-for-a-screen.md @@ -158,5 +158,3 @@ pipeline, in seconds. Logic in ABAP Unit, view in the linter, and the roundtrip in a browser. Keep the client out of the logic, and the logic is testable the way any ABAP class is. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/25-when-one-class-is-not-enough.md b/docs/advanced/insights/25-when-one-class-is-not-enough.md index dbb0bcf1..2fb52ce8 100644 --- a/docs/advanced/insights/25-when-one-class-is-not-enough.md +++ b/docs/advanced/insights/25-when-one-class-is-not-enough.md @@ -45,5 +45,3 @@ container. The seams are classes, method calls and one stack — which is exactl what a larger ABAP program has been made of for thirty years. A larger app is more classes. The unit stayed the same size. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/26-a-tile-like-any-other.md b/docs/advanced/insights/26-a-tile-like-any-other.md index 8695c4e5..cf203c40 100644 --- a/docs/advanced/insights/26-a-tile-like-any-other.md +++ b/docs/advanced/insights/26-a-tile-like-any-other.md @@ -49,5 +49,3 @@ the [ABAP Cloud pages](/configuration/btp) describe the setup. One shell in the UI5 repository, one parameter per tile. No user can tell the difference. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/27-one-codebase-702-to-abap-cloud.md b/docs/advanced/insights/27-one-codebase-702-to-abap-cloud.md index c3a8a23e..2fd32e90 100644 --- a/docs/advanced/insights/27-one-codebase-702-to-abap-cloud.md +++ b/docs/advanced/insights/27-one-codebase-702-to-abap-cloud.md @@ -38,5 +38,3 @@ sentence in this article. A framework that needs nothing from the release does not have to be ported when the release changes. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/28-cloud-ready-is-a-property-of-your-app.md b/docs/advanced/insights/28-cloud-ready-is-a-property-of-your-app.md index 0afdf076..7486df1d 100644 --- a/docs/advanced/insights/28-cloud-ready-is-a-property-of-your-app.md +++ b/docs/advanced/insights/28-cloud-ready-is-a-property-of-your-app.md @@ -53,5 +53,3 @@ property of what the application reads and writes, and it is decided in the A cloud-ready framework does not make a cloud-ready app. It just stops being the reason one is not. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/29-when-the-api-is-not-released.md b/docs/advanced/insights/29-when-the-api-is-not-released.md index 5c6efe8c..ad58de57 100644 --- a/docs/advanced/insights/29-when-the-api-is-not-released.md +++ b/docs/advanced/insights/29-when-the-api-is-not-released.md @@ -49,8 +49,6 @@ level later means changing what it calls, not how it renders: A wrapper does not make the dependency clean. It makes it findable. -Happy ABAPing! 🦖🦕🦣 - *The levels are SAP's, from the [Clean Core Extensibility white paper](https://community.sap.com/t5/technology-blog-posts-by-sap/why-clean-core-matters-get-some-insights-into-our-brand-new-extensibility/ba-p/14163750) of August 2025; the diff --git a/docs/advanced/insights/30-on-stack-or-side-by-side.md b/docs/advanced/insights/30-on-stack-or-side-by-side.md index 5bef89dc..6ac94581 100644 --- a/docs/advanced/insights/30-on-stack-or-side-by-side.md +++ b/docs/advanced/insights/30-on-stack-or-side-by-side.md @@ -38,5 +38,3 @@ side closes the gap and is graded on its own — the BTP side stays Level A: Neither choice touches the app class. The same code renders in both places — only what it reads changes. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/31-one-app-many-systems.md b/docs/advanced/insights/31-one-app-many-systems.md index 2eace3e0..dbb69e18 100644 --- a/docs/advanced/insights/31-one-app-many-systems.md +++ b/docs/advanced/insights/31-one-app-many-systems.md @@ -31,5 +31,3 @@ responsibility the ABAP authorization concept was not asked to carry here. A framework with nothing to install per system is a framework that can serve systems it was never installed on. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/32-from-its-to-abap2ui5.md b/docs/advanced/insights/32-from-its-to-abap2ui5.md index 52d9e300..4d3ffd9a 100644 --- a/docs/advanced/insights/32-from-its-to-abap2ui5.md +++ b/docs/advanced/insights/32-from-its-to-abap2ui5.md @@ -39,5 +39,3 @@ otherwise. What has been moving back ever since is not the rendering but the Which makes Over-the-Wire less of a new idea than a returning one, arriving with the part the old server-rendered world never had — a real control library in the browser. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/33-rap-or-abap2ui5.md b/docs/advanced/insights/33-rap-or-abap2ui5.md index 289720e0..4cd88791 100644 --- a/docs/advanced/insights/33-rap-or-abap2ui5.md +++ b/docs/advanced/insights/33-rap-or-abap2ui5.md @@ -41,5 +41,3 @@ different UI in front of it. Rule of thumb: model the behavior once, in RAP, if more than one thing will use it. Build the screen wherever it is cheapest. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/34-freestyle-or-abap2ui5.md b/docs/advanced/insights/34-freestyle-or-abap2ui5.md index 77ecc344..60801e55 100644 --- a/docs/advanced/insights/34-freestyle-or-abap2ui5.md +++ b/docs/advanced/insights/34-freestyle-or-abap2ui5.md @@ -52,5 +52,3 @@ project for. Nothing about picking one rules out the other later. The view is a string either way, and the controls are the same controls. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/35-low-code-or-abap2ui5.md b/docs/advanced/insights/35-low-code-or-abap2ui5.md index a90045fb..977f18ba 100644 --- a/docs/advanced/insights/35-low-code-or-abap2ui5.md +++ b/docs/advanced/insights/35-low-code-or-abap2ui5.md @@ -43,5 +43,3 @@ from that. Neither is a migration. abap2UI5 is adopted one app at a time, and the first one costs an abapGit pull and an afternoon. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/insights/36-written-for-agents.md b/docs/advanced/insights/36-written-for-agents.md index ca4524a3..cf768f24 100644 --- a/docs/advanced/insights/36-written-for-agents.md +++ b/docs/advanced/insights/36-written-for-agents.md @@ -43,5 +43,3 @@ The setup, from *paste this* to an MCP server, is on [Developing with AI](/get_started/ai). Code-first was the design. Agent-friendly was the consequence. - -Happy ABAPing! 🦖🦕🦣 diff --git a/docs/advanced/mcp_server.md b/docs/advanced/mcp_server.md index d4dfd37b..c3ee0774 100644 --- a/docs/advanced/mcp_server.md +++ b/docs/advanced/mcp_server.md @@ -216,7 +216,7 @@ magnitude apart, and most iterations should end at the first. ## Next Steps -- [Building with AI](/get_started/ai) — the whole AI setup in rising order of +- [Developing with AI](/get_started/ai) — the whole AI setup in rising order of effort; this server is the top rung - [abap2UI5 linter](/advanced/linter) — the gates behind `validate_view` and `screenshot_view` diff --git a/docs/advanced/tooling.md b/docs/advanced/tooling.md index 5ed422b3..f1af487b 100644 --- a/docs/advanced/tooling.md +++ b/docs/advanced/tooling.md @@ -12,7 +12,7 @@ app without leaving the editor. They are independent of each other. Take the first one and stop, or take all three. Building with an AI assistant? That whole side of the tooling — the indexes, the agent conventions, the MCP server — is collected on -[Building with AI](/get_started/ai). +[Developing with AI](/get_started/ai). ## Start a project from the template diff --git a/docs/configuration/installation.md b/docs/configuration/installation.md index f9458577..22c2b91c 100644 --- a/docs/configuration/installation.md +++ b/docs/configuration/installation.md @@ -1,8 +1,8 @@ --- outline: [2, 4] --- -# Installation -abap2UI5 apps run directly via an HTTP service — the fastest and simplest setup. This works on every ABAP release and with both ABAP language versions (Standard ABAP and ABAP for Cloud). +# Deployment Scenarios +To install the framework, start with the [Quickstart](/get_started/quickstart) — this page is about where the endpoint may live, per system. abap2UI5 apps run directly via an HTTP service — the fastest and simplest setup. This works on every ABAP release and with both ABAP language versions (Standard ABAP and ABAP for Cloud). But production use may need extra considerations across different system environments. diff --git a/docs/configuration/productive_usage.md b/docs/configuration/productive_usage.md index 8a4592bc..956f7b5e 100644 --- a/docs/configuration/productive_usage.md +++ b/docs/configuration/productive_usage.md @@ -13,7 +13,7 @@ Every commit to the framework runs through CI before it reaches a release: - Builds and static checks against **NW 7.02, Standard ABAP, and ABAP Cloud** — one workflow per release line, so compatibility across the supported stacks is verified continuously, not per release - **Unit tests** executed on every commit via [open-abap](/technical/tools/#open-abap), plus automated **browser tests** of the running framework -- The sample catalogs double as a regression corpus: hundreds of apps that are validated against the framework +- The sample catalogs double as a regression corpus: over 700 apps that are validated against the framework ## Renaming If you're starting new development but already have abap2UI5 apps in production and want to avoid update risk, install multiple instances of abap2UI5 with the [renaming feature](/advanced/renaming). This lets you keep developing safely without disrupting your existing production apps. diff --git a/docs/cookbook/browser_interaction/clipboard.md b/docs/cookbook/browser_interaction/clipboard.md index 54d6d837..ae28d9f1 100644 --- a/docs/cookbook/browser_interaction/clipboard.md +++ b/docs/cookbook/browser_interaction/clipboard.md @@ -55,7 +55,7 @@ ENDCLASS. ## Copy the App State URL -To share the current app state instead of a custom string, compose the link with `client->app_state_get_href( )` and hand it to the same `clipboard_copy` action — see [App State](../event_navigation/navigation/app_state.md). +To share the current app state instead of a custom string, compose the link with `client->app_state_get_href( )` and hand it to the same `clipboard_copy` action — see [App State](/cookbook/event_navigation/navigation/app_state). ::: warning The browser's Clipboard API requires HTTPS (or `localhost`). On plain HTTP the call is silently ignored. @@ -65,9 +65,10 @@ The browser's Clipboard API requires HTTPS (or `localhost`). On plain HTTP the c ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/browser_interaction/focus.md b/docs/cookbook/browser_interaction/focus.md index 33a01a78..4c95e5e3 100644 --- a/docs/cookbook/browser_interaction/focus.md +++ b/docs/cookbook/browser_interaction/focus.md @@ -95,15 +95,16 @@ client->follow_up_action( val = client->cs_event-set_focus ## Barcode Scanning -Most barcode scanner devices emulate a keyboard. Combine `set_focus` with input fields to capture scans into the right field automatically — see [Barcode Scanning](../device_capabilities/barcode_scanning.md) for a full walkthrough. +Most barcode scanner devices emulate a keyboard. Combine `set_focus` with input fields to capture scans into the right field automatically — see [Barcode Scanning](/cookbook/device_capabilities/barcode_scanning) for a full walkthrough. ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/browser_interaction/keyboard_shortcuts.md b/docs/cookbook/browser_interaction/keyboard_shortcuts.md index 8b5a3484..55dba0b3 100644 --- a/docs/cookbook/browser_interaction/keyboard_shortcuts.md +++ b/docs/cookbook/browser_interaction/keyboard_shortcuts.md @@ -87,9 +87,10 @@ For controlling the *soft keyboard* on mobile devices, see [Soft Keyboard](/cook ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/browser_interaction/scrolling.md b/docs/cookbook/browser_interaction/scrolling.md index 31cb84c9..8082e52c 100644 --- a/docs/cookbook/browser_interaction/scrolling.md +++ b/docs/cookbook/browser_interaction/scrolling.md @@ -107,9 +107,10 @@ client->follow_up_action( val = client->cs_event-scroll_into_view ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/browser_interaction/soft_keyboard.md b/docs/cookbook/browser_interaction/soft_keyboard.md index cdae1354..172411e6 100644 --- a/docs/cookbook/browser_interaction/soft_keyboard.md +++ b/docs/cookbook/browser_interaction/soft_keyboard.md @@ -77,9 +77,10 @@ To re-enable the keyboard, fire the same event with a different mode (`text`, `n ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/browser_interaction/timer.md b/docs/cookbook/browser_interaction/timer.md index 1a77ba5f..a76f6bfc 100644 --- a/docs/cookbook/browser_interaction/timer.md +++ b/docs/cookbook/browser_interaction/timer.md @@ -94,9 +94,10 @@ Each timer tick causes a full backend roundtrip. Use sensible intervals (e.g. 20 ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/browser_interaction/title.md b/docs/cookbook/browser_interaction/title.md index 1e22588d..ee575ea3 100644 --- a/docs/cookbook/browser_interaction/title.md +++ b/docs/cookbook/browser_interaction/title.md @@ -73,9 +73,10 @@ Use `set_title` for the browser tab/window title (standalone) and `set_title_lau ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/browser_interaction/url_handling.md b/docs/cookbook/browser_interaction/url_handling.md index ac1e2c83..87871fd3 100644 --- a/docs/cookbook/browser_interaction/url_handling.md +++ b/docs/cookbook/browser_interaction/url_handling.md @@ -113,9 +113,10 @@ because the backend decides what to push. ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/device_capabilities/audio.md b/docs/cookbook/device_capabilities/audio.md index d210b4db..85505dfa 100644 --- a/docs/cookbook/device_capabilities/audio.md +++ b/docs/cookbook/device_capabilities/audio.md @@ -1,5 +1,7 @@ --- outline: [2, 4] +samples: + - z2ui5_cl_smps_app_487 --- # Audio, Video @@ -61,3 +63,18 @@ CLASS z2ui5_cl_sample_sound IMPLEMENTATION. ENDCLASS. ``` For a complete sound sample, see `Z2UI5_CL_SMPS_APP_487` in the [samples-stack repository](https://github.com/abap2UI5/samples-stack). + + + +## Working Samples + +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. + +| Sample | Class | +|---|---| +| MIME — Audio and Play Sound — a success and an error tone, addressed by their ICF path | [`Z2UI5_CL_SMPS_APP_487`](https://github.com/abap2UI5/samples-stack/blob/main/src/08/z2ui5_cl_smps_app_487.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `08-mime` | + + diff --git a/docs/cookbook/device_capabilities/barcode_scanning.md b/docs/cookbook/device_capabilities/barcode_scanning.md index ef35b3c4..ccfaf7f5 100644 --- a/docs/cookbook/device_capabilities/barcode_scanning.md +++ b/docs/cookbook/device_capabilities/barcode_scanning.md @@ -1,5 +1,7 @@ --- outline: [2, 4] +samples: + - z2ui5_cl_smpc_sapui5_011 --- # Barcode Scanning @@ -166,7 +168,21 @@ CLASS z2ui5_cl_sample_sound IMPLEMENTATION. ENDMETHOD. ENDCLASS. ``` -For a complete sound sample, see `Z2UI5_CL_SMPS_APP_487` in the [samples-stack repository](https://github.com/abap2UI5/samples-stack). ## Render Barcodes To also render barcodes, use bwip-js, which ships with the js-libraries add-on. See [Add-ons](/resources/addons) for details. + + + +## Working Samples + +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. + +| Sample | Class | +|---|---| +| sap.ndc - BarcodeScannerButton — sap.ndc.BarcodeScannerButton expressed in abap2UI5 - a SAPUI5-only control, so the demo kit original is outside OpenUI5 and this is orientation rather than a 1:1 port. | [`Z2UI5_CL_SMPC_SAPUI5_011`](https://github.com/abap2UI5/samples-controls/blob/main/src/03/z2ui5_cl_smpc_sapui5_011.clas.abap) · [samples-controls](https://github.com/abap2UI5/samples-controls) | + + diff --git a/docs/cookbook/device_capabilities/camera.md b/docs/cookbook/device_capabilities/camera.md index 5fa32658..6822f2dd 100644 --- a/docs/cookbook/device_capabilities/camera.md +++ b/docs/cookbook/device_capabilities/camera.md @@ -57,9 +57,10 @@ Camera access relies on browser permissions and security settings. Most browsers ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/device_capabilities/geolocation.md b/docs/cookbook/device_capabilities/geolocation.md index 29ee6d72..a44867dd 100644 --- a/docs/cookbook/device_capabilities/geolocation.md +++ b/docs/cookbook/device_capabilities/geolocation.md @@ -86,9 +86,10 @@ Geolocation needs the user to grant permission in the browser. Serve your HTTP e ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/device_capabilities/info.md b/docs/cookbook/device_capabilities/info.md index 423f3745..12fd6a7f 100644 --- a/docs/cookbook/device_capabilities/info.md +++ b/docs/cookbook/device_capabilities/info.md @@ -8,29 +8,138 @@ samples: abap2UI5 ships the current frontend state with every roundtrip. Read it from `client->get( )` — no custom control, no extra event needed. The relevant sub-structures are `s_device`, `s_ui5`, `s_focus`, and `s_scroll`. +## Reading Two of Them + +Two of the four are enough to see the mechanism. The class below reads +`s_ui5` and `s_device` in its display branch and shows every field in two +lists. Nothing is requested for it: the browser sent the values along with +the roundtrip that started the app, so `client->get( )` already holds them by +the time `main` runs. Press **Run** to see what your own browser reports: + +```abap +CLASS z2ui5_cl_sample_info DEFINITION PUBLIC. + + PUBLIC SECTION. + INTERFACES z2ui5_if_app. + + DATA mv_ui5_version TYPE string. + DATA mv_ui5_theme TYPE string. + DATA mv_ui5_build TYPE string. + DATA mv_system TYPE string. + DATA mv_browser TYPE string. + DATA mv_os TYPE string. + DATA mv_viewport TYPE string. + DATA mv_touch TYPE string. + + PROTECTED SECTION. + PRIVATE SECTION. +ENDCLASS. + +CLASS z2ui5_cl_sample_info IMPLEMENTATION. + METHOD z2ui5_if_app~main. + + IF client->check_on_navigated( ). + + DATA(ui5) = client->get( )-s_ui5. + DATA(device) = client->get( )-s_device. + + mv_ui5_version = ui5-version. + mv_ui5_theme = ui5-theme. + mv_ui5_build = ui5-build_timestamp. + mv_system = device-system. + mv_browser = |{ device-browser-name } { device-browser-version }|. + mv_os = |{ device-os-name } { device-os-version }|. + mv_viewport = |{ device-resize-width } x { device-resize-height } px|. + mv_touch = COND #( WHEN device-support-touch = abap_true THEN `yes` ELSE `no` ). + + DATA(view) = z2ui5_cl_ui5_view_builder=>factory( + )->ele( n = `View` ns = `mvc` + )->a( n = `xmlns` v = `sap.m` + )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc` + + )->ele( `Page` + )->a( n = `title` v = `Frontend Info` + + )->ele( `List` + )->a( n = `headerText` v = `UI5 — s_ui5` + + )->ele( `items` + + )->tag( `DisplayListItem` + )->a( n = `label` v = `Version` + )->a( n = `value` v = client->_bind( mv_ui5_version ) + )->tag( `DisplayListItem` + )->a( n = `label` v = `Theme` + )->a( n = `value` v = client->_bind( mv_ui5_theme ) + )->tag( `DisplayListItem` + )->a( n = `label` v = `Build` + )->a( n = `value` v = client->_bind( mv_ui5_build ) + + )->end( + + )->end( + + )->ele( `List` + )->a( n = `headerText` v = `Device — s_device` + + )->ele( `items` + + )->tag( `DisplayListItem` + )->a( n = `label` v = `System` + )->a( n = `value` v = client->_bind( mv_system ) + )->tag( `DisplayListItem` + )->a( n = `label` v = `Browser` + )->a( n = `value` v = client->_bind( mv_browser ) + )->tag( `DisplayListItem` + )->a( n = `label` v = `Operating system` + )->a( n = `value` v = client->_bind( mv_os ) + )->tag( `DisplayListItem` + )->a( n = `label` v = `Viewport` + )->a( n = `value` v = client->_bind( mv_viewport ) + )->tag( `DisplayListItem` + )->a( n = `label` v = `Touch` + )->a( n = `value` v = client->_bind( mv_touch ) ). + + client->view_display( view->stringify( ) ). + + ENDIF. + + ENDMETHOD. +ENDCLASS. +``` + +`s_ui5` names the UI5 runtime the page loaded — its version, its theme and its +build — and `s_device` is what UI5's device API says about the browser, the +operating system, the viewport and touch support. Both are a snapshot of the +roundtrip that built the view; the device model in +[Device Model](/cookbook/model/device_model) is the frontend half of the same +information, which follows a resize without a roundtrip. The four sections +below say where each structure is explained in full. + ## Device -For reading device information via `client->get( )-s_device`, see [Device Model](../model/device_model.md). +For reading device information via `client->get( )-s_device`, see [Device Model](/cookbook/model/device_model). ## UI5 -For reading the runtime UI5 framework details via `client->get( )-s_ui5`, see [UI5 Versions](../../configuration/ui5_versions.md). +For reading the runtime UI5 framework details via `client->get( )-s_ui5`, see [UI5 Versions](/configuration/ui5_versions). ## Focus -For reading the current focus via `client->get( )-s_focus`, see [Focus](../browser_interaction/focus.md). +For reading the current focus via `client->get( )-s_focus`, see [Focus](/cookbook/browser_interaction/focus). ## Scroll -For reading scroll positions via `client->get( )-s_scroll`, see [Scrolling](../browser_interaction/scrolling.md). +For reading scroll positions via `client->get( )-s_scroll`, see [Scrolling](/cookbook/browser_interaction/scrolling). ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/device_capabilities/pdf.md b/docs/cookbook/device_capabilities/pdf.md index 8ad2f3cb..cf389de0 100644 --- a/docs/cookbook/device_capabilities/pdf.md +++ b/docs/cookbook/device_capabilities/pdf.md @@ -80,7 +80,7 @@ For a ready-made dialog with a title bar and buttons, the ## Download Instead of Display -To let the user save the PDF rather than view it inline, use the [file download](./upload_download.md) pattern: +To let the user save the PDF rather than view it inline, use the [file download](/cookbook/device_capabilities/upload_download) pattern: ```abap client->follow_up_action( @@ -97,9 +97,10 @@ On older ABAP releases without `cl_web_http_utility`, use `cl_http_utility=>if_h ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/device_capabilities/upload_download.md b/docs/cookbook/device_capabilities/upload_download.md index 2d39b5f7..17ed82b0 100644 --- a/docs/cookbook/device_capabilities/upload_download.md +++ b/docs/cookbook/device_capabilities/upload_download.md @@ -94,9 +94,10 @@ ENDMETHOD. ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/eml_cds_sql/abap_sql.md b/docs/cookbook/eml_cds_sql/abap_sql.md index b160f45b..63fd66c3 100644 --- a/docs/cookbook/eml_cds_sql/abap_sql.md +++ b/docs/cookbook/eml_cds_sql/abap_sql.md @@ -138,5 +138,5 @@ WHEN client->check_on_event( `SAVE` ). ``` ::: tip -For data sourced from the Virtual Data Model, prefer reading from a [CDS view](./cds.md) instead of base tables — you get business semantics, associations, and authorization checks out of the box. +For data sourced from the Virtual Data Model, prefer reading from a [CDS view](/cookbook/eml_cds_sql/cds) instead of base tables — you get business semantics, associations, and authorization checks out of the box. ::: diff --git a/docs/cookbook/eml_cds_sql/draft_handling.md b/docs/cookbook/eml_cds_sql/draft_handling.md index b2128325..c00eb135 100644 --- a/docs/cookbook/eml_cds_sql/draft_handling.md +++ b/docs/cookbook/eml_cds_sql/draft_handling.md @@ -1,5 +1,11 @@ --- outline: [2, 4] +samples: + - z2ui5_cl_smps_app_006 + - z2ui5_cl_smps_app_007 + - z2ui5_cl_smps_app_008 + - z2ui5_cl_smps_app_009 + - z2ui5_cl_smps_app_010 --- # Draft Handling @@ -31,7 +37,7 @@ A helpful mental model: While a draft is open, the underlying record is **locked** so nobody else can edit it at the same time — but the lock is held by SAP's RAP framework, not by your app. That means your abap2UI5 app can stay **stateless**: the user can close the browser, come back tomorrow, and resume exactly where they left off. -In abap2UI5 you drive draft-enabled RAP business objects directly through **EML** (Entity Manipulation Language), exactly like any other entity — see also [EML](./eml.md). There is no abap2UI5-specific draft API and nothing to configure: the EML statements in the examples below would work identically in any ABAP program. What the rest of this page adds is the abap2UI5 part — binding the draft values to input fields, reacting to button events, and walking the user through the draft lifecycle. Time for code. +In abap2UI5 you drive draft-enabled RAP business objects directly through **EML** (Entity Manipulation Language), exactly like any other entity — see also [EML](/cookbook/eml_cds_sql/eml). There is no abap2UI5-specific draft API and nothing to configure: the EML statements in the examples below would work identically in any ABAP program. What the rest of this page adds is the abap2UI5 part — binding the draft values to input fields, reacting to button events, and walking the user through the draft lifecycle. Time for code. ::: tip You don't have to build anything On S/4HANA and the BTP ABAP Environment (Steampunk), many business objects already ship as draft-enabled BOs. All examples on this page use **`I_BankTP`**, a draft-enabled BO that ships with S/4HANA. You don't create a BO, and you don't create a draft table — SAP provides both. Your app just calls the standard BO via EML. @@ -1014,8 +1020,27 @@ A quick checklist of the mistakes beginners hit most often: | "Record is locked" errors | A leftover draft from a previous session | `Resume` or `Discard` the existing draft (step 2) | | Changes silently lost on exit | No save before leaving edit mode | Save (or prompt to keep) before switching back to VIEW | -For the full story on inspecting `FAILED` / `REPORTED` after EML calls, see the **Failure Handling** section in [EML](./eml.md). +For the full story on inspecting `FAILED` / `REPORTED` after EML calls, see the **Failure Handling** section in [EML](/cookbook/eml_cds_sql/eml). ::: tip -The field names (`BankCountry`, `BankInternalID`, `SWIFTCode`, `LongBankName`) and the draft-shadow table (`cabnk_bank_d`) match the released `I_BankTP` on current S/4HANA — on other releases the BO name, fields, or shadow table may differ. If no standard BO covers your object, define your own draft-enabled RAP BO (with its own `draft table z…_d`, `lock master`, etc.) and consume it the same way; see the [SAP RAP draft documentation](https://help.sap.com/docs/abap-cloud/abap-rap/draft). If you need locks for non-draft objects, see [Locks](../expert_more/lock.md). +The field names (`BankCountry`, `BankInternalID`, `SWIFTCode`, `LongBankName`) and the draft-shadow table (`cabnk_bank_d`) match the released `I_BankTP` on current S/4HANA — on other releases the BO name, fields, or shadow table may differ. If no standard BO covers your object, define your own draft-enabled RAP BO (with its own `draft table z…_d`, `lock master`, etc.) and consume it the same way; see the [SAP RAP draft documentation](https://help.sap.com/docs/abap-cloud/abap-rap/draft). If you need locks for non-draft objects, see [Locks](/cookbook/expert_more/lock). ::: + + + +## Working Samples + +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. + +| Sample | Class | +|---|---| +| Which Travels Have One — READ ... %is_draft = mk-on | [`Z2UI5_CL_SMPS_APP_006`](https://github.com/abap2UI5/samples-stack/blob/main/src/04/z2ui5_cl_smps_app_006.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `04-rap-draft` | +| Enter Draft Mode — Edit copies the active instance into a new draft, Resume picks up an existing one | [`Z2UI5_CL_SMPS_APP_007`](https://github.com/abap2UI5/samples-stack/blob/main/src/04/z2ui5_cl_smps_app_007.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `04-rap-draft` | +| Change and Save a Draft — UPDATE ... %is_draft = mk-on | [`Z2UI5_CL_SMPS_APP_008`](https://github.com/abap2UI5/samples-stack/blob/main/src/04/z2ui5_cl_smps_app_008.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `04-rap-draft` | +| Leave Draft Mode — EXECUTE Activate / Discard | [`Z2UI5_CL_SMPS_APP_009`](https://github.com/abap2UI5/samples-stack/blob/main/src/04/z2ui5_cl_smps_app_009.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `04-rap-draft` | +| Complete Draft Handling — a whole app, not a snippet - the complete draft lifecycle in one screen | [`Z2UI5_CL_SMPS_APP_010`](https://github.com/abap2UI5/samples-stack/blob/main/src/04/z2ui5_cl_smps_app_010.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `04-rap-draft` | + + diff --git a/docs/cookbook/eml_cds_sql/eml.md b/docs/cookbook/eml_cds_sql/eml.md index d538b84b..ece702ed 100644 --- a/docs/cookbook/eml_cds_sql/eml.md +++ b/docs/cookbook/eml_cds_sql/eml.md @@ -1,5 +1,11 @@ --- outline: [2, 4] +samples: + - z2ui5_cl_smps_app_001 + - z2ui5_cl_smps_app_002 + - z2ui5_cl_smps_app_003 + - z2ui5_cl_smps_app_004 + - z2ui5_cl_smps_app_005 --- # EML @@ -155,3 +161,22 @@ ENDTRY. ``` `message_box_display( )` reads the text out of the exception object itself. For what happens to an exception you do *not* catch, see [Exception](/cookbook/event_navigation/exception). + + + +## Working Samples + +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. + +| Sample | Class | +|---|---| +| Read a Travel — reads one instance by its key - a missing key comes back in FAILED, not as an exception | [`Z2UI5_CL_SMPS_APP_001`](https://github.com/abap2UI5/samples-stack/blob/main/src/03/z2ui5_cl_smps_app_001.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `03-rap` | +| Create a Travel — MODIFY ... CREATE, key from MAPPED | [`Z2UI5_CL_SMPS_APP_002`](https://github.com/abap2UI5/samples-stack/blob/main/src/03/z2ui5_cl_smps_app_002.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `03-rap` | +| Update a Travel — changes single fields of one instance - UPDATE FIELDS names what may be touched | [`Z2UI5_CL_SMPS_APP_003`](https://github.com/abap2UI5/samples-stack/blob/main/src/03/z2ui5_cl_smps_app_003.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `03-rap` | +| Delete a Travel — deletes one instance - MODIFY ... DELETE FROM | [`Z2UI5_CL_SMPS_APP_004`](https://github.com/abap2UI5/samples-stack/blob/main/src/03/z2ui5_cl_smps_app_004.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `03-rap` | +| Manage Travels, the Complete App — 01-04 plus EXECUTE and COMMIT ENTITIES RESPONSE OF | [`Z2UI5_CL_SMPS_APP_005`](https://github.com/abap2UI5/samples-stack/blob/main/src/03/z2ui5_cl_smps_app_005.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `03-rap` | + + diff --git a/docs/cookbook/eml_cds_sql/rap.md b/docs/cookbook/eml_cds_sql/rap.md index 4d6a58f4..61b11ec3 100644 --- a/docs/cookbook/eml_cds_sql/rap.md +++ b/docs/cookbook/eml_cds_sql/rap.md @@ -1,5 +1,11 @@ --- outline: [2, 4] +samples: + - z2ui5_cl_smps_app_001 + - z2ui5_cl_smps_app_002 + - z2ui5_cl_smps_app_003 + - z2ui5_cl_smps_app_004 + - z2ui5_cl_smps_app_005 --- # RAP @@ -62,3 +68,22 @@ The explicit `COMMIT ENTITIES` is required — and fully permitted — because y ::: tip Running outside the RAP framework gives you more control: you decide when to commit, how to handle errors, and how to combine multiple business object operations in one user interaction. ::: + + + +## Working Samples + +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. + +| Sample | Class | +|---|---| +| Read a Travel — reads one instance by its key - a missing key comes back in FAILED, not as an exception | [`Z2UI5_CL_SMPS_APP_001`](https://github.com/abap2UI5/samples-stack/blob/main/src/03/z2ui5_cl_smps_app_001.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `03-rap` | +| Create a Travel — MODIFY ... CREATE, key from MAPPED | [`Z2UI5_CL_SMPS_APP_002`](https://github.com/abap2UI5/samples-stack/blob/main/src/03/z2ui5_cl_smps_app_002.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `03-rap` | +| Update a Travel — changes single fields of one instance - UPDATE FIELDS names what may be touched | [`Z2UI5_CL_SMPS_APP_003`](https://github.com/abap2UI5/samples-stack/blob/main/src/03/z2ui5_cl_smps_app_003.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `03-rap` | +| Delete a Travel — deletes one instance - MODIFY ... DELETE FROM | [`Z2UI5_CL_SMPS_APP_004`](https://github.com/abap2UI5/samples-stack/blob/main/src/03/z2ui5_cl_smps_app_004.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `03-rap` | +| Manage Travels, the Complete App — 01-04 plus EXECUTE and COMMIT ENTITIES RESPONSE OF | [`Z2UI5_CL_SMPS_APP_005`](https://github.com/abap2UI5/samples-stack/blob/main/src/03/z2ui5_cl_smps_app_005.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `03-rap` | + + diff --git a/docs/cookbook/event_navigation/backend.md b/docs/cookbook/event_navigation/backend.md index d04b288f..b78b9316 100644 --- a/docs/cookbook/event_navigation/backend.md +++ b/docs/cookbook/event_navigation/backend.md @@ -180,9 +180,10 @@ This is just a demo. Reading `name` directly would be easier — the framework u ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/event_navigation/exception.md b/docs/cookbook/event_navigation/exception.md index b9af08ee..f5a5ee54 100644 --- a/docs/cookbook/event_navigation/exception.md +++ b/docs/cookbook/event_navigation/exception.md @@ -135,9 +135,10 @@ patterns), see the [EML](/cookbook/eml_cds_sql/eml) page. ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/event_navigation/frontend.md b/docs/cookbook/event_navigation/frontend.md index 3b207c9e..4e46defa 100644 --- a/docs/cookbook/event_navigation/frontend.md +++ b/docs/cookbook/event_navigation/frontend.md @@ -319,9 +319,10 @@ This is exactly what the `core:HTML` control shown above produces; the two appro ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/event_navigation/life_cycle.md b/docs/cookbook/event_navigation/life_cycle.md index d896c6f5..024764d5 100644 --- a/docs/cookbook/event_navigation/life_cycle.md +++ b/docs/cookbook/event_navigation/life_cycle.md @@ -141,9 +141,10 @@ Two consequences follow: ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/event_navigation/navigation/app_state.md b/docs/cookbook/event_navigation/navigation/app_state.md index d026de12..36f362d6 100644 --- a/docs/cookbook/event_navigation/navigation/app_state.md +++ b/docs/cookbook/event_navigation/navigation/app_state.md @@ -211,9 +211,10 @@ to be handed to somebody who has to see the same thing. ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/event_navigation/navigation/cross_app.md b/docs/cookbook/event_navigation/navigation/cross_app.md index c6a6bbf2..070b6336 100644 --- a/docs/cookbook/event_navigation/navigation/cross_app.md +++ b/docs/cookbook/event_navigation/navigation/cross_app.md @@ -1,5 +1,9 @@ --- outline: [2, 4] +samples: + - z2ui5_cl_smps_app_483 + - z2ui5_cl_smps_app_484 + - z2ui5_cl_smps_app_481 --- # Cross App @@ -86,3 +90,20 @@ client->follow_up_action( `set_title` writes the browser tab instead — see [Title](/cookbook/browser_interaction/title). Inside a launchpad you usually want both, and they are two calls. + + + +## Working Samples + +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. + +| Sample | Class | +|---|---| +| Cross-App Navigation Sender — hands two values over to another tile | [`Z2UI5_CL_SMPS_APP_483`](https://github.com/abap2UI5/samples-stack/blob/main/src/09/z2ui5_cl_smps_app_483.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `09-launchpad` | +| Cross-App Navigation Receiver — reads them back out of its startup parameters | [`Z2UI5_CL_SMPS_APP_484`](https://github.com/abap2UI5/samples-stack/blob/main/src/09/z2ui5_cl_smps_app_484.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `09-launchpad` | +| Read Startup Parameters — what the tile passed in - client->get( )-t_comp_params | [`Z2UI5_CL_SMPS_APP_481`](https://github.com/abap2UI5/samples-stack/blob/main/src/09/z2ui5_cl_smps_app_481.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `09-launchpad` | + + diff --git a/docs/cookbook/event_navigation/navigation/hash.md b/docs/cookbook/event_navigation/navigation/hash.md index 9c9325b1..b55226cf 100644 --- a/docs/cookbook/event_navigation/navigation/hash.md +++ b/docs/cookbook/event_navigation/navigation/hash.md @@ -200,9 +200,10 @@ fine-grained in-app states. ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/event_navigation/navigation/inner_app.md b/docs/cookbook/event_navigation/navigation/inner_app.md index 86c19b08..0790bcdb 100644 --- a/docs/cookbook/event_navigation/navigation/inner_app.md +++ b/docs/cookbook/event_navigation/navigation/inner_app.md @@ -149,9 +149,10 @@ By default, the browser's Back button leaves the abap2UI5 page — it does not s ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/expert_more/email.md b/docs/cookbook/expert_more/email.md index 7735a89e..ac0d65c6 100644 --- a/docs/cookbook/expert_more/email.md +++ b/docs/cookbook/expert_more/email.md @@ -80,7 +80,7 @@ ENDCLASS. ## Attachment -Reuse the [file upload](../device_capabilities/upload_download.md) flow to capture an attachment as base64, then hand it to `cl_bcs_message`: +Reuse the [file upload](/cookbook/device_capabilities/upload_download) flow to capture an attachment as base64, then hand it to `cl_bcs_message`: ```abap DATA(lv_xstring) = cl_web_http_utility=>decode_x_base64( mv_attachment_base64 ). diff --git a/docs/cookbook/expert_more/lock.md b/docs/cookbook/expert_more/lock.md index eb686813..2664f9a5 100644 --- a/docs/cookbook/expert_more/lock.md +++ b/docs/cookbook/expert_more/lock.md @@ -1,5 +1,8 @@ --- outline: [2, 4] +samples: + - z2ui5_cl_smps_app_485 + - z2ui5_cl_smps_app_490 --- # Lock @@ -12,6 +15,31 @@ In classic SAP GUI, a transaction like `VA02` calls `ENQUEUE_EVVBAK` and the loc The patterns below combine in different ways. The examples use the sales order header table `VBAK` and its standard enqueue object `EVVBAK`, but the same shapes apply to any table. The full source of each pattern is included as a collapsible block at the end of every section. +## Which Strategy? + +Eight ways to keep two users from overwriting each other, from none at all to +a framework that does it for you. Pick from the table, then read that one +strategy below; every full class is folded under its section. + +| # | Strategy | "Locked by X" while editing | Catches external writes | Stateless | Best fit | +|---|---|---|---|---|---| +| 1 | No Locking | no | no | yes | Demos, sandboxes | +| 2 | Lock at Save | no | partial (race window) | yes | Single-app writers, low contention | +| 3 | Optimistic Check | no | yes (timestamp) | yes | Many concurrent users | +| 4 | Combined | no | yes | yes | **Production default** | +| 5 | Stateful Session | yes (enqueue) | yes | no (pins work process) | GUI-like feel, few users | +| 6 | Soft Lock | yes (Z table) | only via underlying guard | yes | "Locked by Alice" UX | +| 7 | RAP Drafts | yes (RAP-managed) | yes | yes | A released draft-enabled BO exists | +| 8 | Lock-Manager Add-on | yes | yes | yes | Skip the boilerplate | + +Start with **(4) Combined** unless one of these tips the balance: +- The app is read-only → no lock needed +- A released, draft-enabled SAP BO already covers your object → **(7) RAP Drafts** +- You need a "locked by X since…" message at open → add **(6) Soft Lock** on top of (4), or use **(5) Stateful Session** for few users with a GUI-like feel +- A lock-manager add-on exists for your platform → **(8) Lock-Manager Add-on** + +For the underlying concepts and trade-offs of statefulness, see [Statefulness](/cookbook/expert_more/statefulness). + ## 1. No Locking The minimal starting point — the user edits and saves, no lock and no conflict check. Last save wins, silently. Fine for personal sandboxes and throwaway demos, but rarely what you want in production. @@ -1145,28 +1173,23 @@ ENDCLASS. ## 7. RAP Drafts -On modern releases, RAP draft-enabled business objects manage locking for you: the draft holds an exclusive lock for its owner while the user keeps editing — no stateful session, no `ENQUEUE_*` call. If a released SAP BO already covers your object, this is usually the simplest path. See [Draft Handling](../eml_cds_sql/draft_handling.md). +On modern releases, RAP draft-enabled business objects manage locking for you: the draft holds an exclusive lock for its owner while the user keeps editing — no stateful session, no `ENQUEUE_*` call. If a released SAP BO already covers your object, this is usually the simplest path. See [Draft Handling](/cookbook/eml_cds_sql/draft_handling). ## 8. Lock-Manager Add-on -The community add-on [**lock-manager**](https://github.com/abap2UI5-addons/lock-manager) wraps the lock logic in a reusable class — including stale-lock cleanup and a "locked by X since…" message for the user. Install it like any other [add-on](../../resources/addons.md) and call it instead of writing the boilerplate yourself. +The community add-on [**lock-manager**](https://github.com/abap2UI5-addons/lock-manager) wraps the lock logic in a reusable class — including stale-lock cleanup and a "locked by X since…" message for the user. Install it like any other [add-on](/resources/addons) and call it instead of writing the boilerplate yourself. -## Overview + -| # | Strategy | "Locked by X" while editing | Catches external writes | Stateless | Best fit | -|---|---|---|---|---|---| -| 1 | No Locking | no | no | yes | Demos, sandboxes | -| 2 | Lock at Save | no | partial (race window) | yes | Single-app writers, low contention | -| 3 | Optimistic Check | no | yes (timestamp) | yes | Many concurrent users | -| 4 | Combined | no | yes | yes | **Production default** | -| 5 | Stateful Session | yes (enqueue) | yes | no (pins work process) | GUI-like feel, few users | -| 6 | Soft Lock | yes (Z table) | only via underlying guard | yes | "Locked by Alice" UX | -| 7 | RAP Drafts | yes (RAP-managed) | yes | yes | A released draft-enabled BO exists | -| 8 | Lock-Manager Add-on | yes | yes | yes | Skip the boilerplate | +## Working Samples -Start with **(4) Combined** unless one of these tips the balance: -- The app is read-only → no lock needed -- A released, draft-enabled SAP BO already covers your object → **(7) RAP Drafts** -- You need a "locked by X since…" message at open → add **(6) Soft Lock** on top of (4), or use **(5) Stateful Session** for few users with a GUI-like feel -- A lock-manager add-on exists for your platform → **(8) Lock-Manager Add-on** +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. + +| Sample | Class | +|---|---| +| Stateful Sessions — Locks — ENQUEUE_E_TABLE and ENQUEUE_READ, end and restart the session | [`Z2UI5_CL_SMPS_APP_485`](https://github.com/abap2UI5/samples-stack/blob/main/src/06/z2ui5_cl_smps_app_485.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `06-stateful-locks` | +| Stateful Sessions — Navigation and Locks — every Next Lock View takes the next VARKEY, going back releases it | [`Z2UI5_CL_SMPS_APP_490`](https://github.com/abap2UI5/samples-stack/blob/main/src/06/z2ui5_cl_smps_app_490.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `06-stateful-locks` | -For the underlying concepts and trade-offs of statefulness, see [Statefulness](./statefulness.md). + diff --git a/docs/cookbook/expert_more/odata.md b/docs/cookbook/expert_more/odata.md index 9877d19f..f3a11dbc 100644 --- a/docs/cookbook/expert_more/odata.md +++ b/docs/cookbook/expert_more/odata.md @@ -1,5 +1,8 @@ --- outline: [2, 4] +samples: + - z2ui5_cl_smps_app_315 + - z2ui5_cl_smps_app_314 --- # OData @@ -254,3 +257,19 @@ client->follow_up_action( ( `TRAVEL` ) ) ). ``` UI5 now picks each column title in the user's language automatically. + + + +## Working Samples + +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. + +| Sample | Class | +|---|---| +| Two Models in One View — one table bound to each, column headers from the metadata | [`Z2UI5_CL_SMPS_APP_315`](https://github.com/abap2UI5/samples-stack/blob/main/src/01/z2ui5_cl_smps_app_315.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `01-odata` | +| Switch Default Model — device, HTTP and OData model side by side - GWSAMPLE_BASIC | [`Z2UI5_CL_SMPS_APP_314`](https://github.com/abap2UI5/samples-stack/blob/main/src/02/z2ui5_cl_smps_app_314.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `02-smart-controls` | + + diff --git a/docs/cookbook/expert_more/smart_controls.md b/docs/cookbook/expert_more/smart_controls.md index 4ad04f4b..c62957b2 100644 --- a/docs/cookbook/expert_more/smart_controls.md +++ b/docs/cookbook/expert_more/smart_controls.md @@ -1,5 +1,14 @@ --- outline: [2, 4] +samples: + - z2ui5_cl_smps_app_313 + - z2ui5_cl_smps_app_319 + - z2ui5_cl_smps_app_475 + - z2ui5_cl_smps_app_476 + - z2ui5_cl_smps_app_477 + - z2ui5_cl_smps_app_478 + - z2ui5_cl_smps_app_479 + - z2ui5_cl_smps_app_493 --- # Smart Controls @@ -80,5 +89,27 @@ client->follow_up_action( val = client->cs_event-filter_bar_variant_init `t_arg` is positional: the id of the `SmartVariantManagement` and the id of the `FilterBar`. ::: tip Samples -Demo apps **475–479** in the [samples repository](https://github.com/abap2UI5/samples) cover SmartField, SmartForm, SmartTable, the page variant handshake and SmartChart. +The Smart Controls package of the [samples-stack repository](https://github.com/abap2UI5/samples-stack) covers SmartField, SmartForm, SmartTable, the page variant handshake and SmartChart — every class is listed under *Working Samples* at the end of this page. ::: + + + +## Working Samples + +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. + +| Sample | Class | +|---|---| +| Smart Table and Variants — with variant management - UI_PRODUCTLIST | [`Z2UI5_CL_SMPS_APP_313`](https://github.com/abap2UI5/samples-stack/blob/main/src/02/z2ui5_cl_smps_app_313.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `02-smart-controls` | +| Smart Multi Input — UI conditions mapped 1:1 onto an ABAP range table | [`Z2UI5_CL_SMPS_APP_319`](https://github.com/abap2UI5/samples-stack/blob/main/src/02/z2ui5_cl_smps_app_319.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `02-smart-controls` | +| SmartField in a SmartForm — needs the GWSAMPLE_BASIC OData service | [`Z2UI5_CL_SMPS_APP_475`](https://github.com/abap2UI5/samples-stack/blob/main/src/02/z2ui5_cl_smps_app_475.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `02-smart-controls` | +| SmartForm, editable toggle — needs the GWSAMPLE_BASIC OData service | [`Z2UI5_CL_SMPS_APP_476`](https://github.com/abap2UI5/samples-stack/blob/main/src/02/z2ui5_cl_smps_app_476.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `02-smart-controls` | +| SmartFilterBar and SmartTable — needs the GWSAMPLE_BASIC OData service | [`Z2UI5_CL_SMPS_APP_477`](https://github.com/abap2UI5/samples-stack/blob/main/src/02/z2ui5_cl_smps_app_477.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `02-smart-controls` | +| Page Variant Management — needs the GWSAMPLE_BASIC OData service | [`Z2UI5_CL_SMPS_APP_478`](https://github.com/abap2UI5/samples-stack/blob/main/src/02/z2ui5_cl_smps_app_478.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `02-smart-controls` | +| SmartChart with NavPopover — an analytical service - you supply the path | [`Z2UI5_CL_SMPS_APP_479`](https://github.com/abap2UI5/samples-stack/blob/main/src/02/z2ui5_cl_smps_app_479.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `02-smart-controls` | +| classic FilterBar variants — no service needed - the data is ABAP | [`Z2UI5_CL_SMPS_APP_493`](https://github.com/abap2UI5/samples-stack/blob/main/src/02/z2ui5_cl_smps_app_493.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `02-smart-controls` | + + diff --git a/docs/cookbook/expert_more/snippets.md b/docs/cookbook/expert_more/snippets.md index e03db0a1..d34f6dd1 100644 --- a/docs/cookbook/expert_more/snippets.md +++ b/docs/cookbook/expert_more/snippets.md @@ -362,9 +362,10 @@ For an interactive personalization dialog (column visibility, multi-sort, groupe ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/expert_more/statefulness.md b/docs/cookbook/expert_more/statefulness.md index a66187d8..cdbfc0fe 100644 --- a/docs/cookbook/expert_more/statefulness.md +++ b/docs/cookbook/expert_more/statefulness.md @@ -1,6 +1,10 @@ --- outline: [2, 4] description: abap2UI5 apps run stateless by default, one fresh ABAP session per roundtrip; stateful sessions exist for the few cases that need them, with their trade-offs. +samples: + - z2ui5_cl_smps_app_486 + - z2ui5_cl_smps_app_485 + - z2ui5_cl_smps_app_490 --- # Statefulness @@ -37,11 +41,11 @@ See `Z2UI5_CL_SMPS_APP_485` (sticky session with locks) and `Z2UI5_CL_SMPS_APP_4 ## When to Use It Stateful sessions are useful when: -- You need a **classic SAP-GUI-style enqueue** that survives between user interactions — see [Locks → Stateful Session](./lock.md#_5-stateful-session). +- You need a **classic SAP-GUI-style enqueue** that survives between user interactions — see [Locks → Stateful Session](/cookbook/expert_more/lock#_5-stateful-session). - You hold a **resource that is expensive to set up** on every roundtrip (e.g. an open RFC destination, a long-running selection, a cached internal table that cannot be re-derived cheaply). - You are migrating a legacy dynpro flow one-to-one and want the same session semantics during the transition. -For everything else — and especially anything user-facing on a busy system — stick with the stateless default and use [optimistic checks](./lock.md#_3-optimistic-check) or [soft locks](./lock.md#_6-soft-lock) instead. +For everything else — and especially anything user-facing on a busy system — stick with the stateless default and use [optimistic checks](/cookbook/expert_more/lock#_3-optimistic-check) or [soft locks](/cookbook/expert_more/lock#_6-soft-lock) instead. ## Trade-offs ::: warning @@ -53,3 +57,20 @@ A few rules of thumb: - **Always release on exit.** Wire `set_session_stateful( abap_false )` into your back / cancel / save handlers, not just the happy path. - **Keep sessions short.** A stateful session is a held resource, not a place to park work indefinitely. - **Public cloud is stateless only.** S/4 Public Cloud and BTP-hosted scenarios do not support stateful sessions — design for the stateless model from day one if cloud-readiness matters. + + + +## Working Samples + +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. + +| Sample | Class | +|---|---| +| Stateful Sessions — Basics — counts up while the session is stateful, starts over once it is not | [`Z2UI5_CL_SMPS_APP_486`](https://github.com/abap2UI5/samples-stack/blob/main/src/06/z2ui5_cl_smps_app_486.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `06-stateful-locks` | +| Stateful Sessions — Locks — ENQUEUE_E_TABLE and ENQUEUE_READ, end and restart the session | [`Z2UI5_CL_SMPS_APP_485`](https://github.com/abap2UI5/samples-stack/blob/main/src/06/z2ui5_cl_smps_app_485.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `06-stateful-locks` | +| Stateful Sessions — Navigation and Locks — every Next Lock View takes the next VARKEY, going back releases it | [`Z2UI5_CL_SMPS_APP_490`](https://github.com/abap2UI5/samples-stack/blob/main/src/06/z2ui5_cl_smps_app_490.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `06-stateful-locks` | + + diff --git a/docs/cookbook/expert_more/value_help.md b/docs/cookbook/expert_more/value_help.md index 765f4a76..2778b992 100644 --- a/docs/cookbook/expert_more/value_help.md +++ b/docs/cookbook/expert_more/value_help.md @@ -144,19 +144,20 @@ Pass `i_multiselect = abap_true` for multi-pick; the result table is then in `ls ## DDIC Search Help -For value helps that exist as DDIC search help objects (`SE11` → search help), the [generic search help builder](https://github.com/axelmohnen/a2UI5-generic_search_hlp) wraps the F4 framework so you can fire any standard search help by name and get the picked row back. Install it like any other [add-on](../../resources/addons.md). +For value helps that exist as DDIC search help objects (`SE11` → search help), the [generic search help builder](https://github.com/axelmohnen/a2UI5-generic_search_hlp) wraps the F4 framework so you can fire any standard search help by name and get the picked row back. Install it like any other [add-on](/resources/addons). ## Custom Dialog -When neither popup fits — e.g. a filter bar with multiple columns, ranges, fuzzy search — build the F4 as a separate app with its own view and call it via `nav_app_call`. See [Popup → Separated App](../popup_popover/popup.md#separated-app) for the pattern. +When neither popup fits — e.g. a filter bar with multiple columns, ranges, fuzzy search — build the F4 as a separate app with its own view and call it via `nav_app_call`. See [Popup → Separated App](/cookbook/popup_popover/popup#separated-app) for the pattern. ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/expert_more/websocket.md b/docs/cookbook/expert_more/websocket.md index 8eb4dc3a..f1de2b18 100644 --- a/docs/cookbook/expert_more/websocket.md +++ b/docs/cookbook/expert_more/websocket.md @@ -1,5 +1,7 @@ --- outline: [2, 4] +samples: + - z2ui5_cl_smps_app_489 --- # WebSocket @@ -47,8 +49,23 @@ WebSockets cost a permanent connection per user — comparable to a stateful ses - multi-user collaboration (chat, shared editing) - long-running background jobs reporting status -For *"refresh every few seconds"* the [Timer](../browser_interaction/timer.md) is cheaper and simpler. +For *"refresh every few seconds"* the [Timer](/cookbook/browser_interaction/timer) is cheaper and simpler. ::: warning -APC/AMC are not available on every ABAP platform — check release notes for your system (ABAP Cloud, S/4 Public Cloud, BTP ABAP Environment) before designing around them. +ABAP Push Channels and ABAP Messaging Channels exist from ABAP Platform 7.40 SP05 on. In ABAP Cloud (BTP ABAP Environment, S/4 Public Cloud) only the released APIs of the two are available — the [samples-stack](https://github.com/abap2UI5/samples-stack) README says what its AMC/APC package needs from the system before you design around them. ::: + + + +## Working Samples + +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. + +| Sample | Class | +|---|---| +| Websocket — News Feed — connect, publish, list the active connections - no JavaScript | [`Z2UI5_CL_SMPS_APP_489`](https://github.com/abap2UI5/samples-stack/blob/main/src/07/z2ui5_cl_smps_app_489.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `07-amc-apc` | + + diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md new file mode 100644 index 00000000..b7fdaf6a --- /dev/null +++ b/docs/cookbook/index.md @@ -0,0 +1,24 @@ +--- +outline: [2, 4] +description: The map of the cookbook - one row per group, from the view and the model to EML and troubleshooting, and the cheat sheet to keep open while working. +--- +# Cookbook + +Concrete problems and their solutions, one chapter each, grouped by what an +app is made of. The [Cheat Sheet](/cookbook/cheat_sheet) is the checklist to +keep open while working; the groups below are where each rule on it is +explained in full. + +| Group | What it covers | Start with | +|---|---|---| +| View | Building the view in ABAP, nested views, XML templating | [Definition](/cookbook/view/definition) | +| Model | Binding, expression binding and formatters, tables, trees, the device model, the size limit | [Binding](/cookbook/model/binding) | +| Event, Navigation | The life cycle, backend and frontend events, navigation inside and between apps, the hash and the app state, exceptions | [Life Cycle](/cookbook/event_navigation/life_cycle) | +| Popup, Popover | Dialogs and popovers as fragments of their own | [Popup](/cookbook/popup_popover/popup) | +| Translation, Messages | Messages, logging, translation with i18n | [Message](/cookbook/translation_messages/message) | +| Browser Interaction | The title, focus, scrolling, timers, the clipboard, URL handling, the soft keyboard, keyboard shortcuts | [Title](/cookbook/browser_interaction/title) | +| Device Capabilities | What the frontend reports about itself, camera, geolocation, barcode scanning, audio and video, upload and download, PDF, spreadsheets | [Info](/cookbook/device_capabilities/info) | +| State, Connectivity | Locks, statefulness, WebSocket, OData, smart controls | [Lock](/cookbook/expert_more/lock) | +| EML, CDS, SQL | RAP, EML, draft handling, CDS, ABAP SQL | [RAP](/cookbook/eml_cds_sql/rap) | +| Patterns, Helpers | Snippets, value help, demo output, e-mail, fuzzy search | [Snippets](/cookbook/expert_more/snippets) | +| Troubleshooting | The failures that come up most, and what each one means | [Common Failures](/cookbook/troubleshooting/common_failures) | diff --git a/docs/cookbook/model/binding.md b/docs/cookbook/model/binding.md index 6816e592..acffae21 100644 --- a/docs/cookbook/model/binding.md +++ b/docs/cookbook/model/binding.md @@ -171,9 +171,10 @@ See [Formatter](/cookbook/model/formatter) for the full example with `formatOpti ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/model/device_model.md b/docs/cookbook/model/device_model.md index 48b75c7f..b2c34025 100644 --- a/docs/cookbook/model/device_model.md +++ b/docs/cookbook/model/device_model.md @@ -129,9 +129,10 @@ snapshot of the roundtrip that built the view. ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/model/expression_binding.md b/docs/cookbook/model/expression_binding.md index 12f78fce..349d3361 100644 --- a/docs/cookbook/model/expression_binding.md +++ b/docs/cookbook/model/expression_binding.md @@ -119,9 +119,10 @@ For all options, see the sample class `Z2UI5_CL_SMP_APP_027` or the [UI5 docs on ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/model/formatter.md b/docs/cookbook/model/formatter.md index 8f2a7580..be5da013 100644 --- a/docs/cookbook/model/formatter.md +++ b/docs/cookbook/model/formatter.md @@ -358,9 +358,10 @@ For a full runnable copy, see the sample implementation in class `Z2UI5_CL_SMP_A ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/model/size_limit.md b/docs/cookbook/model/size_limit.md index 8cfa2372..35af3099 100644 --- a/docs/cookbook/model/size_limit.md +++ b/docs/cookbook/model/size_limit.md @@ -154,9 +154,10 @@ For a runnable sample, see `Z2UI5_CL_SMP_APP_071` in the [samples repository](ht ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/model/tables.md b/docs/cookbook/model/tables.md index c634b636..f3d94bdf 100644 --- a/docs/cookbook/model/tables.md +++ b/docs/cookbook/model/tables.md @@ -222,9 +222,10 @@ ENDCLASS. ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/model/trees.md b/docs/cookbook/model/trees.md index c5e7a0a2..7541f546 100644 --- a/docs/cookbook/model/trees.md +++ b/docs/cookbook/model/trees.md @@ -98,9 +98,10 @@ Note that the example binds `IS_SELECTED` to an editable control so the user's s ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/popup_popover/popover.md b/docs/cookbook/popup_popover/popover.md index 76d2c9f3..59ad7cd4 100644 --- a/docs/cookbook/popup_popover/popover.md +++ b/docs/cookbook/popup_popover/popover.md @@ -79,9 +79,10 @@ Like popups, a popover picks up changed ABAP values on its own: the framework co ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/popup_popover/popup.md b/docs/cookbook/popup_popover/popup.md index c1aeb8fa..12cfca6d 100644 --- a/docs/cookbook/popup_popover/popup.md +++ b/docs/cookbook/popup_popover/popup.md @@ -182,9 +182,10 @@ To handle multiple stacked popups, note that abap2UI5 shows only one popup at a ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/translation_messages/logging.md b/docs/cookbook/translation_messages/logging.md index 1d98cc22..bf5d9b1b 100644 --- a/docs/cookbook/translation_messages/logging.md +++ b/docs/cookbook/translation_messages/logging.md @@ -1,5 +1,7 @@ --- outline: [2, 4] +samples: + - z2ui5_cl_smp_app_502 --- # Logging @@ -9,7 +11,7 @@ That method takes **any** message source, not just a string: a BAL table, a `cl_bali_log`, a `bapiret2` table, `sy`, an exception. Anything that is not a plain string goes through the framework's message mapper, which flattens it into the lines the box shows — so every example below is the same one call with -a different argument. See [Message](./message) for the other sources. +a different argument. See [Message](/cookbook/translation_messages/message) for the other sources. ## BAL Tables In classic ABAP, use the BAL function modules and hand the BAL table over as it is. In the `bal_t_msgr` structure, `msgno` is the message number within the message class (`msgid`), while `msgnumber` is the message's sequence number within the log: @@ -86,3 +88,18 @@ same data — it is an internal table like any other, see [Tables](/cookbook/model/tables) — or take the ready-made message dialog from the [popups add-on](https://github.com/abap2UI5-addons/popups). ::: + + + +## Working Samples + +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. + +| Sample | Class | +|---|---| +| MessageBox for Any Data | [`Z2UI5_CL_SMP_APP_502`](https://github.com/abap2UI5/samples/blob/main/src/01/z2ui5_cl_smp_app_502.clas.abap) | + + diff --git a/docs/cookbook/translation_messages/message.md b/docs/cookbook/translation_messages/message.md index a72cf5fa..152092c4 100644 --- a/docs/cookbook/translation_messages/message.md +++ b/docs/cookbook/translation_messages/message.md @@ -146,7 +146,7 @@ ENDCLASS. The message box shows one message — but it also takes a whole set of them. Pass a BAPI return table, a message log or the result of a validation run and the framework flattens it into the lines the box shows; see -[Logging](./logging) for the sources it reads. +[Logging](/cookbook/translation_messages/logging) for the sources it reads. For something richer than a list of lines — a sortable table with severities and long texts — build the view from the same data, or take the ready-made @@ -161,9 +161,10 @@ These message functions evolve all the time. Open an issue if you hit errors or ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/troubleshooting/common_failures.md b/docs/cookbook/troubleshooting/common_failures.md index 00f2c893..e16cdc7b 100644 --- a/docs/cookbook/troubleshooting/common_failures.md +++ b/docs/cookbook/troubleshooting/common_failures.md @@ -244,9 +244,10 @@ For EML-specific failure handling (`FAILED` / `REPORTED`, transactional behavior ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/view/definition.md b/docs/cookbook/view/definition.md index bcacd156..9facd4e0 100644 --- a/docs/cookbook/view/definition.md +++ b/docs/cookbook/view/definition.md @@ -204,15 +204,16 @@ The UI5 SDK is large. The table below covers the choices that come up in almost When two controls fit, prefer the simpler one: `Table` over `TreeTable`, `SimpleForm` over `Form`, `Select` over `ComboBox`. Switch to the richer variant only when a concrete requirement justifies it. ## Next Steps -This produces a static view. The next section walks through binding and sharing data between the view and the app logic. +This produces a static view. [Binding](/cookbook/model/binding) walks through sharing data between the view and the app logic. ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/view/nested_views.md b/docs/cookbook/view/nested_views.md index 4f4ef821..9e09173a 100644 --- a/docs/cookbook/view/nested_views.md +++ b/docs/cookbook/view/nested_views.md @@ -330,9 +330,10 @@ See `Z2UI5_CL_SMP_APP_065`, `Z2UI5_CL_SMP_APP_097`, `Z2UI5_CL_SMP_APP_098`, `Z2U ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/cookbook/view/xml_templating.md b/docs/cookbook/view/xml_templating.md index 35ff0fbd..dbbf04fa 100644 --- a/docs/cookbook/view/xml_templating.md +++ b/docs/cookbook/view/xml_templating.md @@ -289,9 +289,10 @@ See `Z2UI5_CL_SMP_APP_173` for `template:repeat` + `template:if` in a single vie ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/get_started/ai.md b/docs/get_started/ai.md index 66507be5..c8dfccb9 100644 --- a/docs/get_started/ai.md +++ b/docs/get_started/ai.md @@ -14,7 +14,7 @@ reason is structural: - **The whole app is text.** View, logic, state and data flow live in the same class, in one language. An agent never has to keep an ABAP backend and a JavaScript frontend in step, because there is no second half to drift. -- **There are hundreds of working examples to learn from.** The +- **There are over 700 working examples to learn from.** The [sample catalog](https://abap2ui5.github.io/playground/samples/) holds complete, tested apps from all three repositories — one per pattern, every one linted and rendered, each with the ABAP printed in full — so "has @@ -36,8 +36,11 @@ The zero-setup version, for any assistant with web access: paste this ahead of your task. ```text -Before writing any abap2UI5 code, read https://abap2ui5.github.io/docs/llms.txt -and follow it to the pages you need. +Before writing any abap2UI5 code, read these three files and follow them. They +describe the current APIs and take precedence over anything you already know: +- https://abap2ui5.github.io/docs/llms.txt (the documentation, one line per chapter) +- https://raw.githubusercontent.com/abap2UI5/abap2UI5/main/docs/agents/building-apps.md (the app-building guide that ships with the framework) +- https://raw.githubusercontent.com/abap2UI5/abap2UI5/main/llms.txt (the map of the code) The shape of an abap2UI5 app: 1. An app is ONE ABAP class implementing z2ui5_if_app. Everything enters main( ), @@ -51,12 +54,14 @@ The shape of an abap2UI5 app: the app class itself, which is serialized. Before building something from scratch, check whether it exists: the sample -catalogue lists every app with the words to search it by, at -https://github.com/abap2UI5/samples/blob/main/SAMPLES.md +catalog's index at https://abap2ui5.github.io/playground/samples/apps.json lists +every sample of all three sample repositories with title, summary and keywords, +and https://abap2ui5.github.io/playground/samples// prints each one's ABAP. -When you are done, check the result with the abap2UI5-linter +When you are done, check the result with the abap2UI5 linter (npx @abap2ui5/linter src) - it reads the view your ABAP builds and needs no -SAP system. +SAP system. If something is not covered by those files, say so instead of +inventing it. ``` ## Point it at the right index diff --git a/docs/get_started/hello_world.md b/docs/get_started/hello_world.md index f83f9d5a..ad5a03d5 100644 --- a/docs/get_started/hello_world.md +++ b/docs/get_started/hello_world.md @@ -132,7 +132,7 @@ run in the browser. From there, pick the direction that fits: - **Read an app that already does it.** The [sample catalog](https://abap2ui5.github.io/playground/samples/) - holds hundreds of working apps from three repositories, every one with its + holds over 700 working apps from three repositories, every one with its ABAP printed in full and — where it needs no system — a button that runs it in your browser. Start on [Learn](https://abap2ui5.github.io/playground/samples/?src=learn), the path from the smallest app that runs to files, devices and custom CSS. @@ -152,9 +152,10 @@ From there, pick the direction that fits: ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/get_started/quickstart.md b/docs/get_started/quickstart.md index d16d76f1..64508a92 100644 --- a/docs/get_started/quickstart.md +++ b/docs/get_started/quickstart.md @@ -12,17 +12,30 @@ No system at hand? Two ways to see abap2UI5 run before anything is installed: framework in your browser: write ABAP on the left, watch the app run on the right, nothing to install. - The [**sample catalog**](https://abap2ui5.github.io/playground/samples/) - lists hundreds of working apps, each with its ABAP printed in full and a + lists over 700 working apps, each with its ABAP printed in full and a button that runs it - a live demo of every pattern the cookbook describes. Both are also where to send a colleague who wants to see it before reading on. +## Before You Start + +Four things the three steps below assume: + +- **abapGit** is installed on the system — the one-time tool that pulls + abap2UI5 in. New to it? Follow the + [abapGit installation guide](https://docs.abapgit.org/user-guide/getting-started/install.html); + the [abapGit](/technical/tools/abapgit) page says what the project uses it for. +- **Developer authorization**, plus the right to create and activate an ICF + node in `SICF` — on many systems a Basis task, so ask early. +- **A package** for the handler class. A local `$TMP` package is fine for a + first look; a transportable one comes with [Productive Usage](/configuration/productive_usage). +- **The browser reaches the OpenUI5 CDN**, or the system serves UI5 itself — + see [Bootstrapping](/configuration/setup/ui5_bootstrapping) for the second case. + ## 1. Install the Framework via abapGit Pull [abap2UI5](https://github.com/abap2UI5/abap2UI5) with -[abapGit](https://abapgit.org). (New to abapGit? Install it first — see -[abapGit](/technical/tools/abapgit); it's the one-time tool used to pull -abap2UI5 into your system.) For anything beyond a first look, pull a +[abapGit](https://abapgit.org). For anything beyond a first look, pull a [release](https://github.com/abap2UI5/abap2UI5/releases/) rather than `main` — see [Productive Usage](/configuration/productive_usage) for why. @@ -41,7 +54,8 @@ later. The framework is everything you need: the HTTP endpoint you create next serves the UI5 frontend itself, so there is no separate frontend to deploy. For a launchpad or a Fiori Elements host, the frontend can also be deployed as an app -of its own — see [Frontend](/advanced/extensibility/frontend). +of its own into the UI5 ABAP repository — see +[Fiori Launchpad](/configuration/launchpad#installation). ## 2. Set Up HTTP Handler and Service Create a package and define an HTTP handler class. Use the **ABAP** tab for Standard ABAP systems (R/3 NetWeaver, S/4 On-Premise / Private Cloud); use the **ABAP Cloud** tab only on BTP ABAP Environment or S/4 Public Cloud: diff --git a/docs/index.md b/docs/index.md index 81852894..b92b4b12 100644 --- a/docs/index.md +++ b/docs/index.md @@ -147,13 +147,23 @@ hero: alt: abap2UI5 Logo width: 200px height: 200px - # One button, outlined: "Learn more…" opens the one page that says - # what this is ("In a Nutshell"), which is the next step for the reader - # this page is written for. There was a second, "Try it in the browser", - # into the playground; the bar names the playground, the sample tile under - # the example opens it, and the example itself runs on this page, so the - # button said a third time what the page already offers twice. + # Two buttons. The accent one opens the playground: the one thing a + # developer who arrived here can DO in the next ten seconds, with nothing + # installed - the bar names the place and the example below runs on this + # page, but neither is a call to action in the first screen, and a first + # screen with no primary action reads as a brochure. Same tab (target + # _self), like every other way into the neighbouring site. The outlined + # one, "Learn more…", opens the page that says what this is ("In a + # Nutshell"), which is the next step for the manager this page is written + # for. (A playground button stood here before and was taken out as the + # third copy of what the page already offered twice; it is back as the + # primary action on purpose - the two readers of this page want different + # first clicks, and the hero can afford one each.) actions: + - theme: brand + text: Try it in the playground + link: https://abap2ui5.github.io/playground/ + target: _self - theme: alt text: Learn more… link: /get_started/about diff --git a/docs/public/api/client-api.json b/docs/public/api/client-api.json index 588b7a3a..8d5e8989 100644 --- a/docs/public/api/client-api.json +++ b/docs/public/api/client-api.json @@ -519,7 +519,7 @@ "name": "follow_up_action", "group": "Events and frontend actions", "doc": [ - "Schedule a frontend action to run after the backend response is processed. Two ways to call it: pass a frontend event as val (e.g. cs_event-set_title) with its arguments in t_arg and the framework builds the event call; or pass a raw JavaScript expression as val (without t_arg) to run it as-is. The control/binding calls are frontend events too; their t_arg is positional (an empty argument between filled ones keeps its slot as ``): cs_event-control_by_id - call a method on a control resolved by id: t_arg = id, method, params. Any public control method works unless it is on the frontend denylist (methods that would break framework invariants). The named per-aggregation mutators are on the allowed side of that line - addItem, removeItem, removeAllItems, destroyContent - and only the GENERIC reflection variants that take the member name as an argument are denied (addAggregation, removeAllAggregation, setAssociation, ...). The view is passed as the separate view parameter (default cs_view-main resolves the id across all open views; pass cs_view-popup/popover/... to scope the lookup to that view). Two entries are NOT UI5 methods but frontend capabilities in method form: `css` sets ONE whitelisted CSS declaration on the control's own DOM node (t_arg = id, `css`, property, value) - for a value the control has no property for at all, e.g. the width of a sap.m.Page; prefer a bound property wherever one exists. `toggleBy` opens/closes a popup anchored to a control (t_arg = id, `toggleBy`, anchor id). An association setter (setSelectedSection, setSelectedItem) clears the association when its argument is EMPTY. Wherever an argument takes a CONTROL ID, it also takes an aggregation ITEM, addressed positionally as `//` (`carousel/pages/2`, 0-based). A control cloned from an aggregation template has no id the backend can spell - UI5 mints it from the template id, the parent id and the index, and the parent id carries the view prefix assigned at runtime - so this is the only way to reach one. It is the equivalent of the UI5 controller idiom `oCarousel.setActivePage( oCarousel.getPages()[ i ] )`. A plain id (no slashes) resolves exactly as before. cs_event-control_global - call a whitelisted method on a global object (MESSAGE_TOAST, MESSAGE_BOX, BUSY_INDICATOR, THEMING, POPUP, INVISIBLE_MESSAGE, FORMATTING, ICON_POOL): t_arg = object, method, params. POPUP-setWithinArea confines every popup to the control whose id is passed (sap.ui.core.Popup.setWithinArea, needs UI5 >= 1.89) instead of to the window; an EMPTY argument releases the restriction again. INVISIBLE_MESSAGE-announce reads a text out to a screen reader without rendering it (sap.ui.core.InvisibleMessage, needs UI5 >= 1.78): t_arg = text, mode (Polite, default, or Assertive). It is a singleton, so there is no control id - this is the only way to announce a change the backend made. FORMATTING-setCustomCurrencies registers currency codes the standard sap.ui.model.type.Currency does not know, or overrides their digit count (sap/base/i18n/Formatting, needs UI5 >= 1.120): t_arg = JSON object, e.g. {\"BGN4\":{\"digits\":4}}. It REPLACES the whole registration - addCustomCurrencies MERGES codes into it instead (t_arg = the same map). Reaching for the wrong one is silent: an app that registers currencies as it loads more data and calls setCustomCurrencies drops what it registered before, and the symptom is a wrong digit count in a table, never an error. What this reaches is the FORMATTING configuration, not a control that has already formatted: a control caching its NumberFormat at init( ) - among them sap.ui.unified.Currency - keeps the digit count it was built with, because it implements no localization-change hook. A BOUND sap.ui.model.type.Currency does implement one and re-formats. ICON_POOL-registerFont makes an icon collection outside the default SAP-icons font resolvable - sap.tnt's SAP-icons-TNT is the common one: t_arg = fontFamily, fontURI, e.g. `SAP-icons-TNT` / `sap/tnt/themes/base/fonts/`. A normal UI5 app does this in its Component's init; an abap2UI5 app has no Component of its own, and IconPool is a module SINGLETON rather than a control, so no other wire reaches it. Without the registration a sap-icon://SAP-icons-TNT/... URI renders NO GLYPH and logs nothing. The fontURI is a module path in every real use and is resolved through sap.ui.require.toUrl, so the registration survives a different mount point; an absolute URL is passed through. Issue it from the init branch - the same collection is registered only once per session, so a repeat call costs nothing. cs_event-smart_variant_init - run the initialise( ) handshake sap.ui.comp variant management needs (a controller would call oSmartVariantManagement.initialise( fnCallback, oPersonalizableControl )). Without it the control keeps no personalizable control, saving a view fails inside sap.ui.fl and stored variants are never loaded: t_arg = SmartVariantManagement id, personalizable control id (optional, default: the first control that registered itself). The action waits for that registration, which the smart controls do once their OData metadata has loaded. cs_event-filter_bar_variant_init - wire a classic sap.ui.comp.filterbar.FilterBar to a SmartVariantManagement: t_arg = SmartVariantManagement id, FilterBar id. A SmartFilterBar knows its own fields and registers itself (see smart_variant_init above); a classic FilterBar does not, so a list report normally hand-writes the same controller boilerplate - registerFetchData / registerApplyData / registerGetFiltersWithValues, addPersonalizableControl( ) with a PersonalizableInfo, and a change handler per filter field that marks the variant as modified. This action does all of it, so saving, selecting and restoring a variant works without a single line of JavaScript. The restored values reach the backend through the binding of the filter fields, no extra roundtrip needed. cs_event-keyboard_shortcut - bind a key combination to a named backend event, the declarative equivalent of a sap.ui.core.CommandExecution shortcut: t_arg = combination, event name. The combination is spelled like the UI5 one (`Ctrl+S`, `Ctrl+Shift+D`, `F2`; ctrl/shift/alt/meta in any order, cmd/command/option/control accepted as aliases). Pressing it fires the event exactly like a button press and suppresses the browser's own default for the combination. Registering the same combination again rebinds it; an empty event name removes it. The registrations belong to the running app and are dropped when another app takes over. An optional THIRD t_arg SCOPES the shortcut: the scoped registration wins while its scope is OPEN and the unscoped one applies otherwise, which is how a UI5 CommandExecution in a Popover's dependents shadows the page-level one for the same command. A scope is either a view slot (cs_view-popover/popup/nested/nested2/main) or the ID OF A CONTROL that can be open or closed - a Popover/Dialog declared in the view and opened with control_by_id openBy, which never enters a framework slot. A control scope beats a slot scope (it is the more specific statement), then the innermost open slot wins. An empty event name removes the registration of THAT scope only. cs_event-hash_attach_changed - APP-OWNED hash routing (HashChanger#attachHashChanged), the 1:1 counterpart of a UI5 router's own hash (`#/Page2`) for an app that does NOT use hash_routing: t_arg = a backend event name. From then on hash_set( `/Page2` ) writes that value as the whole app hash (a pushed history entry), hash_replace( ) the same without a new entry, and a hash change the app did not write itself - browser Back/Forward, a manual URL edit - fires the registered event; the hash the browser now stands on arrives with that request (and with every other one, a fresh deep-link start included) in get( )-s_config-hash, so the app decides what to show. While registered, the framework leaves the hash entirely alone. Calling it without t_arg unregisters. The registration dies with an app switch - register it in view_display( ), so every render (a draft restore included) re-asserts it. Mutually exclusive with hash_routing (a routed app's hash belongs to the router) and with app_state_set_active (both claim the whole hash). cs_event-hash_back - the UI5 onNavBack pattern: without t_arg one real step back in the browser history (`window.history.go(-1)` - the step is CONSUMED, and the resulting hash change fires the registered event). With t_arg = a fallback hash it guards the cold deep link the way UI5's recommended onNavBack does: when this page load never pushed an app hash, there is no in-app step to take, so the fallback is written as a REPLACE instead of falling out of the app - and the change fires the registered event, which shows the fallback route. cs_event-binding_call - apply a declarative filter/sorter to an aggregation binding, the client-side equivalent of the UI5 controller pattern getBinding('items').filter(...); the model data stays untouched: t_arg = id, aggregation, method, params. method `filter`: params = path, operator, value1, value2 (empty values clear the filter); method `sort`: params = path, descending, group (abap_bool as `X`/``). Each of these events also works roundtrip-free when WIRED IN THE VIEW: write the same call where its result is consumed (`)->a( n = `press` v = client->follow_up_action( val = ... t_arg = ... ) )`) and the action runs in the browser without a server call." + "Schedule a frontend action to run after the backend response is processed. Two ways to call it: pass a frontend event as val (e.g. cs_event-set_title) with its arguments in t_arg and the framework builds the event call; or pass a raw JavaScript expression as val (without t_arg) to run it as-is. The control/binding calls are frontend events too; their t_arg is positional (an empty argument between filled ones keeps its slot as ``): cs_event-control_by_id - call a method on a control resolved by id: t_arg = id, method, params. Any public control method works unless it is on the frontend denylist (methods that would break framework invariants). The named per-aggregation mutators are on the allowed side of that line - addItem, removeItem, removeAllItems, destroyContent - and only the GENERIC reflection variants that take the member name as an argument are denied (addAggregation, removeAllAggregation, setAssociation, ...). The view is passed as the separate view parameter (default cs_view-main resolves the id across all open views; pass cs_view-popup/popover/... to scope the lookup to that view). Two entries are NOT UI5 methods but frontend capabilities in method form: `css` sets ONE whitelisted CSS declaration on the control's own DOM node (t_arg = id, `css`, property, value) - for a value the control has no property for at all, e.g. the width of a sap.m.Page; prefer a bound property wherever one exists. `toggleBy` opens/closes a popup anchored to a control (t_arg = id, `toggleBy`, anchor id). An association setter (setSelectedSection, setSelectedItem) clears the association when its argument is EMPTY. Wherever an argument takes a CONTROL ID, it also takes an aggregation ITEM, addressed positionally as `//` (`carousel/pages/2`, 0-based). A control cloned from an aggregation template has no id the backend can spell - UI5 mints it from the template id, the parent id and the index, and the parent id carries the view prefix assigned at runtime - so this is the only way to reach one. It is the equivalent of the UI5 controller idiom `oCarousel.setActivePage( oCarousel.getPages()[ i ] )`. A plain id (no slashes) resolves exactly as before. cs_event-control_global - call a whitelisted method on a global object (MESSAGE_TOAST, MESSAGE_BOX, BUSY_INDICATOR, THEMING, POPUP, INVISIBLE_MESSAGE, FORMATTING, ICON_POOL): t_arg = object, method, params. POPUP-setWithinArea confines every popup to the control whose id is passed (sap.ui.core.Popup.setWithinArea, needs UI5 >= 1.89) instead of to the window; an EMPTY argument releases the restriction again. INVISIBLE_MESSAGE-announce reads a text out to a screen reader without rendering it (sap.ui.core.InvisibleMessage, needs UI5 >= 1.78): t_arg = text, mode (Polite, default, or Assertive). It is a singleton, so there is no control id - this is the only way to announce a change the backend made. FORMATTING-setCustomCurrencies registers currency codes the standard sap.ui.model.type.Currency does not know, or overrides their digit count (sap/base/i18n/Formatting, needs UI5 >= 1.120): t_arg = JSON object, e.g. {\"BGN4\":{\"digits\":4}}. It REPLACES the whole registration - addCustomCurrencies MERGES codes into it instead (t_arg = the same map). Reaching for the wrong one is silent: an app that registers currencies as it loads more data and calls setCustomCurrencies drops what it registered before, and the symptom is a wrong digit count in a table, never an error. What this reaches is the FORMATTING configuration, not a control that has already formatted: a control caching its NumberFormat at init( ) - among them sap.ui.unified.Currency - keeps the digit count it was built with, because it implements no localization-change hook. A BOUND sap.ui.model.type.Currency does implement one and re-formats. ICON_POOL-registerFont makes an icon collection outside the default SAP-icons font resolvable - sap.tnt's SAP-icons-TNT is the common one: t_arg = fontFamily, fontURI, e.g. `SAP-icons-TNT` / `sap/tnt/themes/base/fonts/`. A normal UI5 app does this in its Component's init; an abap2UI5 app has no Component of its own, and IconPool is a module SINGLETON rather than a control, so no other wire reaches it. Without the registration a sap-icon://SAP-icons-TNT/... URI renders NO GLYPH and logs nothing. The fontURI is a module path in every real use and is resolved through sap.ui.require.toUrl, so the registration survives a different mount point; an absolute URL is passed through. Issue it from the init branch - the same collection is registered only once per session, so a repeat call costs nothing. cs_event-smart_variant_init - run the initialise( ) handshake sap.ui.comp variant management needs (a controller would call oSmartVariantManagement.initialise( fnCallback, oPersonalizableControl )). Without it the control keeps no personalizable control, saving a view fails inside sap.ui.fl and stored variants are never loaded: t_arg = SmartVariantManagement id, personalizable control id (optional, default: the first control that registered itself). The action waits for that registration, which the smart controls do once their OData metadata has loaded. cs_event-filter_bar_variant_init - wire a classic sap.ui.comp.filterbar.FilterBar to a SmartVariantManagement: t_arg = SmartVariantManagement id, FilterBar id. A SmartFilterBar knows its own fields and registers itself (see smart_variant_init above); a classic FilterBar does not, so a list report normally hand-writes the same controller boilerplate - registerFetchData / registerApplyData / registerGetFiltersWithValues, addPersonalizableControl( ) with a PersonalizableInfo, and a change handler per filter field that marks the variant as modified. This action does all of it, so saving, selecting and restoring a variant works without a single line of JavaScript. The restored values reach the backend through the binding of the filter fields, no extra roundtrip needed. cs_event-keyboard_shortcut - bind a key combination to a named backend event, the declarative equivalent of a sap.ui.core.CommandExecution shortcut: t_arg = combination, event name. The combination is spelled like the UI5 one (`Ctrl+S`, `Ctrl+Shift+D`, `F2`; ctrl/shift/alt/meta in any order, cmd/command/option/control accepted as aliases). Pressing it fires the event exactly like a button press and suppresses the browser's own default for the combination. Registering the same combination again rebinds it; an empty event name removes it. The registrations belong to the running app and are dropped when another app takes over. An optional THIRD t_arg SCOPES the shortcut: the scoped registration wins while its scope is OPEN and the unscoped one applies otherwise, which is how a UI5 CommandExecution in a Popover's dependents shadows the page-level one for the same command. A scope is either a view slot (cs_view-popover/popup/nested/nested2/main) or the ID OF A CONTROL that can be open or closed - a Popover/Dialog declared in the view and opened with control_by_id openBy, which never enters a framework slot. A control scope beats a slot scope (it is the more specific statement), then the innermost open slot wins. An empty event name removes the registration of THAT scope only. cs_event-hash_attach_changed - APP-OWNED hash routing (HashChanger#attachHashChanged), the 1:1 counterpart of a UI5 router's own hash (`#/Page2`) for an app that does NOT use hash_routing: t_arg = a backend event name. From then on hash_set( `/Page2` ) writes that value as the whole app hash (a pushed history entry), hash_replace( ) the same without a new entry, and a hash change the app did not write itself - browser Back/Forward, a manual URL edit - fires the registered event; the hash the browser now stands on arrives with that request (and with every other one, a fresh deep-link start included) in get( )-s_config-hash, so the app decides what to show. While registered, the framework leaves the hash entirely alone. Calling it without t_arg unregisters. The registration dies with an app switch - register it in view_display( ), so every render (a draft restore included) re-asserts it. Mutually exclusive with hash_routing (a routed app's hash belongs to the router) and with app_state_set_active (both claim the whole hash). cs_event-hash_back - the UI5 onNavBack pattern: without t_arg one real step back in the browser history (`window.history.go(-1)` - the step is CONSUMED, and the resulting hash change fires the registered event). With t_arg = a fallback hash it guards the cold deep link the way UI5's recommended onNavBack does: when this page load never pushed an app hash, there is no in-app step to take, so the fallback is written as a REPLACE instead of falling out of the app - and the change fires the registered event, which shows the fallback route. cs_event-binding_call - apply a declarative filter/sorter to an aggregation binding, the client-side equivalent of the UI5 controller pattern getBinding('items').filter(...); the model data stays untouched: t_arg = id, aggregation, method, params. method `filter`: params = path, operator, value1, value2 (empty values clear the filter); method `sort`: params = path, descending, group (abap_bool as `X`/``). Each of these events also works roundtrip-free when WIRED IN THE VIEW: write the same call where its result is consumed (`)->a( n = `press` v = client->follow_up_action( val = ... t_arg = ... ) )`) and the action runs in the browser without a server call." ], "parameters": [ { diff --git a/docs/resources/api.md b/docs/resources/api.md index ce65f4a0..ddc0d699 100644 --- a/docs/resources/api.md +++ b/docs/resources/api.md @@ -1,6 +1,6 @@ --- outline: 2 -description: The complete abap2UI5 client API — every method, parameter and constant of z2ui5_if_client at the current release, generated from the source. +description: The complete abap2UI5 client API — every method, parameter and constant of z2ui5_if_client on the framework's main branch, generated from the source. --- # Client API @@ -8,10 +8,14 @@ description: The complete abap2UI5 client API — every method, parameter and co Everything an app calls at runtime comes through one interface: [`z2ui5_if_client`](https://github.com/abap2UI5/abap2UI5/blob/main/src/02/z2ui5_if_client.intf.abap), handed to the app as `client` on every roundtrip. This page **is** that -interface, generated from its source at the release named in the nav bar: -every method with its parameters, types and defaults, every `cs_*` constant -structure, and the documentation written in the source itself. What is printed -here is what your install has — not what `main` already promises. +interface, generated from its source on the framework's `main` branch — the +same ref every example on this site is compiled against: every method with +its parameters, types and defaults, every `cs_*` constant structure, and the +documentation written in the source itself. That puts it ahead of the newest +release by design. A name that is on `main` and not yet in a release is marked +*next release* on [Deprecations](/resources/deprecations), next to the +spelling the release still has; the release number itself is in the menu +behind the bar's last button. The interface is deliberately small and deliberately stable. How the pieces fit together — the lifecycle, binding, events — is what the @@ -163,7 +167,7 @@ Returns `string`. ### `follow_up_action` -Schedule a frontend action to run after the backend response is processed. Two ways to call it: pass a frontend event as val (e.g. cs_event-set_title) with its arguments in t_arg and the framework builds the event call; or pass a raw JavaScript expression as val (without t_arg) to run it as-is. The control/binding calls are frontend events too; their t_arg is positional (an empty argument between filled ones keeps its slot as ``): cs_event-control_by_id - call a method on a control resolved by id: t_arg = id, method, params. Any public control method works unless it is on the frontend denylist (methods that would break framework invariants). The named per-aggregation mutators are on the allowed side of that line - addItem, removeItem, removeAllItems, destroyContent - and only the GENERIC reflection variants that take the member name as an argument are denied (addAggregation, removeAllAggregation, setAssociation, ...). The view is passed as the separate view parameter (default cs_view-main resolves the id across all open views; pass cs_view-popup/popover/... to scope the lookup to that view). Two entries are NOT UI5 methods but frontend capabilities in method form: `css` sets ONE whitelisted CSS declaration on the control's own DOM node (t_arg = id, `css`, property, value) - for a value the control has no property for at all, e.g. the width of a sap.m.Page; prefer a bound property wherever one exists. `toggleBy` opens/closes a popup anchored to a control (t_arg = id, `toggleBy`, anchor id). An association setter (setSelectedSection, setSelectedItem) clears the association when its argument is EMPTY. Wherever an argument takes a CONTROL ID, it also takes an aggregation ITEM, addressed positionally as `//` (`carousel/pages/2`, 0-based). A control cloned from an aggregation template has no id the backend can spell - UI5 mints it from the template id, the parent id and the index, and the parent id carries the view prefix assigned at runtime - so this is the only way to reach one. It is the equivalent of the UI5 controller idiom `oCarousel.setActivePage( oCarousel.getPages()[ i ] )`. A plain id (no slashes) resolves exactly as before. cs_event-control_global - call a whitelisted method on a global object (MESSAGE_TOAST, MESSAGE_BOX, BUSY_INDICATOR, THEMING, POPUP, INVISIBLE_MESSAGE, FORMATTING, ICON_POOL): t_arg = object, method, params. POPUP-setWithinArea confines every popup to the control whose id is passed (sap.ui.core.Popup.setWithinArea, needs UI5 >= 1.89) instead of to the window; an EMPTY argument releases the restriction again. INVISIBLE_MESSAGE-announce reads a text out to a screen reader without rendering it (sap.ui.core.InvisibleMessage, needs UI5 >= 1.78): t_arg = text, mode (Polite, default, or Assertive). It is a singleton, so there is no control id - this is the only way to announce a change the backend made. FORMATTING-setCustomCurrencies registers currency codes the standard sap.ui.model.type.Currency does not know, or overrides their digit count (sap/base/i18n/Formatting, needs UI5 >= 1.120): t_arg = JSON object, e.g. {"BGN4":{"digits":4}}. It REPLACES the whole registration - addCustomCurrencies MERGES codes into it instead (t_arg = the same map). Reaching for the wrong one is silent: an app that registers currencies as it loads more data and calls setCustomCurrencies drops what it registered before, and the symptom is a wrong digit count in a table, never an error. What this reaches is the FORMATTING configuration, not a control that has already formatted: a control caching its NumberFormat at init( ) - among them sap.ui.unified.Currency - keeps the digit count it was built with, because it implements no localization-change hook. A BOUND sap.ui.model.type.Currency does implement one and re-formats. ICON_POOL-registerFont makes an icon collection outside the default SAP-icons font resolvable - sap.tnt's SAP-icons-TNT is the common one: t_arg = fontFamily, fontURI, e.g. `SAP-icons-TNT` / `sap/tnt/themes/base/fonts/`. A normal UI5 app does this in its Component's init; an abap2UI5 app has no Component of its own, and IconPool is a module SINGLETON rather than a control, so no other wire reaches it. Without the registration a sap-icon://SAP-icons-TNT/... URI renders NO GLYPH and logs nothing. The fontURI is a module path in every real use and is resolved through sap.ui.require.toUrl, so the registration survives a different mount point; an absolute URL is passed through. Issue it from the init branch - the same collection is registered only once per session, so a repeat call costs nothing. cs_event-smart_variant_init - run the initialise( ) handshake sap.ui.comp variant management needs (a controller would call oSmartVariantManagement.initialise( fnCallback, oPersonalizableControl )). Without it the control keeps no personalizable control, saving a view fails inside sap.ui.fl and stored variants are never loaded: t_arg = SmartVariantManagement id, personalizable control id (optional, default: the first control that registered itself). The action waits for that registration, which the smart controls do once their OData metadata has loaded. cs_event-filter_bar_variant_init - wire a classic sap.ui.comp.filterbar.FilterBar to a SmartVariantManagement: t_arg = SmartVariantManagement id, FilterBar id. A SmartFilterBar knows its own fields and registers itself (see smart_variant_init above); a classic FilterBar does not, so a list report normally hand-writes the same controller boilerplate - registerFetchData / registerApplyData / registerGetFiltersWithValues, addPersonalizableControl( ) with a PersonalizableInfo, and a change handler per filter field that marks the variant as modified. This action does all of it, so saving, selecting and restoring a variant works without a single line of JavaScript. The restored values reach the backend through the binding of the filter fields, no extra roundtrip needed. cs_event-keyboard_shortcut - bind a key combination to a named backend event, the declarative equivalent of a sap.ui.core.CommandExecution shortcut: t_arg = combination, event name. The combination is spelled like the UI5 one (`Ctrl+S`, `Ctrl+Shift+D`, `F2`; ctrl/shift/alt/meta in any order, cmd/command/option/control accepted as aliases). Pressing it fires the event exactly like a button press and suppresses the browser's own default for the combination. Registering the same combination again rebinds it; an empty event name removes it. The registrations belong to the running app and are dropped when another app takes over. An optional THIRD t_arg SCOPES the shortcut: the scoped registration wins while its scope is OPEN and the unscoped one applies otherwise, which is how a UI5 CommandExecution in a Popover's dependents shadows the page-level one for the same command. A scope is either a view slot (cs_view-popover/popup/nested/nested2/main) or the ID OF A CONTROL that can be open or closed - a Popover/Dialog declared in the view and opened with control_by_id openBy, which never enters a framework slot. A control scope beats a slot scope (it is the more specific statement), then the innermost open slot wins. An empty event name removes the registration of THAT scope only. cs_event-hash_attach_changed - APP-OWNED hash routing (HashChanger#attachHashChanged), the 1:1 counterpart of a UI5 router's own hash (`#/Page2`) for an app that does NOT use hash_routing: t_arg = a backend event name. From then on hash_set( `/Page2` ) writes that value as the whole app hash (a pushed history entry), hash_replace( ) the same without a new entry, and a hash change the app did not write itself - browser Back/Forward, a manual URL edit - fires the registered event; the hash the browser now stands on arrives with that request (and with every other one, a fresh deep-link start included) in get( )-s_config-hash, so the app decides what to show. While registered, the framework leaves the hash entirely alone. Calling it without t_arg unregisters. The registration dies with an app switch - register it in view_display( ), so every render (a draft restore included) re-asserts it. Mutually exclusive with hash_routing (a routed app's hash belongs to the router) and with app_state_set_active (both claim the whole hash). cs_event-hash_back - the UI5 onNavBack pattern: without t_arg one real step back in the browser history (`window.history.go(-1)` - the step is CONSUMED, and the resulting hash change fires the registered event). With t_arg = a fallback hash it guards the cold deep link the way UI5's recommended onNavBack does: when this page load never pushed an app hash, there is no in-app step to take, so the fallback is written as a REPLACE instead of falling out of the app - and the change fires the registered event, which shows the fallback route. cs_event-binding_call - apply a declarative filter/sorter to an aggregation binding, the client-side equivalent of the UI5 controller pattern getBinding('items').filter(...); the model data stays untouched: t_arg = id, aggregation, method, params. method `filter`: params = path, operator, value1, value2 (empty values clear the filter); method `sort`: params = path, descending, group (abap_bool as `X`/``). Each of these events also works roundtrip-free when WIRED IN THE VIEW: write the same call where its result is consumed (`)->a( n = `press` v = client->follow_up_action( val = ... t_arg = ... ) )`) and the action runs in the browser without a server call. +Schedule a frontend action to run after the backend response is processed. Two ways to call it: pass a frontend event as val (e.g. cs_event-set_title) with its arguments in t_arg and the framework builds the event call; or pass a raw JavaScript expression as val (without t_arg) to run it as-is. The control/binding calls are frontend events too; their t_arg is positional (an empty argument between filled ones keeps its slot as ``): cs_event-control_by_id - call a method on a control resolved by id: t_arg = id, method, params. Any public control method works unless it is on the frontend denylist (methods that would break framework invariants). The named per-aggregation mutators are on the allowed side of that line - addItem, removeItem, removeAllItems, destroyContent - and only the GENERIC reflection variants that take the member name as an argument are denied (addAggregation, removeAllAggregation, setAssociation, ...). The view is passed as the separate view parameter (default cs_view-main resolves the id across all open views; pass cs_view-popup/popover/... to scope the lookup to that view). Two entries are NOT UI5 methods but frontend capabilities in method form: `css` sets ONE whitelisted CSS declaration on the control's own DOM node (t_arg = id, `css`, property, value) - for a value the control has no property for at all, e.g. the width of a sap.m.Page; prefer a bound property wherever one exists. `toggleBy` opens/closes a popup anchored to a control (t_arg = id, `toggleBy`, anchor id). An association setter (setSelectedSection, setSelectedItem) clears the association when its argument is EMPTY. Wherever an argument takes a CONTROL ID, it also takes an aggregation ITEM, addressed positionally as `//` (`carousel/pages/2`, 0-based). A control cloned from an aggregation template has no id the backend can spell - UI5 mints it from the template id, the parent id and the index, and the parent id carries the view prefix assigned at runtime - so this is the only way to reach one. It is the equivalent of the UI5 controller idiom `oCarousel.setActivePage( oCarousel.getPages()[ i ] )`. A plain id (no slashes) resolves exactly as before. cs_event-control_global - call a whitelisted method on a global object (MESSAGE_TOAST, MESSAGE_BOX, BUSY_INDICATOR, THEMING, POPUP, INVISIBLE_MESSAGE, FORMATTING, ICON_POOL): t_arg = object, method, params. POPUP-setWithinArea confines every popup to the control whose id is passed (sap.ui.core.Popup.setWithinArea, needs UI5 >= 1.89) instead of to the window; an EMPTY argument releases the restriction again. INVISIBLE_MESSAGE-announce reads a text out to a screen reader without rendering it (sap.ui.core.InvisibleMessage, needs UI5 >= 1.78): t_arg = text, mode (Polite, default, or Assertive). It is a singleton, so there is no control id - this is the only way to announce a change the backend made. FORMATTING-setCustomCurrencies registers currency codes the standard sap.ui.model.type.Currency does not know, or overrides their digit count (sap/base/i18n/Formatting, needs UI5 >= 1.120): t_arg = JSON object, e.g. {"BGN4":{"digits":4}}. It REPLACES the whole registration - addCustomCurrencies MERGES codes into it instead (t_arg = the same map). Reaching for the wrong one is silent: an app that registers currencies as it loads more data and calls setCustomCurrencies drops what it registered before, and the symptom is a wrong digit count in a table, never an error. What this reaches is the FORMATTING configuration, not a control that has already formatted: a control caching its NumberFormat at init( ) - among them sap.ui.unified.Currency - keeps the digit count it was built with, because it implements no localization-change hook. A BOUND sap.ui.model.type.Currency does implement one and re-formats. ICON_POOL-registerFont makes an icon collection outside the default SAP-icons font resolvable - sap.tnt's SAP-icons-TNT is the common one: t_arg = fontFamily, fontURI, e.g. `SAP-icons-TNT` / `sap/tnt/themes/base/fonts/`. A normal UI5 app does this in its Component's init; an abap2UI5 app has no Component of its own, and IconPool is a module SINGLETON rather than a control, so no other wire reaches it. Without the registration a sap-icon://SAP-icons-TNT/... URI renders NO GLYPH and logs nothing. The fontURI is a module path in every real use and is resolved through sap.ui.require.toUrl, so the registration survives a different mount point; an absolute URL is passed through. Issue it from the init branch - the same collection is registered only once per session, so a repeat call costs nothing. cs_event-smart_variant_init - run the initialise( ) handshake sap.ui.comp variant management needs (a controller would call oSmartVariantManagement.initialise( fnCallback, oPersonalizableControl )). Without it the control keeps no personalizable control, saving a view fails inside sap.ui.fl and stored variants are never loaded: t_arg = SmartVariantManagement id, personalizable control id (optional, default: the first control that registered itself). The action waits for that registration, which the smart controls do once their OData metadata has loaded. cs_event-filter_bar_variant_init - wire a classic sap.ui.comp.filterbar.FilterBar to a SmartVariantManagement: t_arg = SmartVariantManagement id, FilterBar id. A SmartFilterBar knows its own fields and registers itself (see smart_variant_init above); a classic FilterBar does not, so a list report normally hand-writes the same controller boilerplate - registerFetchData / registerApplyData / registerGetFiltersWithValues, addPersonalizableControl( ) with a PersonalizableInfo, and a change handler per filter field that marks the variant as modified. This action does all of it, so saving, selecting and restoring a variant works without a single line of JavaScript. The restored values reach the backend through the binding of the filter fields, no extra roundtrip needed. cs_event-keyboard_shortcut - bind a key combination to a named backend event, the declarative equivalent of a sap.ui.core.CommandExecution shortcut: t_arg = combination, event name. The combination is spelled like the UI5 one (`Ctrl+S`, `Ctrl+Shift+D`, `F2`; ctrl/shift/alt/meta in any order, cmd/command/option/control accepted as aliases). Pressing it fires the event exactly like a button press and suppresses the browser's own default for the combination. Registering the same combination again rebinds it; an empty event name removes it. The registrations belong to the running app and are dropped when another app takes over. An optional THIRD t_arg SCOPES the shortcut: the scoped registration wins while its scope is OPEN and the unscoped one applies otherwise, which is how a UI5 CommandExecution in a Popover's dependents shadows the page-level one for the same command. A scope is either a view slot (cs_view-popover/popup/nested/nested2/main) or the ID OF A CONTROL that can be open or closed - a Popover/Dialog declared in the view and opened with control_by_id openBy, which never enters a framework slot. A control scope beats a slot scope (it is the more specific statement), then the innermost open slot wins. An empty event name removes the registration of THAT scope only. cs_event-hash_attach_changed - APP-OWNED hash routing (HashChanger#attachHashChanged), the 1:1 counterpart of a UI5 router's own hash (`#/Page2`) for an app that does NOT use hash_routing: t_arg = a backend event name. From then on hash_set( `/Page2` ) writes that value as the whole app hash (a pushed history entry), hash_replace( ) the same without a new entry, and a hash change the app did not write itself - browser Back/Forward, a manual URL edit - fires the registered event; the hash the browser now stands on arrives with that request (and with every other one, a fresh deep-link start included) in get( )-s_config-hash, so the app decides what to show. While registered, the framework leaves the hash entirely alone. Calling it without t_arg unregisters. The registration dies with an app switch - register it in view_display( ), so every render (a draft restore included) re-asserts it. Mutually exclusive with hash_routing (a routed app's hash belongs to the router) and with app_state_set_active (both claim the whole hash). cs_event-hash_back - the UI5 onNavBack pattern: without t_arg one real step back in the browser history (`window.history.go(-1)` - the step is CONSUMED, and the resulting hash change fires the registered event). With t_arg = a fallback hash it guards the cold deep link the way UI5's recommended onNavBack does: when this page load never pushed an app hash, there is no in-app step to take, so the fallback is written as a REPLACE instead of falling out of the app - and the change fires the registered event, which shows the fallback route. cs_event-binding_call - apply a declarative filter/sorter to an aggregation binding, the client-side equivalent of the UI5 controller pattern getBinding('items').filter(...); the model data stays untouched: t_arg = id, aggregation, method, params. method `filter`: params = path, operator, value1, value2 (empty values clear the filter); method `sort`: params = path, descending, group (abap_bool as `X`/``). Each of these events also works roundtrip-free when WIRED IN THE VIEW: write the same call where its result is consumed (`)->a( n = `press` v = client->follow_up_action( val = ... t_arg = ... ) )`) and the action runs in the browser without a server call. | Parameter | Type | Default | Description | |---|---|---|---| diff --git a/docs/resources/deprecations.md b/docs/resources/deprecations.md index 189b45e5..84eb5a31 100644 --- a/docs/resources/deprecations.md +++ b/docs/resources/deprecations.md @@ -423,8 +423,8 @@ ones on `z2ui5_if_ui5_exit`, not copies, so a config structure that gains a field gains it under either name. `z2ui5_if_exit` is deleted after a transition period, so move your exit over -when you next touch it — after the release that brings the new name, which the -examples on this site still wait for. +when you next touch it. The new name shipped with 1.144.0, and every exit +example on this site is written against it. ### `z2ui5_if_types` → the object that uses the type @@ -438,7 +438,7 @@ exists, so the type you need is declared where you already are: | `z2ui5_if_types=>ty_s_event_control` | `z2ui5_if_client=>ty_s_event_control` — the `s_ctrl` of `_event( )` | | `z2ui5_if_types=>ty_s_name_value` / `ty_t_name_value` | `z2ui5_if_client=>ty_s_name_value` / `ty_t_name_value` | | `z2ui5_if_types=>cs_device` | `z2ui5_if_client=>cs_device` | -| `z2ui5_if_types=>ty_s_http_context` / `ty_s_http_config` / `ty_s_http_config_post` | the same names on `z2ui5_if_exit`, whose two methods take them | +| `z2ui5_if_types=>ty_s_http_context` / `ty_s_http_config` / `ty_s_http_config_post` | the same names on `z2ui5_if_ui5_exit`, whose two methods take them | | `z2ui5_if_types=>ty_s_draft` | `z2ui5_cl_ui5_srv_draft=>ty_s_draft` | | `z2ui5_if_types=>ty_s_config` | written out inside `z2ui5_if_client=>ty_s_get-s_config` | diff --git a/docs/technical/tools/abapgit.md b/docs/technical/tools/abapgit.md index 7d80b5aa..b7f6fd0f 100644 --- a/docs/technical/tools/abapgit.md +++ b/docs/technical/tools/abapgit.md @@ -5,6 +5,19 @@ outline: [2, 4] This project uses [abapGit](https://github.com/abapGit/abapGit) for version control and easy code distribution. +## Installing abapGit + +abapGit is itself ABAP, installed once per system before anything else in this +documentation can be pulled. The +[abapGit installation guide](https://docs.abapgit.org/user-guide/getting-started/install.html) +covers both flavors: the standalone report, which is one program to create and +activate and is enough to pull abap2UI5, and the developer version pulled with +it afterwards. ABAP Cloud systems — BTP ABAP Environment and S/4 Public Cloud — +use [abapGit for Eclipse](https://eclipse.abapgit.org/) in ADT instead; the +[S/4 Public Cloud](/configuration/s4_public_cloud) page walks through that pull +screenshot by screenshot. With abapGit in place, the +[Quickstart](/get_started/quickstart) is the next page. + ## Git Client We build all project artifacts directly in an ABAP system and store them in a GitHub repository through abapGit. diff --git a/docs/tutorials/walkthrough/step-1.md b/docs/tutorials/walkthrough/step-1.md index 3269a35b..da814047 100644 --- a/docs/tutorials/walkthrough/step-1.md +++ b/docs/tutorials/walkthrough/step-1.md @@ -68,9 +68,10 @@ Next, we replace the message box with a real UI5 view. ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/tutorials/walkthrough/step-10.md b/docs/tutorials/walkthrough/step-10.md index 7e8044f0..f670c41a 100644 --- a/docs/tutorials/walkthrough/step-10.md +++ b/docs/tutorials/walkthrough/step-10.md @@ -314,9 +314,10 @@ And for everything beyond the walkthrough: ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/tutorials/walkthrough/step-11.md b/docs/tutorials/walkthrough/step-11.md index 85bcf724..9c4970de 100644 --- a/docs/tutorials/walkthrough/step-11.md +++ b/docs/tutorials/walkthrough/step-11.md @@ -91,8 +91,8 @@ https://:/sap/bc/?app_start=zcl_app_walkthrough ``` That URL is a bookmark, an intranet link — or a tile: the -[Installation](/configuration/installation) page says what launching looks -like per system, from the [Fiori Launchpad](/configuration/launchpad) on +[Deployment Scenarios](/configuration/installation) page says what launching +looks like per system, from the [Fiori Launchpad](/configuration/launchpad) on S/4 Private Cloud and On-Premise to the tile chain on [S/4 Public Cloud](/configuration/s4_public_cloud). On a phone, the app can be [added to the home screen](/configuration/mobile_start) like any web app. diff --git a/docs/tutorials/walkthrough/step-12.md b/docs/tutorials/walkthrough/step-12.md index f8da4acf..3ceef166 100644 --- a/docs/tutorials/walkthrough/step-12.md +++ b/docs/tutorials/walkthrough/step-12.md @@ -14,9 +14,11 @@ could have written before ever hearing of this framework. ## The Class Under Test -The app is unchanged from [Step 10](/tutorials/walkthrough/step-10) — printed -here in full so this step stands on its own: +The app is unchanged from [Step 10](/tutorials/walkthrough/step-10). It is +folded here in full so this step stands on its own — open it when you want to +copy the class, or read on to the tests, which are what this step is about: +::: details The complete class — `zcl_app_walkthrough`, as Step 10 left it ```abap CLASS zcl_app_walkthrough DEFINITION PUBLIC. @@ -256,6 +258,7 @@ CLASS zcl_app_walkthrough IMPLEMENTATION. ENDCLASS. ``` +::: ## The Test Class diff --git a/docs/tutorials/walkthrough/step-3.md b/docs/tutorials/walkthrough/step-3.md index bd1eb0b8..11f3ce7a 100644 --- a/docs/tutorials/walkthrough/step-3.md +++ b/docs/tutorials/walkthrough/step-3.md @@ -86,9 +86,10 @@ of transfer code. ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/tutorials/walkthrough/step-4.md b/docs/tutorials/walkthrough/step-4.md index 777dff10..0ba8d949 100644 --- a/docs/tutorials/walkthrough/step-4.md +++ b/docs/tutorials/walkthrough/step-4.md @@ -77,9 +77,10 @@ Next, the same binding moves a whole internal table into a list. ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/tutorials/walkthrough/step-5.md b/docs/tutorials/walkthrough/step-5.md index 7970e268..fc82b96e 100644 --- a/docs/tutorials/walkthrough/step-5.md +++ b/docs/tutorials/walkthrough/step-5.md @@ -90,9 +90,10 @@ Next, the rows learn to react to a click. ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/tutorials/walkthrough/step-6.md b/docs/tutorials/walkthrough/step-6.md index b05ec690..f87cdbad 100644 --- a/docs/tutorials/walkthrough/step-6.md +++ b/docs/tutorials/walkthrough/step-6.md @@ -100,9 +100,10 @@ A toast is a modest way to show an invoice. Next, a real dialog. ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/tutorials/walkthrough/step-7.md b/docs/tutorials/walkthrough/step-7.md index a8011185..c41c92cb 100644 --- a/docs/tutorials/walkthrough/step-7.md +++ b/docs/tutorials/walkthrough/step-7.md @@ -136,9 +136,10 @@ apps use. ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/tutorials/walkthrough/step-8.md b/docs/tutorials/walkthrough/step-8.md index 88ff16f3..3d5be4a6 100644 --- a/docs/tutorials/walkthrough/step-8.md +++ b/docs/tutorials/walkthrough/step-8.md @@ -153,9 +153,10 @@ Next, the list becomes a real table. ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/docs/tutorials/walkthrough/step-9.md b/docs/tutorials/walkthrough/step-9.md index 97ff0b49..7aaba909 100644 --- a/docs/tutorials/walkthrough/step-9.md +++ b/docs/tutorials/walkthrough/step-9.md @@ -182,9 +182,10 @@ One step remains: putting all of it into the shape a real app has. ## Working Samples -Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md) -that use what this page describes. Each is a single class — pull the repository with -[abapGit](https://abapgit.org) and start it with `?app_start=`. +Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. | Sample | Class | |---|---| diff --git a/scripts/check-api-names.mjs b/scripts/check-api-names.mjs index 15a1a13a..7345642c 100644 --- a/scripts/check-api-names.mjs +++ b/scripts/check-api-names.mjs @@ -59,7 +59,7 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { frameworkRef } from './lib/release.mjs'; -import { fetchInterface } from './lib/client-interface.mjs'; +import { fetchInterface, interfaceSource } from './lib/client-interface.mjs'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const PAGES = path.join(ROOT, 'docs'); @@ -81,6 +81,7 @@ if (!REF) { let iface; try { iface = await fetchInterface(REF); + console.log(`read ${interfaceSource(REF)}`); } catch (err) { console.log(`z2ui5_if_client at ${REF}: not resolved (${err.message})`); console.log('SKIPPED: nothing was verified.'); diff --git a/scripts/generate-api-reference.mjs b/scripts/generate-api-reference.mjs index 054eab31..5a05e273 100644 --- a/scripts/generate-api-reference.mjs +++ b/scripts/generate-api-reference.mjs @@ -56,7 +56,7 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { frameworkRef } from './lib/release.mjs'; -import { fetchInterface, parseInterface, interfacePath } from './lib/client-interface.mjs'; +import { fetchInterface, interfaceSource, parseInterface, interfacePath } from './lib/client-interface.mjs'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const PAGE = path.join(ROOT, 'docs', 'resources', 'api.md'); @@ -79,13 +79,14 @@ if (!REF) { let iface; try { iface = await fetchInterface(REF); + console.log(`read ${interfaceSource(REF)}`); } catch (err) { console.log(`z2ui5_if_client at ${REF}: not resolved (${err.message})`); if (CHECK) { console.log('SKIPPED: nothing was verified.'); process.exit(0); } - console.error('nothing to generate from - try again with a network.'); + console.error('nothing to generate from - try again with a network, or point A2UI5_HOME at a clone of abap2UI5/abap2UI5.'); process.exit(1); } diff --git a/scripts/lib/catalogue.mjs b/scripts/lib/catalogue.mjs index 30481f3f..1646af24 100644 --- a/scripts/lib/catalogue.mjs +++ b/scripts/lib/catalogue.mjs @@ -196,3 +196,43 @@ export function parseCatalogue(text) { } return byClass; } + +/* ------------------------------------------------- the checkout, if any */ + +/** The checkout of a sample repository, if one is at hand: the first of its + * homes (an explicit `*_HOME`, then the sibling names CI and a contributor + * use) that holds the repository's catalogue - `catalogue.json`, or the + * SAMPLES.md it is rendered from. `null` when none is, which the caller + * treats as "cannot verify", never as "verified". */ +export function resolveHome(repo, root) { + const dirs = HOMES[repo]; + if (!dirs) throw new Error(`no catalogue location known for ${repo}`); + for (const dir of dirs) { + const at = dir.endsWith('_HOME') ? process.env[dir] : path.join(root, dir); + if (!at) continue; + if (fs.existsSync(path.join(at, 'catalogue.json')) || fs.existsSync(path.join(at, 'SAMPLES.md'))) return at; + } + return null; +} + +/** A parsed catalogue.json as a map from the lower-cased class name to what a + * link needs of an entry: the source path under the repository (`file` in + * samples and samples-controls, `path` in samples-stack - pinned as-is by + * each repository's contract gate), a label made of the title and the one + * sentence under it, and the branch a samples-stack package ships on. + * The three shapes differ on purpose; this is the one place the difference + * is read. */ +export function entriesByClass(catalogue) { + const byClass = new Map(); + for (const entry of entriesOf(catalogue)) { + const title = typeof entry.title === 'string' ? entry.title.trim() : ''; + const sentence = typeof entry.summary === 'string' ? entry.summary.trim() : ''; + const label = [title, sentence].filter(Boolean).join(' — ') || entry.class; + byClass.set(entry.class.toLowerCase(), { + label, + path: entry.file ?? entry.path, + branch: typeof entry.branch === 'string' && entry.branch !== 'main' ? entry.branch : '', + }); + } + return byClass; +} diff --git a/scripts/lib/client-interface.mjs b/scripts/lib/client-interface.mjs index c4af87c8..35079402 100644 --- a/scripts/lib/client-interface.mjs +++ b/scripts/lib/client-interface.mjs @@ -10,9 +10,23 @@ * and in --check mode holds the committed copy * against the interface * - * Both fetch the SAME source at the SAME pin: the raw file at the release the - * site names (lib/release.mjs), never main - a reader installs a release, and - * main is ahead of it by definition. + * Both fetch the SAME source at the SAME ref: the raw file on the framework + * branch this site tracks - `frameworkRef( )` in lib/release.mjs, which is + * main, or whatever A2UI5_REF pins a run back to. It used to be the release + * the site names, and release.mjs says why it no longer is: a page could not + * be corrected to an API that had landed on main until the next tag. + * + * The file can come out of a clone of the framework instead of off the + * network: `A2UI5_HOME` names the clone, and the same ref is then read with + * `git show` from it. That is for a machine that cannot reach + * raw.githubusercontent.com (the gates SKIP there, honestly, and a + * regeneration has nothing to write), and for regenerating the reference + * against a branch that has not merged yet. It is not a sibling-checkout + * convenience like lib/catalogue.mjs has, on purpose: the catalogue lookup + * answers "which classes exist", where any recent copy will do, and this one + * answers "what does main say", where the branch a neighbouring checkout + * happens to be on must not change the gate's verdict. Only an explicit + * A2UI5_HOME does, and the run prints where the interface came from. * * The full parser lives here too, used by generate-api-reference. It answers * a richer question than check-api-names asks - not "does this name exist" @@ -23,29 +37,78 @@ * that produces some of it. */ +import { execFileSync } from 'child_process'; + /** The one file this documentation calls "the client API". */ export const interfacePath = 'src/02/z2ui5_if_client.intf.abap'; export const interfaceUrl = (ref) => `https://raw.githubusercontent.com/abap2UI5/abap2UI5/${ref}/${interfacePath}`; +/** A clone of the framework to read the interface from instead of the + * network, or null: `A2UI5_HOME`. */ +export function frameworkHome() { + return process.env.A2UI5_HOME || null; +} + +/** Where a run reads the interface from, for the line it prints - so the + * output says whether github.com or a clone answered. */ +export function interfaceSource(ref, home = frameworkHome()) { + return home ? `${interfacePath} at ${ref} in the clone at ${home} (A2UI5_HOME)` : interfaceUrl(ref); +} + /** The interface source at a pinned ref. Throws on any network or HTTP * failure - the CALLER decides whether that skips or fails, because a gate * must not go red over an unreachable github.com and must not claim to have - * verified something it did not. */ -export async function fetchInterface(ref) { + * verified something it did not. With A2UI5_HOME set, no network at all: + * the ref is read out of that clone, and not being in it throws the same way. */ +export async function fetchInterface(ref, { home = frameworkHome() } = {}) { + if (home) return readInterface(ref, home); const res = await fetch(interfaceUrl(ref), { signal: AbortSignal.timeout(20000) }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.text(); } +/** The interface at `ref` out of a clone of the framework: what `git show` + * answers for `:src/02/z2ui5_if_client.intf.abap`, trying the remote's + * copy of the branch too, because a clone that has never checked main out + * knows it only as `origin/main`. The WORKING TREE is deliberately not read: + * it is whichever branch the clone is on right now, and a gate that judged + * the site against that would give a different verdict on the same commit + * depending on what its neighbour was doing. A branch is asked for by + * name, through A2UI5_REF, the same way a release is. */ +export function readInterface(ref, home) { + const tried = []; + for (const spec of [ref, `origin/${ref}`]) { + try { + return execFileSync('git', ['-C', home, 'show', `${spec}:${interfacePath}`], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + } catch (err) { + tried.push(`${spec}: ${String(err.stderr ?? err.message).trim().split('\n')[0]}`); + } + } + throw new Error(`not in the clone at ${home} - ${tried.join('; ')}`); +} + /* ---------------------------------------------------------------- parsing */ /** ABAP-Doc escapes, undone. The parsed model carries PLAIN text; whoever - * renders it into markdown or HTML escapes for that target again. */ + * renders it into markdown or HTML escapes for that target again. + * + * All five XML entities and the numeric forms, not the three this knew for a + * while: ABAP-Doc is XML, so the interface writes an apostrophe as `'`, + * and the page printed `sap.tnt's` for as long as the decoder did not + * know the name. `&` goes LAST, so `&lt;` comes out as the literal + * `<` the author escaped, not as `<`. */ const decode = (s) => s .replace(/</g, '<') .replace(/>/g, '>') + .replace(/'/g, "'") + .replace(/"/g, '"') + .replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n))) + .replace(/&#x([0-9a-f]+);/gi, (_, h) => String.fromCodePoint(parseInt(h, 16))) .replace(/&/g, '&') .replace(/\\([{}|])/g, '$1'); diff --git a/scripts/link-samples.mjs b/scripts/link-samples.mjs index 5fc09abd..bc3d9635 100644 --- a/scripts/link-samples.mjs +++ b/scripts/link-samples.mjs @@ -22,11 +22,22 @@ * --- * * Everything else - the title, the folder, the URL - is read from the - * catalogue (SAMPLES.md in a samples checkout), which is itself generated - * from the classes' abapGit short texts. So a sample that gets renamed, - * re-described or moved between packages updates here on the next run, and a - * sample that is DELETED fails this script instead of leaving a 404 on the - * documentation site. + * catalogue of the repository the class belongs to, which is itself + * generated from the classes' abapGit short texts. So a sample that gets + * renamed, re-described or moved between packages updates here on the next + * run, and a sample that is DELETED fails this script instead of leaving a + * 404 on the documentation site. + * + * THE CLASS PREFIX DECIDES THE REPOSITORY. `z2ui5_cl_smp_app_*` is + * abap2UI5/samples, `z2ui5_cl_smpc_*` abap2UI5/samples-controls, + * `z2ui5_cl_smps_*` abap2UI5/samples-stack - the three prefixes are disjoint + * by the family's own naming rule, and no number means anything without one. + * Until 2026-09 only the first repository could be declared: the blob link + * was hardcoded to it, and 668 samples of the other two - every control + * port, every RAP, OData and WebSocket sample - were out of reach of the + * pages that explain exactly them. The samples repository is read from its + * SAMPLES.md as before; the other two from their catalogue.json + * (lib/catalogue.mjs knows where each one is). * * Nothing about a sample is retyped in this repository. The one thing a page * states is which samples are relevant, which is the one thing the catalogue @@ -35,10 +46,14 @@ * The block is written between markers at the end of the page and committed, * so building the site needs no samples checkout - only regenerating does. * - * The reverse direction is checked too: a class this documentation links to - * has to carry a `" @docs` line naming the page. Somebody who arrives in the - * samples repository from a search engine sees the chapter that explains what - * they are reading, and a page rename cannot silently orphan it. + * The reverse direction is checked too: a class of abap2UI5/samples this + * documentation links to has to carry a `" @docs` line naming the page. + * Somebody who arrives in the samples repository from a search engine sees + * the chapter that explains what they are reading, and a page rename cannot + * silently orphan it. The other two repositories carry no such line by their + * own convention (samples-stack's AGENTS.md: the pairing is declared on the + * documentation side); for a class of theirs the check is that it is in the + * repository's catalogue and, when a checkout is at hand, in its tree. * * node scripts/link-samples.mjs rewrite the blocks * node scripts/link-samples.mjs --check fail if a rewrite would change @@ -47,18 +62,35 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; -import { parseCatalogue } from './lib/catalogue.mjs'; +import { parseCatalogue, entriesByClass, loadCatalogue, resolveHome } from './lib/catalogue.mjs'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const DOCS = path.join(ROOT, 'docs'); const CHECK = process.argv.includes('--check'); -const BLOB = 'https://github.com/abap2UI5/samples/blob/main'; +/* The three repositories a page may declare a class from, in the order the + * family numbers them. `blob` is where a row links; `backlinks` says whether + * the classes over there carry a `" @docs` line this script holds them to. */ +const REPOS = { + samples: { blob: 'https://github.com/abap2UI5/samples/blob/main', backlinks: true }, + 'samples-controls': { blob: 'https://github.com/abap2UI5/samples-controls/blob/main', backlinks: false }, + 'samples-stack': { blob: 'https://github.com/abap2UI5/samples-stack/blob/main', backlinks: false }, +}; // how a class's `" @docs` line spells a page of this site const SITE = 'https://abap2ui5.github.io/docs/'; const START = ''; const END = ''; +/** Which repository a declared class belongs to - by its prefix, the one + * thing about a sample's name that carries meaning (AGENTS.md of every + * sample repository: numbers are per repository and reused freely). */ +function repoOf(cls) { + const c = cls.toLowerCase(); + if (c.startsWith('z2ui5_cl_smpc_')) return 'samples-controls'; + if (c.startsWith('z2ui5_cl_smps_')) return 'samples-stack'; + return 'samples'; +} + /* ------------------------------------------------------------------ input */ /** The samples checkout: an explicit path, or a sibling under any of the @@ -101,18 +133,29 @@ function docsPath(file) { return path.relative(DOCS, file).replace(/\\/g, '/').replace(/\.md$/, ''); } +/** One row: the sample's label, then the class linked at its source. A class + * outside abap2UI5/samples says which repository it is in - and, for a + * samples-stack package that ships on a branch of its own, which branch - + * because that is what the reader pulls. */ +function renderRow(e) { + const link = `[\`${e.cls.toUpperCase()}\`](${REPOS[e.repo].blob}/${e.path})`; + const where = e.repo === 'samples' + ? '' + : ` · [${e.repo}](https://github.com/abap2UI5/${e.repo})${e.branch ? `, branch \`${e.branch}\`` : ''}`; + return `| ${e.label} | ${link}${where} |`; +} + function renderBlock(entries) { - const rows = entries - .map((e) => `| ${e.label} | [\`${e.cls.toUpperCase()}\`](${BLOB}/${e.path}) |`) - .join('\n'); + const rows = entries.map(renderRow).join('\n'); return [ START, '', '## Working Samples', '', - 'Complete apps from the [sample catalog](https://github.com/abap2UI5/samples/blob/main/SAMPLES.md)', - 'that use what this page describes. Each is a single class — pull the repository with', - '[abapGit](https://abapgit.org) and start it with `?app_start=`.', + 'Complete apps from the [sample catalog](https://abap2ui5.github.io/playground/samples/)', + 'that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples)', + 'unless its row names another of the three sample repositories — pull that repository with', + '[abapGit](https://abapgit.org) and start the class with `?app_start=`.', '', '| Sample | Class |', '|---|---|', @@ -156,6 +199,21 @@ if (!samplesHome) { const catalogue = parseCatalogue(fs.readFileSync(path.join(samplesHome, 'SAMPLES.md'), 'utf8')); if (catalogue.size === 0) throw new Error(`no rows parsed out of ${samplesHome}/SAMPLES.md`); +/* The other two repositories: their catalogue.json, from a checkout when one + * is here (CI checks all three out) and from the published copy otherwise - + * lib/catalogue.mjs's fallback chain. A checkout is what lets the class's + * file be looked for; a published catalogue only says the class exists. + * Neither is required: a page that declares none of their classes needs + * neither, and a page that does is told when its class cannot be verified. */ +const homes = { samples: samplesHome }; +const catalogues = { samples: catalogue }; +for (const repo of ['samples-controls', 'samples-stack']) { + homes[repo] = resolveHome(repo, ROOT); + const loaded = await loadCatalogue(repo, ROOT); + catalogues[repo] = loaded ? entriesByClass(loaded.catalogue) : null; + if (!loaded) console.log(`no ${repo} catalogue at hand (checkout or network) - its classes cannot be verified`); +} + /** The pages a sample class points at, as paths under docs/. */ function backlinks(relSource) { const file = path.join(samplesHome, relSource); @@ -184,19 +242,32 @@ for (const file of markdownFiles(DOCS)) { const entries = []; for (const cls of declared) { - const hit = catalogue.get(cls.toLowerCase()); + const repo = repoOf(cls); + const rows = catalogues[repo]; + if (!rows) { + const home = repo === 'samples-stack' ? 'SAMPLES_STACK_HOME' : 'SAMPLES_CONTROLS_HOME'; + problems.push(`${docsPath(file)}: \`${cls}\` is a ${repo} class and that catalogue is not at hand — clone abap2UI5/${repo} as a sibling or set ${home}`); + continue; + } + const hit = rows.get(cls.toLowerCase()); if (!hit) { - problems.push(`${docsPath(file)}: \`${cls}\` is not in the sample catalogue — renamed, deleted, or a typo`); + problems.push(`${docsPath(file)}: \`${cls}\` is not in the ${repo} catalogue — renamed, deleted, or a typo`); continue; } - entries.push({ cls, ...hit }); + entries.push({ cls, repo, ...hit }); - // the way back: the class has to name this page - if (!backlinks(hit.path).includes(docsPath(file))) { - problems.push( - `${hit.path}: does not point back at ${docsPath(file)}\n` - + ` add \`${SITE}${docsPath(file)}\` to the class's \`" @docs\` line in abap2UI5/samples`, - ); + if (REPOS[repo].backlinks) { + // the way back: the class has to name this page + if (!backlinks(hit.path).includes(docsPath(file))) { + problems.push( + `${hit.path}: does not point back at ${docsPath(file)}\n` + + ` add \`${SITE}${docsPath(file)}\` to the class's \`" @docs\` line in abap2UI5/samples`, + ); + } + } else if (homes[repo] && !fs.existsSync(path.join(homes[repo], hit.path))) { + // no line to hold the class to over there, so the file itself is held: + // the catalogue says where it is, and the tree has to agree + problems.push(`${docsPath(file)}: \`${cls}\` is in the ${repo} catalogue at ${hit.path}, which is not in the checkout at ${homes[repo]}`); } } links += entries.length; @@ -228,13 +299,15 @@ for (const file of markdownFiles(DOCS)) { * asks it over the network, one HEAD per path. Here the whole checkout is * already open (CI clones it in full for the catalogue), so the answer is a * file lookup: no request, and it fails on a path that moved as readily as on - * one that was deleted. + * one that was deleted. The other two repositories are held the same way + * wherever their tree is here; a published catalogue cannot answer for a + * README or a folder link, so without a checkout their links pass unread. * * The generated blocks are skipped - they are written FROM the catalogue, so * checking them would only be checking this script against itself, and the * declared-samples loop above already fails on a class the catalogue lost. */ { - const seen = new Map(); // path in the samples repo -> pages that link it + const seen = new Map(); // "/" -> pages that link it for (const file of markdownFiles(DOCS)) { let text = fs.readFileSync(file, 'utf8'); const from = text.indexOf(START); @@ -242,18 +315,22 @@ for (const file of markdownFiles(DOCS)) { const to = text.indexOf(END, from); text = to === -1 ? text.slice(0, from) : text.slice(0, from) + text.slice(to); } - for (const m of text.matchAll(/https:\/\/github\.com\/abap2UI5\/samples\/blob\/main\/([^)`"\s>]+)/g)) { - const target = m[1].split('#')[0].replace(/[.,;:)]+$/, ''); + for (const m of text.matchAll(/https:\/\/github\.com\/abap2UI5\/(samples(?:-controls|-stack)?)\/blob\/main\/([^)`"\s>]+)/g)) { + const target = `${m[1]}/${m[2].split('#')[0].replace(/[.,;:)]+$/, '')}`; if (!seen.has(target)) seen.set(target, new Set()); seen.get(target).add(docsPath(file)); } } handwritten = seen.size; for (const [target, onPages] of seen) { - if (fs.existsSync(path.join(samplesHome, target))) continue; + const slash = target.indexOf('/'); + const repo = target.slice(0, slash); + const rel = target.slice(slash + 1); + if (!homes[repo]) continue; + if (fs.existsSync(path.join(homes[repo], rel))) continue; handwrittenGone += 1; problems.push( - `${[...onPages].sort().join(', ')}: links ${target} in abap2UI5/samples, which is not there on main\n` + `${[...onPages].sort().join(', ')}: links ${rel} in abap2UI5/${repo}, which is not there on main\n` + ' renumbered, moved or deleted - a link every reader who clicks it gets a 404 from', ); } @@ -274,7 +351,11 @@ for (const [cls, hit] of catalogue) { } } -console.log(`${catalogue.size} samples in the catalogue (${path.relative(ROOT, samplesHome) || samplesHome})`); +const where = (repo) => (homes[repo] ? path.relative(ROOT, homes[repo]) || homes[repo] : 'published catalogue'); +console.log(`${catalogue.size} samples in the catalogue (${where('samples')})` + + ['samples-controls', 'samples-stack'] + .filter((repo) => catalogues[repo]) + .map((repo) => `, ${catalogues[repo].size} in ${repo} (${where(repo)})`).join('')); console.log(`${pages} page(s) declare ${links} sample link(s)${CHECK ? '' : `, ${written} rewritten`}`); console.log(`${handwritten} source path(s) linked by hand in the prose` + (handwrittenGone ? `, ${handwrittenGone} of them gone` : ', all present in the checkout')); diff --git a/test/catalogue.test.mjs b/test/catalogue.test.mjs index 16a00808..8279592a 100644 --- a/test/catalogue.test.mjs +++ b/test/catalogue.test.mjs @@ -22,7 +22,7 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import { parseCatalogue, countCatalogue, countEntries } from '../scripts/lib/catalogue.mjs'; +import { parseCatalogue, countCatalogue, countEntries, entriesByClass, resolveHome } from '../scripts/lib/catalogue.mjs'; const ROWS = [ '## Basics', @@ -192,3 +192,28 @@ test('an unreachable published catalogue costs the figure, never the build', asy assert.equal(await countCatalogue('samples', dir, { fetchFn: served('not json {') }), null); assert.equal(await countCatalogue('samples', dir, { fetchFn: served('{"counts":{"samples":97}}') }), null); }); + +test('a catalogue.json becomes a map from class to label, path and branch, whatever its shape', () => { + const controls = entriesByClass(JSON.parse(JSON_CONTROLS)); + const stack = entriesByClass(JSON.parse(JSON_STACK)); + assert.equal(controls.size, countEntries(JSON.parse(JSON_CONTROLS))); + assert.equal(stack.size, countEntries(JSON.parse(JSON_STACK))); + for (const [cls, hit] of [...controls, ...stack]) { + assert.equal(cls, cls.toLowerCase(), 'keyed by the lower-cased class, the way pages declare it'); + assert.ok(hit.path && hit.path.endsWith('.clas.abap'), `${cls} has its source path`); + assert.ok(hit.label.length > 0, `${cls} has a label`); + } + // samples-stack rows name the package branch a reader pulls; main is no branch worth naming + const named = [...stack.values()].filter((h) => h.branch); + assert.ok(named.every((h) => h.branch !== 'main')); +}); + +test('a checkout is found by its catalogue, and its absence is null rather than a guess', (t) => { + const root = scratch(); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + assert.equal(resolveHome('samples-controls', root), null); + fs.mkdirSync(path.join(root, '.samples-controls')); + fs.writeFileSync(path.join(root, '.samples-controls', 'catalogue.json'), JSON_CONTROLS); + assert.equal(resolveHome('samples-controls', root), path.join(root, '.samples-controls')); + assert.throws(() => resolveHome('samples-nowhere', root), /no catalogue location/); +}); diff --git a/test/client-interface.test.mjs b/test/client-interface.test.mjs new file mode 100644 index 00000000..96d36f5a --- /dev/null +++ b/test/client-interface.test.mjs @@ -0,0 +1,95 @@ +/* + * The parser behind the generated client API reference, on the one thing it + * got wrong in public: ABAP-Doc is XML, so the interface writes an apostrophe + * as `'` - and resources/api.md printed `sap.tnt's` for weeks, + * because the decoder knew three entities and that was not one of them. The + * model has to carry plain text; the renderer escapes for its own target. + * + * npm test + */ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { parseInterface } from '../scripts/lib/client-interface.mjs'; + +const iface = ` +INTERFACE z2ui5_if_client PUBLIC. + + CONSTANTS: + BEGIN OF cs_event, + "Control + control_by_id TYPE string VALUE \`CONTROL_BY_ID\`, + END OF cs_event. + + "! Registers sap.tnt's font in the Component's init; a "raw" + "! call <works>, &amp; stays the text the author escaped, + "! and 'numeric' 'forms' are entities too. + METHODS follow_up_action + IMPORTING + "! @parameter val | the action, e.g. cs_event-control_by_id + val TYPE clike. + +ENDINTERFACE. +`; + +test('ABAP-Doc entities arrive in the model as the characters they stand for', () => { + const { methods } = parseInterface(iface); + assert.equal(methods.length, 1); + assert.equal( + methods[0].doc[0], + `Registers sap.tnt's font in the Component's init; a "raw" call , & stays the text the author escaped, and 'numeric' 'forms' are entities too.`, + ); +}); + +test('a constant run keeps its label, and the doc line under it is not the label', () => { + const { constants } = parseInterface(iface); + assert.equal(constants[0].name, 'cs_event'); + assert.deepEqual(constants[0].members, [ + { name: 'control_by_id', type: 'string', value: 'CONTROL_BY_ID', label: 'Control' }, + ]); +}); + +/* ---------------------------------------------------- where the file comes from */ + +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; + +import { fetchInterface, interfaceSource, interfaceUrl, readInterface, interfacePath } from '../scripts/lib/client-interface.mjs'; + +/** A throwaway clone of the framework: one commit on `main` carrying the + * interface, and a working tree that has moved on to another branch with a + * DIFFERENT interface - the case the clone source must not be fooled by. */ +function cloneWith(mainText, branchText) { + const home = fs.mkdtempSync(path.join(os.tmpdir(), 'a2ui5-home-')); + const git = (...args) => execFileSync('git', ['-C', home, ...args], { stdio: ['ignore', 'pipe', 'pipe'] }); + git('init', '-q', '-b', 'main'); + git('config', 'user.email', 'test@example.com'); + git('config', 'user.name', 'test'); + fs.mkdirSync(path.join(home, path.dirname(interfacePath)), { recursive: true }); + fs.writeFileSync(path.join(home, interfacePath), mainText); + git('add', '.'); + git('commit', '-q', '-m', 'main'); + git('checkout', '-q', '-b', 'feature'); + fs.writeFileSync(path.join(home, interfacePath), branchText); + git('commit', '-q', '-am', 'feature'); + return home; +} + +test('A2UI5_HOME reads the ref out of the clone, not whatever its working tree is on', async () => { + const home = cloneWith('INTERFACE main.\n', 'INTERFACE feature.\n'); + try { + assert.equal(readInterface('main', home), 'INTERFACE main.\n'); + assert.equal(readInterface('feature', home), 'INTERFACE feature.\n'); + assert.equal(await fetchInterface('main', { home }), 'INTERFACE main.\n'); + assert.throws(() => readInterface('no-such-branch', home), /not in the clone at .*no-such-branch/); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + } +}); + +test('the run says where the interface came from', () => { + assert.equal(interfaceSource('main', null), interfaceUrl('main')); + assert.match(interfaceSource('main', '/srv/abap2UI5'), /at main in the clone at \/srv\/abap2UI5 \(A2UI5_HOME\)/); +}); diff --git a/test/crumbs.test.mjs b/test/crumbs.test.mjs index f3a938b0..0ccc7d03 100644 --- a/test/crumbs.test.mjs +++ b/test/crumbs.test.mjs @@ -106,11 +106,25 @@ test('the real sidebar files the pages where a reader would look for them', () = test('every page the sidebar names gets a trail longer than the word Documentation', () => { // The section headers duplicate their first chapter's link, so a walk that // matched shallowly would leave a whole level of pages with no trail at all. + // + // The one page entitled to the single crumb is a top-level section's OWN + // page - the Cookbook's index, the Advanced Topics index - which no row + // under the section repeats: its title IS the section, so "Documentation" + // is everything above it. Pinned as exactly one crumb rather than skipped, + // so a walk that gave such a page a trail of two would be as wrong here as + // a walk that gave a chapter one. const seen = []; const walk = (items) => (items || []).forEach((i) => { if (i.link) seen.push(i.link); walk(i.items); }); walk(config.themeConfig.sidebar); + const below = (items) => (items || []).flatMap((i) => [i.link, ...below(i.items)]).filter(Boolean); + const own = new Set(config.themeConfig.sidebar + .filter((s) => s.link && !below(s.items).includes(s.link)) + .map((s) => s.link)); + assert.ok(own.size > 0, 'expected at least one section with a page of its own'); assert.ok(seen.length > 100, `expected the manual's sidebar, got ${seen.length} entries`); for (const link of seen) { - assert.ok(trailFor(config.themeConfig.sidebar, link).length > 1, `no trail for ${link}`); + const trail = trailFor(config.themeConfig.sidebar, link); + if (own.has(link)) assert.equal(trail.length, 1, `${link} is a section's own page and gets the one crumb`); + else assert.ok(trail.length > 1, `no trail for ${link}`); } });