Skip to content

Commit 46b9575

Browse files
feat: allow selecting which policies to download when clicking download all
* feat(policies): allow filtering download-all by policyIds * feat(policies): parse policyIds query param on download-all endpoint * fix(policies): use typed mockAuthContext in controller spec * feat(policies): add policy download picker sheet * feat(policies): open download picker sheet from Download All button * chore(policies): import icons from design-system re-export * fix(policies): reset picker selection on reopen and accept array policyIds Addresses cubic review on PR #2672: - PolicyDownloadSheet: reset selection to current policy IDs whenever the sheet opens or the policies prop changes, so reopens and upstream data refreshes don't leave stale or deleted IDs selected. - Controller: accept repeated-key array form (?policyIds=a&policyIds=b) in addition to comma-separated, and flatten both into a single deduped string[]. --------- Co-authored-by: Mariano <marfuen98@gmail.com>
1 parent 56f5ae2 commit 46b9575

9 files changed

Lines changed: 548 additions & 34 deletions

File tree

apps/api/src/policies/policies.controller.spec.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ jest.mock('@db', () => ({
5353
draft: 'draft',
5454
published: 'published',
5555
},
56+
FindingType: {
57+
soc2: 'soc2',
58+
iso27001: 'iso27001',
59+
},
5660
}));
5761

5862
jest.mock('@trigger.dev/sdk', () => ({
@@ -188,17 +192,115 @@ describe('PoliciesController', () => {
188192
const result = await controller.downloadAllPolicies(
189193
orgId,
190194
mockAuthContext,
195+
undefined,
191196
);
192197

193198
expect(policiesService.downloadAllPoliciesPdf).toHaveBeenCalledWith(
194199
orgId,
200+
undefined,
195201
);
196202
expect(result).toEqual({
197203
url: 'https://s3.example.com/bundle.pdf',
198204
authType: 'session',
199205
authenticatedUser: { id: 'usr_123', email: 'test@example.com' },
200206
});
201207
});
208+
209+
it('parses comma-separated policyIds and passes an array to the service', async () => {
210+
const mockResult = { downloadUrl: 'https://s3/signed', name: 'all-policies', policyCount: 2 };
211+
mockPoliciesService.downloadAllPoliciesPdf.mockResolvedValue(mockResult);
212+
213+
await controller.downloadAllPolicies(
214+
orgId,
215+
mockAuthContext,
216+
'p1, p2 ,p3',
217+
);
218+
219+
expect(policiesService.downloadAllPoliciesPdf).toHaveBeenCalledWith(
220+
orgId,
221+
['p1', 'p2', 'p3'],
222+
);
223+
});
224+
225+
it('dedupes policyIds and strips empty entries', async () => {
226+
const mockResult = { downloadUrl: 'https://s3/signed', name: 'all-policies', policyCount: 1 };
227+
mockPoliciesService.downloadAllPoliciesPdf.mockResolvedValue(mockResult);
228+
229+
await controller.downloadAllPolicies(
230+
orgId,
231+
mockAuthContext,
232+
'p1,,p1,p2,',
233+
);
234+
235+
expect(policiesService.downloadAllPoliciesPdf).toHaveBeenCalledWith(
236+
orgId,
237+
['p1', 'p2'],
238+
);
239+
});
240+
241+
it('passes undefined when policyIds query is missing', async () => {
242+
const mockResult = { downloadUrl: 'https://s3/signed', name: 'all-policies', policyCount: 10 };
243+
mockPoliciesService.downloadAllPoliciesPdf.mockResolvedValue(mockResult);
244+
245+
await controller.downloadAllPolicies(
246+
orgId,
247+
mockAuthContext,
248+
undefined,
249+
);
250+
251+
expect(policiesService.downloadAllPoliciesPdf).toHaveBeenCalledWith(
252+
orgId,
253+
undefined,
254+
);
255+
});
256+
257+
it('passes undefined when policyIds query is an empty string', async () => {
258+
const mockResult = { downloadUrl: 'https://s3/signed', name: 'all-policies', policyCount: 10 };
259+
mockPoliciesService.downloadAllPoliciesPdf.mockResolvedValue(mockResult);
260+
261+
await controller.downloadAllPolicies(
262+
orgId,
263+
mockAuthContext,
264+
'',
265+
);
266+
267+
expect(policiesService.downloadAllPoliciesPdf).toHaveBeenCalledWith(
268+
orgId,
269+
undefined,
270+
);
271+
});
272+
273+
it('handles repeated-key array form (policyIds=a&policyIds=b)', async () => {
274+
const mockResult = { downloadUrl: 'https://s3/signed', name: 'all-policies', policyCount: 2 };
275+
mockPoliciesService.downloadAllPoliciesPdf.mockResolvedValue(mockResult);
276+
277+
await controller.downloadAllPolicies(
278+
orgId,
279+
mockAuthContext,
280+
['p1', 'p2'],
281+
);
282+
283+
expect(policiesService.downloadAllPoliciesPdf).toHaveBeenCalledWith(
284+
orgId,
285+
['p1', 'p2'],
286+
);
287+
});
288+
289+
it('handles mixed array form where each value itself contains commas', async () => {
290+
const mockResult = { downloadUrl: 'https://s3/signed', name: 'all-policies', policyCount: 3 };
291+
mockPoliciesService.downloadAllPoliciesPdf.mockResolvedValue(mockResult);
292+
293+
await controller.downloadAllPolicies(
294+
orgId,
295+
mockAuthContext,
296+
['p1,p2', 'p3'],
297+
);
298+
299+
expect(policiesService.downloadAllPoliciesPdf).toHaveBeenCalledWith(
300+
orgId,
301+
['p1', 'p2', 'p3'],
302+
);
303+
});
202304
});
203305

204306
describe('getPolicy', () => {

apps/api/src/policies/policies.controller.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,22 @@ import {
7272
} from './schemas/version-responses';
7373
import { PolicyResponseDto } from './dto/policy-responses.dto';
7474

75+
function parsePolicyIdsParam(
76+
raw: string | string[] | undefined,
77+
): string[] | undefined {
78+
if (!raw) return undefined;
79+
const values = Array.isArray(raw) ? raw : [raw];
80+
const ids = Array.from(
81+
new Set(
82+
values
83+
.flatMap((value) => value.split(','))
84+
.map((s) => s.trim())
85+
.filter((s) => s.length > 0),
86+
),
87+
);
88+
return ids.length > 0 ? ids : undefined;
89+
}
90+
7591
@ApiTags('Policies')
7692
@ApiExtraModels(PolicyResponseDto)
7793
@Controller({ path: 'policies', version: '1' })
@@ -154,9 +170,14 @@ export class PoliciesController {
154170
async downloadAllPolicies(
155171
@OrganizationId() organizationId: string,
156172
@AuthContext() authContext: AuthContextType,
173+
@Query('policyIds') policyIdsParam?: string | string[],
157174
) {
158-
const result =
159-
await this.policiesService.downloadAllPoliciesPdf(organizationId);
175+
const policyIds = parsePolicyIdsParam(policyIdsParam);
176+
177+
const result = await this.policiesService.downloadAllPoliciesPdf(
178+
organizationId,
179+
policyIds,
180+
);
160181

161182
return {
162183
...result,

apps/api/src/policies/policies.service.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1268,7 +1268,10 @@ export class PoliciesService {
12681268
/**
12691269
* Download all published policies as a single PDF bundle (no watermark)
12701270
*/
1271-
async downloadAllPoliciesPdf(organizationId: string) {
1271+
async downloadAllPoliciesPdf(
1272+
organizationId: string,
1273+
policyIds?: string[],
1274+
) {
12721275
// Get organization info
12731276
const organization = await db.organization.findUnique({
12741277
where: { id: organizationId },
@@ -1285,6 +1288,9 @@ export class PoliciesService {
12851288
organizationId,
12861289
isArchived: false,
12871290
archivedAt: null,
1291+
...(policyIds && policyIds.length > 0
1292+
? { id: { in: policyIds } }
1293+
: {}),
12881294
},
12891295
select: {
12901296
id: true,

apps/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150
"@testing-library/dom": "^10.4.0",
151151
"@testing-library/jest-dom": "^6.6.3",
152152
"@testing-library/react": "^16.3.0",
153+
"@testing-library/user-event": "^14.6.1",
153154
"@trigger.dev/build": "4.4.3",
154155
"@types/d3": "^7.4.3",
155156
"@types/jspdf": "^2.0.0",

0 commit comments

Comments
 (0)