diff --git a/.cdsrc.yaml b/.cdsrc.yaml deleted file mode 100644 index 394fa0d..0000000 --- a/.cdsrc.yaml +++ /dev/null @@ -1,2 +0,0 @@ -fiori: - direct_crud: true diff --git a/app/appconfig/fioriSandboxConfig.json b/app/appconfig/fioriSandboxConfig.json index aae8e77..dedcb15 100644 --- a/app/appconfig/fioriSandboxConfig.json +++ b/app/appconfig/fioriSandboxConfig.json @@ -18,14 +18,6 @@ "title": "Browse Books", "targetURL": "#Books-display" } - }, - { - "id": "BrowseGenres", - "tileType": "sap.ushell.ui.tile.StaticTile", - "properties": { - "title": "Browse Genres", - "targetURL": "#Genres-display" - } } ] }, @@ -52,6 +44,14 @@ "targetURL": "#Authors-manage" } }, + { + "id": "ManageGenres", + "tileType": "sap.ushell.ui.tile.StaticTile", + "properties": { + "title": "Manage Genres", + "targetURL": "#Genres-manage" + } + }, { "id": "ManageOrders", "tileType": "sap.ushell.ui.tile.StaticTile", @@ -114,10 +114,10 @@ "url": "/admin-authors/webapp" } }, - "BrowseGenres": { + "ManageGenres": { "semanticObject": "Genres", - "action": "display", - "title": "Browse Genres", + "action": "manage", + "title": "Manage Genres", "signature": { "parameters": { "Genre.ID": { diff --git a/app/genres/fiori-service.cds b/app/genres/fiori-service.cds index 27a1079..53ad3ce 100644 --- a/app/genres/fiori-service.cds +++ b/app/genres/fiori-service.cds @@ -1,31 +1,87 @@ -using { sap.capire.bookshop.Genres } from '@capire/bookshop'; +using {sap.capire.bookshop.Genres} from '@capire/bookshop'; + +annotate AdminService.Genres with @odata.draft.enabled; 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}' }, - ], + 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 } +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; +// TODO: In my setup, using the @hierarchy annotation does not work as expected. +// TODO: > I am getting a UI error that reports 'NodeProperty' can't be read from undefined +// TODO: > Using the 'Manual Approach' seems to work. +// TODO: > Outdated versions? +// TODO: > +// TODO: > @sap/cds: 9.6.3 +// TODO: > @sap/cds-compiler: 6.6.0 +// TODO: > @sap/cds-dk: 9.6.1 +// TODO: > @sap/cds-dk (global): 9.6.1 +// TODO: > @sap/cds-fiori: 2.1.1 +// TODO: > @sap/cds-mtxs: 3.6.1 +// TODO: > @cap-js/asyncapi: 1.0.3 +// TODO: > @cap-js/cds-test: 0.4.1 +// TODO: > @cap-js/db-service: 2.8.1 +// TODO: > @cap-js/openapi: 1.3.1 +// TODO: > @cap-js/sqlite: 2.1.2 +// TODO: > Node.js: v22.21.1 +// annotate AdminService.Genres with @hierarchy; + +// 'Manual Approach' from capire +// declare a hierarchy with the qualifier "GenresHierarchy" +annotate AdminService.Genres with @Aggregation.RecursiveHierarchy #GenresHierarchy: { + NodeProperty : ID, // identifies a node, usually the key + ParentNavigationProperty: parent // navigates to a node's parent +}; + +extend AdminService.Genres with @( + // The computed properties 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 columns from the database + null as LimitedDescendantCount : Int16, + null as DistanceFromRoot : Int16, + null as DrillState : String, + null as LimitedRank : Int16 +}; diff --git a/app/genres/webapp/i18n/i18n.properties b/app/genres/webapp/i18n/i18n.properties index 42da067..ebbb467 100644 --- a/app/genres/webapp/i18n/i18n.properties +++ b/app/genres/webapp/i18n/i18n.properties @@ -1,4 +1,4 @@ #XTIT -appTitle=Browse Genres +appTitle=Manage Genres #XTXT appDescription=Genres as Tree View diff --git a/app/genres/webapp/i18n/i18n_de.properties b/app/genres/webapp/i18n/i18n_de.properties index e8714e9..9d0cacf 100644 --- a/app/genres/webapp/i18n/i18n_de.properties +++ b/app/genres/webapp/i18n/i18n_de.properties @@ -1,2 +1,2 @@ -appTitle=Zeige Genres +appTitle=Verwalte Genres appDescription=Genres als Baumansicht diff --git a/package.json b/package.json index b203a6a..c9e7096 100644 --- a/package.json +++ b/package.json @@ -4,19 +4,19 @@ "description": "Shows how to reuse and compose with CAP.", "repository": "https://github.com/capire/bookstore", "dependencies": { - "@cap-js/hana": ">=1", - "@capire/bookshop": "*", - "@capire/common": "*", - "@capire/data-viewer": "*", - "@capire/orders": "*", - "@capire/reviews": "*", - "@sap-cloud-sdk/http-client": "^4", - "@sap-cloud-sdk/resilience": "^4", - "@sap/cds": ">=5" + "@cap-js/hana": "^2.7.0", + "@capire/bookshop": "^2.1.7", + "@capire/common": "^2.0.2", + "@capire/data-viewer": "^0.1.5", + "@capire/orders": "^2.0.1", + "@capire/reviews": "^2.0.2", + "@sap-cloud-sdk/http-client": "^4.6.0", + "@sap-cloud-sdk/resilience": "^4.6.0", + "@sap/cds": "git+https://github.tools.sap/cap/cds#fix/hierarchy-direct-crud-exclusion" }, "devDependencies": { - "@cap-js/cds-test": ">=0.4.1", - "@cap-js/sqlite": ">=1" + "@cap-js/cds-test": "^0.4.1", + "@cap-js/sqlite": "^2.2.0" }, "scripts": { "start": "cds-serve", @@ -41,6 +41,9 @@ }, "log": { "service": true + }, + "fiori": { + "direct_crud": true } } } diff --git a/srv/mashup.cds b/srv/mashup.cds index 7560bc0..d81433c 100644 --- a/srv/mashup.cds +++ b/srv/mashup.cds @@ -1,34 +1,34 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Enhancing bookshop with Reviews and Orders provided through -// respective reuse packages and services -// +// //////////////////////////////////////////////////////////////////////////// +// // +// // Enhancing bookshop with Reviews and Orders provided through +// // respective reuse packages and services +// // -// -// Extend Books with access to Reviews and average ratings -// -using { ReviewsService.AverageRatings } from '@capire/reviews'; -using { sap.capire.bookshop.Books } from '@capire/bookshop'; -extend Books with { - rating : type of AverageRatings:rating; // average rating - reviews : Integer @title : '{i18n>NumberOfReviews}'; -} +// // +// // Extend Books with access to Reviews and average ratings +// // +// using { ReviewsService.AverageRatings } from '@capire/reviews'; +// using { sap.capire.bookshop.Books } from '@capire/bookshop'; +// extend Books with { +// rating : type of AverageRatings:rating; // average rating +// reviews : Integer @title : '{i18n>NumberOfReviews}'; +// } -// -// Extend Orders with Books as Products -// -using { sap.capire.orders.Orders } from '@capire/orders'; -extend Orders:Items with { - book : Association to Books on product.ID = book.ID -} +// // +// // Extend Orders with Books as Products +// // +// using { sap.capire.orders.Orders } from '@capire/orders'; +// extend Orders:Items with { +// book : Association to Books on product.ID = book.ID +// } -// Ensure models from all imported packages are loaded -using from '@capire/orders/app/fiori'; -using from '@capire/data-viewer'; -using from '@capire/common'; +// // Ensure models from all imported packages are loaded +// using from '@capire/orders/app/fiori'; +// using from '@capire/data-viewer'; +// using from '@capire/common'; -// Restrict admin access to AdminService -annotate AdminService with @requires:'admin'; +// // Restrict admin access to AdminService +// annotate AdminService with @requires:'admin'; diff --git a/srv/mashup.js b/srv/mashup.js index 5dd40f6..bfae714 100644 --- a/srv/mashup.js +++ b/srv/mashup.js @@ -1,65 +1,65 @@ -const cds = require ('@sap/cds') +// const cds = require ('@sap/cds') -// Add routes to UIs from imported packages -if (!cds.env.production) cds.once ('bootstrap', (app) => { - app.serve ('/bookshop') .from ('@capire/bookshop','app/vue') - app.serve ('/reviews') .from ('@capire/reviews','app/vue') - app.serve ('/orders') .from('@capire/orders','app/orders') -}) +// // Add routes to UIs from imported packages +// if (!cds.env.production) cds.once ('bootstrap', (app) => { +// app.serve ('/bookshop') .from ('@capire/bookshop','app/vue') +// app.serve ('/reviews') .from ('@capire/reviews','app/vue') +// app.serve ('/orders') .from('@capire/orders','app/orders') +// }) -// Mashing up bookshop services with required services... -cds.once ('served', async ()=>{ +// // Mashing up bookshop services with required services... +// cds.once ('served', async ()=>{ - const CatalogService = await cds.connect.to ('CatalogService') - const ReviewsService = await cds.connect.to ('ReviewsService') - const OrdersService = await cds.connect.to ('OrdersService') - const db = await cds.connect.to ('db') +// const CatalogService = await cds.connect.to ('CatalogService') +// const ReviewsService = await cds.connect.to ('ReviewsService') +// const OrdersService = await cds.connect.to ('OrdersService') +// const db = await cds.connect.to ('db') - // reflect entity definitions used below... - const { Books } = cds.entities ('sap.capire.bookshop') +// // reflect entity definitions used below... +// const { Books } = cds.entities ('sap.capire.bookshop') - // - // Delegate requests to read reviews to the ReviewsService - // Note: prepend is neccessary to intercept generic default handler - // - CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => { - console.debug ('> delegating request to ReviewsService') // eslint-disable-line no-console - const [id] = req.params, { columns, limit } = req.query.SELECT - return ReviewsService.read ('Reviews',columns).limit(limit).where({subject:String(id)}) - })) +// // +// // Delegate requests to read reviews to the ReviewsService +// // Note: prepend is neccessary to intercept generic default handler +// // +// CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => { +// console.debug ('> delegating request to ReviewsService') // eslint-disable-line no-console +// const [id] = req.params, { columns, limit } = req.query.SELECT +// return ReviewsService.read ('Reviews',columns).limit(limit).where({subject:String(id)}) +// })) - // - // Create an order with the OrdersService when CatalogService signals a new order - // - CatalogService.before ('submitOrder', async (req) => { - const { book, quantity, buyer = req.user.id } = req.data - const { title, price, currency } = await db.read (Books, book, b => { b.title, b.price, b.currency(c => c.code) }) - await OrdersService.create ('OrdersNoDraft').entries({ - OrderNo: 'Order at '+ (new Date).toLocaleString(), - Items: [{ product:{ID:`${book}`}, title, price, quantity }], - buyer, createdBy: buyer, currency - }) - }) +// // +// // Create an order with the OrdersService when CatalogService signals a new order +// // +// CatalogService.before ('submitOrder', async (req) => { +// const { book, quantity, buyer = req.user.id } = req.data +// const { title, price, currency } = await db.read (Books, book, b => { b.title, b.price, b.currency(c => c.code) }) +// await OrdersService.create ('OrdersNoDraft').entries({ +// OrderNo: 'Order at '+ (new Date).toLocaleString(), +// Items: [{ product:{ID:`${book}`}, title, price, quantity }], +// buyer, createdBy: buyer, currency +// }) +// }) - // - // Update Books' average ratings when ReviewsService signals updated reviews - // - ReviewsService.on ('AverageRatings.Changed', (msg) => { - console.debug ('> received:', msg.event, msg.data) // eslint-disable-line no-console - const { subject, reviews, rating } = msg.data - return UPDATE (Books, subject) .with ({ reviews, rating }) - }) +// // +// // Update Books' average ratings when ReviewsService signals updated reviews +// // +// ReviewsService.on ('AverageRatings.Changed', (msg) => { +// console.debug ('> received:', msg.event, msg.data) // eslint-disable-line no-console +// const { subject, reviews, rating } = msg.data +// return UPDATE (Books, subject) .with ({ reviews, rating }) +// }) - // - // Reduce stock of ordered books for orders are created from Orders admin UI - // - OrdersService.on ('OrderChanged', (msg) => { - console.debug ('> received:', msg.event, msg.data) // eslint-disable-line no-console - const { product, deltaQuantity } = msg.data - return UPDATE (Books) .where ('ID =', product) - .and ('stock >=', deltaQuantity) - .set ('stock -=', deltaQuantity) - }) -}) +// // +// // Reduce stock of ordered books for orders are created from Orders admin UI +// // +// OrdersService.on ('OrderChanged', (msg) => { +// console.debug ('> received:', msg.event, msg.data) // eslint-disable-line no-console +// const { product, deltaQuantity } = msg.data +// return UPDATE (Books) .where ('ID =', product) +// .and ('stock >=', deltaQuantity) +// .set ('stock -=', deltaQuantity) +// }) +// })