Skip to content

[DT-4062] Add study recommendation sections - #3933

Merged
otchet-broad merged 29 commits into
otchet-dt-4062-study-dar-historyfrom
otchet-dt-4062-study-recommendations
Sep 23, 2026
Merged

otchet-broad merged 29 commits into
otchet-dt-4062-study-dar-historyfrom
otchet-dt-4062-study-recommendations

Conversation

@otchet-broad

@otchet-broad otchet-broad commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Addresses

https://broadworkbench.atlassian.net/browse/DT-4062

Summary

Adds two recommendation sections below the research outputs: studies often requested in the same
data access request as this one, and studies recommended by shared data type or PI. Each card
carries the study name, PI and description, and links through to that study.

These are responsive grids rather than carousels — every recommendation renders, wrapping onto
further rows as needed, with no horizontal scrolling or next/previous affordance. The PR
previously called them carousels; a grid is what is built and what this describes. Building real
carousel interaction on a page already this long is worth doing deliberately rather than as a
review fix, so it is left as a follow-up.

Each card's action area is a router link with a to value rather than a button that navigates
imperatively, matching how study destinations work elsewhere (StudyCard). That restores the
native link actions a button swallows: open in a new tab, copy link address, middle-click.

Both sections stay on the page when they come back empty, saying so, for the same reason as the
other sections — an absent section on a study page reads as a failure to load.

The response is typed as a new StudyRecommendation rather than the index-shaped
StudyAggregation. The endpoints return only studyId, studyName, studyDescription, piName,
datasetCount and datasetIds, so the richer type would let a reader of study.dataTypes compile
and then fail at runtime. datasetCount therefore comes typed for whoever wants it on the card.

The sections sit near the bottom of a long page, and BrowserRouter preserves the document offset
during an in-app navigation, so the scroll position resets whenever the route points at a
different study — otherwise following a recommendation opens the next study at the same deep
scroll position.

Worth knowing for review: the backend restricts candidates to publicly visible studies, so a
recommendation never reveals a private study to someone who could not otherwise see it.


Have you read Terra's Contributing Guide lately? If not, do that first.

  • Label PR with a Jira ticket number and include a link to the ticket
  • Label PR with a security risk modifier [no, low, medium, high]
  • PR describes scope of changes
  • Get a minimum of one thumbs worth of review, preferably two if enough team members are available
  • Get PO sign-off for all non-trivial UI or workflow changes
  • Verify all tests go green
  • Test this change deployed correctly and works on dev environment after deployment

otchet-broad and others added 10 commits September 9, 2026 20:49
Adds two carousels below the research output sections: studies often
requested in the same data access request as this one, and studies
recommended by shared data type or PI. Each card carries the study name,
PI and description, and links through to that study's page.

Both stay on the page when they come back empty, saying so, rather than
disappearing - the same reasoning as the other sections, where an absent
section reads as a failure to load.

The response is typed as a new `StudyRecommendation` rather than the
index-shaped `StudyAggregation`: the endpoints return only studyId,
studyName, studyDescription, piName, datasetCount and datasetIds, so the
richer type would let a reader of `study.dataTypes` compile and then fail
at runtime. `datasetCount` therefore comes typed for whoever wants it on
the card.

Sixth of the DT-4062 stack. **Pairs with consent #3054**
(otchet-dt-4061-study-recommendations), which adds
/api/metrics/study-recommendations/{studyId}/similar and
/frequently-requested-with. Until that deploys both requests fail and each
carousel shows its error state. Worth knowing for review: the backend
restricts candidates to publicly visible studies, so a recommendation
never reveals a private study to someone who could not otherwise see it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tudy-recommendations

# Conflicts:
#	src/types/model.ts
…tudy-recommendations

# Conflicts:
#	src/components/study_details/StudyDetails.tsx
@github-actions

github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Report for DUOS Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 90.49% 12312 / 13606
🔵 Statements 89.97% 13164 / 14631
🔵 Functions 87.57% 3671 / 4192
🔵 Branches 82.94% 7690 / 9272
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/components/ScrollToTopOnNavigate.tsx 100% 100% 100% 100%
src/components/study_details/FrequentlyRequestedWithSection.tsx 100% 100% 100% 100%
src/components/study_details/SimilarStudiesSection.tsx 100% 100% 100% 100%
src/components/study_details/StudyAssetCountBadges.tsx 100% 100% 100% 100%
src/components/study_details/StudyAssetTable.tsx 100% 100% 100% 100%
src/components/study_details/StudyDetails.tsx 98.46% 100% 88.88% 98.41% 351
src/components/study_details/StudyPublicationCards.tsx 100% 93.33% 100% 100%
src/components/study_details/StudyRecommendationCarousel.tsx 100% 100% 100% 100%
src/hooks/useStudyDetailsData.ts 100% 100% 100% 100%
src/libs/ajax/Study.ts 70% 100% 50% 66.66% 45-49, 62-68
src/libs/ajax/StudyRecommendations.ts 100% 100% 100% 100%
src/pages/DatasetStatistics.tsx 82.97% 81.81% 93.33% 87.5% 76-86, 113, 117, 163, 167, 171-173, 177, 191-192
src/types/model.ts 100% 100% 100% 100%
Generated in workflow #7314 for commit 6079317 by the Vitest Coverage Report Action

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved carousel interaction, refetch-error handling, and link-semantic issues remain.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds study recommendation sections to study detail pages with typed API clients, React Query hooks, recommendation cards, and scroll-reset behavior.

Changes:

  • Adds recommendation models, API calls, and query hooks.
  • Renders related-study recommendation sections and states.
  • Adds navigation scroll behavior and supporting tests.
File summaries
File Summary
test/components/study_details/StudyDetails.spec.tsx Tests navigation scroll behavior; recommendation rendering and error states remain uncovered.
src/types/model.ts Defines the recommendation response type.
src/libs/ajax/StudyRecommendations.ts Adds recommendation API calls; dedicated adapter tests are missing.
src/hooks/useStudyDetailsData.ts Adds recommendation queries.
src/components/study_details/StudyRecommendationCarousel.tsx Renders recommendation cards and states; needs carousel interaction, resilient refetch-error handling, router-link semantics, and broader coverage.
src/components/study_details/StudyDetails.tsx Integrates recommendations and scroll reset; the similar-study heading should mention both recommendation signals.
Review details

Suppressed comments (3)

src/components/study_details/StudyDetails.tsx:213

  • The similar-study recommendations are described as using shared data type or PI, but this heading says they are based only on data type. That makes the user-facing explanation incomplete; mention both signals or use a neutral heading.
            heading="Recommended Studies based on Data Type"

src/components/study_details/StudyRecommendationCarousel.tsx:25

  • After a successful response, a background refetch can fail while recommendations still contains the previous cards. StudyQueryResult checks error before rendering its children, so this path hides the usable cards and shows the error instead, contrary to the isPending contract above. Preserve the previous cards when data exists, while optionally surfacing the refetch failure separately.
      <StudyQueryResult
        isPending={isPending}
        error={error}
        isEmpty={recommendations.length === 0}

src/libs/ajax/StudyRecommendations.ts:7

  • This new AJAX adapter has no dedicated spec, unlike the existing Study and DatasetMetrics adapters. URL suffix construction, auth options, response extraction, and rejection propagation are therefore unverified; add StudyRecommendations tests before relying on this integration.
const get = async (studyId: number | string, path: string): Promise<StudyRecommendation[]> => {
  const url = `${await Config.getApiUrl()}/api/metrics/study-recommendations/${studyId}/${path}`
  return (await fetchGet<StudyRecommendation[]>(url, Config.authOpts())).data
  • Files reviewed: 6/6 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/components/study_details/StudyRecommendationCarousel.tsx
Comment thread src/components/study_details/StudyRecommendationCarousel.tsx Outdated
Comment thread src/components/study_details/StudyRecommendationCarousel.tsx Outdated
otchet-broad and others added 4 commits September 15, 2026 03:35
Each card's action area is a router link with a `to` value rather than a
button that navigates imperatively, matching how study destinations work
elsewhere. A button swallows the native link actions - open in a new tab,
copy link address, middle click - which is most of how people follow a
recommendation.

Adds a spec for the component. The feature tests mock both recommendation
calls to return empty arrays, so no card, link, empty state or error
state was exercised at all. The link case fails against the previous
button.

Says plainly in the component that this is a responsive grid: every
recommendation renders and wraps onto further rows, with no horizontal
scrolling or next/previous affordance. The PR described carousels;
building that interaction on a page already this long is worth doing
deliberately rather than as a review fix, so it stays a follow-up and the
description is corrected instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@otchet-broad otchet-broad changed the title [DT-4062] Add study recommendation carousels [DT-4062] Add study recommendation sections Sep 15, 2026
@otchet-broad
otchet-broad marked this pull request as ready for review September 17, 2026 16:39
@otchet-broad
otchet-broad requested a review from a team as a code owner September 17, 2026 16:39
@otchet-broad
otchet-broad requested review from fboulnois and kevinmarete and removed request for a team September 17, 2026 16:39

@kevinmarete kevinmarete left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After an initial successful request, React Query can retain recommendations while setting error when a background refetch fails. Because error is passed unconditionally to StudyQueryResult, the component replaces valid cached cards with the error state. This also contradicts the comment that background refetches should keep cards on screen.
Please make the error blocking only when no recommendations are available—for example, error={recommendations.length === 0 ? error : undefined}—and add a regression test covering cached data plus a refetch error.

otchet-broad and others added 2 commits September 17, 2026 19:05
Review caught that the error went to StudyQueryResult unconditionally, so
a failed background refetch replaced cards that had loaded fine. That
also contradicted the note on isPending directly above it, which says a
background refetch keeps the cards on screen - it does for the spinner
and did not for the error.

Reports a failure only when there is nothing else to show, as the asset
and publication sections already do. The test pairs cached
recommendations with an error and fails on the previous version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The component test pairs cached recommendations with an error directly,
which is the carousel's whole contract - it is presentational. The two
sibling fixes got integration tests instead, driving a failed refetch
through the query client, and this section should be held to the same
standard: it is the wiring in StudyDetails that decides whether the
component ever sees cached data alongside an error.

Fails with the error passed unconditionally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@kevinmarete kevinmarete left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏾

Non-blocking: the similar-studies endpoint considers both overlapping data types and a shared PI, but the heading says “Recommended Studies based on Data Type.” Could we rename it to “Recommended Studies” or mention both signals?

@fboulnois fboulnois left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few suggestions identified with Claude:

1. Unencoded route parameter in API path: src/libs/ajax/StudyRecommendations.ts:6

studyId from useParams is interpolated raw. useParams decodes the URL, so /studies/1%2F..%2Fdar-summaries%2F5 becomes /api/metrics/study-recommendations/1/../dar-summaries/5/similar, which the browser normalizes into an authenticated GET at a different endpoint. Sibling StudyComments.ts:5-9 wraps the same id in encodeURIComponent for exactly this reason.

2. Scroll reset fires on mount, defeating Back/Forward restoration: src/components/study_details/StudyDetails.tsx:269

The [studyId] effect runs on initial mount, not only when the route changes to a different study. Scroll deep into /studies/5, follow a dataset link, press Back: the browser restores the offset, then the effect snaps to top. A ref that skips the first run, or a pathname-driven reset in the router layer, matches the comment's stated intent.

3. Error guard keys on list length, not cached-ness: src/components/study_details/StudyRecommendationCarousel.tsx:28

A study with zero recommendations shows "No study recommendations yet."; if a background refetch then fails, React Query keeps data=[] and sets error, and recommendations.length === 0 ? error : undefined flips the section to the error banner. StudySecondaryResearchOutputs.tsx:50 uses data ? undefined : error; use recommendations === undefined ? error : undefined here.

4. Recommendation queries hoisted into the page component: src/components/study_details/StudyDetails.tsx:89

useSimilarStudies and useFrequentlyRequestedWithStudies live in StudyDetailsContent and are threaded through props, so every pending→success→refetch transition re-renders the whole page including the DataGrid. Siblings StudyDarHistory.tsx:49 and StudySecondaryResearchOutputs.tsx:41 take studyId and own their hook. A thin wrapper removes 4 imports, 2 hooks and 6 props.

5. Scroll-to-top is a router-level concern patched into one page: src/components/study_details/StudyDetails.tsx:266

The root cause the comment names — BrowserRouter preserves document offset on in-app navigation — applies app-wide: "Back to library" from a deep study page lands on /datalibrary at the same offset. A single pathname-keyed reset next to BrowserRouter in src/index.tsx/AppRoutes.tsx (or a data router with <ScrollRestoration>) fixes every route once.

6. Current study not filtered from its own recommendations: src/components/study_details/StudyRecommendationCarousel.tsx:34

If /similar or /frequently-requested-with returns the viewed study (natural for a data-type similarity query), a card links back to the current page; useParams is unchanged, so neither the key nor the scroll effect fires and the click appears to do nothing. Filter String(r.studyId) !== studyId in the hook, or pass the current id to the carousel.

otchet-broad and others added 2 commits September 18, 2026 15:45
Four items from fboulnois, and the scroll one taken at the altitude he
argued for rather than patched here.

Scroll: the reset moves to a component beside BrowserRouter, so every
route gets it - 'Back to library' from deep inside a study no longer
lands halfway down the library. It skips POP, which is the browser's own
Back and Forward restoring an offset it recorded, and also the initial
load, so a bookmark deep into a page is left where the browser put it.
That was the reported defect: the page-level effect ran on mount and
undid the restoration. The page effect is gone.

The study id goes into the recommendations path encoded. It arrives from
useParams already decoded, so a crafted id was path segments of its own
and the browser would have normalized the request onto another endpoint.
StudyComments was fixed for this; this module had the same hole and no
spec at all, so it has one now.

The carousel's error keys on whether anything has loaded rather than on
the list being empty. A study with no recommendations has loaded fine,
and testing length alone flipped it from 'none yet' to an error banner
the moment a refetch failed. The prop loses its `= []` default, which
would have erased the distinction before it arrived.

A study is no longer recommended to itself. A similarity query matches it
perfectly, so it came back in its own results, and the card linked to the
open page: the route never changed and the click did nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codex review caught that reacting to the location alone misbehaves where
state lives in the query string. The library keeps filters, sort and
paging there, so each of those is a navigation to the same page: the
first one scrolled to the top, because navigationType flips from POP to
PUSH once and re-runs the effect, and none of the later ones did. First
filter jumps, second does not.

Comparing pathnames instead leaves query-string navigation alone
entirely, which is what filtering should do. It also covers the first
render without relying on the initial load being reported as POP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@fboulnois fboulnois left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@otchet-broad
otchet-broad merged commit bde6545 into otchet-dt-4062-study-dar-history Sep 23, 2026
12 checks passed
@otchet-broad
otchet-broad deleted the otchet-dt-4062-study-recommendations branch September 23, 2026 15:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants