Skip to content

Commit c6b30e9

Browse files
committed
fix(documents): support matrix file submission paths
1 parent 52bb3f6 commit c6b30e9

File tree

2 files changed

+89
-71
lines changed

2 files changed

+89
-71
lines changed

apps/app/src/app/(app)/[orgId]/documents/components/CompanySubmissionWizard.tsx

Lines changed: 42 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,37 @@ function createEmptyMatrixRow(columns: ReadonlyArray<MatrixColumnDefinition>): M
6363
return Object.fromEntries(columns.map((column) => [column.key, '']));
6464
}
6565

66+
function buildFileAcceptMap(accept?: string): Record<string, string[]> {
67+
if (!accept) {
68+
return { 'application/pdf': [], 'image/*': [], 'text/*': [] };
69+
}
70+
71+
return Object.fromEntries(
72+
accept
73+
.split(',')
74+
.map((ext): [string, string[]] | null => {
75+
const trimmed = ext.trim().toLowerCase();
76+
if (trimmed === '.pdf') return ['application/pdf', []];
77+
if (trimmed === '.doc') return ['application/msword', []];
78+
if (trimmed === '.docx') {
79+
return ['application/vnd.openxmlformats-officedocument.wordprocessingml.document', []];
80+
}
81+
if (trimmed === '.png') return ['image/png', []];
82+
if (trimmed === '.jpg' || trimmed === '.jpeg') return ['image/jpeg', []];
83+
if (trimmed === '.txt') return ['text/plain', []];
84+
if (trimmed === '.svg') return ['image/svg+xml', []];
85+
if (trimmed === '.vsdx') return ['application/vnd.visio', []];
86+
if (trimmed === '.csv') return ['text/csv', []];
87+
if (trimmed === '.xlsx') {
88+
return ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', []];
89+
}
90+
if (trimmed === '.xls') return ['application/vnd.ms-excel', []];
91+
return null;
92+
})
93+
.filter((entry): entry is [string, string[]] => entry !== null),
94+
);
95+
}
96+
6697
export function CompanySubmissionWizard({
6798
organizationId,
6899
formType,
@@ -251,6 +282,15 @@ export function CompanySubmissionWizard({
251282

252283
const validateRequiredMatrixCells = () => {
253284
for (const field of matrixFields) {
285+
if (field.key === 'matrixRows') {
286+
const matrixFileValue = getValues('matrixFile' as never);
287+
const hasMatrixFile =
288+
typeof matrixFileValue === 'object' &&
289+
matrixFileValue !== null &&
290+
'fileName' in matrixFileValue;
291+
if (hasMatrixFile) continue;
292+
}
293+
254294
const rows = normalizeMatrixRows(getValues(field.key as never));
255295
const rowValues = rows.length > 0 ? rows : [createEmptyMatrixRow(field.columns)];
256296

@@ -649,46 +689,7 @@ export function CompanySubmissionWizard({
649689
<FileUploader
650690
maxFileCount={1}
651691
maxSize={100 * 1024 * 1024}
652-
accept={
653-
field.accept
654-
? Object.fromEntries(
655-
field.accept
656-
.split(',')
657-
.map((ext): [string, string[]] | null => {
658-
const trimmed = ext.trim().toLowerCase();
659-
if (trimmed === '.pdf') return ['application/pdf', []];
660-
if (trimmed === '.doc') return ['application/msword', []];
661-
if (trimmed === '.docx') {
662-
return [
663-
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
664-
[],
665-
];
666-
}
667-
if (trimmed === '.png') return ['image/png', []];
668-
if (trimmed === '.jpg' || trimmed === '.jpeg') {
669-
return ['image/jpeg', []];
670-
}
671-
if (trimmed === '.txt') return ['text/plain', []];
672-
if (trimmed === '.svg') return ['image/svg+xml', []];
673-
if (trimmed === '.vsdx')
674-
return ['application/vnd.visio', []];
675-
if (trimmed === '.csv') return ['text/csv', []];
676-
if (trimmed === '.xlsx') {
677-
return [
678-
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
679-
[],
680-
];
681-
}
682-
if (trimmed === '.xls')
683-
return ['application/vnd.ms-excel', []];
684-
return null;
685-
})
686-
.filter(
687-
(entry): entry is [string, string[]] => entry !== null,
688-
),
689-
)
690-
: { 'application/pdf': [], 'image/*': [], 'text/*': [] }
691-
}
692+
accept={buildFileAcceptMap(field.accept)}
692693
disabled={uploadingField === field.key}
693694
onUpload={async (files) => {
694695
const file = files[0];
@@ -817,34 +818,7 @@ export function CompanySubmissionWizard({
817818
<FileUploader
818819
maxFileCount={1}
819820
maxSize={100 * 1024 * 1024}
820-
accept={
821-
field.accept
822-
? Object.fromEntries(
823-
field.accept
824-
.split(',')
825-
.map((ext): [string, string[]] | null => {
826-
const trimmed = ext.trim().toLowerCase();
827-
if (trimmed === '.pdf') return ['application/pdf', []];
828-
if (trimmed === '.doc') return ['application/msword', []];
829-
if (trimmed === '.docx') {
830-
return [
831-
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
832-
[],
833-
];
834-
}
835-
if (trimmed === '.png') return ['image/png', []];
836-
if (trimmed === '.jpg' || trimmed === '.jpeg') {
837-
return ['image/jpeg', []];
838-
}
839-
if (trimmed === '.txt') return ['text/plain', []];
840-
if (trimmed === '.svg') return ['image/svg+xml', []];
841-
if (trimmed === '.vsdx') return ['application/vnd.visio', []];
842-
return null;
843-
})
844-
.filter((entry): entry is [string, string[]] => entry !== null),
845-
)
846-
: { 'application/pdf': [], 'image/*': [], 'text/*': [] }
847-
}
821+
accept={buildFileAcceptMap(field.accept)}
848822
disabled={uploadingField === field.key}
849823
onUpload={async (files) => {
850824
const file = files[0];

packages/company/src/evidence-forms/submission-schemas.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,55 @@ const rbacMatrixRowSchema = z.object({
5858
lastReviewed: requiredTrimmed('Last reviewed'),
5959
});
6060

61-
const rbacMatrixDataSchema = z.object({
62-
submissionDate: required('Submission date'),
63-
matrixRows: z.array(rbacMatrixRowSchema).min(1, 'At least one RBAC entry is required'),
61+
const optionalRbacMatrixRowSchema = z.object({
62+
system: z.string().trim().optional(),
63+
roleName: z.string().trim().optional(),
64+
permissionsScope: z.string().trim().optional(),
65+
approvedBy: z.string().trim().optional(),
66+
lastReviewed: z.string().trim().optional(),
6467
});
6568

69+
const rbacMatrixDataSchema = z
70+
.object({
71+
submissionDate: required('Submission date'),
72+
matrixRows: z.array(optionalRbacMatrixRowSchema).optional(),
73+
matrixFile: evidenceFormFileSchema.optional(),
74+
})
75+
.superRefine((data, ctx) => {
76+
if (data.matrixFile) return;
77+
78+
const rows = data.matrixRows ?? [];
79+
const nonEmptyRows = rows.filter((row) =>
80+
Object.values(row).some((value) => typeof value === 'string' && value.trim().length > 0),
81+
);
82+
83+
if (nonEmptyRows.length === 0) {
84+
ctx.addIssue({
85+
code: z.ZodIssueCode.custom,
86+
message: 'At least one RBAC entry is required',
87+
path: ['matrixRows'],
88+
});
89+
return;
90+
}
91+
92+
rows.forEach((row, rowIndex) => {
93+
const hasAnyValue = Object.values(row).some(
94+
(value) => typeof value === 'string' && value.trim().length > 0,
95+
);
96+
if (!hasAnyValue) return;
97+
98+
const parsedRow = rbacMatrixRowSchema.safeParse(row);
99+
if (parsedRow.success) return;
100+
101+
parsedRow.error.issues.forEach((issue) => {
102+
ctx.addIssue({
103+
...issue,
104+
path: ['matrixRows', rowIndex, ...issue.path],
105+
});
106+
});
107+
});
108+
});
109+
66110
const infrastructureInventoryRowSchema = z.object({
67111
assetId: requiredTrimmed('Asset ID'),
68112
systemType: requiredTrimmed('System type'),

0 commit comments

Comments
 (0)