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
This commit is contained in:
@@ -62,24 +62,10 @@ annotate AdminService.Books with {
|
|||||||
ValueListProperty: 'ID',
|
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
|
// Hide ID because of the ValueHelp
|
||||||
annotate AdminService.Genres with {
|
annotate AdminService.Genres with {
|
||||||
ID @UI.Hidden;
|
ID @UI.Hidden;
|
||||||
@@ -124,4 +110,3 @@ extend service AdminService {
|
|||||||
|
|
||||||
// Workaround for Fiori popup for asking user to enter a new UUID on Create
|
// Workaround for Fiori popup for asking user to enter a new UUID on Create
|
||||||
annotate AdminService.Books with { ID @Core.Computed; }
|
annotate AdminService.Books with { ID @Core.Computed; }
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
using { sap.capire.bookshop as my } from '@capire/bookstore';
|
using { sap.capire.bookshop as my } from '@capire/bookstore';
|
||||||
using { sap.common } from '@capire/common';
|
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';
|
author @ValueList.entity : 'Authors';
|
||||||
};
|
};
|
||||||
|
|
||||||
annotate Currencies with {
|
annotate common.Currencies with {
|
||||||
symbol @Common.Label : '{i18n>Currency}';
|
symbol @Common.Label : '{i18n>Currency}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,95 +68,6 @@ annotate my.Books with {
|
|||||||
image @title: '{i18n>Image}';
|
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
|
// Authors List
|
||||||
|
|||||||
@@ -1,3 +1,33 @@
|
|||||||
/*
|
using { sap.capire.bookshop.Genres } from '@capire/bookstore';
|
||||||
All annotations needed for UI5 Tree Table View are located in '../common'
|
|
||||||
*/
|
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,
|
"earlyRequests": true,
|
||||||
"groupProperties": {
|
"groupProperties": {
|
||||||
"default": {
|
"default": {
|
||||||
"submit": "Auto"
|
"submit": "Auto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,17 +82,17 @@
|
|||||||
"Genres": {
|
"Genres": {
|
||||||
"detail": {
|
"detail": {
|
||||||
"route": "GenresDetails"
|
"route": "GenresDetails"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"controlConfiguration": {
|
"controlConfiguration": {
|
||||||
"@com.sap.vocabularies.UI.v1.LineItem": {
|
"@com.sap.vocabularies.UI.v1.LineItem": {
|
||||||
"tableSettings": {
|
"tableSettings": {
|
||||||
"hierarchyQualifier": "GenreHierarchy",
|
"hierarchyQualifier": "GenresHierarchy",
|
||||||
"type": "TreeTable"
|
"type": "TreeTable"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -121,4 +121,4 @@
|
|||||||
"registrationIds": [],
|
"registrationIds": [],
|
||||||
"archeType": "transactional"
|
"archeType": "transactional"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
using from './admin-authors/fiori-service';
|
using from './admin-authors/fiori-service';
|
||||||
using from './admin-books/fiori-service';
|
using from './admin-books/fiori-service';
|
||||||
using from './browse/fiori-service';
|
using from './browse/fiori-service';
|
||||||
|
using from './genres/fiori-service';
|
||||||
|
|
||||||
using from './common';
|
using from './common';
|
||||||
using from '@capire/bookstore/srv/mashup';
|
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