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 0c9c8ed4..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,23 +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" }, + { + // 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 82e06357..00000000 --- a/docs/everyday/index.md +++ /dev/null @@ -1,35 +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 | - -## 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 fc1b9773..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). +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/tutorials/everyday/file_attachments.md b/docs/tutorials/everyday/file_attachments.md new file mode 100644 index 00000000..cbc8b2d0 --- /dev/null +++ b/docs/tutorials/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/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/tutorials/everyday/print_forms.md b/docs/tutorials/everyday/print_forms.md new file mode 100644 index 00000000..311f9eba --- /dev/null +++ b/docs/tutorials/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](/tutorials/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](/tutorials/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/tutorials/everyday/react_to_changes.md b/docs/tutorials/everyday/react_to_changes.md new file mode 100644 index 00000000..656f7075 --- /dev/null +++ b/docs/tutorials/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](/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 | + +## 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/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/tutorials/everyday/send_by_mail.md b/docs/tutorials/everyday/send_by_mail.md new file mode 100644 index 00000000..ced8150f --- /dev/null +++ b/docs/tutorials/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](/tutorials/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](/tutorials/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/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: