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" } 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; +} diff --git a/fiori/app/admin-books/fiori-service.cds b/fiori/app/admin-books/fiori-service.cds index 39422ed0..9e860189 100644 --- a/fiori/app/admin-books/fiori-service.cds +++ b/fiori/app/admin-books/fiori-service.cds @@ -66,24 +66,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; @@ -129,4 +115,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 233bbf2d..f370c494 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,129 +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; - Matched : Boolean = null; - MatchedDescendantCount : Integer64 = null; - LimitedRank : Integer64 = null; -} - -annotate Hierarchy with @Capabilities.FilterRestrictions.NonFilterableProperties: [ - 'LimitedDescendantCount', - 'DistanceFromRoot', - 'DrillState', - 'Matched', - 'MatchedDescendantCount', - 'LimitedRank' -]; - -annotate Hierarchy with @Capabilities.SortRestrictions.NonSortableProperties: [ - 'LimitedDescendantCount', - 'DistanceFromRoot', - 'DrillState', - 'Matched', - 'MatchedDescendantCount', - 'LimitedRank' -]; - -extend my.Genres with Hierarchy; -extend my.Contents 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, - Matched : Matched, - MatchedDescendantCount: MatchedDescendantCount, - LimitedRank : LimitedRank -}; - -annotate my.Genres with @( - 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}'; -} - -//////////////////////////////////////////////////////////////////////////// -// -// Contents Tree Table Annotations -// -// DISCLAIMER: The below are an alpha version implementation and will change in final release !!! -// -annotate my.Contents with @Aggregation.RecursiveHierarchy #ContentsHierarchy: { - $Type: 'Aggregation.RecursiveHierarchyType', - NodeProperty: ID, // identifies a node - ParentNavigationProperty: parent // navigates to a node's parent -}; - -annotate my.Contents with @Hierarchy.RecursiveHierarchy #ContentsHierarchy: { - $Type: 'Hierarchy.RecursiveHierarchyType', - LimitedDescendantCount: LimitedDescendantCount, - DistanceFromRoot: DistanceFromRoot, - DrillState: DrillState, - Matched: Matched, - MatchedDescendantCount: MatchedDescendantCount, - LimitedRank: LimitedRank -}; - -annotate my.Contents with @( - cds.search: {name} -); - //////////////////////////////////////////////////////////////////////////// // // Contents 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