Skip to content

Commit 0bb7d79

Browse files
authored
Merge pull request #945 from trycompai/claudio/stripe
[dev] [claudfuen] claudio/stripe
2 parents c9cf5b5 + ddd404d commit 0bb7d79

7 files changed

Lines changed: 254 additions & 315 deletions

File tree

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,29 @@
11
'use server';
22

3+
import { authWithOrgAccessClient } from '../safe-action';
34
import { addFrameworksSchema } from '@/actions/schema';
45
import { db } from '@comp/db';
56
import { Prisma } from '@comp/db/types';
6-
import { revalidatePath } from 'next/cache';
7-
import { z } from 'zod';
87
import { _upsertOrgFrameworkStructureCore } from './lib/initialize-organization';
98

10-
// Duplicating the InitializeOrganizationInput type for clarity, can be shared if preferred
11-
export type AddFrameworksInput = z.infer<typeof addFrameworksSchema>;
12-
139
/**
1410
* Adds specified frameworks and their related entities (controls, policies, tasks)
1511
* to an existing organization. It ensures that entities are not duplicated if they
1612
* already exist (e.g., from a shared template or a previous addition).
1713
*/
18-
export const addFrameworksToOrganizationAction = async (
19-
input: AddFrameworksInput,
20-
): Promise<{ success: boolean; error?: string }> => {
21-
try {
22-
const validatedInput = addFrameworksSchema.parse(input);
23-
const { frameworkIds, organizationId } = validatedInput;
14+
export const addFrameworksToOrganizationAction = authWithOrgAccessClient
15+
.inputSchema(addFrameworksSchema)
16+
.metadata({
17+
name: 'add-frameworks-to-organization',
18+
track: {
19+
event: 'add-frameworks',
20+
description: 'Add frameworks to organization',
21+
channel: 'server',
22+
},
23+
})
24+
.action(async ({ parsedInput, ctx }) => {
25+
const { user, member, organizationId } = ctx;
26+
const { frameworkIds } = parsedInput;
2427

2528
await db.$transaction(async (tx) => {
2629
// 1. Fetch FrameworkEditorFrameworks and their requirements for the given frameworkIds, filtering by visible: true
@@ -45,24 +48,13 @@ export const addFrameworksToOrganizationAction = async (
4548
organizationId,
4649
targetFrameworkEditorIds: finalFrameworkEditorIds,
4750
frameworkEditorFrameworks: frameworksAndRequirements,
48-
tx: tx as unknown as Prisma.TransactionClient, // Use the transaction client from this action
51+
tx: tx as unknown as Prisma.TransactionClient,
4952
});
50-
51-
// The rest of the logic (creating instances, relations) is now inside _upsertOrgFrameworkStructureCore
5253
});
5354

54-
revalidatePath('/'); // Revalidate all paths, or be more specific e.g. /${organizationId}/frameworks
55-
return { success: true };
56-
} catch (error) {
57-
console.error('Error in addFrameworksToOrganizationAction:', error);
58-
if (error instanceof z.ZodError) {
59-
return {
60-
success: false,
61-
error: error.errors.map((e) => e.message).join(', '),
62-
};
63-
} else if (error instanceof Error) {
64-
return { success: false, error: error.message };
65-
}
66-
return { success: false, error: 'An unexpected error occurred.' };
67-
}
68-
};
55+
// The safe action client will handle revalidation automatically
56+
return {
57+
success: true,
58+
frameworksAdded: frameworkIds.length,
59+
};
60+
});

apps/app/src/app/(app)/[orgId]/frameworks/components/AddFrameworkModal.tsx

Lines changed: 89 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22

33
import { zodResolver } from '@hookform/resolvers/zod';
44
import { Loader2 } from 'lucide-react';
5-
import { useState } from 'react';
5+
import { useAction } from 'next-safe-action/hooks';
6+
import { useRouter } from 'next/navigation';
67
import { useForm } from 'react-hook-form';
78
import { toast } from 'sonner';
89
import type { z } from 'zod';
9-
import { useRouter } from 'next/navigation';
1010

11+
import { addFrameworksToOrganizationAction } from '@/actions/organization/add-frameworks-to-organization-action';
12+
import { addFrameworksSchema } from '@/actions/schema';
13+
import { FrameworkCard } from '@/components/framework-card';
14+
import type { FrameworkEditorFramework } from '@comp/db/types';
1115
import { Button } from '@comp/ui/button';
12-
import { Checkbox } from '@comp/ui/checkbox';
13-
import { cn } from '@comp/ui/cn';
1416
import {
1517
DialogContent,
1618
DialogDescription,
@@ -19,9 +21,6 @@ import {
1921
DialogTitle,
2022
} from '@comp/ui/dialog';
2123
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@comp/ui/form';
22-
import type { FrameworkEditorFramework } from '@comp/db/types';
23-
import { addFrameworksToOrganizationAction } from '@/actions/organization/add-frameworks-to-organization-action'; // Will create this action next
24-
import { addFrameworksSchema } from '@/actions/schema'; // Will create/update this schema
2524

2625
type Props = {
2726
onOpenChange: (isOpen: boolean) => void;
@@ -34,7 +33,6 @@ type Props = {
3433

3534
export function AddFrameworkModal({ onOpenChange, availableFrameworks, organizationId }: Props) {
3635
const router = useRouter();
37-
const [isExecuting, setIsExecuting] = useState(false);
3836

3937
const form = useForm<z.infer<typeof addFrameworksSchema>>({
4038
resolver: zodResolver(addFrameworksSchema),
@@ -45,22 +43,30 @@ export function AddFrameworkModal({ onOpenChange, availableFrameworks, organizat
4543
mode: 'onChange',
4644
});
4745

48-
const onSubmit = async (data: z.infer<typeof addFrameworksSchema>) => {
49-
setIsExecuting(true);
50-
try {
51-
const result = await addFrameworksToOrganizationAction(data);
52-
if (result.success) {
53-
toast.success('Success'); // Assuming a generic success message
54-
onOpenChange(false);
55-
router.refresh(); // Refresh page to show new frameworks
46+
const { execute, isExecuting } = useAction(addFrameworksToOrganizationAction, {
47+
onSuccess: (data) => {
48+
toast.success(
49+
`Successfully added ${data.data?.frameworksAdded ?? 0} framework${
50+
data.data?.frameworksAdded && data.data?.frameworksAdded > 1 ? 's' : ''
51+
}`,
52+
);
53+
onOpenChange(false);
54+
router.refresh();
55+
},
56+
onError: (error) => {
57+
if (error.error.serverError) {
58+
toast.error(error.error.serverError);
59+
} else if (error.error.validationErrors) {
60+
const errorMessages = Object.values(error.error.validationErrors).flat().join(', ');
61+
toast.error(errorMessages || 'Validation error occurred');
5662
} else {
57-
toast.error(result.error || 'Error');
63+
toast.error('Failed to add frameworks');
5864
}
59-
} catch (error) {
60-
toast.error('Error');
61-
} finally {
62-
setIsExecuting(false);
63-
}
65+
},
66+
});
67+
68+
const onSubmit = async (data: z.infer<typeof addFrameworksSchema>) => {
69+
execute(data);
6470
};
6571

6672
const handleOpenChange = (open: boolean) => {
@@ -69,125 +75,94 @@ export function AddFrameworkModal({ onOpenChange, availableFrameworks, organizat
6975
};
7076

7177
return (
72-
<DialogContent className="max-w-[455px]">
73-
<DialogHeader className="my-4">
74-
<DialogTitle>{'Add New Frameworks'}</DialogTitle>
75-
<DialogDescription>
78+
<DialogContent className="max-w-md">
79+
<DialogHeader className="space-y-2">
80+
<DialogTitle className="text-base font-medium">Add Frameworks</DialogTitle>
81+
<DialogDescription className="text-muted-foreground text-sm">
7682
{availableFrameworks.length > 0
77-
? 'Select the compliance frameworks you want to add to your organization.'
78-
: 'There are no new frameworks available to add at this time.'}
83+
? 'Select the compliance frameworks to add to your organization.'
84+
: 'No new frameworks are available to add at this time.'}
7985
</DialogDescription>
8086
</DialogHeader>
8187

8288
{!isExecuting && availableFrameworks.length > 0 && (
8389
<Form {...form}>
84-
<form
85-
onSubmit={form.handleSubmit(onSubmit)}
86-
className="space-y-6"
87-
suppressHydrationWarning
88-
>
90+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
8991
<FormField
9092
control={form.control}
9193
name="frameworkIds"
9294
render={({ field }) => (
93-
<FormItem className="space-y-2">
94-
<FormLabel className="text-sm font-medium">{'Select Frameworks'}</FormLabel>
95+
<FormItem>
96+
<FormLabel className="text-sm font-normal">Available Frameworks</FormLabel>
9597
<FormControl>
96-
<fieldset className="flex flex-col gap-2 select-none">
97-
<div className="flex max-h-[300px] flex-col gap-2 overflow-y-auto">
98-
{availableFrameworks
99-
.filter((framework) => framework.visible)
100-
.map((framework) => {
101-
const frameworkId = framework.id;
102-
return (
103-
<label
104-
key={frameworkId}
105-
htmlFor={`add-framework-${frameworkId}`}
106-
className={cn(
107-
'focus-within:ring-ring relative flex w-full cursor-pointer flex-col rounded-sm border p-4 text-left transition-colors focus-within:ring-2 focus-within:ring-offset-2',
108-
field.value.includes(frameworkId) &&
109-
'border-primary bg-primary/5',
110-
)}
111-
>
112-
<div className="flex items-start justify-between">
113-
<div>
114-
<h3 className="font-semibold">{framework.name}</h3>
115-
<p className="text-muted-foreground mt-1 text-sm">
116-
{framework.description}
117-
</p>
118-
<p className="text-muted-foreground/75 mt-2 text-xs">
119-
{`${'Version'}: ${framework.version}`}
120-
</p>
121-
</div>
122-
<div>
123-
<Checkbox
124-
id={`add-framework-${frameworkId}`}
125-
checked={field.value.includes(frameworkId)}
126-
className="mt-1"
127-
onCheckedChange={(checked) => {
128-
const newValue = checked
129-
? [...field.value, frameworkId]
130-
: field.value.filter((id) => id !== frameworkId);
131-
field.onChange(newValue);
132-
}}
133-
/>
134-
</div>
135-
</div>
136-
</label>
137-
);
138-
})}
139-
</div>
140-
</fieldset>
98+
<div className="max-h-80 space-y-3 overflow-y-auto pr-1">
99+
{availableFrameworks
100+
.filter((framework) => framework.visible)
101+
.map((framework) => (
102+
<FrameworkCard
103+
key={framework.id}
104+
framework={framework}
105+
isSelected={field.value.includes(framework.id)}
106+
onSelectionChange={(checked) => {
107+
const newValue = checked
108+
? [...field.value, framework.id]
109+
: field.value.filter((id) => id !== framework.id);
110+
field.onChange(newValue);
111+
}}
112+
/>
113+
))}
114+
</div>
141115
</FormControl>
142-
<FormMessage className="text-xs" />
116+
<FormMessage />
143117
</FormItem>
144118
)}
145119
/>
146-
<DialogFooter>
147-
<div className="space-x-4">
148-
<Button
149-
type="button"
150-
variant="outline"
151-
onClick={() => handleOpenChange(false)}
152-
disabled={isExecuting}
153-
>
154-
{'Cancel'}
155-
</Button>
156-
<Button
157-
type="submit"
158-
disabled={
159-
isExecuting ||
160-
form.getValues('frameworkIds').length === 0 ||
161-
availableFrameworks.length === 0
162-
}
163-
suppressHydrationWarning
164-
>
165-
{isExecuting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
166-
{'Add'}
167-
</Button>
168-
</div>
120+
121+
<DialogFooter className="gap-2 border-t pt-4">
122+
<Button
123+
type="button"
124+
variant="outline"
125+
size="sm"
126+
onClick={() => handleOpenChange(false)}
127+
disabled={isExecuting}
128+
>
129+
Cancel
130+
</Button>
131+
<Button
132+
type="submit"
133+
size="sm"
134+
disabled={isExecuting || form.getValues('frameworkIds').length === 0}
135+
>
136+
{isExecuting && <Loader2 className="mr-2 h-3 w-3 animate-spin" />}
137+
Add Selected
138+
</Button>
169139
</DialogFooter>
170140
</form>
171141
</Form>
172142
)}
173143

174144
{!isExecuting && availableFrameworks.length === 0 && (
175-
<div className="py-8 text-center">
176-
<p className="text-md text-foreground">
177-
{'All available frameworks are already enabled in your account.'}
178-
</p>
179-
<DialogFooter className="mt-8">
180-
<Button type="button" variant="outline" onClick={() => handleOpenChange(false)}>
181-
{'Close'}
145+
<div className="py-6 text-center">
146+
<div className="text-muted-foreground text-sm">
147+
All available frameworks are already enabled in your organization.
148+
</div>
149+
<DialogFooter className="mt-6 border-t pt-4">
150+
<Button
151+
type="button"
152+
variant="outline"
153+
size="sm"
154+
onClick={() => handleOpenChange(false)}
155+
>
156+
Close
182157
</Button>
183158
</DialogFooter>
184159
</div>
185160
)}
186161

187162
{isExecuting && (
188-
<div className="flex items-center justify-center p-8">
189-
<Loader2 className="text-primary h-12 w-12 animate-spin" />
190-
<p className="text-muted-foreground ml-4">{'Adding frameworks...'}</p>
163+
<div className="flex items-center justify-center py-8">
164+
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
165+
<span className="text-muted-foreground ml-3 text-sm">Adding frameworks...</span>
191166
</div>
192167
)}
193168
</DialogContent>

apps/app/src/app/(app)/[orgId]/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export default async function Layout({
3737
});
3838

3939
const isOnboardingRunning = !!onboarding?.triggerJobId && !onboarding.completed;
40-
const navbarHeight = 69 + 1; // 1 for border
40+
const navbarHeight = 53 + 1; // 1 for border
4141
const onboardingHeight = 132 + 1; // 1 for border
4242

4343
const pixelsOffset = isOnboardingRunning ? navbarHeight + onboardingHeight : navbarHeight;

0 commit comments

Comments
 (0)