From 987611b009cc456df3a2db467762327ae8d869db Mon Sep 17 00:00:00 2001 From: Daniel Hutzel Date: Wed, 14 May 2025 14:09:13 +0200 Subject: [PATCH 1/4] cosmetics --- common/currencies.cds | 13 ++++++++++++ common/index.cds | 47 ++----------------------------------------- common/regions.cds | 22 ++++++++++++++++++++ 3 files changed, 37 insertions(+), 45 deletions(-) create mode 100644 common/currencies.cds create mode 100644 common/regions.cds diff --git a/common/currencies.cds b/common/currencies.cds new file mode 100644 index 00000000..2b85915b --- /dev/null +++ b/common/currencies.cds @@ -0,0 +1,13 @@ +using { sap } from '@sap/cds/common'; + +extend sap.common.Currencies with { + // Currencies.code = ISO 4217 alphabetic three-letter code + // with the first two letters being equal to ISO 3166 alphabetic country codes + // See also: + // [1] https://www.iso.org/iso-4217-currency-codes.html + // [2] https://www.currency-iso.org/en/home/tables/table-a1.html + // [3] https://www.ibm.com/support/knowledgecenter/en/SSZLC2_7.0.0/com.ibm.commerce.payments.developer.doc/refs/rpylerl2mst97.htm + numcode : Integer; + exponent : Integer; //> e.g. 2 --> 1 Dollar = 10^2 Cent + minor : String; //> e.g. 'Cent' +} diff --git a/common/index.cds b/common/index.cds index cb89e28d..74f9033c 100644 --- a/common/index.cds +++ b/common/index.cds @@ -1,45 +1,2 @@ -using { sap } from '@sap/cds/common'; - -extend sap.common.Currencies with { - // Currencies.code = ISO 4217 alphabetic three-letter code - // with the first two letters being equal to ISO 3166 alphabetic country codes - // See also: - // [1] https://www.iso.org/iso-4217-currency-codes.html - // [2] https://www.currency-iso.org/en/home/tables/table-a1.html - // [3] https://www.ibm.com/support/knowledgecenter/en/SSZLC2_7.0.0/com.ibm.commerce.payments.developer.doc/refs/rpylerl2mst97.htm - numcode : Integer; - exponent : Integer; //> e.g. 2 --> 1 Dollar = 10^2 Cent - minor : String; //> e.g. 'Cent' -} - - -/** - * The Code Lists below are designed as optional extensions to - * the base schema. Switch them on by adding an Association to - * one of the code list entities in your models or by: - * annotate sap.common.Countries with @cds.persistence.skip:false; - */ - -context sap.common.countries { - - extend sap.common.Countries { - regions : Composition of many Regions on regions._parent = $self.code; - } - - entity Regions : sap.common.CodeList { - key code : String(5); // ISO 3166-2 alpha5 codes, e.g. DE-BW - children : Composition of many Regions on children._parent = $self.code; - cities : Composition of many Cities on cities.region = $self; - _parent : String(11); - } - entity Cities : sap.common.CodeList { - key code : String(11); - region : Association to Regions; - districts : Composition of many Districts on districts.city = $self; - } - entity Districts : sap.common.CodeList { - key code : String(11); - city : Association to Cities; - } - -} +using from './currencies'; +using from './regions'; diff --git a/common/regions.cds b/common/regions.cds new file mode 100644 index 00000000..73e93755 --- /dev/null +++ b/common/regions.cds @@ -0,0 +1,22 @@ +using { sap.common } from '@sap/cds/common'; +namespace sap.common.countries; + +extend common.Countries { + regions : Composition of many Regions on regions._parent = $self.code; +} + +entity Regions : common.CodeList { + key code : String(5); // ISO 3166-2 alpha5 codes, e.g. DE-BW + children : Composition of many Regions on children._parent = $self.code; + cities : Composition of many Cities on cities.region = $self; + _parent : String(11); +} +entity Cities : common.CodeList { + key code : String(11); + region : Association to Regions; + districts : Composition of many Districts on districts.city = $self; +} +entity Districts : common.CodeList { + key code : String(11); + city : Association to Cities; +} From 6fa2aaee34e862337c5bc5a413817355ab283437 Mon Sep 17 00:00:00 2001 From: Johannes Vogel <31311694+johannes-vogel@users.noreply.github.com> Date: Thu, 15 May 2025 15:43:21 +0200 Subject: [PATCH 2/4] fiori: rm unused hierarchy elements (#838) --- fiori/app/common.cds | 8 -------- 1 file changed, 8 deletions(-) diff --git a/fiori/app/common.cds b/fiori/app/common.cds index e4ebc121..7561f118 100644 --- a/fiori/app/common.cds +++ b/fiori/app/common.cds @@ -79,8 +79,6 @@ aspect Hierarchy { LimitedDescendantCount : Integer64 = null; DistanceFromRoot : Integer64 = null; DrillState : String = null; - Matched : Boolean = null; - MatchedDescendantCount : Integer64 = null; LimitedRank : Integer64 = null; } @@ -88,8 +86,6 @@ annotate Hierarchy with @Capabilities.FilterRestrictions.NonFilterableProperties 'LimitedDescendantCount', 'DistanceFromRoot', 'DrillState', - 'Matched', - 'MatchedDescendantCount', 'LimitedRank' ]; @@ -97,8 +93,6 @@ annotate Hierarchy with @Capabilities.SortRestrictions.NonSortableProperties: [ 'LimitedDescendantCount', 'DistanceFromRoot', 'DrillState', - 'Matched', - 'MatchedDescendantCount', 'LimitedRank' ]; @@ -121,8 +115,6 @@ annotate my.Genres with @Hierarchy.RecursiveHierarchy #GenreHierarchy: { LimitedDescendantCount: LimitedDescendantCount, DistanceFromRoot : DistanceFromRoot, DrillState : DrillState, - Matched : Matched, - MatchedDescendantCount: MatchedDescendantCount, LimitedRank : LimitedRank }; From 600afb704a02373cdb67a8e03c2de366a92477df Mon Sep 17 00:00:00 2001 From: Daniel Hutzel Date: Thu, 5 Jun 2025 15:39:50 +0200 Subject: [PATCH 3/4] Fiori Tree Views towards GA (#839) * Fiori Tree Views towards GA * Also automate addition of Aggregation.RecursiveHierarchy * Cleanup models for Genres Tree View * Implementing @hierarchy shortcut * . * Formatting * Using verbose config --- fiori/app/admin-books/fiori-service.cds | 15 ---- fiori/app/common.cds | 92 +------------------------ fiori/app/genres/fiori-service.cds | 36 +++++++++- fiori/app/genres/tree-view.cds | 42 +++++++++++ fiori/app/genres/value-help.cds | 6 ++ fiori/app/genres/webapp/manifest.json | 20 +++--- fiori/app/services.cds | 1 + fiori/server.js | 50 ++++++++++++++ 8 files changed, 143 insertions(+), 119 deletions(-) create mode 100644 fiori/app/genres/tree-view.cds create mode 100644 fiori/app/genres/value-help.cds create mode 100644 fiori/server.js diff --git a/fiori/app/admin-books/fiori-service.cds b/fiori/app/admin-books/fiori-service.cds index 6be07610..d432f1c5 100644 --- a/fiori/app/admin-books/fiori-service.cds +++ b/fiori/app/admin-books/fiori-service.cds @@ -62,24 +62,10 @@ annotate AdminService.Books with { ValueListProperty: 'ID', } ], - PresentationVariantQualifier: 'VH', } }); } -annotate AdminService.Genres with @UI: { - PresentationVariant #VH: { - $Type : 'UI.PresentationVariantType', - Visualizations : ['@UI.LineItem'], - RecursiveHierarchyQualifier: 'GenreHierarchy' - }, - LineItem : [{ - $Type: 'UI.DataField', - Value: name, - Label :'{i18n>Name}' - }], -}; - // Hide ID because of the ValueHelp annotate AdminService.Genres with { ID @UI.Hidden; @@ -124,4 +110,3 @@ extend service AdminService { // Workaround for Fiori popup for asking user to enter a new UUID on Create annotate AdminService.Books with { ID @Core.Computed; } - diff --git a/fiori/app/common.cds b/fiori/app/common.cds index 7561f118..f6e2e94f 100644 --- a/fiori/app/common.cds +++ b/fiori/app/common.cds @@ -4,7 +4,6 @@ using { sap.capire.bookshop as my } from '@capire/bookstore'; using { sap.common } from '@capire/common'; -using { sap.common.Currencies } from '@sap/cds/common'; //////////////////////////////////////////////////////////////////////////// // @@ -38,7 +37,7 @@ annotate my.Books with @( author @ValueList.entity : 'Authors'; }; -annotate Currencies with { +annotate common.Currencies with { symbol @Common.Label : '{i18n>Currency}'; } @@ -69,95 +68,6 @@ annotate my.Books with { image @title: '{i18n>Image}'; } -//////////////////////////////////////////////////////////////////////////// -// -// Computed Fields for Tree Tables -// -// DISCLAIMER: The below are an alpha version implementation and will change in final release !!! -// -aspect Hierarchy { - LimitedDescendantCount : Integer64 = null; - DistanceFromRoot : Integer64 = null; - DrillState : String = null; - LimitedRank : Integer64 = null; -} - -annotate Hierarchy with @Capabilities.FilterRestrictions.NonFilterableProperties: [ - 'LimitedDescendantCount', - 'DistanceFromRoot', - 'DrillState', - 'LimitedRank' -]; - -annotate Hierarchy with @Capabilities.SortRestrictions.NonSortableProperties: [ - 'LimitedDescendantCount', - 'DistanceFromRoot', - 'DrillState', - 'LimitedRank' -]; - -extend my.Genres with Hierarchy; - -//////////////////////////////////////////////////////////////////////////// -// -// Genres Tree Table Annotations -// -// DISCLAIMER: The below are an alpha version implementation and will change in final release !!! -// -annotate my.Genres with @Aggregation.RecursiveHierarchy #GenreHierarchy: { - $Type : 'Aggregation.RecursiveHierarchyType', - NodeProperty : ID, // identifies a node - ParentNavigationProperty: parent // navigates to a node's parent -}; - -annotate my.Genres with @Hierarchy.RecursiveHierarchy #GenreHierarchy: { - $Type : 'Hierarchy.RecursiveHierarchyType', - LimitedDescendantCount: LimitedDescendantCount, - DistanceFromRoot : DistanceFromRoot, - DrillState : DrillState, - LimitedRank : LimitedRank -}; - -annotate my.Genres with @( - readonly, - cds.search: {name} -); -//////////////////////////////////////////////////////////////////////////// -// -// Genres List -// -annotate my.Genres with @( - Common.SemanticKey : [name], - UI : { - SelectionFields : [name], - LineItem : [ - { Value : name, Label : '{i18n>Name}' }, - ], - } -); - -//////////////////////////////////////////////////////////////////////////// -// -// Genre Details -// -annotate my.Genres with @(UI : { - Identification : [{ Value: name}], - HeaderInfo : { - TypeName : '{i18n>Genre}', - TypeNamePlural : '{i18n>Genres}', - Title : { Value: name }, - Description : { Value: ID } - } -}); - -//////////////////////////////////////////////////////////////////////////// -// -// Genres Elements -// -annotate my.Genres with { - name @title: '{i18n>Genre}'; -} - //////////////////////////////////////////////////////////////////////////// // // Authors List diff --git a/fiori/app/genres/fiori-service.cds b/fiori/app/genres/fiori-service.cds index e1961cf0..834f2b57 100644 --- a/fiori/app/genres/fiori-service.cds +++ b/fiori/app/genres/fiori-service.cds @@ -1,3 +1,33 @@ -/* -All annotations needed for UI5 Tree Table View are located in '../common' -*/ +using { sap.capire.bookshop.Genres } from '@capire/bookstore'; + +annotate Genres with @cds.search: {name}; +annotate Genres with @readonly; +annotate Genres with { + name @title: '{i18n>Genre}'; +} + +// Lists +annotate Genres with @( + Common.SemanticKey : [name], + UI.SelectionFields : [name], + UI.LineItem : [ + { Value: name, Label: '{i18n>Name}' }, + ], +); + +// Details +annotate Genres with @(UI : { + Identification : [{ Value: name }], + HeaderInfo : { + TypeName : '{i18n>Genre}', + TypeNamePlural : '{i18n>Genres}', + Title : { Value: name }, + Description : { Value: ID } + } +}); + + +// Tree Views +// annotate AdminService.Genres with @hierarchy; // upcomming simplification +using from './tree-view'; +using from './value-help'; diff --git a/fiori/app/genres/tree-view.cds b/fiori/app/genres/tree-view.cds new file mode 100644 index 00000000..eeb80ea7 --- /dev/null +++ b/fiori/app/genres/tree-view.cds @@ -0,0 +1,42 @@ +using { AdminService } from '@capire/bookstore'; + +//////////////////////////////////////////////////////////////////////////// +// +// Genres Tree View +// + +// Tell Fiori about the structure of the hierarchy +annotate AdminService.Genres with @Aggregation.RecursiveHierarchy #GenresHierarchy : { + ParentNavigationProperty : parent, // navigates to a node's parent + NodeProperty : ID, // identifies a node, usually the key +}; + +// Fiori expects the following to be defined explicitly, even though they're always the same +extend AdminService.Genres with @( + // The columns expected by Fiori to be present in hierarchy entities + Hierarchy.RecursiveHierarchy #GenresHierarchy : { + LimitedDescendantCount : LimitedDescendantCount, + DistanceFromRoot : DistanceFromRoot, + DrillState : DrillState, + LimitedRank : LimitedRank + }, + // Disallow filtering on these properties from Fiori UIs + Capabilities.FilterRestrictions.NonFilterableProperties: [ + 'LimitedDescendantCount', + 'DistanceFromRoot', + 'DrillState', + 'LimitedRank' + ], + // Disallow sorting on these properties from Fiori UIs + Capabilities.SortRestrictions.NonSortableProperties : [ + 'LimitedDescendantCount', + 'DistanceFromRoot', + 'DrillState', + 'LimitedRank' + ], +) columns { // Ensure we can query these fields from database + null as LimitedDescendantCount : Int16, + null as DistanceFromRoot : Int16, + null as DrillState : String, + null as LimitedRank : Int16, +}; diff --git a/fiori/app/genres/value-help.cds b/fiori/app/genres/value-help.cds new file mode 100644 index 00000000..a0f29e85 --- /dev/null +++ b/fiori/app/genres/value-help.cds @@ -0,0 +1,6 @@ +// Value help with Tree View +using from '../admin-books/fiori-service'; +annotate AdminService.Books:genre with @Common.ValueList.PresentationVariantQualifier: 'VH'; +annotate AdminService.Genres with @UI.PresentationVariant #VH: { + RecursiveHierarchyQualifier : 'GenresHierarchy', +}; diff --git a/fiori/app/genres/webapp/manifest.json b/fiori/app/genres/webapp/manifest.json index a43f4a83..857a4203 100644 --- a/fiori/app/genres/webapp/manifest.json +++ b/fiori/app/genres/webapp/manifest.json @@ -51,7 +51,7 @@ "earlyRequests": true, "groupProperties": { "default": { - "submit": "Auto" + "submit": "Auto" } } } @@ -82,17 +82,17 @@ "Genres": { "detail": { "route": "GenresDetails" - } + } } }, "controlConfiguration": { - "@com.sap.vocabularies.UI.v1.LineItem": { - "tableSettings": { - "hierarchyQualifier": "GenreHierarchy", - "type": "TreeTable" - } - } - } + "@com.sap.vocabularies.UI.v1.LineItem": { + "tableSettings": { + "hierarchyQualifier": "GenresHierarchy", + "type": "TreeTable" + } + } + } } } }, @@ -121,4 +121,4 @@ "registrationIds": [], "archeType": "transactional" } -} +} \ No newline at end of file diff --git a/fiori/app/services.cds b/fiori/app/services.cds index 6949caae..8b47e508 100644 --- a/fiori/app/services.cds +++ b/fiori/app/services.cds @@ -5,6 +5,7 @@ using from './admin-authors/fiori-service'; using from './admin-books/fiori-service'; using from './browse/fiori-service'; +using from './genres/fiori-service'; using from './common'; using from '@capire/bookstore/srv/mashup'; diff --git a/fiori/server.js b/fiori/server.js new file mode 100644 index 00000000..0d030429 --- /dev/null +++ b/fiori/server.js @@ -0,0 +1,50 @@ +const cds = require('@sap/cds/lib') + +// PoC for simplified Fiori Tree Views +cds.on('compile.for.runtime', csn => { + for (let each of cds.linked(csn).definitions) { + if (each.is_entity && each._service && each['@hierarchy']) _hierarchy (each) + } +}) + + +const _hierarchy = entity => { + + // Add annotations explaining the hierarchy structure to Fiori + const Qualifier = entity.name.slice (entity._service.name.length+1) + 'Hierarchy' + const parent = _parent4(entity) + entity[`@Aggregation.RecursiveHierarchy#${Qualifier}.ParentNavigationProperty`] ??= {'=': parent.name } + entity[`@Aggregation.RecursiveHierarchy#${Qualifier}.NodeProperty`] ??= {'=': parent.keys[0].ref[0] } + + // Add expected hierarchy elements to the entity + const columns = entity.projection.columns ??= ['*'] + const elements = entity.elements + for (let e of Hierarchy.elements) { + entity[`@Hierarchy.RecursiveHierarchy#${Qualifier}.${e.name}`] = {'=': e.name } + if (e.name in elements) continue + const { name, value, ...rest } = e + elements[e.name] = Object.defineProperty ({ __proto__:e, ...rest }, 'parent', { value: entity }) + columns.push ({ ...value, as: name, cast: { type: e.type } }) + } + + // Disable filter and sort for hierarchy elements + entity['@Capabilities.FilterRestrictions.NonFilterableProperties'] = + entity['@Capabilities.SortRestrictions.NonSortableProperties'] = + Object.keys (Hierarchy.elements) +} + + +const _parent4 = entity => { + const parent = entity['@hierarchy.parent'] || entity['@hierarchy.via'] + if (parent) return entity.elements [parent['=']||parent] + else for (let e of entity.elements) // use first recursive uplink association + if (e.is2one && e._target === entity) return e +} + + +const { Hierarchy } = cds.linked `aspect Hierarchy { + LimitedDescendantCount : Int16 = null; + DistanceFromRoot : Int16 = null; + DrillState : String = null; + LimitedRank : Int16 = null; +}`.definitions From f8252207a0cd62426b47bbc63aafa53831ca3ad5 Mon Sep 17 00:00:00 2001 From: Eric Peairs Date: Tue, 24 Jun 2025 14:51:25 +0200 Subject: [PATCH 4/4] Fix: adjust IDs and size of Genre objects for testing Changed ID values of Genre objects to UUIDs and decreased depth of the POSTed Genre object to match the maximum depth of DELETE --- bookshop/test/genres.http | 27 +++++++++++++-------------- bookshop/test/requests.http | 5 ++--- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/bookshop/test/genres.http b/bookshop/test/genres.http index a1eb770f..7849f11f 100644 --- a/bookshop/test/genres.http +++ b/bookshop/test/genres.http @@ -14,25 +14,24 @@ GET http://localhost:4004/odata/v4/test/Genres? POST http://localhost:4004/odata/v4/test/Genres? Content-Type: application/json -{ "ID":100, "name":"Some Sample Genres...", "children":[ - { "ID":101, "name":"Cat", "children":[ - { "ID":102, "name":"Kitty", "children":[ - { "ID":103, "name":"Kitty Cat", "children":[ - { "ID":104, "name":"Aristocat" } ]}, - { "ID":105, "name":"Kitty Bat" } ]}, - { "ID":106, "name":"Catwoman", "children":[ - { "ID":107, "name":"Catalina" } ]} ]}, - { "ID":108, "name":"Catweazle" } +{ "ID":"100aaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "name":"Some Sample Genres...", "children":[ + { "ID":"101aaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "name":"Cat", "children":[ + { "ID":"102aaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "name":"Kitty", "children":[ + { "ID":"103aaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "name":"Aristocat" }, + { "ID":"104aaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "name":"Kitty Bat" } ]}, + { "ID":"105aaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "name":"Catwoman", "children":[ + { "ID":"106aaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "name":"Catalina" } ]} ]}, + { "ID":"107aaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "name":"Catweazle" } ]} ### -GET http://localhost:4004/odata/v4/test/Genres(100)? -# &$expand=children -# &$expand=children($expand=children($expand=children($expand=children))) +GET http://localhost:4004/odata/v4/test/Genres(100aaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa)? +&$expand=children +&$expand=children($expand=children($expand=children($expand=children))) ### -DELETE http://localhost:4004/odata/v4/test/Genres(103) +DELETE http://localhost:4004/odata/v4/test/Genres(103aaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa) ### -DELETE http://localhost:4004/odata/v4/test/Genres(100) +DELETE http://localhost:4004/odata/v4/test/Genres(100aaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa) ### diff --git a/bookshop/test/requests.http b/bookshop/test/requests.http index cbd8faff..e4a52674 100644 --- a/bookshop/test/requests.http +++ b/bookshop/test/requests.http @@ -40,8 +40,7 @@ Authorization: Basic alice: { "ID": 112, - "name": "Shakespeeeeere", - "age": 22 + "name": "Shakespeeeeere" } @@ -56,7 +55,7 @@ Authorization: Basic alice: "title": "Poems : Pocket Poets", "descr": "The Everyman's Library Pocket Poets hardcover series is popular for its compact size and reasonable price which does not compromise content. Poems: Bronte contains poems that demonstrate a sensibility elemental in its force with an imaginative discipline and flexibility of the highest order. Also included are an Editor's Note and an index of first lines.", "author": { "ID": 101 }, - "genre": { "ID": 12 }, + "genre": { "ID": "12aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" }, "stock": 5, "price": "12.05", "currency": { "code": "USD" }