From ad01cc63082b5f3c210a12a0ad2a3116fa0d5395 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:26:19 +0000 Subject: [PATCH 1/2] Add the Everyday Work section with an Excel import guide A new section between the Tutorial and the Cookbook for whole jobs from real projects: each guide combines several cookbook chapters into one task and ends with an installable sample. The first guide imports an Excel file into a RAP business object: FileUploader, XCO XLSX, a preview with a status per row, one MODIFY ENTITIES for all rows with the row number as %cid, and every refusal from the CREATE or the COMMIT mapped back to its row. Backed by Z2UI5_CL_SMPS_APP_028 in abap2UI5/samples-stack. check-examples skips fences using XCO, a fluent API of its own like ajson. The Spreadsheet chapter links the guide; the controls reference is regenerated for the new page using z2ui5.cc.FileUploader. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014YZFmDjwUy369k1iZKoBT5 --- docs/.vitepress/config.mjs | 14 + .../device_capabilities/spreadsheet.md | 4 + docs/everyday/excel_import_rap.md | 253 ++++++++++++++++++ docs/everyday/index.md | 31 +++ docs/resources/controls.md | 2 +- scripts/check-examples.mjs | 5 + 6 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 docs/everyday/excel_import_rap.md create mode 100644 docs/everyday/index.md diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index c9ae8dd6..79790505 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -167,6 +167,7 @@ export default defineConfig({ nav: [ { text: "Guide", link: "/get_started/about", activeMatch: "^/(get_started|configuration|advanced)/" }, { text: "Tutorial", link: "/tutorials/", activeMatch: "^/tutorials/" }, + { text: "Everyday Work", link: "/everyday/", activeMatch: "^/everyday/" }, { text: "Cookbook", link: "/cookbook/", activeMatch: "^/cookbook/" }, { text: "API", link: "/resources/api", activeMatch: "^/resources/" }, { @@ -227,6 +228,19 @@ export default defineConfig({ { text: "12. Unit Tests", link: "/tutorials/step-12" }, ], }, + { + // Between the Tutorial and the Cookbook: a tutorial teaches one + // concept per step, a cookbook chapter answers one question - a guide + // here is a whole job from a real project, built from several + // chapters and backed by an installable sample. The row opens the + // index, which says what the section is and lists the guides. + text: "Everyday Work", + link: "/everyday/", + collapsed: true, + items: [ + { text: "Excel Import into RAP", link: "/everyday/excel_import_rap" }, + ], + }, { // An index page again, for DISCOVERY rather than lookup. It was // taken out once, with reason: a reader looking a recipe up has this diff --git a/docs/cookbook/device_capabilities/spreadsheet.md b/docs/cookbook/device_capabilities/spreadsheet.md index 4e862c9e..4fa70e37 100644 --- a/docs/cookbook/device_capabilities/spreadsheet.md +++ b/docs/cookbook/device_capabilities/spreadsheet.md @@ -208,6 +208,10 @@ ENDCLASS. ``` ::: +::: tip **Into a business object** +To create the uploaded rows in a RAP business object - with a preview, one `MODIFY ENTITIES` for all rows and every refusal shown on its row - see the guide [Excel Import into RAP](/everyday/excel_import_rap). +::: + ::: tip **ABAP Cloud** The code snippets above aren't ABAP Cloud compatible. To make them cloud-compatible, replace the code in the `lcl_help` class with functions from the `XCO_CP_XLSX` APIs. ::: diff --git a/docs/everyday/excel_import_rap.md b/docs/everyday/excel_import_rap.md new file mode 100644 index 00000000..718db3ad --- /dev/null +++ b/docs/everyday/excel_import_rap.md @@ -0,0 +1,253 @@ +--- +outline: [2, 4] +samples: + - z2ui5_cl_smps_app_028 +description: Import an Excel file into a RAP business object - upload with the FileUploader, read with XCO XLSX, check and preview every row, create all rows with one MODIFY ENTITIES and show each refusal on the row it belongs to. +--- +# Excel Import into RAP + +A user has a spreadsheet with a few hundred rows and wants them in the system. +The rows belong in a RAP business object, so its validations and +determinations must run for every row exactly as they do for a row typed in by +hand. This guide builds that import as one abap2UI5 app: no OData service, no +action parameter, no frontend project. The business object stays as it is. + +The finished app is `Z2UI5_CL_SMPS_APP_028` in +[abap2UI5/samples-stack](https://github.com/abap2UI5/samples-stack) (package +`13`). It imports travels into the business object `Z2UI5_R_SMPS_TRV` that +ships with package `03`, and the snippets below are taken from it. + +## The Shape of the App + +Three roundtrips, each with one job: + +| Roundtrip | What happens | The business object | +|---|---|---| +| **Upload** | The file arrives, XCO reads the first worksheet, every cell is converted into the type of its field | not touched | +| **Preview** | The table shows every row with a status: ready, or what is wrong with it | not touched | +| **Import** | One `MODIFY ENTITIES` creates all ready rows, `COMMIT ENTITIES` saves them, every answer goes back to its row | does all the deciding | + +Splitting upload and import is the important decision. A file with a date in +the wrong format should be fixed before anything is created, and a user who +sees the rows first notices a wrong column at a glance. The business object +still has the last word: the preview only checks what the app can check on +its own. + +## Step 1: Bring the File to the Backend + +The `z2ui5:FileUploader` custom control reads the file in the browser and +sends it with the next roundtrip as a base64 data URL: + +```abap +page->tag( n = `FileUploader` ns = `z2ui5` + )->a( n = `fileType` v = `xlsx` + )->a( n = `upload` v = client->_event( `UPLOAD` ) + )->a( n = `path` v = client->_bind( file_name ) + )->a( n = `value` v = client->_bind( file ) ). +``` + +Everything after the comma is the file. Decode it, and clear the attribute +right away: the app keeps its attributes between roundtrips, and the file +would travel along with every one of them. + +```abap +DATA(xlsx) = cl_web_http_utility=>decode_x_base64( substring_after( val = file sub = `,` ) ). +CLEAR file. +``` + +[Upload, Download](/cookbook/device_capabilities/upload_download) has every +property of the control. + +## Step 2: Read the Worksheet + +The XCO XLSX library is released for ABAP Cloud and available on-premise from +2023. It maps a range of cells onto an internal table; with the string value +transformation every cell arrives as text, so one row type serves any sheet: + +```abap +DATA(worksheet) = xco_cp_xlsx=>document->for_file_content( xlsx + )->read_access( )->get_workbook( )->worksheet->at_position( 1 ). + +DATA(pattern) = xco_cp_xlsx_selection=>pattern_builder->simple_from_to( + )->from_column( xco_cp_xlsx=>coordinate->for_alphabetic_value( `A` ) + )->to_column( xco_cp_xlsx=>coordinate->for_alphabetic_value( `F` ) + )->from_row( xco_cp_xlsx=>coordinate->for_numeric_value( 1 ) + )->get_pattern( ). + +worksheet->select( pattern )->row_stream( + )->operation->write_to( REF #( t_cells ) + )->set_value_transformation( xco_cp_xlsx_read_access=>value_transformation->string_value + )->execute( ). +``` + +`t_cells` is a table with one string component per column, `a` to `f`. A file +that is no `.xlsx` at all raises an exception here; catch it and show its text +instead of ending the roundtrip with a dump. On a release without XCO, the +[Spreadsheet](/cookbook/device_capabilities/spreadsheet) chapter shows the +same step with `cl_fdt_xl_spreadsheet`. + +## Step 3: Convert and Check Every Row + +Row 1 is the header. For every row below it the app converts each cell into +the type of its field and collects what does not fit: + +- **Numbers with leading zeros** - an ID column formatted as a number arrives + as `70001`, not `070001`. Check the digits, and let the assignment to the + `NUMC` field add the zeros. +- **Dates** - a date cell is stored as the number of days since 1899-12-30, a + cell formatted as text arrives as it was typed. Accept both, and turn the + first into a date with `CONV d( '18991230' ) + days`. A date such as + 2026-02-30 has the right shape and is still invalid; it converts to the day + number 0, which is the test for it. +- **Amounts** - assign the text to the amount field inside `TRY` and catch + `cx_sy_conversion_error`, so `20,00` becomes a message on the row and not a + dump. + +What the app does **not** check is anything the business object decides: here, +that a customer is given and that the end date is not before the begin date. +Checking it twice means two places to keep in step, and the business object's +answer is the one that counts. + +The row type of the preview carries the original cells, a `state` and a +`status` text. The state is a `sap.ui.core.ValueState` name, so an +`ObjectStatus` shows it in the right color: + +```abap +)->tag( `ObjectStatus` + )->a( n = `text` v = `{STATUS}` + )->a( n = `state` v = `{STATE}` +``` + +## Step 4: Create All Rows at Once + +One `MODIFY ENTITIES` with every ready row, not one statement per row. The +`%cid` is chosen by the caller, and the sample uses the Excel row number for +it: + +```abap +MODIFY ENTITIES OF z2ui5_r_smps_trv + ENTITY travel + CREATE FIELDS ( agencyid customerid begindate enddate bookingfee currencycode description ) + WITH VALUE #( FOR t IN t_travel + ( %cid = |ROW_{ t-row }| + agencyid = t-agency_id + customerid = t-customer_id + begindate = t-begin_date + enddate = t-end_date + bookingfee = t-booking_fee + currencycode = t-currency + description = |{ file_name }, row { t-row }| ) ) + MAPPED DATA(s_mapped) + FAILED DATA(s_failed) + REPORTED DATA(s_reported). +``` + +The `%cid` is the one thing the business object hands back with an instance +before the instance has a key. Built from the row number, it leads every +answer straight back to its row. A refusal of the CREATE itself, such as a +missing authorization, carries the `%cid` in `FAILED` and its messages in +`REPORTED`: + +```abap +LOOP AT s_failed-travel INTO DATA(failed). + row = substring_after( val = failed-%cid sub = `ROW_` ). + texts = VALUE #( FOR r IN s_reported-travel WHERE ( %cid = failed-%cid AND %msg IS BOUND ) + ( r-%msg->if_message~get_text( ) ) ). + " ... the texts go into the status of that row +ENDLOOP. +``` + +## Step 5: Save, and Map the Answers Back + +Validations run at `COMMIT ENTITIES`, not at the CREATE, so a CREATE that +came back clean can still be refused here: + +```abap +COMMIT ENTITIES RESPONSE OF z2ui5_r_smps_trv + FAILED DATA(s_failed_commit) + REPORTED DATA(s_reported_commit). +``` + +The answers of the save carry the key, not the `%cid`. `MAPPED` from the +CREATE has both, so it translates one into the other: + +```abap +LOOP AT s_failed_commit-travel INTO DATA(failed_commit). + DATA(mapped) = VALUE #( s_mapped-travel[ travelid = failed_commit-travelid ] OPTIONAL ). + row = substring_after( val = mapped-%cid sub = `ROW_` ). + " ... the messages of s_reported_commit with the same key go to that row +ENDLOOP. +``` + +That works because the travel business object uses early numbering: the key +exists from the CREATE on. With late numbering the final key only exists after +the save; map by the preliminary `%pid` there, which `MAPPED` carries next to +the `%cid`. + +After a refused save, call `ROLLBACK ENTITIES`. The instances stay in the +transactional buffer otherwise, and the next EML statement - or the +`COMMIT WORK` abap2UI5 runs at the end of the roundtrip - runs into them. + +## All or Nothing, or Row by Row + +A COMMIT saves everything or nothing: one refused row, and none of the others +is saved either. Which of the two behaviors is right depends on the data. + +- **All or nothing** fits rows that belong together: the lines of one price + list, the positions of one order. The user fixes the file and uploads it + again. The sample marks the refused rows as errors and the others as "not + saved". +- **Row by row** fits rows that stand alone, like the travels here. The sample + offers it as *Skip refused rows*: after a refused save it rolls back, drops + the refused rows and runs the same CREATE once more with the rest. One retry + is enough, because the same rows give the same answers. + +Rows that already have a key are left out of the next import, so pressing +Import twice does not create them twice. + +## Limits and Variants + +- **File size.** The file travels base64-encoded inside the JSON of one + roundtrip, about a third larger than the file, and sits in the memory of one + work process. A few thousand rows are no problem; for files the ICF request + limit refuses, upload in parts or let a background job do the import. +- **Many rows.** For tens of thousands of rows, create in packages of a few + hundred per COMMIT, so one refused row does not cost the whole file and the + transactional buffer stays small. +- **Draft instead of active.** If the business object is draft-enabled, create + the instances with `%is_draft = if_abap_behv=>mk-on` and let the user check + and activate them in the app that already edits that business object. See + [Draft Handling](/cookbook/eml_cds_sql/draft_handling). +- **A template for the user.** Offer the empty sheet as a download, written + with XCO the same way it is read, so the columns are always in the order the + import expects. The sample's *Download Example File* does that, with one row + the business object refuses on purpose. +- **A file inside the business object.** When the file itself has to be + stored, not its rows, the business object takes it as an action parameter - + package `11` of samples-stack shows that route. + +## Related + +- [Upload, Download](/cookbook/device_capabilities/upload_download) - the + FileUploader and the download action. +- [Spreadsheet](/cookbook/device_capabilities/spreadsheet) - XLSX in and out + without RAP. +- [EML](/cookbook/eml_cds_sql/eml) and [RAP](/cookbook/eml_cds_sql/rap) - the + statements one by one. +- [Message](/cookbook/translation_messages/message) - message boxes, toasts + and message lists. + + + +## Working Samples + +Complete apps from the [sample catalog](https://abap2ui5.github.io/samples-site/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. + +| Sample | Class | +|---|---| +| Excel — Import Travels into a RAP Business Object — An .xlsx file of travels checked row by row and created in the RAP business object with one MODIFY ENTITIES - every refusal lands on the row it belongs to | [`Z2UI5_CL_SMPS_APP_028`](https://github.com/abap2UI5/samples-stack/blob/main/src/13/z2ui5_cl_smps_app_028.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `13-excel-email` | + + diff --git a/docs/everyday/index.md b/docs/everyday/index.md new file mode 100644 index 00000000..a8b19c0e --- /dev/null +++ b/docs/everyday/index.md @@ -0,0 +1,31 @@ +--- +outline: [2, 4] +description: Guides for the tasks that come up in real projects - each one a whole job from the first click to the saved data, built from the cookbook's pieces and backed by a sample you can install. +--- +# Everyday Work + +The [Tutorial](/tutorials/) teaches the framework one concept at a time, and +the [Cookbook](/cookbook/) answers one question per chapter: how to bind a +table, how to open a popup, how to read a file. Real work is rarely one of +those on its own. An import, a mass change or an approval screen needs half a +dozen of them, in the right order, against a business object that has its own +rules. + +The guides here take one such job and walk through it from start to finish: +what the user sees, which roundtrip does what, where the business logic stays, +and what goes wrong in practice. Each guide links the cookbook chapters it is +built from and ends with a sample you can pull with abapGit and run. + +## Guides + +| Guide | The job | Needs | +|---|---|---| +| [Excel Import into RAP](/everyday/excel_import_rap) | Upload an `.xlsx` file, check every row, create the rows in a RAP business object and show each refusal on its row | XCO XLSX (ABAP Cloud, or on-premise 2023 and later), a RAP business object | + +## What makes a good guide + +A guide belongs here when the job is common, takes more than one cookbook +chapter, and has a decision in it that a reader would otherwise get wrong the +first time - all or nothing versus row by row, a draft versus an active +instance, where a check belongs. Suggestions are welcome in the +[discussions](https://github.com/abap2UI5/abap2UI5/discussions). diff --git a/docs/resources/controls.md b/docs/resources/controls.md index a1ca4d7f..fc1b9773 100644 --- a/docs/resources/controls.md +++ b/docs/resources/controls.md @@ -121,7 +121,7 @@ Used on [Microsoft Excel](/integration/excel). Module `z2ui5.cc.FileUploader` ([source](https://github.com/abap2UI5/abap2UI5/blob/main/app/webapp/cc/FileUploader.js)). Extends `sap.ui.core.Control`. -Used on [Spreadsheet](/cookbook/device_capabilities/spreadsheet), [Upload, Download](/cookbook/device_capabilities/upload_download). +Used on [Spreadsheet](/cookbook/device_capabilities/spreadsheet), [Upload, Download](/cookbook/device_capabilities/upload_download), [Excel Import into RAP](/everyday/excel_import_rap). | Property | Type | Default | |---|---|---| diff --git a/scripts/check-examples.mjs b/scripts/check-examples.mjs index d214c3a9..5e72c4b1 100644 --- a/scripts/check-examples.mjs +++ b/scripts/check-examples.mjs @@ -161,6 +161,11 @@ function legacyFragments() { * `list( )->write( )`, which are its verbs, not the view builder's * (advanced/report_migration shows a converted report). */ if (/z2ui5_cl_cgui_/i.test(code)) continue; + /* And SAP's XCO library, which is fluent throughout: an XLSX read is + * `xco_cp_xlsx=>document->for_file_content( )->read_access( )->…` + * (everyday/excel_import_rap). Its verbs are its own, and a fence that + * reads a worksheet builds no view. */ + if (/\bxco_cp_/i.test(code)) continue; /* mid-chain: `)->input( )` */ for (const call of code.matchAll(/\)->([a-z_][a-z0-9_]*)\(/gi)) { const verb = call[1].toLowerCase(); From c3af2d959a6162e3b7da4a45d5632fdd358b82a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:42:42 +0000 Subject: [PATCH 2/2] Add four more Everyday Work guides - Mass Change: select rows, describe the change once, preview old and new values, one MODIFY ... UPDATE, refusals by key on their row - Approval Worklist: only what waits for a decision, several items decided with one EXECUTE of the business object's action, RESULT for the confirmation, the list shrinking on the next read - Report and Export: the selection as range tables, one SELECT on the CDS view entity, a total per currency, the rows on the screen as .xlsx - Concurrent Editing: optimistic locking in a stateless app with the business object's ETag - UPDATE (takes the lock), check against the database, COMMIT - and where the other strategies of Lock fit better Each guide is backed by a sample in abap2UI5/samples-stack (Z2UI5_CL_SMPS_APP_029 to 032). The section index lists all five. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014YZFmDjwUy369k1iZKoBT5 --- docs/.vitepress/config.mjs | 4 + docs/everyday/approval_worklist.md | 138 ++++++++++++++++++++++ docs/everyday/concurrent_editing.md | 161 +++++++++++++++++++++++++ docs/everyday/index.md | 4 + docs/everyday/mass_change.md | 175 ++++++++++++++++++++++++++++ docs/everyday/report_export.md | 156 +++++++++++++++++++++++++ 6 files changed, 638 insertions(+) create mode 100644 docs/everyday/approval_worklist.md create mode 100644 docs/everyday/concurrent_editing.md create mode 100644 docs/everyday/mass_change.md create mode 100644 docs/everyday/report_export.md diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index 79790505..0c9c8ed4 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -239,6 +239,10 @@ export default defineConfig({ collapsed: true, items: [ { text: "Excel Import into RAP", link: "/everyday/excel_import_rap" }, + { text: "Mass Change", link: "/everyday/mass_change" }, + { text: "Approval Worklist", link: "/everyday/approval_worklist" }, + { text: "Report and Export", link: "/everyday/report_export" }, + { text: "Concurrent Editing", link: "/everyday/concurrent_editing" }, ], }, { diff --git a/docs/everyday/approval_worklist.md b/docs/everyday/approval_worklist.md new file mode 100644 index 00000000..f97372b4 --- /dev/null +++ b/docs/everyday/approval_worklist.md @@ -0,0 +1,138 @@ +--- +outline: [2, 4] +samples: + - z2ui5_cl_smps_app_030 +description: An approval worklist on a RAP business object - the open items, several selected and accepted or rejected with one EXECUTE of the business object's action, and the list shrinking as decisions are made. +--- +# Approval Worklist + +Somebody has to decide: accept or reject the travel requests, release the +purchase orders, confirm the time sheets. The screen for it is not a +maintenance app. It shows only what is waiting, lets the user decide one or +many at once, and gets shorter with every decision. When it is empty, the +work is done. + +The worked example is `Z2UI5_CL_SMPS_APP_030` in +[abap2UI5/samples-stack](https://github.com/abap2UI5/samples-stack) (package +`03`). The travel business object `Z2UI5_R_SMPS_TRV` has two actions, +`acceptTravel` and `rejectTravel`, which set the status. + +## The Shape of the App + +| Part | What it does | +|---|---| +| **The list** | The open travels only, oldest first. A `SegmentedButton` switches to the decided ones, so a decision can be looked up | +| **The decision** | Accept and Reject in the table's toolbar, for every selected row at once | +| **The action** | One `EXECUTE` of the business object's action for all selected keys, then `COMMIT ENTITIES` | +| **After** | A fresh read; the decided rows are gone from the list | + +## Step 1: Show What Is Waiting + +The filter belongs in the `SELECT`. A worklist that reads everything and hides +the decided rows on the screen grows with every decision ever made; one that +selects only the open ones stays as short as the work is: + +```abap +DATA(t_status) = VALUE ty_r_status( ( sign = `I` + option = COND #( WHEN filter = `OPEN` THEN `EQ` ELSE `NE` ) + low = z2ui5_cl_smps_context=>cs_status-open ) ). + +SELECT FROM z2ui5_r_smps_trv + FIELDS TravelId, CustomerId, BeginDate, EndDate, TotalPrice, CurrencyCode, Description, OverallStatus + WHERE OverallStatus IN @t_status + ORDER BY BeginDate, TravelId + INTO TABLE @DATA(t_result) UP TO 100 ROWS. +``` + +Sort by what makes an item urgent - here the begin date of the trip. The +oldest request at the top is the one the user should look at first. + +## Step 2: Offer the Decision Where It Applies + +The table runs in `MultiSelect` mode only on the worklist, and the buttons are +visible only there. On the list of decided travels there is nothing to decide: + +```abap +DATA(table) = page->ele( `Table` + )->a( n = `items` v = client->_bind( t_travels ) + )->a( n = `mode` v = COND #( WHEN filter = `OPEN` THEN `MultiSelect` ELSE `None` ) ). +``` + +The button types `Accept` and `Reject` give the two buttons their green and +red, which every Fiori user reads without a label. + +## Step 3: Call the Action + +The action is called like any other operation of the business object: by +key, for many instances at once. The app never writes `OverallStatus` itself; +the behavior definition declares it `readonly`, and the action is the only way +to change it: + +```abap +MODIFY ENTITIES OF z2ui5_r_smps_trv + ENTITY travel + EXECUTE acceptTravel FROM VALUE #( FOR k IN t_keys ( travelid = k ) ) + RESULT DATA(t_result) + FAILED DATA(s_failed) + REPORTED DATA(s_reported). +``` + +Write two statements, one per action, rather than a dynamic action name. The +action is part of the statement, so the compiler checks that it exists and +that its parameter fits. + +The action is declared with `result [1] $self`, so `RESULT` hands back the +changed instances. The confirmation can then say what the business object +did - the new status comes from `%param` - instead of what the app expected +it to do. + +Then `COMMIT ENTITIES RESPONSE OF`, and on any `FAILED` a +`ROLLBACK ENTITIES` with the messages of `REPORTED` in a message box. One +decision for twenty travels is one transaction: if the business object +refuses one of them, none of them is decided, and the user sees why. + +## Step 4: Read Again + +After the save, read the list again. The decided travels leave it because +the `SELECT` no longer finds them, not because the app removed rows from its +table. That way the list shows what the database says, including decisions +someone else made in the meantime. + +## Variants + +- **A reason for a rejection.** An action can take a parameter. The business + object declares it with an abstract entity, the app asks for it in a popup + and passes it as `%param`. `Z2UI5_CL_SMPS_APP_018` in package `12` of + samples-stack does exactly that for a reject. +- **Only what the user may decide.** With instance feature control, the + business object says per item whether the action is enabled. + `GET PERMISSIONS` asks it before the buttons are offered; + `Z2UI5_CL_SMPS_APP_017` in package `12` shows it. +- **One item at a time.** For decisions that need a closer look, put the + buttons on a detail page instead, and navigate back to the shortened list + after each decision - see [Inner App](/cookbook/event_navigation/navigation/inner_app) + navigation. +- **In the launchpad.** A worklist is a typical tile. The number of open items + can be the tile's KPI - see [Launchpad](/configuration/launchpad). + +## Related + +- [Tables](/cookbook/model/tables) - selection and toolbars. +- [EML](/cookbook/eml_cds_sql/eml) - `EXECUTE` and the responses. +- [Message](/cookbook/translation_messages/message) - toasts and message + boxes. + + + +## Working Samples + +Complete apps from the [sample catalog](https://abap2ui5.github.io/samples-site/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. + +| Sample | Class | +|---|---| +| Approval Worklist — An approval worklist - the open travels, several selected and accepted or rejected with one EXECUTE, the list shrinking as they are decided | [`Z2UI5_CL_SMPS_APP_030`](https://github.com/abap2UI5/samples-stack/blob/main/src/03/z2ui5_cl_smps_app_030.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `03-rap` | + + diff --git a/docs/everyday/concurrent_editing.md b/docs/everyday/concurrent_editing.md new file mode 100644 index 00000000..1bb4a190 --- /dev/null +++ b/docs/everyday/concurrent_editing.md @@ -0,0 +1,161 @@ +--- +outline: [2, 4] +samples: + - z2ui5_cl_smps_app_032 +description: Two users edit the same RAP instance - optimistic locking in a stateless abap2UI5 app, with LastChangedAt remembered on read and compared after the UPDATE has taken the business object's lock. +--- +# Concurrent Editing + +Two users open the same travel. The first changes the fee and saves; the +second changes the description a minute later and saves too. Without a check, +the second save writes the fee the second user saw when the popup opened, and +the first change is gone without anyone noticing. + +In SAP GUI the answer was an enqueue lock held while the screen was open. An +abap2UI5 app holds no session between two roundtrips, so a lock set when the +popup opens is released before the user types the first character. The +answer here is an optimistic check: remember the version of the data when +reading, and refuse the save if it changed. + +The worked example is `Z2UI5_CL_SMPS_APP_032` in +[abap2UI5/samples-stack](https://github.com/abap2UI5/samples-stack) (package +`03`), on the travel business object `Z2UI5_R_SMPS_TRV`. Its edit popup has a +*Change by Someone Else* button that plays the second user. + +## The Version of an Instance + +A RAP business object usually has it already. The behavior definition of the +travel names its ETag: + +```abap +define behavior for z2ui5_r_smps_trv alias Travel +... +etag master LastChangedAt +``` + +`LastChangedAt` carries the annotation +`@Semantics.systemDateTime.localInstanceLastChangedAt`, so the managed +runtime sets it on every save, whoever saves - this app, +another app, a Fiori elements UI, a background job going through the business +object. A Fiori elements app compares it automatically through the OData +ETag. An app that consumes the business object with EML compares it itself, +in two places. + +## Step 1: Remember the Version When Reading + +Read the instance fresh when the user opens it, not from the list on the +screen, and keep `LastChangedAt` with the values the user is about to edit: + +```abap +READ ENTITIES OF z2ui5_r_smps_trv + ENTITY travel + FIELDS ( bookingfee currencycode description lastchangedat ) + WITH VALUE #( ( travelid = travel_id ) ) + RESULT DATA(t_result) + FAILED DATA(s_failed). + +s_edit = VALUE #( ... + last_changed_at = t_result[ 1 ]-lastchangedat ). +``` + +The attribute does not have to be bound to anything. abap2UI5 keeps the app +instance between roundtrips, so it is still there when the user presses Save. + +## Step 2: Lock, Then Check, Then Save + +The order of the three statements is the whole technique: + +```abap +" 1. the UPDATE - the managed runtime takes the lock of the instance here +MODIFY ENTITIES OF z2ui5_r_smps_trv + ENTITY travel + UPDATE FIELDS ( bookingfee currencycode description ) + WITH VALUE #( ( travelid = travel_id ... ) ) + FAILED DATA(s_failed) + REPORTED DATA(s_reported). + +" 2. the check - against the database, where this change is not written yet +SELECT SINGLE FROM z2ui5_r_smps_trv + FIELDS LastChangedAt + WHERE TravelId = @travel_id + INTO @DATA(current). + +IF current <> s_edit-last_changed_at. + ROLLBACK ENTITIES. " drops the change and releases the lock + " ... tell the user, read the current state + RETURN. +ENDIF. + +" 3. the save +COMMIT ENTITIES RESPONSE OF z2ui5_r_smps_trv + FAILED DATA(s_failed_commit) + REPORTED DATA(s_reported_commit). +``` + +Why this order and not check first, then update: + +- **The UPDATE takes the lock.** The travel is its own lock master, so the + runtime locks the instance when it processes the UPDATE, and holds the lock + until the COMMIT or the ROLLBACK. If somebody else is saving the same travel + at this moment, the UPDATE fails right here, with a message in `REPORTED`. +- **The check reads the database.** The UPDATE lives in the transactional + buffer until the COMMIT, so a `SELECT` still sees the last saved version. + A `READ ENTITIES` would read the buffer, where the change of this roundtrip + is already applied. +- **Between the check and the COMMIT, nobody else can save** the travel, + because the lock is held. A check before the UPDATE would leave a gap in + which a second save could slip through. + +## Step 3: Tell the User + +A refused save is not an error of the user. Say what happened and what to do +next, close the popup and show the current data, so the next attempt starts +from what is really saved: + +```abap +client->popup_destroy( ). +data_read( ). +view_display( ). +client->message_box_display( text = |Travel { s_edit-travel_id } was changed by someone else after you opened it. | && + `Your change was not saved - open the travel again to see the current values.` + type = `warning` ). +``` + +Do not offer "save anyway". Overwriting a change the user has not seen is +exactly what the check exists to prevent. + +## When Another Strategy Fits Better + +| Situation | Strategy | +|---|---| +| Many users, short edits, conflicts are rare | This one: no lock while typing, a check at save | +| The user must see "locked by X" when opening | A soft lock, or a stateful session with an enqueue - both in [Lock](/cookbook/expert_more/lock) | +| Long edits that may be interrupted, a draft-enabled business object | RAP drafts: the draft holds an exclusive lock and survives the session - see [Draft Handling](/cookbook/eml_cds_sql/draft_handling) | +| No business object, a plain table | The same check on a timestamp column of the table - [Lock](/cookbook/expert_more/lock#_3-optimistic-check) | + +[Lock](/cookbook/expert_more/lock) compares eight strategies side by side; +this guide is its "combined" strategy for an instance of a RAP business +object, where the business object brings both the lock and the version. + +## Related + +- [Statefulness](/cookbook/expert_more/statefulness) - why nothing survives a + roundtrip except the app instance. +- [EML](/cookbook/eml_cds_sql/eml) - `READ ENTITIES`, `MODIFY ENTITIES` and + the responses. +- [Popup](/cookbook/popup_popover/popup) - the edit dialog. + + + +## Working Samples + +Complete apps from the [sample catalog](https://abap2ui5.github.io/samples-site/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. + +| Sample | Class | +|---|---| +| Edit with Conflict Check — Edit a travel without a stateful session - the app remembers LastChangedAt when it reads, and Save refuses the change if someone else saved the travel in between | [`Z2UI5_CL_SMPS_APP_032`](https://github.com/abap2UI5/samples-stack/blob/main/src/03/z2ui5_cl_smps_app_032.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `03-rap` | + + diff --git a/docs/everyday/index.md b/docs/everyday/index.md index a8b19c0e..82e06357 100644 --- a/docs/everyday/index.md +++ b/docs/everyday/index.md @@ -21,6 +21,10 @@ built from and ends with a sample you can pull with abapGit and run. | Guide | The job | Needs | |---|---|---| | [Excel Import into RAP](/everyday/excel_import_rap) | Upload an `.xlsx` file, check every row, create the rows in a RAP business object and show each refusal on its row | XCO XLSX (ABAP Cloud, or on-premise 2023 and later), a RAP business object | +| [Mass Change](/everyday/mass_change) | Select many rows, describe the change once, preview old and new values, save all with one `UPDATE` | a RAP business object (1909 and later) | +| [Approval Worklist](/everyday/approval_worklist) | Show what waits for a decision, accept or reject several items at once through the business object's actions | a RAP business object with actions | +| [Report and Export](/everyday/report_export) | A selection as range tables, one `SELECT`, a total, and the rows on the screen as an `.xlsx` download | XCO XLSX for the export | +| [Concurrent Editing](/everyday/concurrent_editing) | Let two users edit the same instance without a lock while typing, and refuse a save over a change the user has not seen | a RAP business object with an ETag field | ## What makes a good guide diff --git a/docs/everyday/mass_change.md b/docs/everyday/mass_change.md new file mode 100644 index 00000000..fe51c608 --- /dev/null +++ b/docs/everyday/mass_change.md @@ -0,0 +1,175 @@ +--- +outline: [2, 4] +samples: + - z2ui5_cl_smps_app_029 +description: Change many instances of a RAP business object at once - select the rows, describe the change once, preview old and new values, save with one MODIFY ENTITIES ... UPDATE and show each refusal on its row. +--- +# Mass Change + +Fifty travels need a new booking fee, or all trips of one agency move by a +week. Opening each one is not an option, and a database `UPDATE` would skip +the business object's validations. The job is a screen where the user selects +the rows, says once what should change, sees the result and then saves it all +through the business object. + +The worked example is `Z2UI5_CL_SMPS_APP_029` in +[abap2UI5/samples-stack](https://github.com/abap2UI5/samples-stack) (package +`03`), on the travel business object `Z2UI5_R_SMPS_TRV`. + +## The Shape of the App + +| Roundtrip | What happens | +|---|---| +| **Select** | A table in `MultiSelect` mode; the selection travels back in a bound field of each row | +| **Preview** | The app computes the new values for every selected row and shows them next to the old ones. The business object is not touched | +| **Apply** | One `MODIFY ENTITIES ... UPDATE` for all previewed rows, then `COMMIT ENTITIES`. Every refusal goes back to its row | + +The preview is the part users ask for. A change described as "move the begin +date by 30 days" is easy to get wrong, and the table that shows `2026-12-01` +under `2026-11-01` for every row makes the mistake visible before it is saved. + +## Step 1: Let the User Select Rows + +Give the row structure a flag and bind the table's `selected` property to it. +The binding is two-way, so after the next roundtrip the flag says which rows +the user selected: + +```abap +DATA(table) = page->ele( `Table` + )->a( n = `items` v = client->_bind( t_travels ) + )->a( n = `mode` v = `MultiSelect` ). + +table->ele( `items` + )->ele( `ColumnListItem` + )->a( n = `selected` v = `{SELKZ}` +``` + +The row structure also carries the new values and a status, so the preview +needs no second table: `new_begin_date`, `new_end_date`, `new_fee`, a `state` +for the color and a `status` text. + +## Step 2: Describe the Change Once + +The form above the table holds the change, not a value per row: a new fee, +and a number of days to move each date by. Convert it **before** looping over +the rows. A fee that is no number is then one message, not fifty: + +```abap +TRY. + IF s_change-booking_fee IS NOT INITIAL. + fee = s_change-booking_fee. + ENDIF. + begin_shift = s_change-begin_shift. + end_shift = s_change-end_shift. + CATCH cx_sy_conversion_error. + client->message_box_display( text = `The fee must be a number, the shifts whole days` type = `error` ). + RETURN. +ENDTRY. +``` + +An empty field means "keep the value". That rule belongs in the form's +placeholder text, so the user does not have to guess it. + +## Step 3: Preview + +For every selected row, compute the new values and write them into the row. +In the table, an `ObjectIdentifier` shows the new value as its title and the +old one below it, which reads as "before and after" without two columns per +field: + +```abap +)->tag( `ObjectIdentifier` + )->a( n = `title` v = `{NEW_BEGIN_DATE}` + )->a( n = `text` v = `{BEGIN_DATE}` +``` + +Keep the preview as the contract: Apply saves what the preview shows, not +what the form says now. The sample remembers that a preview exists and only +enables Apply after one. + +## Step 4: Save All Rows at Once + +One statement with every row. An UPDATE names its instances by key, so there +is no `%cid` to invent; the key is what every answer carries back: + +```abap +MODIFY ENTITIES OF z2ui5_r_smps_trv + ENTITY travel + UPDATE FIELDS ( bookingfee begindate enddate ) + WITH VALUE #( FOR u IN t_update + ( travelid = u-travel_id + bookingfee = u-booking_fee + begindate = u-begin_date + enddate = u-end_date ) ) + FAILED DATA(s_failed) + REPORTED DATA(s_reported). +``` + +`UPDATE FIELDS` lists exactly the fields that change. A field that is not +listed keeps its value even if the structure carries one, which is what makes +a mass change safe: the description of fifty travels is not overwritten with +an empty string. + +Then `COMMIT ENTITIES RESPONSE OF`, where the validations run. For both +answers, the refused keys are in `FAILED` and their messages in `REPORTED` +under the same key: + +```abap +LOOP AT s_failed_commit-travel INTO DATA(failed_commit). + DATA(texts) = VALUE string_table( FOR r IN s_reported_commit-travel + WHERE ( travelid = failed_commit-travelid AND %msg IS BOUND ) + ( r-%msg->if_message~get_text( ) ) ). + " ... the texts go into the status of the row with this key +ENDLOOP. +``` + +In the sample, moving only the begin date by 30 days pushes some trips past +their end date. `validateDates` refuses those rows at the COMMIT, and the +preview table shows the reason on each of them. + +## Step 5: After a Refusal + +A COMMIT saves all rows or none. After a refusal, call `ROLLBACK ENTITIES` so +the changes leave the transactional buffer, mark the refused rows as errors +and the others as "not saved", and keep the preview on screen. The user can +deselect the refused rows, or change the description of the change, and press +Preview again. + +If the rows are independent and the user would rather save what can be saved, +use the retry from [Excel Import into RAP](/everyday/excel_import_rap#all-or-nothing-or-row-by-row): +roll back, drop the refused rows, send the rest again. + +## Limits and Variants + +- **Many rows.** Several hundred rows per COMMIT are fine. For thousands, + save in packages, and say in the status which package a row was in. +- **Only what the user may change.** If the business object has instance + feature control, ask it before offering the change: `GET PERMISSIONS` tells + per row whether an update is allowed, and the preview can mark the rows it + would not accept. `Z2UI5_CL_SMPS_APP_017` in package `12` of samples-stack + shows the statement. +- **Draft-enabled business objects.** A mass change usually goes to the + active instances. If a row has an open draft, the business object refuses + the update of the active one, and the message says so. + +## Related + +- [Tables](/cookbook/model/tables) - selection, columns and cells. +- [EML](/cookbook/eml_cds_sql/eml) - `MODIFY ... UPDATE` and the responses. +- [Excel Import into RAP](/everyday/excel_import_rap) - the same pattern for + rows that come from a file. + + + +## Working Samples + +Complete apps from the [sample catalog](https://abap2ui5.github.io/samples-site/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. + +| Sample | Class | +|---|---| +| Mass Change with Preview — Several travels changed in one go - the new values are previewed next to the old ones, one MODIFY ENTITIES ... UPDATE saves them, and a refusal is shown on its row | [`Z2UI5_CL_SMPS_APP_029`](https://github.com/abap2UI5/samples-stack/blob/main/src/03/z2ui5_cl_smps_app_029.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `03-rap` | + + diff --git a/docs/everyday/report_export.md b/docs/everyday/report_export.md new file mode 100644 index 00000000..b75c4132 --- /dev/null +++ b/docs/everyday/report_export.md @@ -0,0 +1,156 @@ +--- +outline: [2, 4] +samples: + - z2ui5_cl_smps_app_031 +description: The everyday report - a selection turned into range tables, one SELECT on a CDS view entity, a result table with a total per currency, and the rows on the screen exported as an Excel file with XCO. +--- +# Report and Export + +Most screens in a system are reports: a few fields to narrow the data down, a +table with the result, a total underneath, and a button that hands the same +rows to Excel. In classic ABAP that is a selection screen and an ALV. In +abap2UI5 it is one class with a form, a table and a download, and the +selection logic stays the same. + +The worked example is `Z2UI5_CL_SMPS_APP_031` in +[abap2UI5/samples-stack](https://github.com/abap2UI5/samples-stack) (package +`13`). It reports on the travels of the business object `Z2UI5_R_SMPS_TRV` +and exports with the XCO XLSX library. + +## The Shape of the App + +| Part | What it is | +|---|---| +| **Selection** | A `SimpleForm`: agency, customer, a window for the begin date, a row limit. Enter in a field searches | +| **Result** | A table, with the number of rows and the total per currency in its toolbar | +| **Export** | A button that writes the rows on the screen into an `.xlsx` file and downloads it | + +The report runs once on start, with an empty selection. A user who opens it +sees data right away and narrows it down from there, instead of facing an +empty table and a form. + +## Step 1: The Selection as Range Tables + +Bind each field of the form to a component of one structure. When the search +runs, turn each field into a range table: an empty field is an empty range, +and an empty range selects everything. That is the behavior of a selection +screen, without one `IF` per field in the `WHERE`: + +```abap +DATA(t_agency) = VALUE ty_r_agency( ). +IF s_selection-agency_id IS NOT INITIAL. + t_agency = VALUE #( ( sign = `I` option = `EQ` low = s_selection-agency_id ) ). +ENDIF. +``` + +A date window is a `BT` when both ends are given, a `GE` or `LE` when only one +is. Two `DatePicker` controls with `valueFormat` `yyyyMMdd` deliver the dates +in the internal format, so they go into the range unchanged. + +Check what the user typed before it reaches the `SELECT`. A letter in a `NUMC` +field is not an error in ABAP, it silently becomes zeros - and the report +shows nothing without saying why. + +## Step 2: One SELECT + +Read from the CDS view entity, not from the table. The view carries the +field names the application uses and whatever the data model adds on top - +associations, calculated fields, access control: + +```abap +SELECT FROM z2ui5_r_smps_trv + FIELDS TravelId, AgencyId, CustomerId, BeginDate, EndDate, TotalPrice, CurrencyCode, OverallStatus + WHERE AgencyId IN @t_agency + AND CustomerId IN @t_customer + AND BeginDate IN @t_begin + ORDER BY BeginDate, TravelId + INTO TABLE @DATA(t_result) UP TO @max_rows ROWS. +``` + +The row limit is part of the selection, as on a selection screen. When the +result reaches it, say so in the toolbar ("the first 200 - narrow the +selection"), so nobody takes a cut-off list for the whole answer. + +## Step 3: Show the Result + +The table's row type holds strings formatted for the screen: keys with +`ALPHA = OUT`, dates with `DATE = ISO`. The total is calculated from the +typed result of the `SELECT`, not from those strings, and once per currency - +a sum across currencies is a number without a meaning: + +```abap +LOOP AT t_result INTO DATA(result). + DATA(total) = VALUE ty_s_total( currency = result-currencycode amount = result-totalprice ). + COLLECT total INTO t_total. +ENDLOOP. +``` + +## Step 4: Export What the User Sees + +The export writes the rows that are on the screen, not the result of a second +`SELECT`. The file then shows exactly what the user looked at, even if +somebody changed the data in between: + +```abap +LOOP AT t_travels INTO DATA(travel). + APPEND VALUE #( a = travel-travel_id + b = travel-customer_id + ... + f = travel-status ) TO t_cells. +ENDLOOP. + +DATA(xlsx) = z2ui5_cl_smps_xlsx=>write( t_cells ). + +client->follow_up_action( + val = client->cs_event-download_b64_file + t_arg = VALUE #( ( |data:{ z2ui5_cl_smps_xlsx=>mime_type };base64,{ cl_web_http_utility=>encode_x_base64( xlsx ) }| ) + ( |travels_{ cl_abap_context_info=>get_system_date( ) }.xlsx| ) ) ). +``` + +The first row carries the column headers. A file name with the date tells +two exports of the same report apart in the user's download folder. + +`z2ui5_cl_smps_xlsx` is a small helper of the samples around the XCO XLSX +library; [Spreadsheet](/cookbook/device_capabilities/spreadsheet) shows the +same export with the on-premise APIs and with abap2xlsx. + +## Variants + +- **Export in the browser.** For a table that is on the screen anyway, the + browser can write the file itself, without a roundtrip - see + [Spreadsheet](/cookbook/device_capabilities/spreadsheet#export-in-the-browser). + The backend export is the right choice when the file needs more than the + screen shows, or must look the same for everybody. +- **Value help for the fields.** An agency is easier to pick than to + remember - see [Value Help](/cookbook/expert_more/value_help). +- **Multiple values per field.** A `MultiInput` with tokens maps onto the same + range table, one row per token, with `EQ` and `BT` as the user entered them. +- **Variants and filter bars.** For many fields, a `FilterBar` with variant + management saves the user's selections. Package `02` of samples-stack shows + the classic FilterBar, `Z2UI5_CL_SMPS_APP_493`. +- **Large results.** A report that regularly finds tens of thousands of rows + wants paging or an aggregation in the `SELECT`, not a longer table - see + [Size Limit](/cookbook/model/size_limit). + +## Related + +- [Tables](/cookbook/model/tables) - columns, cells and toolbars. +- [ABAP SQL](/cookbook/eml_cds_sql/abap_sql) and [CDS](/cookbook/eml_cds_sql/cds) - + reading the data. +- [Upload, Download](/cookbook/device_capabilities/upload_download) - the + download action. + + + +## Working Samples + +Complete apps from the [sample catalog](https://abap2ui5.github.io/samples-site/) +that use what this page describes. Each is a single class in [abap2UI5/samples](https://github.com/abap2UI5/samples) +unless its row names another of the three sample repositories — pull that repository with +[abapGit](https://abapgit.org) and start the class with `?app_start=`. + +| Sample | Class | +|---|---| +| Excel — Report with Selection and Export — A report as it is written every day - a selection with ranges, the result in a table with a total, and the same rows exported as .xlsx with XCO | [`Z2UI5_CL_SMPS_APP_031`](https://github.com/abap2UI5/samples-stack/blob/main/src/13/z2ui5_cl_smps_app_031.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `13-excel-email` | + +