Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6254d9b
feat: add RoleBasedLink and role-aware proposal links from experiments
shivoomiess Jun 17, 2026
6bb41a8
feat: add role-aware links to User, Instrument, Calls, FAP in ProposalsT
shivoomiess Jun 17, 2026
c3cfd0a
feat: link redirects to call / instrument page with search
shivoomiess Jun 17, 2026
e70b01c
fix: link to Principal Investigator (add id to graphql query)
shivoomiess Jun 21, 2026
4729dfb
feat: add RoleBasedLink and role-aware proposal links from experiments
shivoomiess Jun 17, 2026
549b406
feat: add role-aware links to User, Instrument, Calls, FAP in ProposalsT
shivoomiess Jun 17, 2026
5692684
feat: link redirects to call / instrument page with search
shivoomiess Jun 17, 2026
c8d3b9a
fix: link to Principal Investigator (add id to graphql query)
shivoomiess Jun 21, 2026
d0acc19
Merge branch 'SWAP-5403-links' of github.com:UserOfficeProject/user-o…
shivoomiess Jun 22, 2026
bd65ffc
fix: use MUI Link styles with Router Link
shivoomiess Jun 22, 2026
c7fdfa3
Merge branch 'develop' into SWAP-5403-links
shivoomiess Jun 30, 2026
d2cd1de
Merge branch 'develop' into SWAP-5403-links
yoganandaness Jul 6, 2026
b796686
Merge branch 'develop' into SWAP-5403-links
shivoomiess Jul 7, 2026
bf7965f
Merge branch 'develop' into SWAP-5403-links
jekabs-karklins Jul 8, 2026
1458304
Merge branch 'develop' into SWAP-5403-links
yoganandaness Jul 8, 2026
f971176
Merge branch 'develop' into SWAP-5403-links
jekabs-karklins Jul 14, 2026
1a74d92
Merge branch 'develop' into SWAP-5403-links
jekabs-karklins Jul 15, 2026
1133837
Merge branch 'develop' into SWAP-5403-links
jekabs-karklins Jul 19, 2026
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
34 changes: 34 additions & 0 deletions apps/frontend/src/components/common/RoleBasedLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Link as MuiLink, LinkProps as MuiLinkProps } from '@mui/material';
import React, { useContext } from 'react';
import { Link as RouterLink, To } from 'react-router-dom';

import { UserContext } from 'context/UserContextProvider';
import { UserRole } from 'generated/sdk';

export type RoleBasedLinkProps = Omit<MuiLinkProps, 'href' | 'component'> & {
/** Map of role -> destination. Roles omitted here render children as-is without a link. */
roleRoutes: Partial<Record<UserRole, To>>;
};

const RoleBasedLink = ({
roleRoutes,
children,
...linkProps
}: RoleBasedLinkProps) => {
const { currentRole } = useContext(UserContext);

const to: To | undefined = currentRole ? roleRoutes[currentRole] : undefined;

// With a destination for this role, render an MUI-styled link that navigates
// via react-router (client-side); otherwise render children as plain content
// with no link behavior.
return to !== undefined ? (
<MuiLink component={RouterLink} to={to} {...linkProps}>
{children}
</MuiLink>
) : (
<>{children}</>
);
};

export default RoleBasedLink;
24 changes: 21 additions & 3 deletions apps/frontend/src/components/experiment/ExperimentsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ import MaterialTable, {
import { Visibility } from '@mui/icons-material';
import { IconButton, Tooltip, Typography } from '@mui/material';
import React, { useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom';

import { Experiment, PaginationSortDirection, SettingsId } from 'generated/sdk';
import { useLocation, useSearchParams } from 'react-router-dom';

import RoleBasedLink from 'components/common/RoleBasedLink';
import {
Experiment,
PaginationSortDirection,
SettingsId,
UserRole,
} from 'generated/sdk';
import { useFormattedDateTime } from 'hooks/admin/useFormattedDateTime';
import { setSortDirectionOnSortField } from 'utils/helperFunctions';
import useDataApiWithFeedback from 'utils/useDataApiWithFeedback';
Expand Down Expand Up @@ -52,6 +58,8 @@ export default function ExperimentsTable({

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

Expand Down Expand Up @@ -168,6 +176,16 @@ export default function ExperimentsTable({
{
title: 'Proposal ID',
field: 'proposal.proposalId',
render: (rowData: Experiment) => (
<RoleBasedLink
roleRoutes={{
[UserRole.USER_OFFICER]: `/Proposals?reviewModal=${rowData.proposal.primaryKey}&from=${from}`,
[UserRole.INSTRUMENT_SCIENTIST]: `/Proposals?reviewModal=${rowData.proposal.primaryKey}&from=${from}`,
}}
>
{rowData.proposal.proposalId}
</RoleBasedLink>
),
Comment thread
shivoomiess marked this conversation as resolved.
},
{
title: 'Start',
Expand Down
63 changes: 56 additions & 7 deletions apps/frontend/src/components/proposal/ProposalTableOfficer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ import { TFunction } from 'i18next';
import React, { useContext, useEffect, useRef, useState } from 'react';
import isEqual from 'react-fast-compare';
import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router-dom';

import i18n from 'i18n';

import CopyToClipboard from 'components/common/CopyToClipboard';
import MaterialTable from 'components/common/DenseMaterialTable';
import ListStatusIcon from 'components/common/icons/ListStatusIcon';
import ScienceIcon from 'components/common/icons/ScienceIcon';
import RoleBasedLink from 'components/common/RoleBasedLink';
import UOLoader from 'components/common/UOLoader';
import AssignProposalsToFaps from 'components/fap/Proposals/ProposalsView/AssignProposalsToFaps';
import AssignProposalsToInstruments from 'components/instrument/AssignProposalsToInstruments';
Expand Down Expand Up @@ -68,6 +69,7 @@ import { useDownloadPDFProposal } from 'hooks/proposal/useDownloadPDFProposal';
import { useDownloadProposalAttachment } from 'hooks/proposal/useDownloadProposalAttachment';
import { useDownloadXLSXProposal } from 'hooks/proposal/useDownloadXLSXProposal';
import { ProposalViewData } from 'hooks/proposal/useProposalsCoreData';
import { fromArrayToCommaSeparatedLinks } from 'utils/fromArrayToCommaSeparatedLinks';
import {
addColumns,
fromArrayToCommaSeparated,
Expand Down Expand Up @@ -130,9 +132,17 @@ let columns: Column<ProposalViewData>[] = [
sorting: false,
emptyValue: '-',
render: (proposalView) =>
proposalView.principalInvestigator?.lastname
? `${proposalView.principalInvestigator.lastname}, ${getPreferredName(proposalView.principalInvestigator)}`
: '',
proposalView.principalInvestigator?.lastname ? (
<RoleBasedLink
roleRoutes={{
[UserRole.USER_OFFICER]: `/People/${proposalView.principalInvestigator.id}`,
}}
>
{`${proposalView.principalInvestigator.lastname}, ${getPreferredName(proposalView.principalInvestigator)}`}
</RoleBasedLink>
) : (
''
),
},
{
title: 'PI Email',
Expand All @@ -157,6 +167,19 @@ let columns: Column<ProposalViewData>[] = [
{
title: 'Call',
field: 'callShortCode',
// No per-call route exists, so officers link to the Calls list page.
render: (rowData) =>
rowData.callShortCode ? (
<RoleBasedLink
roleRoutes={{
[UserRole.USER_OFFICER]: `/Calls?search=${encodeURIComponent(rowData.callShortCode)}`,
}}
>
{rowData.callShortCode}
</RoleBasedLink>
) : (
''
),
},
];

Expand Down Expand Up @@ -200,10 +223,19 @@ const instrumentManagementColumns = (
{
title: t('instrument'),
field: 'instruments.name',
// No per-instrument route exists, so officers link to the Instruments list page.

render: (rowData: ProposalViewData) =>
fromArrayToCommaSeparated(
rowData.instruments?.map((instrument) => instrument.name)
fromArrayToCommaSeparatedLinks(
rowData.instruments?.map((instrument) => ({
key: instrument.id,
label: instrument.name,
roleRoutes: {
[UserRole.USER_OFFICER]: `/Instruments?search=${instrument.name}`,
Comment thread
shivoomiess marked this conversation as resolved.
},
Comment thread
shivoomiess marked this conversation as resolved.
}))
),

customFilterAndSearch: () => true,
},
];
Expand All @@ -223,7 +255,13 @@ const fapReviewColumns = (t: TFunction<'translation', undefined>) => [
title: t('FAP'),
field: 'faps.code',
render: (rowData: ProposalViewData) =>
fromArrayToCommaSeparated(rowData.faps?.map((fap) => fap.code)),
fromArrayToCommaSeparatedLinks(
rowData.faps?.map((fap) => ({
key: fap.id,
label: fap.code,
roleRoutes: { [UserRole.USER_OFFICER]: `/FapPage/${fap.id}` },
}))
),
},
];
const SELECT_ALL_ACTION_TOOLTIP = 'select-all-prefetched-proposals';
Expand Down Expand Up @@ -429,6 +467,7 @@ const ProposalTableOfficer = ({
const userContext = useContext(UserContext);
const featureContext = useContext(FeatureContext);
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
const [bulkReassignData, setBulkReassignData] = useState<ReviewData[]>([]);
const isReadOnly = useCheckAccess([UserRole.PROPOSAL_READER]);

Expand Down Expand Up @@ -1219,13 +1258,23 @@ const ProposalTableOfficer = ({
title={`View proposal: ${proposalToReview?.title} (${proposalToReview?.proposalId})`}
proposalReviewModalOpen={!!proposalToReview}
setProposalReviewModalOpen={() => {
const from = searchParams.get('from');

if (searchParams.get('proposalId')) {
setProposalFilter({
...proposalFilter,
referenceNumbers: undefined,
});
}

if (from) {
// Return to the page that opened the modal (e.g. Experiments),
// restoring its filters, instead of staying on /Proposals.
navigate(decodeURIComponent(from), { replace: true });

return;
}
Comment thread
shivoomiess marked this conversation as resolved.

setSearchParams((searchParams) => {
searchParams.delete('reviewModal');
searchParams.delete('proposalId');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ query getProposalsCore(
workflowId
allocationTimeUnit
principalInvestigator {
id
firstname
lastname
preferredname
Expand Down
28 changes: 28 additions & 0 deletions apps/frontend/src/utils/fromArrayToCommaSeparatedLinks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';

import RoleBasedLink, {
RoleBasedLinkProps,
} from 'components/common/RoleBasedLink';

type CommaSeparatedLink = {
key: React.Key;
label: React.ReactNode;
roleRoutes: RoleBasedLinkProps['roleRoutes'];
};

export function fromArrayToCommaSeparatedLinks(
links: CommaSeparatedLink[] | null | undefined
) {
return (
<>
{links?.map((link, index) => (
<React.Fragment key={link.key}>
{index > 0 && ', '}
<RoleBasedLink roleRoutes={link.roleRoutes}>
{link.label}
</RoleBasedLink>
</React.Fragment>
))}
</>
);
}
Comment thread
shivoomiess marked this conversation as resolved.
Loading