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
9 changes: 9 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
## April 13, 2026

- **Feature** Migrate all links to use getPath() [🎟️ DEP-255](https://citz-gdx.atlassian.net/browse/DEP-255)
- Updated all hardcoded links in the codebase to use the new `getPath()` function from `routes/routes.ts`, which generates URLs based on a centralized ROUTES constant. This ensures that all links are consistent and automatically updated if route paths change in the future.
- This includes links in components, API calls, navigation, and anywhere else URLs are generated or used in the application.
- Also updated links to use RouterLinkRenderer where appropriate to take advantage of client-side routing and prevent full page reloads.
- Leveraged new routing system to remove redundant routes in public view and restructure public engagement URLs to use the slug-based format
- Fixed several bugs related to link construction

## April 13, 2026

- **Feature** Add constant ROUTES list in routes/routes.ts [🎟️ DEP-254](https://citz-gdx.atlassian.net/browse/DEP-254)
- Created a new file `routes/routes.ts` that exports a constant `ROUTES` object containing all the route paths used in the application, organized by section (e.g. ENGAGEMENTS, SURVEYS, USERS, etc.).
- Also created a type-safe `getRoute` function based on generatePath from react-router that takes a route key and optional parameters, and returns the corresponding route path with parameters interpolated. This centralizes route management and makes it easier to update routes in the future without having to search through the entire codebase.
Expand Down
5 changes: 3 additions & 2 deletions web/src/components/comments/admin/review/CommentReview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { faMessageCheck } from '@fortawesome/pro-solid-svg-icons/faMessageCheck'
import { faMessageSlash } from '@fortawesome/pro-solid-svg-icons/faMessageSlash';
import { LanguageState } from 'reduxSlices/languageSlice';
import { TenantState } from 'reduxSlices/tenantSlice';
import { ROUTES, getPath } from 'routes/routes';

const CommentReview = () => {
const [submission, setSubmission] = useState<SurveySubmission>(createDefaultSubmission());
Expand Down Expand Up @@ -107,7 +108,7 @@ const CommentReview = () => {
setIsLoading(false);
} catch {
dispatch(openNotification({ severity: 'error', text: 'Error occurred while fetching comments' }));
navigate('/');
navigate(getPath(ROUTES.PUBLIC_LANDING));
}
};

Expand Down Expand Up @@ -169,7 +170,7 @@ const CommentReview = () => {
});
setIsSaving(false);
dispatch(openNotification({ severity: 'success', text: 'Comments successfully reviewed.' }));
navigate(`/surveys/${submission.survey_id}/comments`);
navigate(getPath(ROUTES.SURVEY_COMMENTS, { surveyId: submission.survey_id }));
} catch {
dispatch(openNotification({ severity: 'error', text: 'Error occurred while sending comments review.' }));
setIsSaving(false);
Expand Down
17 changes: 12 additions & 5 deletions web/src/components/comments/admin/reviewListing/Submissions.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { useState, useContext } from 'react';
import CustomTable from 'components/common/Table';
import Grid from '@mui/material/Grid2';
import { Link, useLocation } from 'react-router';
import { useLocation } from 'react-router';
import { ResponsiveContainer } from 'components/common/Layout';
import { HeadCell, PaginationOptions } from 'components/common/Table/types';
import { formatDate } from 'components/common/dateHelper';
import { Collapse, Link as MuiLink } from '@mui/material';
import { Collapse, Link } from '@mui/material';
import TextField from '@mui/material/TextField';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faMagnifyingGlass } from '@fortawesome/pro-regular-svg-icons/faMagnifyingGlass';
Expand All @@ -21,6 +21,7 @@ import { USER_COMPOSITE_ROLE } from 'models/user';
import { Heading1 } from 'components/common/Typography';
import { Button } from 'components/common/Input/Button';
import { RouterLinkRenderer } from 'components/common/Navigation/Link';
import { ROUTES, getPath } from 'routes/routes';

const Submissions = () => {
const {
Expand Down Expand Up @@ -61,9 +62,15 @@ const Submissions = () => {
userDetail.composite_roles?.includes('/' + USER_COMPOSITE_ROLE.TEAM_MEMBER.value))
) {
return (
<MuiLink component={Link} to={`/surveys/${Number(row.survey_id)}/submissions/${row.id}/review`}>
<Link
component={RouterLinkRenderer}
href={getPath(ROUTES.SURVEY_SUBMISSION_REVIEW, {
surveyId: Number(row.survey_id),
submissionId: row.id,
})}
>
{row.id}
</MuiLink>
</Link>
);
}
return row.id;
Expand Down Expand Up @@ -153,7 +160,7 @@ const Submissions = () => {
size="small"
variant="primary"
component={RouterLinkRenderer}
href={`/surveys/${survey.id}/comments/all`}
href={getPath(ROUTES.SURVEY_COMMENTS_ALL, { surveyId: survey.id })}
>
Read All Comments
</Button>
Expand Down
17 changes: 12 additions & 5 deletions web/src/components/comments/admin/textListing/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { useState, useEffect } from 'react';
import CustomTable from 'components/common/Table';
import { Link, useLocation, useParams } from 'react-router';
import { useLocation, useParams } from 'react-router';
import { ResponsiveContainer } from 'components/common/Layout';
import { HeadCell, PageInfo, PaginationOptions } from 'components/common/Table/types';
import { Link as MuiLink, Grid2 as Grid, Stack, TextField, Menu, MenuItem, Tooltip } from '@mui/material';
import { Link, Grid2 as Grid, Stack, TextField, Menu, MenuItem, Tooltip } from '@mui/material';
import { Button } from 'components/common/Input/Button';
import { RouterLinkRenderer } from 'components/common/Navigation/Link';
import { BodyText } from 'components/common/Typography/Body';
Expand All @@ -30,6 +30,7 @@ import { Survey, createDefaultSurvey } from 'models/survey';
import { PermissionsGate } from 'components/permissionsGate';
import { HTTP_STATUS_CODES } from 'constants/httpResponseCodes';
import axios from 'axios';
import { ROUTES, getPath } from 'routes/routes';

const CommentTextListing = () => {
const { roles, userDetail, assignedEngagements } = useAppSelector((state) => state.user);
Expand Down Expand Up @@ -184,9 +185,15 @@ const CommentTextListing = () => {
userDetail.composite_roles?.includes('/' + USER_COMPOSITE_ROLE.TEAM_MEMBER.value))
) {
return (
<MuiLink component={Link} to={`/surveys/${Number(row.survey_id)}/submissions/${row.id}/review`}>
<Link
component={RouterLinkRenderer}
href={getPath(ROUTES.SURVEY_SUBMISSION_REVIEW, {
surveyId: Number(row.survey_id),
submissionId: row.id,
})}
>
{row.id}
</MuiLink>
</Link>
);
}
return row.id;
Expand Down Expand Up @@ -363,7 +370,7 @@ const CommentTextListing = () => {
<Button
variant="primary"
LinkComponent={RouterLinkRenderer}
href={`/surveys/${submissions[0]?.survey_id || 0}/comments`}
href={getPath(ROUTES.SURVEY_COMMENTS, { surveyId: submissions[0]?.survey_id || 0 })}
>
Return to Comments List
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ActionFunction, redirect } from 'react-router';
import { postEngagement as createEngagement } from 'services/engagementService';
import { patchEngagementSlug } from 'services/engagementSlugService';
import { addTeamMemberToEngagement } from 'services/membershipService';
import { ROUTES, getPath } from 'routes/routes';

export const engagementCreateAction: ActionFunction = async ({ request }) => {
const formData = (await request.formData()) as FormData;
Expand All @@ -25,7 +26,7 @@ export const engagementCreateAction: ActionFunction = async ({ request }) => {
formData.getAll('users').forEach((user_id) => {
addTeamMemberToEngagement({ user_id: user_id.toString(), engagement_id: engagement.id });
});
return redirect(`/engagements/${engagement.id}/details/config`);
return redirect(getPath(ROUTES.ENGAGEMENT_DETAILS_CONFIG, { engagementId: engagement.id }));
};

export default engagementCreateAction;
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getTeamMembers,
} from 'services/membershipService';
import { store } from 'store';
import { ROUTES, getPath } from 'routes/routes';

export const engagementUpdateAction: ActionFunction = async ({ request, params }) => {
const formData = (await request.formData()) as FormData;
Expand Down Expand Up @@ -72,7 +73,7 @@ export const engagementUpdateAction: ActionFunction = async ({ request, params }
console.error('Error updating team members', e);
}

return redirect(`/engagements/${engagementId}/details/config`);
return redirect(getPath(ROUTES.ENGAGEMENT_DETAILS_CONFIG, { engagementId }));
};

export default engagementUpdateAction;
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import MultiSelect from './MultiSelect';
import { SystemMessage } from 'components/common/Layout/SystemMessage';
import { LanguageLoaderData } from './LanguageLoader';
import { Awaited } from 'utils';
import { ROUTES, getPath } from 'routes/routes';

export const LanguageManager = () => {
const SINGLE_LANGUAGE = [{ code: 'en', name: 'English' }] as Language[];
Expand All @@ -39,7 +40,7 @@ export const LanguageManager = () => {
const isSingleLanguage = determineSingleLanguage(selectedLanguages);

useEffect(() => {
fetcher.load('/languages/');
fetcher.load(`${getPath(ROUTES.LANGUAGES)}/`);
}, []);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Heading1, Heading2 } from 'components/common/Typography';
import dayjs from 'dayjs';
import { Language } from 'models/language';
import { Grid2 as Grid, Skeleton } from '@mui/material';
import { ROUTES, getPath } from 'routes/routes';

const EngagementConfigurationWizard = () => {
const { engagement, teamMembers, slug } = useRouteLoaderData('single-engagement') as EngagementLoaderAdminData;
Expand Down Expand Up @@ -84,7 +85,7 @@ const ConfigForm = ({
}),
{
method: 'patch',
action: `/engagements/${engagement.id}/details/config/edit`,
action: getPath(ROUTES.ENGAGEMENT_DETAILS_CONFIG_EDIT, { engagementId: engagement.id }),
},
);
};
Expand All @@ -104,7 +105,7 @@ const ConfigForm = ({

return (
<FormProvider {...engagementConfigForm}>
<EngagementForm onSubmit={onSubmit} />
<EngagementForm engagement={engagement} onSubmit={onSubmit} />
</FormProvider>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import EngagementForm, { EngagementConfigurationData } from '.';
import { Heading1, Heading2 } from 'components/common/Typography';
import { SystemMessage } from 'components/common/Layout/SystemMessage';
import Grid from '@mui/material/Grid2';
import { ROUTES, getPath } from 'routes/routes';

const EngagementCreationWizard = () => {
const fetcher = useFetcher({ key: 'config-update' });
Expand Down Expand Up @@ -42,7 +43,7 @@ const EngagementCreationWizard = () => {
}),
{
method: 'post',
action: '/engagements/create/',
action: `${getPath(ROUTES.ENGAGEMENT_CREATE)}/`,
},
);
};
Expand All @@ -66,7 +67,7 @@ const EngagementCreationWizard = () => {
</Grid>
<Grid size={12} mt={5}>
<FormProvider {...engagementCreationForm}>
<EngagementForm isNewEngagement onSubmit={onSubmit} />
<EngagementForm engagement={null} onSubmit={onSubmit} />
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.

Nice, one less unneeded prop.

</FormProvider>
</Grid>
</ResponsiveContainer>
Expand Down
16 changes: 11 additions & 5 deletions web/src/components/engagement/admin/config/wizard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import { DateRangePickerWithCalculation } from '../DateRangePickerWithCalculatio
import { LanguageManager } from '../LanguageManager';
import { UserManager } from '../UserManager';
import { User } from 'models/user';
import { Engagement } from 'models/engagement';
import { Language } from 'models/language';
import { FormStep } from 'components/common/Layout/FormStep';
import { Modal, CircularProgress } from '@mui/material';
import { modalStyle } from 'components/common';
import UnsavedWorkConfirmation from 'components/common/Navigation/UnsavedWorkConfirmation';
import { ROUTES, getPath } from 'routes/routes';

export interface EngagementConfigurationData {
// 1. Title
Expand All @@ -38,10 +40,10 @@ export interface EngagementConfigurationData {

const EngagementForm = ({
onSubmit,
isNewEngagement,
engagement,
}: {
onSubmit: (data: EngagementConfigurationData) => void;
isNewEngagement?: boolean;
engagement: Engagement | null;
}) => {
const engagementForm = useFormContext<EngagementConfigurationData>();

Expand All @@ -54,10 +56,14 @@ const EngagementForm = ({

const [nameHasBeenEdited, setNameHasBeenEdited] = useState(false);

const parentPageLink = engagement
? getPath(ROUTES.ENGAGEMENT_DETAILS_CONFIG, { engagementId: engagement.id })
: getPath(ROUTES.ENGAGEMENTS);

return (
<Form onSubmit={handleSubmit(onSubmit)} id="engagement-config-form">
<Grid container sx={{ maxWidth: '49.25rem' }}>
<Heading2 decorated>{isNewEngagement ? 'Configure Engagement' : 'Edit Configuration'}</Heading2>
<Heading2 decorated>{engagement ? 'Edit Configuration' : 'Configure Engagement'}</Heading2>
<Controller
control={control}
name="name"
Expand Down Expand Up @@ -152,9 +158,9 @@ const EngagementForm = ({
variant="primary"
type="submit"
>
{isNewEngagement ? 'Create Engagement' : 'Save Changes'}
{engagement ? 'Save Changes' : 'Create Engagement'}
</Button>
<Button href={isNewEngagement ? '/engagements' : '../'}>Cancel</Button>
<Button href={parentPageLink}>Cancel</Button>
</Grid>
<UnsavedWorkConfirmation blockNavigationWhen={isDirty && !isSubmitting} />
<Modal open={isSubmitting || isSubmitted}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { RouterLinkRenderer } from 'components/common/Navigation/Link';
import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons';
import { EngagementViewSections } from 'components/engagement/public/view';
import { useAuthoringPreviewWindow } from './AuthoringPreviewWindowContext';
import { getPath, ROUTES } from 'routes/routes';
const PREVIEW_CLOSE_GRACE_MS = 800;

const AuthoringBottomNav = ({
Expand Down Expand Up @@ -86,7 +87,8 @@ const AuthoringBottomNav = ({
}
};

const getTargetPreviewBasePath = () => `${getBasePathPrefix()}/engagements/${engagementId}/preview`;
const getTargetPreviewBasePath = () =>
`${getBasePathPrefix()}/${getPath(ROUTES.ADMIN_ENGAGEMENT_PREVIEW, { engagementId: engagementId ?? '' })}`;
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.

You could simplify this line a bit if you validate the engagementId (or any other route params) within your getPath() function. Then you just need to use { engagementId } for the second arg.


const postPreviewScrollMessage = (previewWindow: Window, section?: string) => {
const targetHash = getPreviewSectionHash(section);
Expand Down Expand Up @@ -167,7 +169,7 @@ const AuthoringBottomNav = ({
const previewWindow = getActivePreviewWindow();
if (!previewWindow || previewWindow.closed) return;

const expectedPathPrefix = `${getBasePathPrefix()}/engagements/${engagementId}/preview`;
const expectedPathPrefix = `${getBasePathPrefix()}/${getPath(ROUTES.ADMIN_ENGAGEMENT_PREVIEW, { engagementId: engagementId ?? '' })}`;
try {
if (!previewWindow.location.pathname.startsWith(expectedPathPrefix)) {
syncPreviewWindowUrl(pageName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { openNotification } from 'services/notificationService/notificationSlice
import { saveObject } from 'services/objectStorageService';
import { FormDetailsTab } from './types';
import { AuthoringPreviewWindowProvider } from './AuthoringPreviewWindowContext';
import { ROUTES, getPath } from 'routes/routes';

const tabSchema = yup.object({
id: yup.number().required(),
Expand Down Expand Up @@ -274,7 +275,7 @@ export const AuthoringContext = () => {
fetcher.data = undefined;
}
}, [fetcher.data]);
const pageName = useMatch('/engagements/:engagementId/details/authoring/:page')?.params.page;
const pageName = useMatch(ROUTES.AUTHORING_PAGE)?.params.page;
/* Changes the resolver based on the page name.
If you require more complex validation, you can
define your own resolver and add a case for it here.
Expand Down Expand Up @@ -380,7 +381,7 @@ export const AuthoringContext = () => {
}),
{
method: 'post',
action: `/engagements/${data.id}/details/authoring/${pageName}`,
action: getPath(ROUTES.AUTHORING_PAGE, { engagementId: data.id, page: pageName ?? 'banner' }),
},
);
};
Expand Down
Loading
Loading