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:
Daniel Hutzel
2025-06-05 15:39:50 +02:00
committed by GitHub
parent 6fa2aaee34
commit 600afb704a
8 changed files with 143 additions and 119 deletions

View File

@@ -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; }

View File

@@ -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

View File

@@ -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';

View 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,
};

View 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',
};

View File

@@ -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"
}
}
}

View File

@@ -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
View 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