diff --git a/docs/cookbook/eml_cds_sql/draft_handling.md b/docs/cookbook/eml_cds_sql/draft_handling.md index bd738362..46f2189f 100644 --- a/docs/cookbook/eml_cds_sql/draft_handling.md +++ b/docs/cookbook/eml_cds_sql/draft_handling.md @@ -3,89 +3,233 @@ outline: [2, 4] --- # Draft Handling -Draft handling lets users save unfinished work — for example a half-filled form — without writing through to the active database state. In abap2UI5 apps you drive RAP draft-enabled business objects directly through EML, just like any other entity (see also [EML](./eml.md)). +## What Is a Draft? -All examples on this page use `I_BankTP`, a draft-enabled BO that ships with S/4HANA. +Imagine a user opens a form, fills in half of it, and then gets pulled into a meeting. With a normal save, they'd have to either commit half-finished (and possibly invalid) data, or lose everything. A **draft** is the third option: a private, work-in-progress copy that is parked safely on the server until the user is ready to finalize it. -### The EML Primitives +A helpful mental model: -#### Create a Draft -Use `MODIFY ENTITIES` with `%is_draft = if_abap_behv=>mk-on` to create a new draft instance: +| Concept | Everyday analogy | +|---|---| +| **Active record** | The published document everyone can read | +| **Draft** | Your private "unsaved changes" copy of that document | +| **Activate** | Hitting *Publish* — the draft replaces the active record | +| **Discard** | Hitting *Close without saving* — the draft is thrown away | + +While a draft is open, the underlying record is **locked** so nobody else can edit it at the same time — but the lock is held by SAP's RAP framework, not by your app. That means your abap2UI5 app can stay **stateless**: the user can close the browser, come back tomorrow, and resume exactly where they left off. + +In abap2UI5 you drive draft-enabled RAP business objects directly through **EML** (Entity Manipulation Language), exactly like any other entity — see also [EML](./eml.md). + +::: tip You don't have to build anything +On S/4HANA and the BTP ABAP Environment (Steampunk), many business objects already ship as draft-enabled BOs. All examples on this page use **`I_BankTP`**, a draft-enabled BO that ships with S/4HANA. You don't create a BO, and you don't create a draft table — SAP provides both. Your app just calls the standard BO via EML. +::: + +## The Draft Lifecycle + +Every draft follows the same simple lifecycle. Get this picture in your head and the rest of the page is just code for each arrow: + +``` + Activate + ┌────────────▶ ACTIVE record updated ✔ + │ (draft saved to database) + Edit ┌───────────┴──┐ +ACTIVE ─────▶│ DRAFT │ +record │ (user edits) │ + └───────────┬──┘ + │ + └────────────▶ Draft thrown away ✘ + Discard (active record unchanged) +``` + +1. **Edit** — open a draft from the active record (acquires the lock). +2. **Change** — the user types; you save their input into the draft. +3. **Finish** — either **Activate** (write the draft to the database) or **Discard** (throw it away). + +## The Four Building Blocks + +Everything on this page is built from just four EML operations. Read these once — the full app below is simply these four, wired to buttons. + +#### 1. Open a Draft — `Edit` +Open (acquire) a draft for an existing active record. This takes the lock: ```abap MODIFY ENTITIES OF i_banktp ENTITY Bank - CREATE - FIELDS ( BankCountry BankInternalID LongBankName SWIFTCode ) - WITH VALUE #( ( %cid = `NEW_BANK` - %is_draft = if_abap_behv=>mk-on - %data = VALUE #( - BankCountry = `DE` - BankInternalID = `99999999` - LongBankName = `My Bank` - SWIFTCode = `DEUTDEFF` ) ) ) - MAPPED DATA(ls_mapped) - FAILED DATA(ls_failed) - REPORTED DATA(ls_reported). + EXECUTE Edit + FROM VALUE #( ( %key-BankCountry = `DE` + %key-BankInternalID = `50070010` ) ) + FAILED DATA(failed) + REPORTED DATA(reported). COMMIT ENTITIES. ``` -#### Read a Draft -Set `%is_draft = if_abap_behv=>mk-on` to read the draft instead of the active record: +#### 2. Read a Draft — `%is_draft = on` +Read the **draft** values (what the user is working on) instead of the active record. The only difference from a normal read is the `%is_draft` flag: ```abap READ ENTITIES OF i_banktp ENTITY Bank FIELDS ( SWIFTCode LongBankName ) WITH VALUE #( ( %key-BankCountry = `DE` %key-BankInternalID = `50070010` - %is_draft = if_abap_behv=>mk-on ) ) - RESULT DATA(lt_drafts). + %is_draft = if_abap_behv=>mk-on ) ) " on = draft, off = active + RESULT DATA(drafts). ``` -#### Activate a Draft -Promote the draft to the active state with the `Activate` action: +#### 3. Activate a Draft — `Activate` +Promote the draft to the active state. **This is the actual save** — the database row is updated: ```abap MODIFY ENTITIES OF i_banktp ENTITY Bank EXECUTE Activate FROM VALUE #( ( %key-BankCountry = `DE` %key-BankInternalID = `50070010` ) ) - FAILED DATA(ls_failed) - REPORTED DATA(ls_reported). + FAILED DATA(failed) + REPORTED DATA(reported). COMMIT ENTITIES. ``` -#### Discard a Draft -Throw the draft away with the `Discard` action, releasing the lock: +#### 4. Discard a Draft — `Discard` +Throw the draft away and release the lock. The active record is untouched: ```abap MODIFY ENTITIES OF i_banktp ENTITY Bank EXECUTE Discard FROM VALUE #( ( %key-BankCountry = `DE` %key-BankInternalID = `50070010` ) ) - FAILED DATA(ls_failed) - REPORTED DATA(ls_reported). + FAILED DATA(failed) + REPORTED DATA(reported). + +COMMIT ENTITIES. +``` +::: tip Creating a brand-new record as a draft? +Use `CREATE` with `%is_draft = if_abap_behv=>mk-on` instead of `Edit`. The rest of the lifecycle (read / activate / discard) is identical. + +```abap +MODIFY ENTITIES OF i_banktp + ENTITY Bank + CREATE FIELDS ( BankCountry BankInternalID LongBankName SWIFTCode ) + WITH VALUE #( ( %cid = `NEW_BANK` + %is_draft = if_abap_behv=>mk-on + %data = VALUE #( BankCountry = `DE` + BankInternalID = `99999999` + LongBankName = `My Bank` + SWIFTCode = `DEUTDEFF` ) ) ) + MAPPED DATA(mapped) FAILED DATA(failed) REPORTED DATA(reported). COMMIT ENTITIES. ``` +::: + +## Your First Draft App (Minimal) + +Before the full-featured version, here is the **smallest app that actually works**. It does exactly three things: read the record, let the user edit, and save with one button. Start here — once this makes sense, the advanced version is just more buttons. + +```abap +CLASS z2ui5_cl_sample_draft_min DEFINITION PUBLIC. + PUBLIC SECTION. + INTERFACES z2ui5_if_app. + DATA bank_country TYPE c LENGTH 3 VALUE `DE`. + DATA bank_internal_id TYPE c LENGTH 15 VALUE `50070010`. + DATA swift_code TYPE c LENGTH 11. + DATA long_bank_name TYPE string. + PROTECTED SECTION. + DATA client TYPE REF TO z2ui5_if_client. + METHODS view_display. + PRIVATE SECTION. +ENDCLASS. + +CLASS z2ui5_cl_sample_draft_min IMPLEMENTATION. + METHOD z2ui5_if_app~main. + me->client = client. + + IF client->check_on_init( ). + " 1. Open a draft so we can edit + MODIFY ENTITIES OF i_banktp ENTITY Bank EXECUTE Edit + FROM VALUE #( ( %key-BankCountry = bank_country + %key-BankInternalID = bank_internal_id ) ) + FAILED DATA(f1) REPORTED DATA(r1). + COMMIT ENTITIES. + + " 2. Read the draft values into our fields + READ ENTITIES OF i_banktp ENTITY Bank + FIELDS ( SWIFTCode LongBankName ) + WITH VALUE #( ( %key-BankCountry = bank_country + %key-BankInternalID = bank_internal_id + %is_draft = if_abap_behv=>mk-on ) ) + RESULT DATA(drafts). + IF drafts IS NOT INITIAL. + swift_code = drafts[ 1 ]-SWIFTCode. + long_bank_name = drafts[ 1 ]-LongBankName. + ENDIF. + view_display( ). + + ELSEIF client->check_on_event( `SAVE` ). + " 3. Push the typed values into the draft, then activate (= save) + MODIFY ENTITIES OF i_banktp ENTITY Bank + UPDATE FIELDS ( SWIFTCode LongBankName ) + WITH VALUE #( ( %key-BankCountry = bank_country + %key-BankInternalID = bank_internal_id + %is_draft = if_abap_behv=>mk-on + SWIFTCode = swift_code + LongBankName = long_bank_name + %control-SWIFTCode = if_abap_behv=>mk-on + %control-LongBankName = if_abap_behv=>mk-on ) ) + FAILED DATA(f2) REPORTED DATA(r2). + + MODIFY ENTITIES OF i_banktp ENTITY Bank EXECUTE Activate + FROM VALUE #( ( %key-BankCountry = bank_country + %key-BankInternalID = bank_internal_id ) ) + FAILED DATA(f3) REPORTED DATA(r3). + COMMIT ENTITIES. + + client->message_toast_display( `Saved!` ). + ENDIF. + ENDMETHOD. + + METHOD view_display. + DATA(view) = z2ui5_cl_xml_view=>factory( ). + view->shell( + )->page( `Edit Bank (minimal draft)` + )->simple_form( editable = abap_true + )->content( `form` + )->label( `Bank Name` + )->input( client->_bind_edit( long_bank_name ) + )->label( `SWIFT Code` + )->input( client->_bind_edit( swift_code ) + )->button( + text = `Save` + type = `Emphasized` + press = client->_event( `SAVE` ) ). + client->view_display( view->stringify( ) ). + ENDMETHOD. +ENDCLASS. +``` + +That's a complete, working draft app. The three numbered comments map one-to-one onto the lifecycle diagram: **open → edit → activate**. -### Tutorial — Building a Bank Edit App +::: warning Why two steps to save? +Notice that *Save* does two things: `UPDATE FIELDS` (copy the user's typed values into the draft) **then** `Activate` (promote the draft to active). The fields are two-way bound with `_bind_edit`, so on each roundtrip the user's input lives in your ABAP variables — but it is **not** in the draft yet until you push it back with `UPDATE FIELDS`. Forgetting this step is the most common beginner mistake: the activate succeeds but saves the *old* values. +::: -With the primitives in hand, the rest of this page walks step by step through a full abap2UI5 app that edits a bank record through its standard BO draft. +## The Full App — A Real-World Edit Screen -On S/4HANA or BTP ABAP Environment (Steampunk), many business objects already ship as draft-enabled BOs — `I_BankTP` is one of them. You don't build a BO and you don't create a draft table — SAP provides both. Your abap2UI5 app just calls the standard BO via EML, which sidesteps the whole lock-during-think-time problem. +The minimal app always opens a draft on startup, which isn't ideal: it locks the record the moment anyone looks at it. A production app needs a few more behaviors: -The session can stay **stateless**: the draft survives between roundtrips in SAP's draft-shadow table, and the lock is held by the BO framework as long as the draft exists. Closing the browser without activating or discarding leaves the draft for the same user to resume later — no `set_session_stateful( )`, no `ENQUEUE_*`, no custom Z table. +- **Start read-only.** Show the active record first; only lock when the user clicks *Edit*. +- **Two modes.** *VIEW* (read-only, no draft) and *EDIT* (draft open, inputs editable). +- **Handle an existing draft.** What if the user (or someone else) already has a draft open for this record? +- **Confirm before discarding.** Don't silently throw away work the user typed. + +The rest of this page builds that app step by step. It uses two modes: -The app uses two modes: - **VIEW mode** — read-only display of the active database record. No lock, no draft. -- **EDIT mode** — a draft is open; inputs are editable. The BO framework holds the lock. +- **EDIT mode** — a draft is open; inputs are editable. The RAP framework holds the lock. -Toggling between modes drives the entire draft lifecycle (acquire, resume, save, activate, discard). +Toggling between the modes drives the entire draft lifecycle (acquire, resume, save, activate, discard). #### 1. App Startup — Always Begin in View Mode -On `on_init` the app reads the active record and renders it read-only. No draft is touched yet, so other users can still edit the same record. +On `on_init` the app reads the **active** record and renders it read-only. No draft is touched yet, so other users can still edit the same record. ```abap METHOD on_init. mode = `VIEW`. @@ -109,7 +253,7 @@ METHOD read_active. ENDIF. ENDMETHOD. ``` -`%is_draft = if_abap_behv=>mk-off` makes the read return the active row, not any open draft. +`%is_draft = if_abap_behv=>mk-off` makes the read return the **active** row, not any open draft. #### 2. Entering Edit Mode — Check for an Existing Draft When the user clicks **Switch to Edit Mode**, the app first looks in the BO's draft-shadow table (here `cabnk_bank_d`) joined with `sdraft_admin` to find out whether a draft already exists for this key — and who owns it. @@ -154,7 +298,9 @@ Three branches: - **same user** — show a popup so the user can resume or throw the draft away, - **other user** — refuse and stay in VIEW with an explanatory status text. -The shadow-table name (`cabnk_bank_d`) is BO-specific; check `Draft Table` in the behavior definition for your own BO. +::: tip +The shadow-table name (`cabnk_bank_d`) is BO-specific. To find it for your own BO, check the `draft table` keyword in its behavior definition. +::: #### 3. Acquiring a Fresh Draft A new draft is created by the `Edit` action. `%param-preserve_changes = abap_true` tells RAP to keep any existing draft instead of overwriting it — defensive even though we already checked. @@ -180,7 +326,7 @@ METHOD draft_acquire. draft_open = abap_true. ENDMETHOD. ``` -After a successful `Edit`, the lock is held by the BO framework and a row exists in the draft-shadow table. `draft_open = abap_true` switches the view's inputs from read-only to editable. +After a successful `Edit`, the lock is held by the RAP framework and a row exists in the draft-shadow table. `draft_open = abap_true` switches the view's inputs from read-only to editable. #### 4. Resuming an Existing Draft If the user picks **Resume Draft**, the `Resume` action re-takes the lock on the existing draft without overwriting its contents. @@ -805,6 +951,20 @@ ENDCLASS. ``` ::: +## Common Pitfalls + +A quick checklist of the mistakes beginners hit most often: + +| Symptom | Cause | Fix | +|---|---|---| +| Activate saves the *old* values | Typed values were never pushed into the draft | Call `UPDATE FIELDS` **before** `Activate` (step 6) | +| Nothing is persisted at all | Missing `COMMIT ENTITIES` | EML stays in the buffer until you commit | +| Read returns the active record, not the draft | Wrong `%is_draft` flag | Use `mk-on` to read the draft, `mk-off` for the active record | +| "Record is locked" errors | A leftover draft from a previous session | `Resume` or `Discard` the existing draft (step 2) | +| Changes silently lost on exit | No save before leaving edit mode | Save (or prompt to keep) before switching back to VIEW | + +For the full story on inspecting `FAILED` / `REPORTED` after EML calls, see the **Failure Handling** section in [EML](./eml.md). + ::: tip The field names (`BankCountry`, `BankInternalID`, `SWIFTCode`, `LongBankName`) and the draft-shadow table (`cabnk_bank_d`) match the released `I_BankTP` on current S/4HANA — on other releases the BO name, fields, or shadow table may differ. If no standard BO covers your object, define your own draft-enabled RAP BO (with its own `draft table z…_d`, `lock master`, etc.) and consume it the same way; see the [SAP RAP draft documentation](https://help.sap.com/docs/abap-cloud/abap-rap/draft). If you need locks for non-draft objects, see [Locks](../expert_more/lock.md). :::