Skip to content

Commit 6254d9b

Browse files
shivoomiessclaude
andcommitted
feat: add RoleBasedLink and role-aware proposal links from experiments
Add a generic RoleBasedLink wrapper that resolves its destination from the current user's role. Use it in the experiments table so a user officer's proposal-id link opens the officer proposal modal, carrying a `from` param so closing the modal returns to the originating page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1a5595f commit 6254d9b

3 files changed

Lines changed: 65 additions & 4 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React, { useContext } from 'react';
2+
import { Link, LinkProps, To } from 'react-router-dom';
3+
4+
import { UserContext } from 'context/UserContextProvider';
5+
import { UserRole } from 'generated/sdk';
6+
7+
export type RoleBasedLinkProps = Omit<LinkProps, 'to'> & {
8+
/** Map of role -> destination. Roles omitted here render children as-is without a link. */
9+
roleRoutes: Partial<Record<UserRole, To>>;
10+
};
11+
12+
const RoleBasedLink = ({
13+
roleRoutes,
14+
children,
15+
...linkProps
16+
}: RoleBasedLinkProps) => {
17+
const { currentRole } = useContext(UserContext);
18+
19+
const to: To | undefined = currentRole ? roleRoutes[currentRole] : undefined;
20+
21+
// With a destination for this role, render a link; otherwise render
22+
// children as plain content with no link behavior.
23+
return to !== undefined ? (
24+
<Link to={to} {...linkProps}>
25+
{children}
26+
</Link>
27+
) : (
28+
<>{children}</>
29+
);
30+
};
31+
32+
export default RoleBasedLink;

apps/frontend/src/components/experiment/ExperimentsTable.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@ import MaterialTable, {
66
import { Visibility } from '@mui/icons-material';
77
import { IconButton, Tooltip, Typography } from '@mui/material';
88
import React, { useRef, useState } from 'react';
9-
import { useSearchParams } from 'react-router-dom';
10-
11-
import { Experiment, PaginationSortDirection, SettingsId } from 'generated/sdk';
9+
import { useLocation, useSearchParams } from 'react-router-dom';
10+
11+
import RoleBasedLink from 'components/common/RoleBasedLink';
12+
import {
13+
Experiment,
14+
PaginationSortDirection,
15+
SettingsId,
16+
UserRole,
17+
} from 'generated/sdk';
1218
import { useFormattedDateTime } from 'hooks/admin/useFormattedDateTime';
1319
import { setSortDirectionOnSortField } from 'utils/helperFunctions';
1420
import useDataApiWithFeedback from 'utils/useDataApiWithFeedback';
@@ -52,6 +58,8 @@ export default function ExperimentsTable({
5258

5359
const { api } = useDataApiWithFeedback();
5460
const [searchParams, setSearchParams] = useSearchParams();
61+
const location = useLocation();
62+
const from = encodeURIComponent(location.pathname + location.search);
5563
const [tableData, setTableData] = useState<Experiment[]>([]);
5664
const [selectedExperiment, setSelectedExperiment] = useState<Experiment>();
5765

@@ -168,6 +176,16 @@ export default function ExperimentsTable({
168176
{
169177
title: 'Proposal ID',
170178
field: 'proposal.proposalId',
179+
render: (rowData: Experiment) => (
180+
<RoleBasedLink
181+
roleRoutes={{
182+
[UserRole.USER_OFFICER]: `/Proposals?reviewModal=${rowData.proposal.primaryKey}&from=${from}`,
183+
// [UserRole.INSTRUMENT_SCIENTIST]: `/Proposals?reviewModal=${rowData.proposal.primaryKey}&from=${from}`,
184+
}}
185+
>
186+
{rowData.proposal.proposalId}
187+
</RoleBasedLink>
188+
),
171189
},
172190
{
173191
title: 'Start',

apps/frontend/src/components/proposal/ProposalTableOfficer.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { TFunction } from 'i18next';
2929
import React, { useContext, useEffect, useRef, useState } from 'react';
3030
import isEqual from 'react-fast-compare';
3131
import { useTranslation } from 'react-i18next';
32-
import { useSearchParams } from 'react-router-dom';
32+
import { useNavigate, useSearchParams } from 'react-router-dom';
3333

3434
import i18n from 'i18n';
3535

@@ -429,6 +429,7 @@ const ProposalTableOfficer = ({
429429
const userContext = useContext(UserContext);
430430
const featureContext = useContext(FeatureContext);
431431
const [searchParams, setSearchParams] = useSearchParams();
432+
const navigate = useNavigate();
432433
const [bulkReassignData, setBulkReassignData] = useState<ReviewData[]>([]);
433434
const isReadOnly = useCheckAccess([UserRole.PROPOSAL_READER]);
434435

@@ -1215,13 +1216,23 @@ const ProposalTableOfficer = ({
12151216
title={`View proposal: ${proposalToReview?.title} (${proposalToReview?.proposalId})`}
12161217
proposalReviewModalOpen={!!proposalToReview}
12171218
setProposalReviewModalOpen={() => {
1219+
const from = searchParams.get('from');
1220+
12181221
if (searchParams.get('proposalId')) {
12191222
setProposalFilter({
12201223
...proposalFilter,
12211224
referenceNumbers: undefined,
12221225
});
12231226
}
12241227

1228+
if (from) {
1229+
// Return to the page that opened the modal (e.g. Experiments),
1230+
// restoring its filters, instead of staying on /Proposals.
1231+
navigate(decodeURIComponent(from), { replace: true });
1232+
1233+
return;
1234+
}
1235+
12251236
setSearchParams((searchParams) => {
12261237
searchParams.delete('reviewModal');
12271238
searchParams.delete('proposalId');

0 commit comments

Comments
 (0)