Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions srv/__tests__/tutorial-feedback.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,4 +319,44 @@ describe('markFeedbackIrrelevant / unmarkFeedbackIrrelevant', () => {
);
expect(status).toBe(404);
});

// Regression for #3080: the admin UI renders the action button inside the
// feedbackItems LineItem on a Tutorials object page, so it invokes the
// action through the navigation path Tutorials(...)/feedbackItems(...)/
// markFeedbackIrrelevant. In that case req.params carries BOTH keys
// ([tutorialKey, feedbackKey]); the handler must resolve the feedback row
// from its OWN (last) key, not req.params[0] (the tutorial key).
it('marks irrelevant via the Tutorials/feedbackItems navigation path (#3080)', async () => {
const { Tutorials, TutorialFeedback } = cds.entities('com.sap.developers.ims');
const tutorialId = cds.utils.uuid();
const navFeedbackId = cds.utils.uuid();
await INSERT.into(Tutorials).entries([{ ID: tutorialId, slug: 'nav-demo-tutorial' }]);
await INSERT.into(TutorialFeedback).entries([
{ ID: navFeedbackId, tutorialSlug: 'nav-demo-tutorial', npsScore: 5 }
]);

const { status } = await project.post(
`/admin/Tutorials(ID=${tutorialId},IsActiveEntity=true)/feedbackItems(${navFeedbackId})/markFeedbackIrrelevant`,
{ reason: 'Off-topic' },
{ ...auth, validateStatus: () => true }
);
expect(status).toBe(200);
const [row] = await SELECT.from('com.sap.developers.ims.TutorialFeedback').where({ ID: navFeedbackId });
expect(row.isIrrelevant).toBe(true);
expect(row.irrelevantReason).toBe('Off-topic');

// and unmark via the same navigation path
const { status: unmarkStatus } = await project.post(
`/admin/Tutorials(ID=${tutorialId},IsActiveEntity=true)/feedbackItems(${navFeedbackId})/unmarkFeedbackIrrelevant`,
{},
{ ...auth, validateStatus: () => true }
);
expect(unmarkStatus).toBe(200);
const [after] = await SELECT.from('com.sap.developers.ims.TutorialFeedback').where({ ID: navFeedbackId });
expect(after.isIrrelevant).toBe(false);
expect(after.irrelevantReason).toBeNull();

await DELETE.from(TutorialFeedback).where({ ID: navFeedbackId });
await DELETE.from(Tutorials).where({ ID: tutorialId });
});
});
17 changes: 15 additions & 2 deletions srv/admin-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -1829,10 +1829,23 @@ export default class AdminService extends cds.ApplicationService {
});

// --- Feedback moderation ---
//
// #3080: the action is bound to TutorialFeedback, but the admin UI renders
// its button inside the feedbackItems LineItem on a Tutorials object page,
// so it is invoked through the navigation path
// Tutorials(...)/feedbackItems(<feedbackID>)/markFeedbackIrrelevant
// In that case req.params is [tutorialKey, feedbackKey] — the bound
// entity's own key is the LAST element, not req.params[0] (the tutorial
// key). Read the last param so BOTH the direct path (1 key) and the
// navigation path (2 keys) resolve the correct feedback row.
const feedbackKeyOf = (req) => {
const last = req.params[req.params.length - 1];
return last?.ID ?? last;
};

this.on('markFeedbackIrrelevant', 'TutorialFeedback', async (req) => {
const { TutorialFeedback } = cds.entities('com.sap.developers.ims');
const id = req.params[0]?.ID ?? req.params[0];
const id = feedbackKeyOf(req);
if (!id) return req.error(400, 'Missing entity key');
const row = await SELECT.one.from(TutorialFeedback).where({ ID: id });
if (!row) return req.error(404, 'Feedback entry not found');
Expand All @@ -1842,7 +1855,7 @@ export default class AdminService extends cds.ApplicationService {

this.on('unmarkFeedbackIrrelevant', 'TutorialFeedback', async (req) => {
const { TutorialFeedback } = cds.entities('com.sap.developers.ims');
const id = req.params[0]?.ID ?? req.params[0];
const id = feedbackKeyOf(req);
if (!id) return req.error(400, 'Missing entity key');
const row = await SELECT.one.from(TutorialFeedback).where({ ID: id });
if (!row) return req.error(404, 'Feedback entry not found');
Expand Down
Loading