Manage Products
Created with the 'Worklist App' tutorial. - the UI5 demo app "Manage Products", rebuilt as one self-contained abap2UI5 class.
Z2UI5_CL_SMPC_DEMO_001
Controls
sap.m
UI5 1.71
The facts
- Repository
- abap2UI5/samples-controls
- Class
Z2UI5_CL_SMPC_DEMO_001- Source file
z2ui5_cl_smpc_demo_001.clas.abap↗- Library
- sap.m
- Minimum UI5
- 1.71 — the floor abap2UI5 holds its samples to
- In the playground
- runs in the browser, with no system and nothing installed
Keywords: manage, products, app, title, icontabbar, icontabfilter, icontabseparator, table, toolbar, toolbarspacer, searchfield, column
Controls it builds
- sap.f.semantic.NegativeAction
- sap.f.semantic.PositiveAction
- sap.f.semantic.SemanticPage
- sap.f.semantic.SendEmailAction
- sap.m.App
- sap.m.Button
- sap.m.Column
- sap.m.ColumnListItem
- sap.m.FeedInput
- sap.m.FeedListItem
- sap.m.FlexBox
- sap.m.FlexItemData
- sap.m.HBox
- sap.m.IconTabBar
- sap.m.IconTabFilter
- sap.m.IconTabSeparator
- sap.m.Label
- sap.m.List
- sap.m.ObjectAttribute
- sap.m.ObjectIdentifier
- sap.m.ObjectNumber
- sap.m.ObjectStatus
- sap.m.Page
- sap.m.Panel
- sap.m.ProgressIndicator
- sap.m.SearchField
- sap.m.Shell
- sap.m.Table
- sap.m.Text
- sap.m.Title
- sap.m.Toolbar
- sap.m.ToolbarSpacer
- sap.m.VBox
- sap.ui.core.Icon
- sap.ui.core.mvc.View
- sap.ui.layout.VerticalLayout
- sap.ui.layout.form.SimpleForm
Read out of the builder chain by the abap2UI5 linter, not from the category this sample is filed under — which is what makes “which samples build one of these” a question the catalogue can answer at all.
Libraries
Run it here
The sample running, in this page — the app on its own, with no editor over it.
Switch to Playground with this code
Nothing is installed and nothing is sent anywhere: the ABAP is compiled to JavaScript in this browser and runs against abap2UI5 itself.
The ABAP
z2ui5_cl_smpc_demo_001.clas.abap — 991 lines
Click a number to link to a line — shift-click for a range.
Read it on GitHub ↗
" @keywords manage products app title icontabbar icontabfilter icontabseparator table toolbar toolbarspacer searchfield column
" @summary Created with the 'Worklist App' tutorial. - the UI5 demo app "Manage Products", rebuilt as one self-contained abap2UI5 class.
" @origin demo app Manage Products (sap.m/tutorial/worklist) - https://sdk.openui5.org/demoapps (status: generated - machine-written, not yet reviewed)
"! <p class="shorttext">demo app - Manage Products</p>
"!
"! The UI5 demo app Manage Products (the "Worklist App" tutorial, step 07) -
"! a whole application rather than a control sample, rebuilt as ONE abap2UI5
"! class, page for page and control for control: the worklist with its quick
"! filters and their counts, the case-sensitive search, the multi-selection
"! and the two mass actions with their MessageBox and MessageToasts, the
"! object page with the supplier form and the comments feed, the share
"! menu's e-mail on both pages, and the two not-found pages. The original's
"! App and its router are kept: the four targets of its manifest.json are
"! the four pages of one App here, and the URL hash decides which one shows
"! - "" is the worklist, "Products/{objectId}" the object page (or the
"! object-not-found page for an id the stock does not hold), anything else
"! the router's bypassed not-found page. Opening a product and the "Show
"! Manage Products" button push a hash as navTo( ) does, so the browser Back
"! button, a reload and a deep link work the way the original's router makes
"! them work.
"!
"! Texts are the original's i18n.properties, resolved in English. Two of its
"! keys are missing from its own bundle (the view binds
"! i18n>TableNameColumnTitle, the bundle carries tableNameColumnTitle; the
"! controller asks for TableSelectProduct, the bundle carries
"! TableNoProductsSelected), so the demo kit shows the raw key in the name
"! column header and in the no-selection MessageBox - and so does this class.
"!
"! Where it differs from the original, and why - only what an app without a
"! browser-side model layer cannot do the same way:
"!
"! - the OData V2 service and its mock server become ABAP data: the 14
"! products and 7 suppliers of the mock, verbatim, with the supplier joined
"! into the row as the original's expand does. Filtering, searching,
"! sorting and the four $count reads run in ABAP with the mock server's
"! semantics: a case-sensitive substringof on ProductName, the
"! code-point order of $orderby. The table remembers the selection of rows
"! a filter hides, as its rememberSelections does, while the mass actions
"! act on the rows on show, as getSelectedItems( ) does.
"! - the formatter module (numberUnit, quantityState) is business logic and
"! moves to the backend with it - the view binds the finished text and the
"! finished ValueState.
"! - a posted comment is stamped by the server clock in the user's time zone
"! (sy-datlo/sy-timlo) in the en medium DateTimeFormat ("Sep 24, 2026,
"! 9:05:30 AM") where the original formats the browser clock; the CLDR
"! pattern puts a narrow no-break space before AM/PM, this a plain one.
"! - the not-found pages show an IllustratedMessage, @since 1.98, and a Page
"! titleAlignment, @since 1.72, where this package holds the 1.71 floor.
"! The 1.71 equivalent is built instead: the same Page, HBox and button,
"! with an icon, the illustration type's default title ("Sorry, we can't
"! find this page") and the description in a centered VBox; the page
"! title is centered on the 1.71 themes and follows the theme on newer
"! ones.
"! - the share menu's e-mail goes through abap2UI5's URLHELPER action,
"! which is sap.m.URLHelper.triggerEmail itself, with the original's
"! subject and body and the page's location.href. That action refuses a
"! CR/LF in a parameter, so the "\r\n" of the body text is a space here;
"! the worklist's href is read when the e-mail is sent, not when the
"! worklist was first created.
"! - the busy handling (busyIndicatorDelay, the app and object view models'
"! busy flags) and the ErrorHandler's service-error MessageBox have
"! nothing to wait for or to fail: the data is already on the server.
"!
"! Original: src/sap.m/test/sap/m/demokit/tutorial/worklist/07 in OpenUI5,
"! archived under ui5/demoapps/sap.m/tutorial/worklist.
"! Demo apps: https://sdk.openui5.org/demoapps
CLASS z2ui5_cl_smpc_demo_001 DEFINITION PUBLIC.
PUBLIC SECTION.
INTERFACES z2ui5_if_app.
TYPES:
BEGIN OF ty_s_row,
productid TYPE i,
productname TYPE string,
suppliername TYPE string,
unitprice_text TYPE string,
unitsonorder_text TYPE string,
unitsinstock_text TYPE string,
unitsinstock_state TYPE string,
selected TYPE abap_bool,
END OF ty_s_row.
TYPES:
BEGIN OF ty_s_comment,
productid TYPE i,
type TYPE string,
date TYPE string,
comment TYPE string,
END OF ty_s_comment.
DATA t_rows TYPE STANDARD TABLE OF ty_s_row WITH EMPTY KEY.
DATA t_comments TYPE STANDARD TABLE OF ty_s_comment WITH EMPTY KEY.
" the worklistView model of the original
DATA filter_key TYPE string VALUE `all`.
DATA table_title TYPE string VALUE `ProductsPlural`.
DATA table_no_data TYPE string VALUE `No ProductsPlural are currently available`.
DATA count_all TYPE string VALUE `0`.
DATA count_instock TYPE string VALUE `0`.
DATA count_shortage TYPE string VALUE `0`.
DATA count_outofstock TYPE string VALUE `0`.
" the bound product of the object page
DATA obj_productname TYPE string.
DATA obj_productid TYPE string.
DATA obj_unitprice TYPE string.
DATA obj_unitsinstock TYPE string.
DATA obj_units_display TYPE string.
DATA obj_units_percent TYPE i.
" ObjectNumber.state is enum-typed: an empty value is rejected outright
" (validateProperty), so the worklist start carries the UI5 default until
" a product is opened
DATA obj_units_state TYPE string VALUE `None`.
DATA obj_discontinued TYPE abap_bool.
DATA obj_suppliername TYPE string.
DATA obj_address TYPE string.
DATA obj_postal_city TYPE string.
DATA obj_country TYPE string.
PROTECTED SECTION.
TYPES:
BEGIN OF ty_s_product,
productid TYPE i,
productname TYPE string,
supplierid TYPE i,
unitsinstock TYPE i,
unitsonorder TYPE i,
unitprice TYPE i,
discontinued TYPE abap_bool,
END OF ty_s_product.
TYPES:
BEGIN OF ty_s_supplier,
supplierid TYPE i,
companyname TYPE string,
address TYPE string,
postalcode TYPE string,
city TYPE string,
country TYPE string,
END OF ty_s_supplier.
DATA client TYPE REF TO z2ui5_if_client.
DATA t_products TYPE STANDARD TABLE OF ty_s_product WITH EMPTY KEY.
DATA t_suppliers TYPE STANDARD TABLE OF ty_s_supplier WITH EMPTY KEY.
" the productFeedback model: every comment, of every product
DATA t_feedback TYPE STANDARD TABLE OF ty_s_comment WITH EMPTY KEY.
" the table's remembered selection (rememberSelections), rows a quick
" filter or the search hides included
DATA t_selected TYPE STANDARD TABLE OF i WITH EMPTY KEY.
" the query of the last search event - the Application filter
DATA search_query TYPE string.
DATA product_shown TYPE i.
" the router target on show - the id of its page in the App
DATA page TYPE string VALUE `worklist`.
METHODS view_display.
METHODS page_worklist
IMPORTING
parent TYPE REF TO z2ui5_cl_ui5_view_builder.
METHODS page_object
IMPORTING
parent TYPE REF TO z2ui5_cl_ui5_view_builder.
METHODS page_not_found
IMPORTING
parent TYPE REF TO z2ui5_cl_ui5_view_builder
id TYPE string
illustration TYPE string
title TYPE string
description TYPE string
link TYPE string.
METHODS on_event.
METHODS on_mass_action.
METHODS route_match
RETURNING
VALUE(result) TYPE string.
METHODS app_hash
RETURNING
VALUE(result) TYPE string.
METHODS page_to
IMPORTING
target TYPE string.
METHODS object_bind
IMPORTING
productid TYPE i
RETURNING
VALUE(result) TYPE abap_bool.
METHODS comments_refresh.
METHODS list_refresh.
METHODS share_email
IMPORTING
subject TYPE string
body TYPE string.
METHODS json_text
IMPORTING
val TYPE string
RETURNING
VALUE(result) TYPE string.
METHODS date_medium
RETURNING
VALUE(result) TYPE string.
METHODS number_unit
IMPORTING
val TYPE i
RETURNING
VALUE(result) TYPE string.
METHODS quantity_state
IMPORTING
val TYPE i
RETURNING
VALUE(result) TYPE string.
METHODS model_init.
PRIVATE SECTION.
ENDCLASS.
CLASS z2ui5_cl_smpc_demo_001 IMPLEMENTATION.
METHOD z2ui5_if_app~main.
me->client = client.
IF client->check_on_init( ).
model_init( ).
list_refresh( ).
view_display( ).
" index.html's <title>
client->follow_up_action( val = client->cs_event-set_title
t_arg = VALUE #( ( `Manage Products` ) ) ).
ELSEIF client->check_on_navigated( ).
view_display( ).
ELSEIF client->check_on_event( ).
on_event( ).
ENDIF.
ENDMETHOD.
METHOD view_display.
" the router's initialize( ): the live hash rides in s_config-hash on
" every request, so a reload or a shared link starts on the page its hash
" names - the App opens on it (initialPage)
page = route_match( ).
DATA(view) = z2ui5_cl_ui5_view_builder=>factory(
)->ele( n = `View` ns = `mvc`
)->a( n = `displayBlock` v = `true`
)->a( n = `xmlns` v = `sap.m`
)->a( n = `xmlns:mvc` v = `sap.ui.core.mvc`
)->a( n = `xmlns:core` v = `sap.ui.core`
)->a( n = `xmlns:semantic` v = `sap.f.semantic`
)->a( n = `xmlns:form` v = `sap.ui.layout.form`
)->a( n = `xmlns:l` v = `sap.ui.layout` ).
" App.view.xml - the router's controlId "app", aggregation "pages"
DATA(app) = view->ele( `Shell`
)->ele( `App`
)->a( n = `id` v = `app`
)->a( n = `initialPage` t = page ).
page_worklist( app ).
page_object( app ).
page_not_found( parent = app
id = `objectNotFound`
illustration = `objectNotFoundIllustration`
title = `Products`
description = `This Product is not available`
link = `linkObject` ).
page_not_found( parent = app
id = `notFound`
illustration = `notFoundIllustration`
title = `Not Found`
description = `The requested resource was not found`
link = `link` ).
client->view_display( view->stringify( ) ).
" hash changes the app did not write itself - the browser Back/Forward
" buttons, a hand-edited URL - round-trip as HASH_CHANGED. Registered per
" render, since the registration dies with an app switch
client->follow_up_action( val = client->cs_event-hash_attach_changed
t_arg = VALUE #( ( `HASH_CHANGED` ) ) ).
ENDMETHOD.
METHOD page_worklist.
" Worklist.view.xml
DATA(worklist) = parent->ele( n = `SemanticPage` ns = `semantic`
)->a( n = `id` v = `worklist`
)->a( n = `headerPinnable` b = abap_false
)->a( n = `toggleHeaderOnTitleClick` b = abap_false
)->a( n = `showFooter` b = abap_true ).
worklist->ele( n = `titleHeading` ns = `semantic`
)->tag( `Title`
)->a( n = `text` v = `Manage Products` ).
" onQuickFilter: the tab's key is the Control filter of the list binding
DATA(table) = worklist->ele( n = `headerContent` ns = `semantic`
)->ele( `IconTabBar`
)->a( n = `id` v = `iconTabBar`
)->a( n = `select` v = client->_event( `FILTER` )
)->a( n = `expandable` b = abap_false
)->a( n = `selectedKey` v = client->_bind( filter_key )
)->ele( `items`
)->tag( `IconTabFilter`
)->a( n = `key` v = `all`
)->a( n = `showAll` b = abap_true
)->a( n = `count` v = client->_bind( count_all )
)->a( n = `text` v = `Products`
)->tag( `IconTabSeparator`
)->tag( `IconTabFilter`
)->a( n = `key` v = `inStock`
)->a( n = `icon` v = `sap-icon://message-success`
)->a( n = `iconColor` v = `Positive`
)->a( n = `count` v = client->_bind( count_instock )
)->a( n = `text` v = `Plenty in Stock`
)->tag( `IconTabFilter`
)->a( n = `key` v = `shortage`
)->a( n = `icon` v = `sap-icon://message-warning`
)->a( n = `iconColor` v = `Critical`
)->a( n = `count` v = client->_bind( count_shortage )
)->a( n = `text` v = `Shortage`
)->tag( `IconTabFilter`
)->a( n = `key` v = `outOfStock`
)->a( n = `icon` v = `sap-icon://message-error`
)->a( n = `iconColor` v = `Negative`
)->a( n = `count` v = client->_bind( count_outofstock )
)->a( n = `text` v = `Out of Stock`
)->end(
)->ele( `content`
)->ele( `Table`
)->a( n = `id` v = `table`
)->a( n = `growing` b = abap_true
)->a( n = `growingScrollToLoad` b = abap_true
)->a( n = `noDataText` v = client->_bind( table_no_data )
)->a( n = `width` v = `auto`
)->a( n = `mode` v = `MultiSelect`
)->a( n = `items` v = client->_bind( t_rows ) ).
" onSearch: the query travels as the event's argument - the field is not
" bound, as in the original, where nothing binds its value
table->ele( `headerToolbar`
)->ele( `Toolbar`
)->tag( `Title`
)->a( n = `id` v = `tableHeader`
)->a( n = `text` v = client->_bind( table_title )
)->tag( `ToolbarSpacer`
)->tag( `SearchField`
)->a( n = `id` v = `searchField`
)->a( n = `tooltip` v = `Enter a Product name or a part of it.`
)->a( n = `search` v = client->_event( val = `SEARCH` arg = `${$parameters>/query}` )
)->a( n = `width` v = `auto` ).
" the name column binds i18n>TableNameColumnTitle, a key the bundle does
" not carry - the demo kit shows the key itself
table->ele( `columns`
)->ele( `Column`
)->a( n = `id` v = `nameColumn`
)->tag( `Text`
)->a( n = `id` v = `nameColumnTitle`
)->a( n = `text` v = `TableNameColumnTitle`
)->end(
)->ele( `Column`
)->a( n = `id` v = `supplierNameColumn`
)->a( n = `demandPopin` b = abap_false
)->a( n = `minScreenWidth` v = `Tablet`
)->tag( `Text`
)->a( n = `text` v = `Supplier`
)->end(
)->ele( `Column`
)->a( n = `id` v = `unitPriceColumn`
)->a( n = `hAlign` v = `End`
)->a( n = `demandPopin` b = abap_true
)->a( n = `minScreenWidth` v = `Tablet`
)->tag( `Text`
)->a( n = `text` v = `Price`
)->end(
)->ele( `Column`
)->a( n = `id` v = `unitsOnOrderColumn`
)->a( n = `demandPopin` b = abap_true
)->a( n = `minScreenWidth` v = `Tablet`
)->a( n = `hAlign` v = `End`
)->tag( `Text`
)->a( n = `text` v = `Units Ordered`
)->end(
)->ele( `Column`
)->a( n = `id` v = `unitsInStockColumn`
)->a( n = `hAlign` v = `End`
)->tag( `Text`
)->a( n = `text` v = `Units in Stock` ).
" onPress: navTo( "object", { objectId: ProductID } )
table->ele( `items`
)->ele( `ColumnListItem`
)->a( n = `type` v = `Navigation`
)->a( n = `selected` v = `{SELECTED}`
)->a( n = `press` v = client->_event( val = `SHOW` arg = `${PRODUCTID}` )
)->ele( `cells`
)->tag( `ObjectIdentifier`
)->a( n = `title` v = `{PRODUCTNAME}`
)->tag( `Text`
)->a( n = `text` v = `{SUPPLIERNAME}`
)->tag( `ObjectNumber`
)->a( n = `unit` v = `EUR`
)->a( n = `number` v = `{UNITPRICE_TEXT}`
)->tag( `ObjectNumber`
)->a( n = `number` v = `{UNITSONORDER_TEXT}`
)->a( n = `unit` v = `PC`
)->tag( `ObjectNumber`
)->a( n = `number` v = `{UNITSINSTOCK_TEXT}`
)->a( n = `unit` v = `PC`
)->a( n = `state` v = `{UNITSINSTOCK_STATE}` ).
worklist->ele( n = `sendEmailAction` ns = `semantic`
)->tag( n = `SendEmailAction` ns = `semantic`
)->a( n = `id` v = `shareEmail`
)->a( n = `press` v = client->_event( `SHARE_EMAIL` ) ).
worklist->ele( n = `positiveAction` ns = `semantic`
)->tag( n = `PositiveAction` ns = `semantic`
)->a( n = `text` v = `Order`
)->a( n = `press` v = client->_event( `REORDER` ) ).
worklist->ele( n = `negativeAction` ns = `semantic`
)->tag( n = `NegativeAction` ns = `semantic`
)->a( n = `text` v = `Remove`
)->a( n = `press` v = client->_event( `UNLIST` ) ).
ENDMETHOD.
METHOD page_object.
" Object.view.xml - no footer and no back button, as there: the way back
" is the browser's Back button
DATA(object) = parent->ele( n = `SemanticPage` ns = `semantic`
)->a( n = `id` v = `object`
)->a( n = `headerPinnable` b = abap_false
)->a( n = `toggleHeaderOnTitleClick` b = abap_false ).
object->ele( n = `titleHeading` ns = `semantic`
)->tag( `Title`
)->a( n = `text` v = client->_bind( obj_productname ) ).
DATA(header) = object->ele( n = `headerContent` ns = `semantic`
)->ele( `FlexBox`
)->a( n = `alignItems` v = `Start`
)->a( n = `justifyContent` v = `SpaceBetween` ).
header->ele( `Panel`
)->a( n = `backgroundDesign` v = `Transparent`
)->tag( `ObjectAttribute`
)->a( n = `title` v = `Product ID`
)->a( n = `text` v = client->_bind( obj_productid )
)->tag( `ObjectAttribute`
)->a( n = `title` v = `Price`
)->a( n = `text` v = client->_bind( obj_unitprice ) ).
header->ele( `Panel`
)->a( n = `backgroundDesign` v = `Transparent`
)->tag( `ObjectNumber`
)->a( n = `id` v = `objectHeader`
)->a( n = `unit` v = `PC`
)->a( n = `textAlign` v = `End`
)->a( n = `state` v = client->_bind( obj_units_state )
)->a( n = `number` v = client->_bind( obj_unitsinstock )
)->tag( `ObjectStatus`
)->a( n = `text` v = `Discontinued`
)->a( n = `state` v = `Error`
)->a( n = `visible` v = client->_bind( obj_discontinued )
)->tag( `ProgressIndicator`
)->a( n = `width` v = `300px`
)->a( n = `percentValue` v = client->_bind( obj_units_percent )
)->a( n = `displayValue` v = client->_bind( obj_units_display )
)->a( n = `showValue` b = abap_true
)->a( n = `state` v = client->_bind( obj_units_state ) ).
DATA(body) = object->ele( n = `content` ns = `semantic`
)->ele( n = `VerticalLayout` ns = `l`
)->a( n = `width` v = `100%` ).
body->ele( `Panel`
)->a( n = `backgroundDesign` v = `Transparent`
)->a( n = `headerText` v = `Supplier Info`
)->ele( `content`
)->ele( n = `SimpleForm` ns = `form`
)->a( n = `maxContainerCols` v = `2`
)->a( n = `editable` b = abap_false
)->a( n = `layout` v = `ResponsiveGridLayout`
)->a( n = `labelSpanL` v = `3`
)->a( n = `labelSpanM` v = `3`
)->a( n = `emptySpanL` v = `4`
)->a( n = `emptySpanM` v = `4`
)->a( n = `columnsL` v = `1`
)->a( n = `columnsM` v = `1`
)->ele( n = `content` ns = `form`
)->tag( `Label`
)->a( n = `text` v = `Name`
)->tag( `Text`
)->a( n = `text` v = client->_bind( obj_suppliername )
)->tag( `Label`
)->a( n = `text` v = `Address`
)->tag( `Text`
)->a( n = `text` v = client->_bind( obj_address )
)->tag( `Label`
)->a( n = `text` v = `ZIP Code / City`
)->tag( `Text`
)->a( n = `text` v = client->_bind( obj_postal_city )
)->tag( `Label`
)->a( n = `text` v = `Country`
)->tag( `Text`
)->a( n = `text` v = client->_bind( obj_country ) ).
" onPost: the FeedInput empties itself and hands over its value
body->ele( `Panel`
)->a( n = `backgroundDesign` v = `Transparent`
)->a( n = `headerText` v = `Comments`
)->ele( `content`
)->tag( `FeedInput`
)->a( n = `post` v = client->_event( val = `POST` arg = `${$parameters>/value}` )
)->ele( `List`
)->a( n = `id` v = `idCommentsList`
)->a( n = `noDataText` v = `No Comments`
)->a( n = `showSeparators` v = `Inner`
)->a( n = `items` v = client->_bind( t_comments )
)->ele( `items`
)->tag( `FeedListItem`
)->a( n = `info` v = `{TYPE}`
)->a( n = `text` v = `{COMMENT}`
)->a( n = `timestamp` v = `{DATE}` ).
object->ele( n = `sendEmailAction` ns = `semantic`
)->tag( n = `SendEmailAction` ns = `semantic`
)->a( n = `id` v = `shareEmailObject`
)->a( n = `press` v = client->_event( `SHARE_EMAIL_OBJECT` ) ).
ENDMETHOD.
METHOD page_not_found.
" NotFound.view.xml and ObjectNotFound.view.xml. The IllustratedMessage
" (@since 1.98) becomes its 1.71 equivalent: an icon for the
" illustration, the PageNotFound type's default title, the description
" and the additional content, centered in a VBox that takes the
" IllustratedMessage's place and its FlexItemData
parent->ele( `Page`
)->a( n = `id` v = id
)->a( n = `title` v = title
)->ele( `HBox`
)->a( n = `height` v = `100%`
)->a( n = `alignItems` v = `Center`
)->ele( `VBox`
)->a( n = `id` v = illustration
)->a( n = `alignItems` v = `Center`
)->a( n = `justifyContent` v = `Center`
)->ele( `layoutData`
)->tag( `FlexItemData`
)->a( n = `growFactor` v = `1`
)->end(
)->tag( n = `Icon` ns = `core`
)->a( n = `src` v = `sap-icon://document`
)->a( n = `size` v = `6rem`
)->a( n = `class` v = `sapUiMediumMarginBottom`
)->tag( `Title`
)->a( n = `text` v = `Sorry, we can’t find this page`
)->a( n = `titleStyle` v = `H2`
)->a( n = `textAlign` v = `Center`
)->a( n = `wrapping` b = abap_true
)->tag( `Text`
)->a( n = `text` v = description
)->a( n = `textAlign` v = `Center`
)->a( n = `class` v = `sapUiSmallMarginTop`
)->tag( `Button`
)->a( n = `id` v = link
)->a( n = `text` v = `Show Manage Products`
)->a( n = `press` v = client->_event( `SHOW_WORKLIST` )
)->a( n = `class` v = `sapUiSmallMarginTop` ).
ENDMETHOD.
METHOD on_event.
CASE client->get_event( ).
WHEN `FILTER`.
" onQuickFilter: the list binding's filter( ), done where the data is
list_refresh( ).
WHEN `SEARCH`.
" onSearch/_applySearch: a Contains filter on ProductName, or none for
" an empty query; a real search swaps the no-data text for good
search_query = client->get_event_arg( ).
IF search_query IS NOT INITIAL.
table_no_data = `No matching ProductsPlural found`.
ENDIF.
list_refresh( ).
WHEN `SHOW`.
" onPress: navTo( "object", { objectId } ) pushes the hash; the
" route's patternMatched binds the page
client->hash_set( |/Products/{ client->get_event_arg( ) }| ).
page_to( COND #( WHEN object_bind( CONV i( client->get_event_arg( ) ) ) = abap_true
THEN `object`
ELSE `objectNotFound` ) ).
WHEN `SHOW_WORKLIST`.
" NotFound's onLinkPressed: navTo( "worklist" ) - a new history entry
client->hash_set( `/` ).
page_to( `worklist` ).
WHEN `HASH_CHANGED`.
" the router's hashChanged: show the target the hash now names
page_to( route_match( ) ).
WHEN `REORDER` OR `UNLIST`.
on_mass_action( ).
WHEN `POST`.
" onPost: the entry lands in the productFeedback model
INSERT VALUE #( productid = product_shown
type = `Comment`
date = date_medium( )
comment = client->get_event_arg( ) ) INTO TABLE t_feedback.
comments_refresh( ).
WHEN `SHARE_EMAIL`.
" onShareEmailPress with the worklistView model's two texts
share_email( subject = `Email subject PLEASE REPLACE ACCORDING TO YOUR USE CASE`
body = `Email body PLEASE REPLACE ACCORDING TO YOUR USE CASE` ).
WHEN `SHARE_EMAIL_OBJECT`.
" onShareEmailPress with the objectView model's two texts
share_email( subject = |Email subject including object identifier PLEASE REPLACE ACCORDING TO YOUR USE CASE { product_shown }|
body = |Email body PLEASE REPLACE ACCORDING TO YOUR USE CASE { obj_productname } (id: { product_shown })| ).
ENDCASE.
ENDMETHOD.
METHOD on_mass_action.
" onUpdateStockObjects / onUnlistObjects over getSelectedItems( ) - the
" selected rows ON SHOW, so a selection a filter hides is not acted on
DATA(done) = 0.
LOOP AT t_rows INTO DATA(row) WHERE selected = abap_true.
IF client->get_event( ) = `REORDER`.
ASSIGN t_products[ productid = row-productid ] TO FIELD-SYMBOL(<product>).
IF <product> IS ASSIGNED.
<product>-unitsinstock = <product>-unitsinstock + 10.
UNASSIGN <product>.
ENDIF.
ELSE.
DELETE t_products WHERE productid = row-productid.
ENDIF.
done = done + 1.
ENDLOOP.
IF done = 0.
" _showErrorMessage( getText( "TableSelectProduct" ) ) - a key the
" bundle does not carry, so MessageBox.error shows the key
client->message_box_display( text = `TableSelectProduct` type = `error` ).
RETURN.
ENDIF.
" the table's refresh and its updateFinished: rows, title and counts;
" the last request's handler toasts once
list_refresh( ).
client->message_toast_display( COND #( WHEN client->get_event( ) = `REORDER`
THEN `Product stock level updated`
ELSE `Product removed` ) ).
ENDMETHOD.
METHOD route_match.
" the manifest's routes: "" is the worklist, "Products/{objectId}" the
" object - crossroads matches the pattern case-insensitively - and
" anything else the bypassed target notFound
DATA(hash) = app_hash( ).
IF hash IS INITIAL.
result = `worklist`.
RETURN.
ENDIF.
SPLIT hash AT `/` INTO TABLE DATA(segments).
IF lines( segments ) <> 2 OR to_upper( segments[ 1 ] ) <> `PRODUCTS`.
result = `notFound`.
RETURN.
ENDIF.
" _onObjectMatched binds /Products(<id>); _onBindingChange displays the
" target objectNotFound when that entity does not exist (the conditions
" short-circuit, so only a number that fits reaches CONV i)
DATA(id) = segments[ 2 ].
result = `objectNotFound`.
IF id CO `0123456789` AND strlen( id ) <= 9 AND object_bind( CONV i( id ) ) = abap_true.
result = `object`.
ENDIF.
ENDMETHOD.
METHOD app_hash.
" the part of the URL hash the app's own routes live in: behind a
" launchpad's shell hash (`<intent>&/`) and behind abap2UI5's own
" segments (`/z2ui5-xapp-state=<id>`, `/app/<class>/<draft>`), without
" the slashes around it - `#/Products/1` reads `Products/1`
DATA(get) = client->get( ).
DATA(hash) = get-s_config-hash.
SHIFT hash LEFT DELETING LEADING `#`.
DATA(shell) = find( val = hash sub = `&/` ).
IF shell >= 0.
hash = substring( val = hash off = shell + 2 ).
ELSEIF get-check_launchpad_active = abap_true.
hash = ``.
ENDIF.
IF hash IS INITIAL.
RETURN.
ENDIF.
SPLIT hash AT `/` INTO TABLE DATA(segments).
DELETE segments WHERE table_line IS INITIAL.
IF segments IS NOT INITIAL AND segments[ 1 ] CP `z2ui5-xapp-state=*`.
DELETE segments INDEX 1.
ENDIF.
IF lines( segments ) >= 2 AND segments[ 1 ] = `app`.
DELETE segments FROM 1 TO 2.
IF segments IS NOT INITIAL AND strlen( segments[ 1 ] ) = 32.
DELETE segments INDEX 1.
ENDIF.
ENDIF.
result = concat_lines_of( table = segments sep = `/` ).
ENDMETHOD.
METHOD page_to.
" the target's display( ): the App navigates to the target's page
IF target = page.
RETURN.
ENDIF.
page = target.
client->follow_up_action( val = client->cs_event-control_by_id
t_arg = VALUE #( ( `app` ) ( `to` ) ( target ) ) ).
ENDMETHOD.
METHOD object_bind.
" the object view's bindElement( /Products(<id>) ) with the expanded
" supplier, through the formatters
ASSIGN t_products[ productid = productid ] TO FIELD-SYMBOL(<product>).
IF <product> IS NOT ASSIGNED.
RETURN.
ENDIF.
result = abap_true.
product_shown = productid.
obj_productname = <product>-productname.
obj_productid = number_unit( <product>-productid ).
obj_unitprice = |{ number_unit( <product>-unitprice ) } EUR|.
obj_unitsinstock = number_unit( <product>-unitsinstock ).
obj_units_display = |{ <product>-unitsinstock }|.
obj_units_percent = <product>-unitsinstock.
obj_units_state = quantity_state( <product>-unitsinstock ).
obj_discontinued = <product>-discontinued.
CLEAR: obj_suppliername, obj_address, obj_postal_city, obj_country.
ASSIGN t_suppliers[ supplierid = <product>-supplierid ] TO FIELD-SYMBOL(<supplier>).
IF <supplier> IS ASSIGNED.
obj_suppliername = <supplier>-companyname.
obj_address = <supplier>-address.
obj_postal_city = |{ <supplier>-postalcode } / { <supplier>-city }|.
obj_country = <supplier>-country.
ENDIF.
comments_refresh( ).
ENDMETHOD.
METHOD comments_refresh.
" _onBindingChange filters the feed list on productID; its sorter orders
" the date TEXT descending (localeCompare, equal texts in the order they
" were posted)
t_comments = VALUE #( FOR comment IN t_feedback WHERE ( productid = product_shown ) ( comment ) ).
SORT t_comments STABLE BY date DESCENDING AS TEXT.
ENDMETHOD.
METHOD list_refresh.
" declared here, not inline at the ASSIGN below, so it can be UNASSIGNed
" before each one - see there
FIELD-SYMBOLS <supplier> LIKE LINE OF t_suppliers.
" rememberSelections: the rows on show carry the live selection, the
" rows a filter hid keep theirs
LOOP AT t_rows INTO DATA(row).
DELETE t_selected WHERE table_line = row-productid.
IF row-selected = abap_true.
INSERT row-productid INTO TABLE t_selected.
ENDIF.
ENDLOOP.
t_rows = VALUE #( ).
LOOP AT t_products INTO DATA(product).
" the Control filter of the quick filter tab
CASE filter_key.
WHEN `inStock`.
IF product-unitsinstock <= 10.
CONTINUE.
ENDIF.
WHEN `shortage`.
IF product-unitsinstock < 1 OR product-unitsinstock > 10.
CONTINUE.
ENDIF.
WHEN `outOfStock`.
IF product-unitsinstock > 0.
CONTINUE.
ENDIF.
ENDCASE.
" the Application filter of the search: the mock server's substringof,
" case-sensitive
IF search_query IS NOT INITIAL AND find( val = product-productname sub = search_query ) < 0.
CONTINUE.
ENDIF.
" IS ASSIGNED, not sy-subrc (#1937: a SUCCESSFUL dynamic ASSIGN does not
" reset sy-subrc on every release, so a product whose supplier IS in the
" table lost its supplier name). UNASSIGN first because this sits in a
" LOOP: a failed ASSIGN leaves the previous iteration's binding in place,
" and IS ASSIGNED would then read TRUE - and print the WRONG supplier
UNASSIGN <supplier>.
ASSIGN t_suppliers[ supplierid = product-supplierid ] TO <supplier>.
INSERT VALUE #( productid = product-productid
productname = product-productname
suppliername = COND #( WHEN <supplier> IS ASSIGNED THEN <supplier>-companyname )
unitprice_text = number_unit( product-unitprice )
unitsonorder_text = number_unit( product-unitsonorder )
unitsinstock_text = number_unit( product-unitsinstock )
unitsinstock_state = quantity_state( product-unitsinstock )
selected = xsdbool( line_exists( t_selected[ table_line = product-productid ] ) ) ) INTO TABLE t_rows.
ENDLOOP.
" the sorter on ProductName ascending - the mock server's $orderby, which
" compares code points
SORT t_rows BY productname.
" onUpdateFinished: the title counts the rows on show, the four $count
" reads count the whole stock
table_title = COND #( WHEN t_rows IS INITIAL THEN `ProductsPlural` ELSE |Products ({ lines( t_rows ) })| ).
count_all = |{ lines( t_products ) }|.
count_instock = |{ REDUCE i( INIT n = 0 FOR p IN t_products NEXT n = COND #( WHEN p-unitsinstock > 10 THEN n + 1 ELSE n ) ) }|.
count_shortage = |{ REDUCE i( INIT s = 0 FOR q IN t_products NEXT s = COND #( WHEN q-unitsinstock BETWEEN 1 AND 10 THEN s + 1 ELSE s ) ) }|.
count_outofstock = |{ REDUCE i( INIT o = 0 FOR r IN t_products NEXT o = COND #( WHEN r-unitsinstock <= 0 THEN o + 1 ELSE o ) ) }|.
ENDMETHOD.
METHOD share_email.
" BaseController.onShareEmailPress: URLHelper.triggerEmail( null, subject,
" body ) - the body ends in location.href after a "\r\n", which the
" URLHELPER action refuses, so a space stands in for it
The first 900 lines of 991 — the whole class is on GitHub.
More in sap.m
- Combo box with value state — The combo box can show different value states.
- Icon Tab Bar - Background Design — In this example the backgroundDesign and headerBackgroundDesign properties are demonstrat…
- Icon Tab Bar - Process — In this example, the Icon Tab Bar is used to apply filters on the same content along a bu…
- Icon Tab Bar - Responsive Padding — This example demonstrates the Icon Tab Bar's support for paddings that depend on the widt…
- Icon Tab Bar - Tab Density Mode — In this example, the Icon Tab Bar is used in different tab density modes.
- Input - Assisted Tabular Suggestions — In this example assisted input is provided with table-like suggestions where several colu…
- Input - Checked — Input checks are handled via the validation of the data binding. In this example there ar…
- Input - States — This example demonstrates the different input field states, e.g. disabled, editable, with…
- Browse Orders — Master-detail app for browsing orders. - the UI5 demo app "Browse Orders", rebuilt as one…
- Team Calendar — Calendar demo app for team and team members. - the UI5 demo app "Team Calendar", rebuilt…
- Shopping Cart — The classic business process of finding and ordering products. - the UI5 demo app "Shoppi…
- Bulletin Board — Worklist app created with the 'Testing' tutorial. - the UI5 demo app "Bulletin Board", re…
On this page