From 318330ef7b3d55b51152c1eee4c31cc960d8bd9c Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Sat, 30 May 2026 16:43:03 -0400 Subject: [PATCH] fix(app): fit requirements table columns --- .../components/FrameworkRequirements.tsx | 80 +++++++++---------- .../FrameworkRequirementsGrouped.tsx | 70 ++++++++-------- .../components/GroupedRequirementRow.tsx | 11 +-- .../requirements-table-layout.test.tsx | 31 +++++++ .../components/requirements-table-layout.tsx | 55 +++++++++++++ 5 files changed, 162 insertions(+), 85 deletions(-) create mode 100644 apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/requirements-table-layout.test.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/requirements-table-layout.tsx diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx index 6809df96b..04bdbb62a 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx @@ -1,36 +1,39 @@ 'use client'; +import type { StatusType } from '@/components/status-indicator'; +import { + type EvidenceSubmissionInfo, + type RequirementArtifactCounts, + getControlProgressPercent, + getControlStatus, + getRequirementArtifactCounts, + getRequirementCompliancePercent, + getRequirementStatus, +} from '@/lib/control-compliance'; +import type { FrameworkInstanceWithControls } from '@/lib/types/framework'; import type { Control, FrameworkEditorRequirement, Task } from '@db'; import { Badge, - InputGroup, InputGroupAddon, InputGroupInput, Table, TableBody, TableCell, - TableHead, - TableHeader, TableRow, Text, } from '@trycompai/design-system'; import { Search } from '@trycompai/design-system/icons'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useMemo, useState } from 'react'; +import { + REQUIREMENTS_TABLE_COLUMN_COUNT, + REQUIREMENTS_TABLE_STYLE, + RequirementsTableColumnGroup, + RequirementsTableHeader, +} from './requirements-table-layout'; const PAGE_SIZE_OPTIONS = [10, 25, 50, 100]; -import type { FrameworkInstanceWithControls } from '@/lib/types/framework'; -import { - type EvidenceSubmissionInfo, - type RequirementArtifactCounts, - getControlProgressPercent, - getControlStatus, - getRequirementArtifactCounts, - getRequirementCompliancePercent, - getRequirementStatus, -} from '@/lib/control-compliance'; -import type { StatusType } from '@/components/status-indicator'; interface RequirementItem extends FrameworkEditorRequirement { mappedControlsCount: number; @@ -109,9 +112,10 @@ export function FrameworkRequirements({ }, [requirementDefinitions, frameworkInstanceWithControls.controls, tasks, evidenceSubmissions]); const sortedItems = useMemo( - () => [...items].sort((a, b) => - (a.identifier ?? '').localeCompare(b.identifier ?? '', undefined, { numeric: true }), - ), + () => + [...items].sort((a, b) => + (a.identifier ?? '').localeCompare(b.identifier ?? '', undefined, { numeric: true }), + ), [items], ); @@ -151,12 +155,15 @@ export function FrameworkRequirements({ ) => setSearchTerm(event.target.value)} + onChange={(event: React.ChangeEvent) => + setSearchTerm(event.target.value) + } /> - - - Identifier - Name - Description - Compliance - Status - Controls - Policies - Tasks - Documents - - + + {paginatedItems.length === 0 ? ( - + No requirements found. @@ -214,24 +210,18 @@ export function FrameworkRequirements({ {identifier || '—'} - + {item.name} - + {item.description || '—'} -
-
+
+
- {item.artifactCounts.policies.completed}/{item.artifactCounts.policies.total} + {item.artifactCounts.policies.completed}/ + {item.artifactCounts.policies.total}
@@ -271,7 +262,8 @@ export function FrameworkRequirements({
- {item.artifactCounts.documents.completed}/{item.artifactCounts.documents.total} + {item.artifactCounts.documents.completed}/ + {item.artifactCounts.documents.total}
diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirementsGrouped.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirementsGrouped.tsx index 535dbf88d..ac10dafb9 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirementsGrouped.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirementsGrouped.tsx @@ -11,8 +11,6 @@ import { Table, TableBody, TableCell, - TableHead, - TableHeader, TableRow, Text, } from '@trycompai/design-system'; @@ -20,13 +18,13 @@ import { ChevronDown, ChevronRight, Search } from '@trycompai/design-system/icon import { useParams, useRouter } from 'next/navigation'; import { parseAsArrayOf, parseAsString, useQueryState } from 'nuqs'; import { useCallback, useMemo, useState } from 'react'; -import { FamilyFilterDropdown } from './FamilyFilterDropdown'; import { areAllFamiliesExpanded, isFamilyExpanded, toggleAllFamilyExpansion, toggleFamilyExpansion, } from './family-expansion-state'; +import { FamilyFilterDropdown } from './FamilyFilterDropdown'; import { buildRequirementItems, getFamilyDisplayLabel, @@ -34,8 +32,12 @@ import { type RequirementFamilyGroup, } from './framework-controls-shared'; import { GroupedRequirementRow } from './GroupedRequirementRow'; - -const COLUMN_COUNT = 9; +import { + REQUIREMENTS_TABLE_COLUMN_COUNT, + REQUIREMENTS_TABLE_STYLE, + RequirementsTableColumnGroup, + RequirementsTableHeader, +} from './requirements-table-layout'; export function FrameworkRequirementsGrouped({ requirementDefinitions, @@ -48,7 +50,10 @@ export function FrameworkRequirementsGrouped({ tasks?: (Task & { controls: Control[] })[]; evidenceSubmissions?: EvidenceSubmissionInfo[]; }) { - const { orgId, frameworkInstanceId } = useParams<{ orgId: string; frameworkInstanceId: string }>(); + const { orgId, frameworkInstanceId } = useParams<{ + orgId: string; + frameworkInstanceId: string; + }>(); const router = useRouter(); const handleRowClick = useCallback( @@ -58,19 +63,26 @@ export function FrameworkRequirementsGrouped({ [orgId, frameworkInstanceId, router], ); - const [searchTerm, setSearchTerm] = useQueryState('rq', parseAsString.withDefault('').withOptions({ shallow: true, throttleMs: 300 })); - const [familyFilterParam, setFamilyFilterParam] = useQueryState('rfamilies', parseAsArrayOf(parseAsString, '|').withDefault([]).withOptions({ shallow: true })); + const [searchTerm, setSearchTerm] = useQueryState( + 'rq', + parseAsString.withDefault('').withOptions({ shallow: true, throttleMs: 300 }), + ); + const [familyFilterParam, setFamilyFilterParam] = useQueryState( + 'rfamilies', + parseAsArrayOf(parseAsString, '|').withDefault([]).withOptions({ shallow: true }), + ); const [expandedFamilies, setExpandedFamilies] = useState>(new Set()); const selectedFamilyFilter = useMemo(() => new Set(familyFilterParam), [familyFilterParam]); const allItems = useMemo( - () => buildRequirementItems( - requirementDefinitions, - frameworkInstanceWithControls.controls, - tasks ?? [], - evidenceSubmissions, - ), + () => + buildRequirementItems( + requirementDefinitions, + frameworkInstanceWithControls.controls, + tasks ?? [], + evidenceSubmissions, + ), [requirementDefinitions, frameworkInstanceWithControls.controls, tasks, evidenceSubmissions], ); @@ -93,7 +105,10 @@ export function FrameworkRequirementsGrouped({ }, [allGroups, selectedFamilyFilter]); const allFamilyNames = useMemo(() => allGroups.map((g) => g.family), [allGroups]); - const familyCounts = useMemo(() => new Map(allGroups.map((g) => [g.family, g.items.length])), [allGroups]); + const familyCounts = useMemo( + () => new Map(allGroups.map((g) => [g.family, g.items.length])), + [allGroups], + ); const isSearching = searchTerm.trim().length > 0; const visibleFamilyNames = useMemo(() => groups.map((g) => g.family), [groups]); @@ -103,9 +118,7 @@ export function FrameworkRequirementsGrouped({ }); const handleToggleFamily = (family: string) => { - setExpandedFamilies((prev) => - toggleFamilyExpansion({ expandedFamilies: prev, family }), - ); + setExpandedFamilies((prev) => toggleFamilyExpansion({ expandedFamilies: prev, family })); }; const handleToggleAll = () => { @@ -164,24 +177,13 @@ export function FrameworkRequirementsGrouped({ )}
-
- - - Identifier - Name - Description - Compliance - Status - Controls - Policies - Tasks - Documents - - +
+ + {groups.length === 0 ? ( - + No requirements found. @@ -226,7 +228,7 @@ function RequirementFamilySection({ return ( <> - +
+ + +
, + ); + + expect(container.querySelectorAll('col')).toHaveLength(REQUIREMENTS_TABLE_COLUMN_COUNT); + expect(screen.getByRole('columnheader', { name: 'Identifier' })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: 'Description' })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: 'Controls' })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: 'Compliance' })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: 'Status' })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: 'Docs' })).toHaveAttribute( + 'title', + 'Documents', + ); + expect(REQUIREMENTS_TABLE_STYLE).toMatchObject({ tableLayout: 'fixed' }); + }); +}); diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/requirements-table-layout.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/requirements-table-layout.tsx new file mode 100644 index 000000000..21a7a1df8 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/components/requirements-table-layout.tsx @@ -0,0 +1,55 @@ +import { TableHead, TableHeader, TableRow } from '@trycompai/design-system'; +import type { CSSProperties } from 'react'; + +interface RequirementsTableColumn { + id: string; + label: string; + title?: string; + width: string; +} + +const REQUIREMENTS_TABLE_COLUMNS = [ + { id: 'identifier', label: 'Identifier', width: '10%' }, + { id: 'name', label: 'Name', width: '19%' }, + { id: 'description', label: 'Description', width: '22%' }, + { id: 'compliance', label: 'Compliance', width: '13%' }, + { id: 'status', label: 'Status', width: '11%' }, + { id: 'controls', label: 'Controls', width: '7%' }, + { id: 'policies', label: 'Policies', width: '6.5%' }, + { id: 'tasks', label: 'Tasks', width: '5.5%' }, + { id: 'documents', label: 'Docs', title: 'Documents', width: '6%' }, +] as const satisfies readonly RequirementsTableColumn[]; + +export const REQUIREMENTS_TABLE_COLUMN_COUNT = REQUIREMENTS_TABLE_COLUMNS.length; + +export const REQUIREMENTS_TABLE_STYLE: CSSProperties = { + tableLayout: 'fixed', +}; + +export function RequirementsTableColumnGroup() { + return ( + + {REQUIREMENTS_TABLE_COLUMNS.map((column) => ( + + ))} + + ); +} + +export function RequirementsTableHeader() { + return ( + + + {REQUIREMENTS_TABLE_COLUMNS.map((column) => ( + + {column.label} + + ))} + + + ); +}