From ad01cc63082b5f3c210a12a0ad2a3116fa0d5395 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:26:19 +0000 Subject: [PATCH 1/4] 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/4] 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` | + + From 70bcb44df4410bbdd7a8f9caa186f4314b1a8159 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 18:30:44 +0000 Subject: [PATCH 3/4] Add four Everyday Work guides on existing samples - File Attachments (Z2UI5_CL_SMPS_APP_016): a static action with a deep parameter as the only way in, a list without the content, download and delete by key - Send by Mail (Z2UI5_CL_SMPS_APP_023): the form filled in where the app knows the answer, the attachment written when sending, cl_bcs_mail_message, the outbound configuration per stack - Print Forms as PDF (Z2UI5_CL_SMPS_APP_024): FP_FUNCTION_MODULE_NAME, FP_JOB_OPEN with getpdf and nodialog, the generated function module, FP_JOB_CLOSE in every case, one PDF or a ZIP - React to Changes (Z2UI5_CL_SMPS_APP_011, 012): business events raised in the additional save, a handler class, notification versus data event, and how the result reaches a screen The section index and the sidebar list all nine guides; the controls reference names File Attachments as a FileUploader page. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014YZFmDjwUy369k1iZKoBT5 --- docs/.vitepress/config.mjs | 4 + docs/everyday/file_attachments.md | 166 ++++++++++++++++++++++++++++++ docs/everyday/index.md | 4 + docs/everyday/print_forms.md | 148 ++++++++++++++++++++++++++ docs/everyday/react_to_changes.md | 147 ++++++++++++++++++++++++++ docs/everyday/send_by_mail.md | 152 +++++++++++++++++++++++++++ docs/resources/controls.md | 2 +- 7 files changed, 622 insertions(+), 1 deletion(-) create mode 100644 docs/everyday/file_attachments.md create mode 100644 docs/everyday/print_forms.md create mode 100644 docs/everyday/react_to_changes.md create mode 100644 docs/everyday/send_by_mail.md diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index 0c9c8ed4..68c335b8 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -243,6 +243,10 @@ export default defineConfig({ { text: "Approval Worklist", link: "/everyday/approval_worklist" }, { text: "Report and Export", link: "/everyday/report_export" }, { text: "Concurrent Editing", link: "/everyday/concurrent_editing" }, + { text: "File Attachments", link: "/everyday/file_attachments" }, + { text: "Send by Mail", link: "/everyday/send_by_mail" }, + { text: "Print Forms as PDF", link: "/everyday/print_forms" }, + { text: "React to Changes", link: "/everyday/react_to_changes" }, ], }, { diff --git a/docs/everyday/file_attachments.md b/docs/everyday/file_attachments.md new file mode 100644 index 00000000..cbc8b2d0 --- /dev/null +++ b/docs/everyday/file_attachments.md @@ -0,0 +1,166 @@ +--- +outline: [2, 4] +samples: + - z2ui5_cl_smps_app_016 +description: Store files in a RAP business object and give them back - upload with the FileUploader into a static action with a deep parameter, a list that never loads the content, download by key and delete through the business object. +--- +# File Attachments + +A contract as PDF, a photo of the damage, the signed delivery note: users +want to keep a file with the record it belongs to, and get it back later. The +job has four parts - upload, list, download, delete - and one decision that +matters more than the others: the business object decides what it stores, +not the app. + +The worked example is `Z2UI5_CL_SMPS_APP_016` in +[abap2UI5/samples-stack](https://github.com/abap2UI5/samples-stack) (package +`11`). Its business object `Z2UI5_R_SMPS_FIL` keeps one row per file and +takes new files only through its static action `uploadFile`. + +## The Shape of the App + +| Part | How | +|---|---| +| **Upload** | `z2ui5:FileUploader` brings the file as a data URL; the app decodes it and calls the action | +| **List** | A `SELECT` on the CDS view with name, type, size and date - never the content | +| **Download** | `READ ENTITIES` by key with the content, back to a data URL, `download_b64_file` | +| **Delete** | `MODIFY ENTITIES ... DELETE`, then `COMMIT ENTITIES` | + +## Step 1: Let the Business Object Own the Upload + +The behavior definition declares `create` as internal and offers a static +action instead: + +```abap +static action uploadFile deep parameter z2ui5_a_smps_file_upload result [1] $self; +``` + +The action is the only way in, so every file goes through the same checks - +the behavior pool refuses an empty file with a message - no matter whether +it comes from this app, a Fiori elements UI or another program. The +parameter is deep: a description on the first level, and the file one level +below, as content, MIME type and name. + +## Step 2: Upload + +The FileUploader delivers `data:;base64,`. Split it, take the +MIME type from the header - it is the browser's guess from the extension and +may be missing - and decode the payload: + +```abap +SPLIT file AT `,` INTO DATA(header) DATA(base64). +DATA(mime_type) = substring_before( val = substring_after( val = header sub = `data:` ) sub = `;` ). +IF mime_type IS INITIAL. + mime_type = `application/octet-stream`. +ENDIF. +DATA(content) = cl_web_http_utility=>decode_x_base64( base64 ). +``` + +Then call the action. A static action has no key, so the `%cid` is the +caller's own name for the call: + +```abap +MODIFY ENTITIES OF z2ui5_r_smps_fil + ENTITY file + EXECUTE uploadfile + FROM VALUE #( ( %cid = `UPLOAD_1` + %param = VALUE #( description = description + _streamproperties = VALUE #( filecontent = content + mimetype = mime_type + filename = file_name ) ) ) ) + FAILED DATA(s_failed) + REPORTED DATA(s_reported). +``` + +`COMMIT ENTITIES RESPONSE OF` saves it, and a refusal from either statement +goes into a message box. Afterwards clear the attribute that held the data +URL: the app instance is kept between roundtrips, and the file would travel +along with every one of them. + +## Step 3: List Without the Content + +EML reads by key, so the list is a `SELECT` on the CDS view. Leave the +content column out. The list needs the size of a file, not its bytes, and a +bound table goes to the browser in full: + +```abap +SELECT FROM z2ui5_r_smps_fil + FIELDS FileUuid, FileName, MimeType, FileSize, Description, CreatedAt + ORDER BY CreatedAt DESCENDING + INTO TABLE @DATA(t_result) UP TO 50 ROWS. +``` + +## Step 4: Download by Key + +The row's button sends the key with the event, +`` client->_event( val = `DOWNLOAD` arg = `${FILE_UUID}` ) ``, and +`client->get_event_arg( )` reads it. The handler reads that one file with its +content and hands it back the way it came: + +```abap +READ ENTITIES OF z2ui5_r_smps_fil + ENTITY file + FIELDS ( filename mimetype content ) + WITH VALUE #( ( fileuuid = file_uuid ) ) + RESULT DATA(t_result). + +DATA(s_file) = t_result[ 1 ]. +client->follow_up_action( + val = client->cs_event-download_b64_file + t_arg = VALUE #( ( |data:{ s_file-mimetype };base64,{ cl_web_http_utility=>encode_x_base64( s_file-content ) }| ) + ( |{ s_file-filename }| ) ) ). +``` + +Check `t_result` before the table expression: a file deleted in another tab +is not an exception, the result is simply empty. The frontend refuses data +URLs of an active type such as HTML or SVG, so a stored file cannot run as a +page in the user's browser - see +[Upload, Download](/cookbook/device_capabilities/upload_download#download). + +## Step 5: Delete + +`MODIFY ENTITIES ... DELETE FROM` by key and a COMMIT. The business object +decides whether a file may be deleted; the app only shows its answer. + +## Limits and Variants + +- **File size.** The file travels base64-encoded inside one roundtrip and is + held in one work process, decoded and copied into the business object. + Request limits of the ICF and the memory of the session end this long + before the sample does. Files of tens of megabytes belong in a document + store - SAP Document Management, an object store on BTP - with the + business object keeping a reference. +- **Attachments of an existing record.** Here the file is its own business + object. To attach files to, say, a travel, model the file as a child + entity of the travel and create it by association, or keep the travel's + key in the file's table. +- **Several files at once.** `UploadSet` takes many files and drag and drop - + `Z2UI5_CL_SMP_APP_517` in [abap2UI5/samples](https://github.com/abap2UI5/samples) + shows it. +- **Release.** A deep action parameter with `@Semantics.largeObject` needs + ABAP Platform 2025 or a current ABAP Cloud release. On older systems, pass + the file to an action with a flat parameter, or create the instance with + the content field directly. + +## Related + +- [Upload, Download](/cookbook/device_capabilities/upload_download) - the + FileUploader and the download action. +- [EML](/cookbook/eml_cds_sql/eml) - actions, reads and deletes. +- [PDF](/cookbook/device_capabilities/pdf) - showing a stored PDF in the view + instead of downloading it. + + + +## 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 | +|---|---| +| Deep Action Parameter — EXECUTE uploadFile with a deep parameter - the file travels as a stream property, download and delete included | [`Z2UI5_CL_SMPS_APP_016`](https://github.com/abap2UI5/samples-stack/blob/main/src/11/z2ui5_cl_smps_app_016.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `11-rap-file-upload` | + + diff --git a/docs/everyday/index.md b/docs/everyday/index.md index 82e06357..f999e9a4 100644 --- a/docs/everyday/index.md +++ b/docs/everyday/index.md @@ -25,6 +25,10 @@ built from and ends with a sample you can pull with abapGit and run. | [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 | +| [File Attachments](/everyday/file_attachments) | Store files in a business object, list them without their content, download and delete by key | a deep action parameter (ABAP Platform 2025 or ABAP Cloud) | +| [Send by Mail](/everyday/send_by_mail) | A mail form next to the data, the attachment written when sending, a clear answer whether the mail left | `cl_bcs_mail_message` and outbound mail configured | +| [Print Forms as PDF](/everyday/print_forms) | Render an existing Adobe Form into a PDF instead of a printer, one file or a ZIP of several | Adobe Document Services (on-premise) | +| [React to Changes](/everyday/react_to_changes) | Log, notify or follow up whenever a business object changes, whoever changed it | RAP business events (ABAP Platform 2021 or ABAP Cloud) | ## What makes a good guide diff --git a/docs/everyday/print_forms.md b/docs/everyday/print_forms.md new file mode 100644 index 00000000..ecbd8f45 --- /dev/null +++ b/docs/everyday/print_forms.md @@ -0,0 +1,148 @@ +--- +outline: [2, 4] +samples: + - z2ui5_cl_smps_app_024 +description: Turn an Adobe Form into a PDF from an abap2UI5 app - the generated function module between FP_JOB_OPEN and FP_JOB_CLOSE with the PDF returned instead of printed, one PDF as a download or several as a ZIP, and what ADS needs. +--- +# Print Forms as PDF + +The invoice, the delivery note, the certificate: most systems already have +the form, built as an Adobe Form years ago and printed from a SAP GUI +transaction. A browser app has no spool dialog and usually no printer - what +the user wants is the PDF, to look at, download, mail or print from the +browser. The form itself stays as it is; only the output changes. + +The worked example is `Z2UI5_CL_SMPS_APP_024` in +[abap2UI5/samples-stack](https://github.com/abap2UI5/samples-stack) (package +`14`). It renders any form whose name is typed in, and downloads one PDF or a +ZIP of several. It needs Standard ABAP and Adobe Document Services (ADS). + +## The Shape of the App + +| Step | What happens | +|---|---| +| **Find the function module** | `FP_FUNCTION_MODULE_NAME` returns the name the form generated on this system | +| **Open the job** | `FP_JOB_OPEN` with `getpdf` and `nodialog` - the PDF comes back instead of going to a printer | +| **Render** | The generated function module, with the form's data, returns the PDF in `/1BCDWB/FORMOUTPUT` | +| **Close the job** | `FP_JOB_CLOSE`, in every case | +| **Hand it over** | `download_b64_file`, or a ZIP with `cl_abap_zip` for several forms | + +## Step 1: Ask for the Function Module + +An Adobe Form generates a function module, and its name differs from system +to system. Never write it down; ask for it: + +```abap +TRY. + CALL FUNCTION 'FP_FUNCTION_MODULE_NAME' + EXPORTING + i_name = CONV fpname( form ) + IMPORTING + e_funcname = function. + CATCH cx_fp_api INTO DATA(api_error). + error = api_error->get_text( ). + RETURN. +ENDTRY. +``` + +## Step 2: Render Into a PDF, Not a Printer + +Two output parameters make the difference between printing and returning +the file: `getpdf` hands the PDF back, `nodialog` suppresses the print +dialog, which a browser app has no SAP GUI to show in: + +```abap +outputpar-getpdf = abap_true. +outputpar-nodialog = abap_true. + +CALL FUNCTION 'FP_JOB_OPEN' + CHANGING + ie_outputparams = outputpar + EXCEPTIONS + OTHERS = 1. +``` + +Then the generated function module. In a real app you call it with the +parameters of your form's interface - the order, the customer, the items - +plus `/1BCDWB/DOCPARAMS` in and `/1BCDWB/FORMOUTPUT` out. The sample does not +know the form in advance, so it calls it with a parameter table that holds +only those two; a form whose interface wants more says so in the result list. + +```abap +CALL FUNCTION function + EXPORTING + /1bcdwb/docparams = docparams + is_order = s_order " your form's own parameters + IMPORTING + /1bcdwb/formoutput = output. +``` + +`output-pdf` is the file. + +## Step 3: Close the Job, Always + +Call `FP_JOB_CLOSE` whether the rendering worked or not. An open job makes +the next `FP_JOB_OPEN` of the same session fail, and the user sees an error +for a form that would render fine. + +## Step 4: Hand the PDF Over + +One file goes to the browser as a download: + +```abap +client->follow_up_action( + val = client->cs_event-download_b64_file + t_arg = VALUE #( ( |data:application/pdf;base64,{ cl_http_utility=>encode_x_base64( pdf ) }| ) + ( |{ to_lower( form ) }.pdf| ) ) ). +``` + +Several go into one ZIP, so the browser asks once instead of once per file: + +```abap +DATA(zip) = NEW cl_abap_zip( ). +LOOP AT t_pdf INTO DATA(s_pdf). + zip->add( name = |{ to_lower( s_pdf-form ) }.pdf| + content = s_pdf-pdf ). +ENDLOOP. +DATA(zip_file) = zip->save( ). +``` + +To show the PDF inside the app rather than download it, put it into a +`PDFViewer` - see [PDF](/cookbook/device_capabilities/pdf). To send it, it is +the attachment of [Send by Mail](/everyday/send_by_mail). + +## What the System Needs + +- **ADS.** Adobe Document Services do the rendering; the system reaches them + through the RFC destination `ADS` in `SM59`. When `FP_JOB_OPEN` fails right + away, this is the first thing to check - the sample says so in its error. +- **ABAP Cloud.** The classic runtime with `FP_JOB_OPEN` is not released + there. Forms are rendered by the Forms Service by Adobe through + `cl_fp_ads_util`, with the form template and the data as XML; the shape of + the app - render in the handler, hand the PDF to the browser - stays the + same. +- **Many forms.** Rendering takes a noticeable moment per form. A few at a + time are fine in a roundtrip; for a whole month of invoices, render in a + background job and offer the result for download when it is done. + +## Related + +- [PDF](/cookbook/device_capabilities/pdf) - showing a PDF in the view. +- [Upload, Download](/cookbook/device_capabilities/upload_download) - the + download action and the data URL types it refuses. +- [Send by Mail](/everyday/send_by_mail) - the PDF as an attachment. + + + +## 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 | +|---|---| +| Render to PDF, Download One or a ZIP — Renders Adobe Forms of the system to PDF with FP_JOB_OPEN and the generated function module - one form as a PDF download, several at once as one ZIP file | [`Z2UI5_CL_SMPS_APP_024`](https://github.com/abap2UI5/samples-stack/blob/main/src/14/z2ui5_cl_smps_app_024.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `14-adobe-forms` | + + diff --git a/docs/everyday/react_to_changes.md b/docs/everyday/react_to_changes.md new file mode 100644 index 00000000..d4430323 --- /dev/null +++ b/docs/everyday/react_to_changes.md @@ -0,0 +1,147 @@ +--- +outline: [2, 4] +samples: + - z2ui5_cl_smps_app_011 + - z2ui5_cl_smps_app_012 +description: Do something whenever a RAP business object changes - a log, a mail, a follow-up document - with business events raised in the save sequence and consumed by a handler class, and how the result reaches an abap2UI5 screen. +--- +# React to Changes + +Every new ticket should be logged, a manager informed when a status changes, +a follow-up document created when an order is released. The tempting place +for that is the app's Save handler: after the COMMIT, write the log, send the +mail. It works until the second app, the Fiori elements UI or the background +job changes the same data and nothing happens. + +The reaction belongs to the business object, not to one of its screens. RAP +business events are the mechanism: the business object announces what +happened, a handler reacts, and neither knows the other. + +The worked example is package `05` of +[abap2UI5/samples-stack](https://github.com/abap2UI5/samples-stack): +`Z2UI5_CL_SMPS_APP_011` creates and changes tickets, `Z2UI5_CL_SMPS_APP_012` +shows the log the handler writes. Business events need ABAP Platform 2021 or +ABAP Cloud. + +## The Shape of the Solution + +| Part | Where it lives | What it does | +|---|---|---| +| **The event** | The behavior definition | Declares what the business object announces | +| **Raising** | The additional save of the behavior pool | Raises the event for what this transaction created or changed | +| **Consuming** | A handler class of its own | Reacts - here, writes a log row | +| **The screens** | Two abap2UI5 apps | One changes data, one shows what the handler did | + +## Step 1: Declare What Is Announced + +Two kinds of event, and the choice between them is the main decision: + +```abap +" notification event: the key, nothing else +event TicketCreated; + +" data event: the state at the time of the change, typed by an abstract entity +event StatusChanged parameter Z2UI5_R_SMPS_TCK_STAT; +``` + +A **notification** event says "this instance changed" and leaves it to the +handler to read the current state. A **data** event carries the state along, +so the handler needs no read - at the price of data that may be outdated by +the time the handler runs. Take the notification when the handler needs the +latest state anyway; take the data event when it needs to know what the +state was at the moment of the change, as an audit log does. + +## Step 2: Raise in the Save Sequence + +Events are raised where the business object knows the transaction went +through - in the additional save of the behavior pool, which sees what was +created and updated: + +```abap +METHOD save_modified. + IF create-ticket IS NOT INITIAL. + RAISE ENTITY EVENT z2ui5_r_smps_tck~TicketCreated + FROM VALUE #( FOR c IN create-ticket ( %key = VALUE #( TicketUUID = c-TicketUUID ) ) ). + ENDIF. + ... +ENDMETHOD. +``` + +Raised here, an event is raised only for data that is actually saved. A +validation that refuses the save also prevents the event, which a log +written in the app's handler would not get right. + +## Step 3: Consume in a Handler Class + +The handler is a class of its own that inherits from +`cl_abap_behavior_event_handler` and names the events it wants: + +```abap +CLASS lhe_ticket DEFINITION INHERITING FROM cl_abap_behavior_event_handler. + PRIVATE SECTION. + METHODS on_ticket_created FOR ENTITY EVENT + ticketcreated FOR ticket~TicketCreated. +ENDCLASS. +``` + +That declaration is all it takes to listen; nothing is registered anywhere else. +A second handler for the same event runs as well, and deleting one leaves +the business object unchanged. The handler receives all instances of the +transaction as one table, so it writes its log rows with one `INSERT`. + +The handler runs **after** the transaction that raised the event, in a +session of its own. Two consequences: + +- An error in the handler does not undo the change. Handle failures in the + handler - log them, retry them - instead of expecting the save to fail. +- The app that saved does not see the result in the same roundtrip. The log + row exists a moment later. + +## Step 4: Show the Result on Screen + +The abap2UI5 apps take no part in the event mechanism: one changes tickets +through EML as any app would, the other reads the log table. Because the +handler runs after the save, the log screen shows a new row on its next +read - the sample has a refresh button for it. + +For a screen that should update by itself: + +- **Polling.** A timer that reads again every few seconds - simple, and fine + for a monitor page. See [Timer](/cookbook/browser_interaction/timer). +- **Push.** The handler publishes a message through ABAP Messaging Channels, + and the open screens receive it over a WebSocket without asking - see + [WebSocket](/cookbook/expert_more/websocket). On-premise only. + +## What Fits Here, and What Does Not + +| Reaction | Business event? | +|---|---| +| A log or audit trail of changes | Yes - a data event and an `INSERT` | +| A mail or a notification to someone | Yes - the handler sends it, see [Send by Mail](/everyday/send_by_mail) | +| A follow-up document in another business object | Yes - the handler calls that business object with EML | +| A check that may refuse the save | No - that is a validation of the business object | +| A value that must be correct right after the save | No - that is a determination | + +## Related + +- [RAP](/cookbook/eml_cds_sql/rap) and [EML](/cookbook/eml_cds_sql/eml) - + the business object and how the apps call it. +- [Timer](/cookbook/browser_interaction/timer) and + [WebSocket](/cookbook/expert_more/websocket) - getting the result onto the + screen without a click. + + + +## 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 | +|---|---| +| Ticket App — every create and update raises an entity event | [`Z2UI5_CL_SMPS_APP_011`](https://github.com/abap2UI5/samples-stack/blob/main/src/05/z2ui5_cl_smps_app_011.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `05-business-events` | +| Event Log App — what the handler wrote, newest first | [`Z2UI5_CL_SMPS_APP_012`](https://github.com/abap2UI5/samples-stack/blob/main/src/05/z2ui5_cl_smps_app_012.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `05-business-events` | + + diff --git a/docs/everyday/send_by_mail.md b/docs/everyday/send_by_mail.md new file mode 100644 index 00000000..5b0de4b5 --- /dev/null +++ b/docs/everyday/send_by_mail.md @@ -0,0 +1,152 @@ +--- +outline: [2, 4] +samples: + - z2ui5_cl_smps_app_023 +description: Send what the app shows by mail - the attachment written in the backend, cl_bcs_mail_message on ABAP Cloud and current releases, the outbound configuration a mail needs, the status after the send, and how to test it. +--- +# Send by Mail + +"Can you send me that list?" is the most common follow-up to any report. The +job: a button next to the data, a small form for the recipient, the data as +an attachment, and a clear answer whether the mail left the system. Nothing +in it is specific to abap2UI5 except the form - the rest is the mail API of +the system and its configuration, which is where most of the trouble is. + +The worked example is `Z2UI5_CL_SMPS_APP_023` in +[abap2UI5/samples-stack](https://github.com/abap2UI5/samples-stack) (package +`13`). It sends a table as an `.xlsx` attachment with `cl_bcs_mail_message`. + +## The Shape of the App + +| Part | What it does | +|---|---| +| **Form** | Sender, recipient, subject, text - bound attributes, filled in where the app knows the answer | +| **Attachment** | Written in the backend at the moment of sending, from the data the app holds | +| **Send** | `cl_bcs_mail_message`: sender, recipient, subject, body, attachment, `send( )` | +| **Answer** | The status the mail system returns, or the text of the exception | + +## Step 1: Fill In What the App Knows + +A mail form that starts empty is a form the user abandons. Fill the subject +and the text when the app starts, and the recipient where the record knows +one - the customer's contact, the creator of a request: + +```abap +IF client->check_on_init( ). + subject = `Products as XLSX`. + text = |Hello,\n\nthe current product list is attached.\n\nSent from an abap2UI5 app.|. + view_display( ). +``` + +The sender is a configuration question, not a user's choice. Most mail +setups accept only addresses of the own domain, or one fixed sender per +system. Fill it from a customizing table or a constant, and show it read +only, rather than letting each user type one the mail server then refuses. + +Use `type` `Email` on the address inputs, so a phone shows the keyboard with +the `@`. + +## Step 2: Build the Attachment When Sending + +Write the file in the handler of the Send button, from the rows the app has, +not from a file the browser kept. The attachment then shows exactly the data +the user saw, and nothing large travels through the roundtrips before it is +needed: + +```abap +DATA(xlsx) = z2ui5_cl_smps_xlsx=>write( t_cells ). +``` + +The same idea works for any format: a PDF from a form (see +[Print Forms as PDF](/everyday/print_forms)), a CSV built with +`concat_lines_of( )`, a ZIP of several files with `cl_abap_zip`. + +## Step 3: Send + +`cl_bcs_mail_message` is the mail API of ABAP Cloud, and available on +on-premise releases from 2023 on: + +```abap +TRY. + DATA(mail) = cl_bcs_mail_message=>create_instance( ). + mail->set_sender( CONV #( sender ) ). + mail->add_recipient( CONV #( recipient ) ). + mail->set_subject( CONV #( subject ) ). + mail->set_main( cl_bcs_mail_textpart=>create_text_plain( text ) ). + mail->add_attachment( cl_bcs_mail_binarypart=>create_instance( + iv_content = xlsx + iv_content_type = CONV #( z2ui5_cl_smps_xlsx=>mime_type ) + iv_filename = `products.xlsx` ) ). + mail->send( IMPORTING ev_mail_status = DATA(status) ). + + CATCH cx_bcs_mail INTO DATA(error). + client->message_box_display( text = error->get_text( ) type = `error` ). +ENDTRY. +``` + +Everything that can go wrong arrives as `cx_bcs_mail`: an address that is +not one, a sender the configuration does not allow, a system without +outbound mail. Show its text. It is the one piece of information the user +can pass on to the person who runs the system. + +On older on-premise releases the API is `cl_bcs` with `cl_document_bcs` - +[E-Mail](/cookbook/expert_more/email) shows that variant, attachment +included. + +## Step 4: Say What Happened + +`send( )` hands the mail to the outbound queue of the system. The status it +returns says whether the mail left already or is waiting there. A mail that +is queued is not an error, but it is not delivered either, so say which one +it is instead of a plain "sent": + +```abap +client->message_box_display( text = |Mail handed over to the mail system, status { status }| type = `success` ). +``` + +## What the System Needs + +The code is the small part. A mail only leaves the system when outbound mail +is configured: + +| Stack | Configuration | Where to look when nothing arrives | +|---|---|---| +| ABAP Cloud | A communication arrangement for the scenario `SAP_COM_0548` | The communication arrangement and the mail monitoring app of the launchpad | +| On-premise | SMTP in transaction `SCOT` | Transaction `SOST` | + +Agree on the sender address with whoever runs the mail server before the +first test. Most failures that look like a bug in the app are a sender the +server does not accept. + +## Testing + +- Send to yourself first, and open the attachment: a file that downloads + fine can still be refused by a mail client if its MIME type is wrong. +- On a development system without a real mail server, an SMTP sink such as + a local test server shows the mail exactly as it would be sent. +- Large attachments are the mail server's limit, not the app's. Several + megabytes are usually fine; beyond that, send a link to the record + instead. + +## Related + +- [E-Mail](/cookbook/expert_more/email) - the classic BCS API. +- [Report and Export](/everyday/report_export) - the report a mail button + usually sits on. +- [Spreadsheet](/cookbook/device_capabilities/spreadsheet) - writing the + attachment. + + + +## 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 | +|---|---| +| E-Mail — Send a Table as XLSX Attachment — A mail with an .xlsx attachment, sent with cl_bcs_mail_message - recipient, subject and text from the screen, the attachment written with the XCO XLSX library | [`Z2UI5_CL_SMPS_APP_023`](https://github.com/abap2UI5/samples-stack/blob/main/src/13/z2ui5_cl_smps_app_023.clas.abap) · [samples-stack](https://github.com/abap2UI5/samples-stack), branch `13-excel-email` | + + diff --git a/docs/resources/controls.md b/docs/resources/controls.md index fc1b9773..4559ea0d 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), [Excel Import into RAP](/everyday/excel_import_rap). +Used on [Spreadsheet](/cookbook/device_capabilities/spreadsheet), [Upload, Download](/cookbook/device_capabilities/upload_download), [Excel Import into RAP](/everyday/excel_import_rap), [File Attachments](/everyday/file_attachments). | Property | Type | Default | |---|---|---| From 9c6e28125e4376a398997619238c710be8b94c2b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 19:04:01 +0000 Subject: [PATCH 4/4] Move Everyday Work under the Tutorial The guides leave the bar: they are a group of the Tutorial's sidebar section now, after the twelve steps, and live at /tutorials/everyday/. The bar keeps its four sections. The Tutorial's index introduces the guides, every link points at the new address, and the six pages published under /everyday/ keep answering through redirect stubs that carry the #fragment (AGENTS.md counts them). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014YZFmDjwUy369k1iZKoBT5 --- AGENTS.md | 7 ++- docs/.vitepress/config.mjs | 45 ++++++++++--------- .../device_capabilities/spreadsheet.md | 2 +- docs/everyday/index.md | 39 ---------------- docs/public/everyday/approval_worklist.html | 30 +++++++++++++ docs/public/everyday/concurrent_editing.html | 30 +++++++++++++ docs/public/everyday/excel_import_rap.html | 30 +++++++++++++ docs/public/everyday/index.html | 30 +++++++++++++ docs/public/everyday/mass_change.html | 30 +++++++++++++ docs/public/everyday/report_export.html | 30 +++++++++++++ docs/resources/controls.md | 2 +- .../everyday/approval_worklist.md | 0 .../everyday/concurrent_editing.md | 0 .../everyday/excel_import_rap.md | 0 .../everyday/file_attachments.md | 0 docs/tutorials/everyday/index.md | 39 ++++++++++++++++ docs/{ => tutorials}/everyday/mass_change.md | 4 +- docs/{ => tutorials}/everyday/print_forms.md | 4 +- .../everyday/react_to_changes.md | 2 +- .../{ => tutorials}/everyday/report_export.md | 0 docs/{ => tutorials}/everyday/send_by_mail.md | 4 +- docs/tutorials/index.md | 11 ++++- 22 files changed, 267 insertions(+), 72 deletions(-) delete mode 100644 docs/everyday/index.md create mode 100644 docs/public/everyday/approval_worklist.html create mode 100644 docs/public/everyday/concurrent_editing.html create mode 100644 docs/public/everyday/excel_import_rap.html create mode 100644 docs/public/everyday/index.html create mode 100644 docs/public/everyday/mass_change.html create mode 100644 docs/public/everyday/report_export.html rename docs/{ => tutorials}/everyday/approval_worklist.md (100%) rename docs/{ => tutorials}/everyday/concurrent_editing.md (100%) rename docs/{ => tutorials}/everyday/excel_import_rap.md (100%) rename docs/{ => tutorials}/everyday/file_attachments.md (100%) create mode 100644 docs/tutorials/everyday/index.md rename docs/{ => tutorials}/everyday/mass_change.md (97%) rename docs/{ => tutorials}/everyday/print_forms.md (97%) rename docs/{ => tutorials}/everyday/react_to_changes.md (99%) rename docs/{ => tutorials}/everyday/report_export.md (100%) rename docs/{ => tutorials}/everyday/send_by_mail.md (97%) diff --git a/AGENTS.md b/AGENTS.md index 41a384ae..52347b9c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -206,7 +206,7 @@ hand-written HTML file behind at its old address — a canonical link, a browser does not follow it. `docs/public/configuration/troubleshooting.html` is the worked example. -There are seventy-nine of them. Eight are `/technical/*`: that whole section became +There are eighty-five of them. Eight are `/technical/*`: that whole section became the Insights series under `/advanced/insights/` in one commit, and eight addresses that had been public for years stopped existing with it — one of them, `/technical/concept`, is linked from the framework's README, which is @@ -242,6 +242,11 @@ decision makers) and the `/technical/*` ones point at the new site directly, so no old address takes two hops. The essays, their SVGs and their sidebar group are gone from this repository; the sidebar keeps one row, `Insights`, that leads there. +The newest six are `/everyday/*`: the Everyday Work guides started on +2026-11-02 as a section of their own in the bar and moved under the Tutorial +the same day (`/tutorials/everyday/`), so the bar keeps its four sections. +The five guides and the index that had been published for a few hours keep +their old address, with the `#fragment`. Two things make this safe rather than a second set of pages to maintain: diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index 68c335b8..4dfe945c 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -167,7 +167,6 @@ 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/" }, { @@ -226,27 +225,29 @@ export default defineConfig({ { text: "10. App Structure", link: "/tutorials/step-10" }, { text: "11. To Production", link: "/tutorials/step-11" }, { 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" }, - { 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" }, - { text: "File Attachments", link: "/everyday/file_attachments" }, - { text: "Send by Mail", link: "/everyday/send_by_mail" }, - { text: "Print Forms as PDF", link: "/everyday/print_forms" }, - { text: "React to Changes", link: "/everyday/react_to_changes" }, + { + // The second half of the section: the steps teach one concept + // each, a guide here is a whole job from a real project, built + // from several cookbook chapters and backed by an installable + // sample. A group of its own under the Tutorial rather than a + // section in the bar - it is learning by building, like the + // steps, and the bar keeps its four sections. The row opens the + // index, which says what the guides are and lists them. + text: "Everyday Work", + link: "/tutorials/everyday/", + collapsed: true, + items: [ + { text: "Excel Import into RAP", link: "/tutorials/everyday/excel_import_rap" }, + { text: "Mass Change", link: "/tutorials/everyday/mass_change" }, + { text: "Approval Worklist", link: "/tutorials/everyday/approval_worklist" }, + { text: "Report and Export", link: "/tutorials/everyday/report_export" }, + { text: "Concurrent Editing", link: "/tutorials/everyday/concurrent_editing" }, + { text: "File Attachments", link: "/tutorials/everyday/file_attachments" }, + { text: "Send by Mail", link: "/tutorials/everyday/send_by_mail" }, + { text: "Print Forms as PDF", link: "/tutorials/everyday/print_forms" }, + { text: "React to Changes", link: "/tutorials/everyday/react_to_changes" }, + ], + }, ], }, { diff --git a/docs/cookbook/device_capabilities/spreadsheet.md b/docs/cookbook/device_capabilities/spreadsheet.md index 4fa70e37..0e52542d 100644 --- a/docs/cookbook/device_capabilities/spreadsheet.md +++ b/docs/cookbook/device_capabilities/spreadsheet.md @@ -209,7 +209,7 @@ 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). +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](/tutorials/everyday/excel_import_rap). ::: ::: tip **ABAP Cloud** diff --git a/docs/everyday/index.md b/docs/everyday/index.md deleted file mode 100644 index f999e9a4..00000000 --- a/docs/everyday/index.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -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 | -| [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 | -| [File Attachments](/everyday/file_attachments) | Store files in a business object, list them without their content, download and delete by key | a deep action parameter (ABAP Platform 2025 or ABAP Cloud) | -| [Send by Mail](/everyday/send_by_mail) | A mail form next to the data, the attachment written when sending, a clear answer whether the mail left | `cl_bcs_mail_message` and outbound mail configured | -| [Print Forms as PDF](/everyday/print_forms) | Render an existing Adobe Form into a PDF instead of a printer, one file or a ZIP of several | Adobe Document Services (on-premise) | -| [React to Changes](/everyday/react_to_changes) | Log, notify or follow up whenever a business object changes, whoever changed it | RAP business events (ABAP Platform 2021 or ABAP Cloud) | - -## 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/public/everyday/approval_worklist.html b/docs/public/everyday/approval_worklist.html new file mode 100644 index 00000000..92dc652d --- /dev/null +++ b/docs/public/everyday/approval_worklist.html @@ -0,0 +1,30 @@ + + + + + + Page moved - abap2UI5 + + + + + + +

+ This page moved to + Approval Worklist. +

+ + diff --git a/docs/public/everyday/concurrent_editing.html b/docs/public/everyday/concurrent_editing.html new file mode 100644 index 00000000..d175941c --- /dev/null +++ b/docs/public/everyday/concurrent_editing.html @@ -0,0 +1,30 @@ + + + + + + Page moved - abap2UI5 + + + + + + +

+ This page moved to + Concurrent Editing. +

+ + diff --git a/docs/public/everyday/excel_import_rap.html b/docs/public/everyday/excel_import_rap.html new file mode 100644 index 00000000..6a705461 --- /dev/null +++ b/docs/public/everyday/excel_import_rap.html @@ -0,0 +1,30 @@ + + + + + + Page moved - abap2UI5 + + + + + + +

+ This page moved to + Excel Import into RAP. +

+ + diff --git a/docs/public/everyday/index.html b/docs/public/everyday/index.html new file mode 100644 index 00000000..a9ba023f --- /dev/null +++ b/docs/public/everyday/index.html @@ -0,0 +1,30 @@ + + + + + + Page moved - abap2UI5 + + + + + + +

+ This page moved to + Everyday Work. +

+ + diff --git a/docs/public/everyday/mass_change.html b/docs/public/everyday/mass_change.html new file mode 100644 index 00000000..dc11830b --- /dev/null +++ b/docs/public/everyday/mass_change.html @@ -0,0 +1,30 @@ + + + + + + Page moved - abap2UI5 + + + + + + +

+ This page moved to + Mass Change. +

+ + diff --git a/docs/public/everyday/report_export.html b/docs/public/everyday/report_export.html new file mode 100644 index 00000000..1bd193a2 --- /dev/null +++ b/docs/public/everyday/report_export.html @@ -0,0 +1,30 @@ + + + + + + Page moved - abap2UI5 + + + + + + +

+ This page moved to + Report and Export. +

+ + diff --git a/docs/resources/controls.md b/docs/resources/controls.md index 4559ea0d..2fc0ea1e 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), [Excel Import into RAP](/everyday/excel_import_rap), [File Attachments](/everyday/file_attachments). +Used on [Spreadsheet](/cookbook/device_capabilities/spreadsheet), [Upload, Download](/cookbook/device_capabilities/upload_download), [Excel Import into RAP](/tutorials/everyday/excel_import_rap), [File Attachments](/tutorials/everyday/file_attachments). | Property | Type | Default | |---|---|---| diff --git a/docs/everyday/approval_worklist.md b/docs/tutorials/everyday/approval_worklist.md similarity index 100% rename from docs/everyday/approval_worklist.md rename to docs/tutorials/everyday/approval_worklist.md diff --git a/docs/everyday/concurrent_editing.md b/docs/tutorials/everyday/concurrent_editing.md similarity index 100% rename from docs/everyday/concurrent_editing.md rename to docs/tutorials/everyday/concurrent_editing.md diff --git a/docs/everyday/excel_import_rap.md b/docs/tutorials/everyday/excel_import_rap.md similarity index 100% rename from docs/everyday/excel_import_rap.md rename to docs/tutorials/everyday/excel_import_rap.md diff --git a/docs/everyday/file_attachments.md b/docs/tutorials/everyday/file_attachments.md similarity index 100% rename from docs/everyday/file_attachments.md rename to docs/tutorials/everyday/file_attachments.md diff --git a/docs/tutorials/everyday/index.md b/docs/tutorials/everyday/index.md new file mode 100644 index 00000000..f4205d08 --- /dev/null +++ b/docs/tutorials/everyday/index.md @@ -0,0 +1,39 @@ +--- +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 [steps of the Tutorial](/tutorials/) teach 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](/tutorials/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](/tutorials/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](/tutorials/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](/tutorials/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](/tutorials/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 | +| [File Attachments](/tutorials/everyday/file_attachments) | Store files in a business object, list them without their content, download and delete by key | a deep action parameter (ABAP Platform 2025 or ABAP Cloud) | +| [Send by Mail](/tutorials/everyday/send_by_mail) | A mail form next to the data, the attachment written when sending, a clear answer whether the mail left | `cl_bcs_mail_message` and outbound mail configured | +| [Print Forms as PDF](/tutorials/everyday/print_forms) | Render an existing Adobe Form into a PDF instead of a printer, one file or a ZIP of several | Adobe Document Services (on-premise) | +| [React to Changes](/tutorials/everyday/react_to_changes) | Log, notify or follow up whenever a business object changes, whoever changed it | RAP business events (ABAP Platform 2021 or ABAP Cloud) | + +## 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/everyday/mass_change.md b/docs/tutorials/everyday/mass_change.md similarity index 97% rename from docs/everyday/mass_change.md rename to docs/tutorials/everyday/mass_change.md index fe51c608..e1f39df2 100644 --- a/docs/everyday/mass_change.md +++ b/docs/tutorials/everyday/mass_change.md @@ -136,7 +136,7 @@ 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): +use the retry from [Excel Import into RAP](/tutorials/everyday/excel_import_rap#all-or-nothing-or-row-by-row): roll back, drop the refused rows, send the rest again. ## Limits and Variants @@ -156,7 +156,7 @@ roll back, drop the refused rows, send the rest again. - [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 +- [Excel Import into RAP](/tutorials/everyday/excel_import_rap) - the same pattern for rows that come from a file. diff --git a/docs/everyday/print_forms.md b/docs/tutorials/everyday/print_forms.md similarity index 97% rename from docs/everyday/print_forms.md rename to docs/tutorials/everyday/print_forms.md index ecbd8f45..311f9eba 100644 --- a/docs/everyday/print_forms.md +++ b/docs/tutorials/everyday/print_forms.md @@ -109,7 +109,7 @@ DATA(zip_file) = zip->save( ). To show the PDF inside the app rather than download it, put it into a `PDFViewer` - see [PDF](/cookbook/device_capabilities/pdf). To send it, it is -the attachment of [Send by Mail](/everyday/send_by_mail). +the attachment of [Send by Mail](/tutorials/everyday/send_by_mail). ## What the System Needs @@ -130,7 +130,7 @@ the attachment of [Send by Mail](/everyday/send_by_mail). - [PDF](/cookbook/device_capabilities/pdf) - showing a PDF in the view. - [Upload, Download](/cookbook/device_capabilities/upload_download) - the download action and the data URL types it refuses. -- [Send by Mail](/everyday/send_by_mail) - the PDF as an attachment. +- [Send by Mail](/tutorials/everyday/send_by_mail) - the PDF as an attachment. diff --git a/docs/everyday/react_to_changes.md b/docs/tutorials/everyday/react_to_changes.md similarity index 99% rename from docs/everyday/react_to_changes.md rename to docs/tutorials/everyday/react_to_changes.md index d4430323..656f7075 100644 --- a/docs/everyday/react_to_changes.md +++ b/docs/tutorials/everyday/react_to_changes.md @@ -117,7 +117,7 @@ For a screen that should update by itself: | Reaction | Business event? | |---|---| | A log or audit trail of changes | Yes - a data event and an `INSERT` | -| A mail or a notification to someone | Yes - the handler sends it, see [Send by Mail](/everyday/send_by_mail) | +| A mail or a notification to someone | Yes - the handler sends it, see [Send by Mail](/tutorials/everyday/send_by_mail) | | A follow-up document in another business object | Yes - the handler calls that business object with EML | | A check that may refuse the save | No - that is a validation of the business object | | A value that must be correct right after the save | No - that is a determination | diff --git a/docs/everyday/report_export.md b/docs/tutorials/everyday/report_export.md similarity index 100% rename from docs/everyday/report_export.md rename to docs/tutorials/everyday/report_export.md diff --git a/docs/everyday/send_by_mail.md b/docs/tutorials/everyday/send_by_mail.md similarity index 97% rename from docs/everyday/send_by_mail.md rename to docs/tutorials/everyday/send_by_mail.md index 5b0de4b5..ced8150f 100644 --- a/docs/everyday/send_by_mail.md +++ b/docs/tutorials/everyday/send_by_mail.md @@ -58,7 +58,7 @@ DATA(xlsx) = z2ui5_cl_smps_xlsx=>write( t_cells ). ``` The same idea works for any format: a PDF from a form (see -[Print Forms as PDF](/everyday/print_forms)), a CSV built with +[Print Forms as PDF](/tutorials/everyday/print_forms)), a CSV built with `concat_lines_of( )`, a ZIP of several files with `cl_abap_zip`. ## Step 3: Send @@ -131,7 +131,7 @@ server does not accept. ## Related - [E-Mail](/cookbook/expert_more/email) - the classic BCS API. -- [Report and Export](/everyday/report_export) - the report a mail button +- [Report and Export](/tutorials/everyday/report_export) - the report a mail button usually sits on. - [Spreadsheet](/cookbook/device_capabilities/spreadsheet) - writing the attachment. diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index b39a6c0f..ab8df36e 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -45,6 +45,14 @@ no frontend project, no OData service. - **[Step 12: Unit Tests](/tutorials/step-12)** — test the app class like any ABAP class; no UI5 runtime involved. +## Everyday Work + +The steps build one app to teach the framework. The +[Everyday Work](/tutorials/everyday/) guides start where they end: each one +takes a job from a real project - an Excel import, a mass change, an approval +worklist, a report with export - and walks through it from the first click to +the saved data, with a sample you can install. + ## What You Should Know No prior abap2UI5 or UI5 experience is needed — the tutorial introduces every @@ -62,7 +70,8 @@ popup calls. ## After the Tutorial -The [Cookbook](/cookbook/) covers each topic again as a +The [Everyday Work](/tutorials/everyday/) guides apply the steps to whole +jobs, the [Cookbook](/cookbook/) covers each topic again as a reference chapter, and three catalogs of complete, tested apps continue from there: