Skip to content

Commit 4613555

Browse files
[dev] [tofikwest] tofik/faq-logic-trust-portal (#1958)
* feat: faq trust portal logic in trust setting * fix: change field name for faq, update UI * chore: update db package to canory version to check logic in stage * feat(trust): normalize FAQ order on save and update handling * feat(trust): create collision-safe temporary FAQ IDs and optimize dirty state handling * fix(trust): update FAQ prompt text for clarity --------- Co-authored-by: Tofik Hasanov <annexcies@gmail.com>
1 parent 0a2bd7a commit 4613555

18 files changed

Lines changed: 553 additions & 14 deletions

File tree

apps/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"@react-email/components": "^0.0.41",
2727
"@trigger.dev/build": "4.0.6",
2828
"@trigger.dev/sdk": "4.0.6",
29-
"@trycompai/db": "1.3.19",
29+
"@trycompai/db": "^1.3.20-canary.0",
3030
"@trycompai/email": "workspace:*",
3131
"@upstash/vector": "^1.2.2",
3232
"adm-zip": "^0.5.16",

apps/api/src/trust-portal/trust-access.controller.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,4 +488,45 @@ export class TrustAccessController {
488488
framework as any,
489489
);
490490
}
491+
492+
@Get(':friendlyUrl/faqs')
493+
@HttpCode(HttpStatus.OK)
494+
@ApiOperation({
495+
summary: 'Get FAQs for a trust portal',
496+
description:
497+
'Retrieve the frequently asked questions for a published trust portal as structured data.',
498+
})
499+
@ApiParam({
500+
name: 'friendlyUrl',
501+
description: 'Trust Portal friendly URL or Organization ID',
502+
})
503+
@ApiResponse({
504+
status: HttpStatus.OK,
505+
description: 'FAQs retrieved successfully',
506+
schema: {
507+
type: 'object',
508+
properties: {
509+
faqs: {
510+
type: 'array',
511+
items: {
512+
type: 'object',
513+
properties: {
514+
id: { type: 'string' },
515+
question: { type: 'string' },
516+
answer: { type: 'string' },
517+
order: { type: 'number' },
518+
},
519+
},
520+
nullable: true,
521+
},
522+
},
523+
},
524+
})
525+
@ApiResponse({
526+
status: HttpStatus.NOT_FOUND,
527+
description: 'Trust site not found or not published',
528+
})
529+
async getFaqs(@Param('friendlyUrl') friendlyUrl: string) {
530+
return this.trustAccessService.getFaqs(friendlyUrl);
531+
}
491532
}

apps/api/src/trust-portal/trust-access.service.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,30 @@ export class TrustAccessService {
11871187
}));
11881188
}
11891189

1190+
/**
1191+
* Get FAQ markdown for a published trust portal.
1192+
*
1193+
* Recommended markdown format:
1194+
* - Use ### headings for questions (e.g., "### What is your security policy?")
1195+
* - Use regular markdown text for answers
1196+
* - Supports standard markdown: links, lists, code blocks, etc.
1197+
*
1198+
* Important: Render markdown WITHOUT rehype-raw (no raw HTML) for security.
1199+
*
1200+
* @param friendlyUrl - Trust portal friendly URL or organization ID
1201+
* @returns FAQ markdown content (empty string if not configured)
1202+
*/
1203+
async getFaqs(friendlyUrl: string): Promise<{ faqs: any[] | null }> {
1204+
const trust = await this.findPublishedTrustByRouteId(friendlyUrl);
1205+
const organization = await db.organization.findUnique({
1206+
where: { id: trust.organizationId },
1207+
select: { trustPortalFaqs: true },
1208+
});
1209+
1210+
const faqs = organization?.trustPortalFaqs;
1211+
return { faqs: Array.isArray(faqs) ? faqs : null };
1212+
}
1213+
11901214
async getComplianceResourceUrlByAccessToken(
11911215
token: string,
11921216
framework: TrustFramework,

apps/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"@tiptap/extension-table-row": "^3.4.4",
5757
"@trigger.dev/react-hooks": "4.0.6",
5858
"@trigger.dev/sdk": "4.0.6",
59-
"@trycompai/db": "1.3.19",
59+
"@trycompai/db": "^1.3.20-canary.0",
6060
"@trycompai/email": "workspace:*",
6161
"@types/canvas-confetti": "^1.9.0",
6262
"@types/react-syntax-highlighter": "^15.5.13",
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
'use server';
2+
3+
import { authActionClient } from '@/actions/safe-action';
4+
import { db } from '@db';
5+
import { revalidatePath } from 'next/cache';
6+
import { z } from 'zod';
7+
import { faqArraySchema } from '../types/faq';
8+
9+
const updateTrustPortalFaqsSchema = z.object({
10+
faqs: faqArraySchema,
11+
});
12+
13+
export const updateTrustPortalFaqsAction = authActionClient
14+
.inputSchema(updateTrustPortalFaqsSchema)
15+
.metadata({
16+
name: 'update-trust-portal-faqs',
17+
track: {
18+
event: 'update-trust-portal-faqs',
19+
channel: 'server',
20+
},
21+
})
22+
.action(async ({ parsedInput, ctx }) => {
23+
const { faqs } = parsedInput;
24+
const { activeOrganizationId } = ctx.session;
25+
26+
if (!activeOrganizationId) {
27+
throw new Error('No active organization');
28+
}
29+
30+
// Normalize order values on the server to prevent gaps/duplicates and ensure stable rendering.
31+
const normalizedFaqs = faqs.map((faq, index) => ({
32+
...faq,
33+
order: index,
34+
}));
35+
36+
try {
37+
await db.organization.update({
38+
where: {
39+
id: activeOrganizationId,
40+
},
41+
data: {
42+
trustPortalFaqs:
43+
normalizedFaqs.length > 0
44+
? (JSON.parse(JSON.stringify(normalizedFaqs)) as any)
45+
: (null as any),
46+
},
47+
});
48+
49+
revalidatePath(`/${activeOrganizationId}/trust/portal-settings`);
50+
51+
return {
52+
success: true,
53+
};
54+
} catch (error) {
55+
console.error('Error updating FAQs:', error);
56+
const errorMessage =
57+
error instanceof Error ? error.message : 'Failed to update FAQs';
58+
throw new Error(errorMessage);
59+
}
60+
});
61+

0 commit comments

Comments
 (0)