Merge branch 'main' into hierarch-draft-crud
This commit is contained in:
@@ -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)
|
||||
###
|
||||
|
||||
@@ -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" }
|
||||
|
||||
13
common/currencies.cds
Normal file
13
common/currencies.cds
Normal file
@@ -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'
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
22
common/regions.cds
Normal file
22
common/regions.cds
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
42
fiori/app/genres/tree-view.cds
Normal file
42
fiori/app/genres/tree-view.cds
Normal file
@@ -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,
|
||||
};
|
||||
6
fiori/app/genres/value-help.cds
Normal file
6
fiori/app/genres/value-help.cds
Normal file
@@ -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',
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
50
fiori/server.js
Normal file
50
fiori/server.js
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user