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