22
33import { zodResolver } from '@hookform/resolvers/zod' ;
44import { Loader2 } from 'lucide-react' ;
5- import { useState } from 'react' ;
5+ import { useAction } from 'next-safe-action/hooks' ;
6+ import { useRouter } from 'next/navigation' ;
67import { useForm } from 'react-hook-form' ;
78import { toast } from 'sonner' ;
89import 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' ;
1115import { Button } from '@comp/ui/button' ;
12- import { Checkbox } from '@comp/ui/checkbox' ;
13- import { cn } from '@comp/ui/cn' ;
1416import {
1517 DialogContent ,
1618 DialogDescription ,
@@ -19,9 +21,6 @@ import {
1921 DialogTitle ,
2022} from '@comp/ui/dialog' ;
2123import { 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
2625type Props = {
2726 onOpenChange : ( isOpen : boolean ) => void ;
@@ -34,7 +33,6 @@ type Props = {
3433
3534export 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 >
0 commit comments