Skip to content

Commit d08555e

Browse files
authored
Replace edit system modal with system detail page (#322) (#324)
* feat: replace edit system modal with system detail page (#322) Add routed system detail page at /systems/:fismasystemid with read-only and edit modes using card-based layout. System name column in table is now a clickable link, admin edit icon navigates to detail page. Create mode remains in existing modal. Upgrade @cmsgov/design-system to v13. * fix: match read/edit layouts, resolve decommissioned_by name, fix refresh - Align edit view layout to match read view (System Status card above Organization, Contacts card standalone) - Show Active/Decommissioned chip in System Identity header for both views - Resolve decommissioned_by UUID to human-readable name in both read and edit modes, not just edit mode - Fix user API response path (res.data.data.fullname) in both SystemDetailPage and EditSystemModal - Set decommissioned_by name immediately from userInfo after decommission action instead of waiting for separate API roundtrip - Add retry fetch for individual system on page refresh when system is decommissioned and not in default active-only list - Add bottom margin for spacing before footer * fix: address code review findings from codex reviewer - Add cancellation cleanup to retry fetch effect to prevent memory leak on unmount - Remove stale closure comparison in decommissionedByName effect - Add NaN guard for invalid systemId URL parameter - Fix UTC date validation: use setUTCHours for consistent timezone comparison - Extract shared getTodayISO, truncateNotes, MAX_NOTES_LENGTH to src/utils/decommission.ts (DRY) - Replace magic number 500/100 with named constants - Standardize error handling in handleSave to match handleDecommission (add 403, 404 checks) - Fix breadcrumb underscore replacement to handle all occurrences and numeric segments * style: apply prettier formatting * fix: upgrade axios 1.12.0 to 1.13.5 to fix prototype pollution vulnerability
1 parent 29411a9 commit d08555e

File tree

15 files changed

+2457
-242
lines changed

15 files changed

+2457
-242
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
"@mui/x-data-grid": "^6.19.4",
6060
"@popperjs/core": "^2.11.8",
6161
"apollo-client": "^2.6.10",
62-
"axios": "1.12.0",
62+
"axios": "1.13.5",
6363
"classnames": "^2.5.1",
6464
"clipboard-copy": "^4.0.1",
6565
"core-js": "^3.37.1",
@@ -86,7 +86,7 @@
8686
"devDependencies": {
8787
"@axe-core/react": "^4.10.2",
8888
"@babel/core": "^7.23.9",
89-
"@cmsgov/design-system": "^10.1.2",
89+
"@cmsgov/design-system": "^13.2.0",
9090
"@commitlint/cli": "^18.6.1",
9191
"@commitlint/config-conventional": "^18.6.2",
9292
"@semantic-release/changelog": "^6.0.3",

src/components/BreadCrumbs/BreadCrumbs.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ interface LinkRouterProps extends LinkProps {
1111
function LinkRouter(props: LinkRouterProps) {
1212
return <Link {...props} component={RouterLink as any} />
1313
}
14-
export default function BreadCrumbs() {
14+
interface BreadCrumbsProps {
15+
segmentLabels?: Record<string, string>
16+
}
17+
18+
export default function BreadCrumbs({ segmentLabels }: BreadCrumbsProps) {
1519
const location = useLocation()
1620
// let currentLink: string = ''
1721
const homeLink = [
@@ -33,13 +37,19 @@ export default function BreadCrumbs() {
3337
]
3438
const crumbs = location.pathname.split('/').filter((x) => x)
3539
const path = crumbs.map((value) => {
36-
const text = value.replace('_', ' ')
40+
const displayText =
41+
segmentLabels && segmentLabels[value]
42+
? segmentLabels[value]
43+
: (() => {
44+
const text = value.replace(/_/g, ' ')
45+
return /^[A-Z]/.test(text) ? text : capitalize(text)
46+
})()
3747
return (
3848
<Typography
3949
sx={{ display: 'inline', whiteSpace: 'nowrap', color: '#5a5a5a' }}
4050
key={value}
4151
>
42-
{text[0] === text[0].toUpperCase() ? text : capitalize(text)}
52+
{displayText}
4353
</Typography>
4454
)
4555
})

src/router/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export enum RouteIds {
1010
DATA = 'data',
1111
USERS = 'users',
1212
SIGNIN = 'signin',
13+
SYSTEM_DETAIL = 'system-detail',
1314
}
1415

1516
export enum RouteNames {
@@ -30,4 +31,5 @@ export enum Routes {
3031
AUTH_LOGIN = `/${RouteIds.AUTH}/${RouteIds.LOGIN}`,
3132
AUTH_LOGOUT = `/${RouteIds.AUTH}/${RouteIds.LOGOUT}`,
3233
SIGNIN = `/${RouteIds.SIGNIN}`,
34+
SYSTEM_DETAIL = '/systems/:fismasystemid',
3335
}

src/router/router.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import HomePageContainer from '@/views/Home/Home'
1212
import UserTable from '@/views/UserTable/UserTable'
1313
import LoginPage from '@/views/LoginPage/LoginPage'
1414
import QuestionnarePage from '@/views/QuestionnairePage/QuestionnairePage'
15+
import SystemDetailPage from '@/views/SystemDetailPage/SystemDetailPage'
1516
/**
1617
* The hash router for the application that defines routes
1718
* and specifies the loaders for routes with dynamic data.
@@ -44,6 +45,12 @@ const router = createHashRouter([
4445
element: <QuestionnarePage />,
4546
errorElement: <ErrorBoundary />,
4647
},
48+
{
49+
path: Routes.SYSTEM_DETAIL,
50+
id: RouteIds.SYSTEM_DETAIL,
51+
element: <SystemDetailPage />,
52+
errorElement: <ErrorBoundary />,
53+
},
4754
{
4855
path: Routes.SIGNIN,
4956
id: RouteIds.SIGNIN,

src/utils/decommission.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/** Max character length for decommission notes input */
2+
export const MAX_NOTES_LENGTH = 500
3+
4+
/** Max characters to display in confirmation dialog before truncating */
5+
export const MAX_NOTES_DISPLAY_LENGTH = 100
6+
7+
/** Returns today's date as an ISO date string (YYYY-MM-DD) in local time */
8+
export function getTodayISO(): string {
9+
const today = new Date()
10+
const yyyy = today.getFullYear()
11+
const mm = String(today.getMonth() + 1).padStart(2, '0')
12+
const dd = String(today.getDate()).padStart(2, '0')
13+
return `${yyyy}-${mm}-${dd}`
14+
}
15+
16+
/** Truncates notes for display in confirmation dialogs */
17+
export function truncateNotes(notes: string): string {
18+
const trimmed = notes.trim()
19+
if (!trimmed) return ''
20+
return trimmed.length > MAX_NOTES_DISPLAY_LENGTH
21+
? trimmed.substring(0, MAX_NOTES_DISPLAY_LENGTH) + '...'
22+
: trimmed
23+
}

src/views/EditSystemModal/EditSystemModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,8 @@ export default function EditSystemModal({
158158
.get(`users/${userId}`)
159159
.then((res) => {
160160
if (!cancelled && system?.decommissioned_by === userId) {
161-
if (res.data?.fullname) {
162-
setDecommissionedByName(res.data.fullname)
161+
if (res.data?.data?.fullname) {
162+
setDecommissionedByName(res.data.data.fullname)
163163
} else {
164164
setDecommissionedByName(userId)
165165
}

src/views/FismaTable/FismaTable.tsx

Lines changed: 12 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,11 @@ import FormControlLabel from '@mui/material/FormControlLabel'
2020
import Switch from '@mui/material/Switch'
2121
import FileDownloadSharpIcon from '@mui/icons-material/FileDownloadSharp'
2222
import QuestionnareModal from '../QuestionnareModal/QuestionnareModal'
23-
import EditSystemModal from '../EditSystemModal/EditSystemModal'
2423
import CustomSnackbar from '../Snackbar/Snackbar'
2524
import axiosInstance from '@/axiosConfig'
2625
import { useContextProp } from '../Title/Context'
2726
import { EMPTY_USER } from '../../constants'
28-
import { useNavigate } from 'react-router-dom'
27+
import { useNavigate, Link } from 'react-router-dom'
2928
import { RouteNames, Routes } from '@/router/constants'
3029
import { ERROR_MESSAGES } from '../../constants'
3130
import EditIcon from '@mui/icons-material/Edit'
@@ -222,18 +221,12 @@ const pillarScoresCache = new Map<number, CachedScore>()
222221

223222
export default function FismaTable({ scores }: FismaTableProps) {
224223
const apiRef = useGridApiRef()
225-
const {
226-
fismaSystems,
227-
latestDataCallId,
228-
showDecommissioned,
229-
fetchFismaSystems,
230-
} = useContextProp()
224+
const { fismaSystems, latestDataCallId } = useContextProp()
231225
const [open, setOpen] = useState<boolean>(false)
232226
const { userInfo } = useContextProp() || EMPTY_USER
233227
const [selectedRow, setSelectedRow] = useState<FismaSystemType | null>(null)
234228
const [selectedRows, setSelectedRows] = useState<GridRowId[]>([])
235229
const navigate = useNavigate()
236-
const [openEditModal, setOpenEditModal] = useState<boolean>(false)
237230
const [pillarScoresModal, setPillarScoresModal] = useState<{
238231
open: boolean
239232
systemName: string
@@ -292,28 +285,6 @@ export default function FismaTable({ scores }: FismaTableProps) {
292285
const handleClosePillarScores = () => {
293286
setPillarScoresModal((prev) => ({ ...prev, open: false }))
294287
}
295-
const handleEditOpenModal = (
296-
event: React.MouseEvent<HTMLButtonElement>,
297-
row: FismaSystemType
298-
) => {
299-
event.stopPropagation()
300-
setSelectedRow(row)
301-
setOpenEditModal(true)
302-
}
303-
const handleCloseEditModal = (newRowData: FismaSystemType) => {
304-
if (selectedRow) {
305-
if (newRowData.decommissioned && !selectedRow.decommissioned) {
306-
fetchFismaSystems(showDecommissioned)
307-
} else {
308-
const row = apiRef.current.getRow(selectedRow?.fismasystemid)
309-
if (row) {
310-
apiRef.current.updateRows([newRowData])
311-
}
312-
}
313-
}
314-
setOpenEditModal(false)
315-
setSelectedRow(null)
316-
}
317288
const columns: GridColDef[] = [
318289
{
319290
field: 'fismaname',
@@ -322,6 +293,15 @@ export default function FismaTable({ scores }: FismaTableProps) {
322293
minWidth: 300,
323294
maxWidth: 450,
324295
hideable: false,
296+
renderCell: (params: GridRenderCellParams) => (
297+
<Link
298+
to={`/systems/${params.row.fismasystemid}`}
299+
style={{ color: '#004297', textDecoration: 'none' }}
300+
onClick={(e) => e.stopPropagation()}
301+
>
302+
{params.value}
303+
</Link>
304+
),
325305
},
326306
{
327307
field: 'fismaacronym',
@@ -462,7 +442,7 @@ export default function FismaTable({ scores }: FismaTableProps) {
462442
className="textPrimary"
463443
onClick={(event) => {
464444
event.stopPropagation()
465-
handleEditOpenModal(event, params.row as FismaSystemType)
445+
navigate(`/systems/${params.row.fismasystemid}?edit=true`)
466446
}}
467447
color="inherit"
468448
/>
@@ -544,13 +524,6 @@ export default function FismaTable({ scores }: FismaTableProps) {
544524
systemAcronym={pillarScoresModal.systemAcronym}
545525
scores={pillarScoresModal.scores}
546526
/>
547-
<EditSystemModal
548-
title={'Edit'}
549-
open={openEditModal}
550-
onClose={handleCloseEditModal}
551-
system={selectedRow}
552-
mode={'edit'}
553-
/>
554527
</Box>
555528
)
556529
}

0 commit comments

Comments
 (0)