Skip to content

Commit c05ab4d

Browse files
committed
feat(controls): add Edit and Delete Control dialogs for enhanced control management; implement update and delete functionalities in actions
1 parent b88abfb commit c05ab4d

File tree

4 files changed

+279
-4
lines changed

4 files changed

+279
-4
lines changed

apps/framework-editor/app/(pages)/controls/[controlId]/ControlDetailsClientPage.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import {
2525
unlinkTaskTemplateFromControl,
2626
} from '../actions';
2727
import { ManageLinksDialog } from './components/ManageLinksDialog'; // We will create this next
28+
import { EditControlDialog } from './components/EditControlDialog'; // Import Edit Dialog
29+
import { DeleteControlDialog } from './components/DeleteControlDialog'; // Import Delete Dialog
2830

2931
// We'll need to define these dialogs later
3032
// import { EditControlDialog } from './components/EditControlDialog';
@@ -40,16 +42,15 @@ export function ControlDetailsClientPage({ controlDetails }: ControlDetailsClien
4042
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
4143
const [isManageRequirementsDialogOpen, setIsManageRequirementsDialogOpen] = useState(false);
4244

43-
// Placeholder functions for edit/delete actions
4445
const handleControlUpdated = () => {
45-
setIsEditDialogOpen(false);
46+
setIsEditDialogOpen(false); // Dialog handles its own closing on success
4647
router.refresh();
4748
};
4849

4950
const handleControlDeleted = () => {
50-
setIsDeleteDialogOpen(false);
51+
setIsDeleteDialogOpen(false); // Dialog handles its own closing on success
5152
router.push('/controls'); // Navigate back to the controls list after deletion
52-
router.refresh();
53+
// router.refresh(); // Navigating away, so refresh of current page isn't primary focus
5354
};
5455

5556
// --- Requirements Management Data & Handlers (will be passed to dialog) ---
@@ -304,6 +305,27 @@ export function ControlDetailsClientPage({ controlDetails }: ControlDetailsClien
304305
renderItemDisplay={renderTaskTemplateDisplay}
305306
/>
306307
)}
308+
309+
{/* Edit Control Dialog */}
310+
{isEditDialogOpen && (
311+
<EditControlDialog
312+
isOpen={isEditDialogOpen}
313+
onOpenChange={setIsEditDialogOpen}
314+
control={controlDetails}
315+
onControlUpdated={handleControlUpdated}
316+
/>
317+
)}
318+
319+
{/* Delete Control Dialog */}
320+
{isDeleteDialogOpen && (
321+
<DeleteControlDialog
322+
isOpen={isDeleteDialogOpen}
323+
onOpenChange={setIsDeleteDialogOpen}
324+
controlId={controlDetails.id}
325+
controlName={controlDetails.name}
326+
onControlDeleted={handleControlDeleted}
327+
/>
328+
)}
307329
</PageLayout>
308330
);
309331
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
'use client'
2+
3+
import { useState } from 'react';
4+
import { Button } from '@comp/ui/button';
5+
import {
6+
Dialog,
7+
DialogContent,
8+
DialogHeader,
9+
DialogTitle,
10+
DialogDescription,
11+
DialogFooter,
12+
DialogClose, // For an explicit close button if needed
13+
} from '@comp/ui/dialog';
14+
import { deleteControl } from '../../actions'; // Corrected path
15+
import { toast } from 'sonner';
16+
17+
interface DeleteControlDialogProps {
18+
isOpen: boolean;
19+
onOpenChange: (isOpen: boolean) => void;
20+
controlId: string;
21+
controlName: string;
22+
onControlDeleted: () => void;
23+
}
24+
25+
export function DeleteControlDialog({
26+
isOpen,
27+
onOpenChange,
28+
controlId,
29+
controlName,
30+
onControlDeleted,
31+
}: DeleteControlDialogProps) {
32+
const [isDeleting, setIsDeleting] = useState(false);
33+
34+
const handleDelete = async () => {
35+
setIsDeleting(true);
36+
try {
37+
await deleteControl(controlId);
38+
toast.success(`Control "${controlName}" deleted successfully.`);
39+
onControlDeleted(); // This will typically close dialog and navigate/refresh
40+
onOpenChange(false); // Explicitly close dialog
41+
} catch (error) {
42+
console.error("Failed to delete control:", error);
43+
toast.error('Failed to delete control. Please try again.');
44+
setIsDeleting(false); // Only reset if error, otherwise dialog closes
45+
}
46+
// setIsDeleting(false); // Moved to finally or only on error
47+
};
48+
49+
return (
50+
<Dialog open={isOpen} onOpenChange={onOpenChange}>
51+
<DialogContent className="sm:max-w-md rounded-sm">
52+
<DialogHeader>
53+
<DialogTitle>Delete Control</DialogTitle>
54+
<DialogDescription>
55+
Are you sure you want to delete the control "<strong>{controlName}</strong>"?
56+
This action cannot be undone.
57+
</DialogDescription>
58+
</DialogHeader>
59+
<DialogFooter className="mt-4">
60+
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isDeleting} className="rounded-sm">
61+
Cancel
62+
</Button>
63+
<Button variant="destructive" onClick={handleDelete} disabled={isDeleting} className="rounded-sm">
64+
{isDeleting ? 'Deleting...' : 'Delete Control'}
65+
</Button>
66+
</DialogFooter>
67+
</DialogContent>
68+
</Dialog>
69+
);
70+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
'use client'
2+
3+
import { useState, useEffect } from 'react';
4+
import { useForm } from 'react-hook-form';
5+
import { zodResolver } from '@hookform/resolvers/zod';
6+
import * as z from 'zod';
7+
import { Button } from '@comp/ui/button';
8+
import {
9+
Dialog,
10+
DialogContent,
11+
DialogHeader,
12+
DialogTitle,
13+
DialogDescription,
14+
DialogFooter,
15+
} from '@comp/ui/dialog';
16+
import { Input } from '@comp/ui/input';
17+
import { Textarea } from '@comp/ui/textarea'; // Assuming you have a Textarea component
18+
import { Label } from '@comp/ui/label';
19+
import { updateControlDetails } from '../../actions'; // Path to your server actions
20+
import type { ControlDetailsWithRelations } from '../page'; // Type for control details
21+
import { toast } from 'sonner'; // For notifications
22+
23+
const formSchema = z.object({
24+
name: z.string().min(1, { message: 'Control name is required.' }).max(255),
25+
description: z.string().max(1024).optional(), // Optional, adjust as needed
26+
});
27+
28+
type EditControlFormValues = z.infer<typeof formSchema>;
29+
30+
interface EditControlDialogProps {
31+
isOpen: boolean;
32+
onOpenChange: (isOpen: boolean) => void;
33+
control: Pick<ControlDetailsWithRelations, 'id' | 'name' | 'description'>;
34+
onControlUpdated: () => void;
35+
}
36+
37+
export function EditControlDialog({
38+
isOpen,
39+
onOpenChange,
40+
control,
41+
onControlUpdated,
42+
}: EditControlDialogProps) {
43+
const [isSubmitting, setIsSubmitting] = useState(false);
44+
45+
const form = useForm<EditControlFormValues>({
46+
resolver: zodResolver(formSchema),
47+
defaultValues: {
48+
name: control.name || '',
49+
description: control.description || '',
50+
},
51+
});
52+
53+
useEffect(() => {
54+
if (isOpen) {
55+
form.reset({
56+
name: control.name || '',
57+
description: control.description || '',
58+
});
59+
}
60+
}, [isOpen, control, form]);
61+
62+
const onSubmit = async (values: EditControlFormValues) => {
63+
setIsSubmitting(true);
64+
try {
65+
await updateControlDetails(control.id, {
66+
name: values.name,
67+
description: values.description || '', // Ensure empty string if undefined
68+
});
69+
toast.success('Control details updated successfully!');
70+
onControlUpdated(); // This will typically close dialog and refresh data
71+
onOpenChange(false); // Explicitly close dialog
72+
} catch (error) {
73+
console.error("Failed to update control:", error);
74+
toast.error('Failed to update control. Please try again.');
75+
} finally {
76+
setIsSubmitting(false);
77+
}
78+
};
79+
80+
return (
81+
<Dialog open={isOpen} onOpenChange={onOpenChange}>
82+
<DialogContent className="sm:max-w-[480px] rounded-sm">
83+
<DialogHeader>
84+
<DialogTitle>Edit Control Details</DialogTitle>
85+
<DialogDescription>
86+
Make changes to the control name and description.
87+
</DialogDescription>
88+
</DialogHeader>
89+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 py-2">
90+
<div className="space-y-2">
91+
<Label htmlFor="name">Control Name</Label>
92+
<Input
93+
id="name"
94+
{...form.register('name')}
95+
placeholder="Enter control name"
96+
className="rounded-sm"
97+
/>
98+
{form.formState.errors.name && (
99+
<p className="text-xs text-red-500">{form.formState.errors.name.message}</p>
100+
)}
101+
</div>
102+
<div className="space-y-2">
103+
<Label htmlFor="description">Description (Optional)</Label>
104+
<Textarea
105+
id="description"
106+
{...form.register('description')}
107+
placeholder="Enter control description"
108+
rows={4}
109+
className="rounded-sm"
110+
/>
111+
{form.formState.errors.description && (
112+
<p className="text-xs text-red-500">{form.formState.errors.description.message}</p>
113+
)}
114+
</div>
115+
<DialogFooter>
116+
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isSubmitting} className="rounded-sm">
117+
Cancel
118+
</Button>
119+
<Button type="submit" disabled={isSubmitting} className="rounded-sm">
120+
{isSubmitting ? 'Saving...' : 'Save Changes'}
121+
</Button>
122+
</DialogFooter>
123+
</form>
124+
</DialogContent>
125+
</Dialog>
126+
);
127+
}

apps/framework-editor/app/(pages)/controls/actions.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,4 +222,60 @@ export async function unlinkTaskTemplateFromControl(controlId: string, taskTempl
222222
console.error("Error unlinking task template from control:", error);
223223
throw new Error("Failed to unlink task template.");
224224
}
225+
}
226+
227+
// --- Control Actions ---
228+
229+
interface UpdateControlPayload {
230+
name: string;
231+
description: string;
232+
}
233+
234+
/**
235+
* Updates the details of a control template.
236+
*/
237+
export async function updateControlDetails(controlId: string, data: UpdateControlPayload): Promise<void> {
238+
if (!controlId) {
239+
throw new Error("Control ID must be provided.");
240+
}
241+
if (!data.name || data.name.trim() === "") {
242+
throw new Error("Control name must be provided.");
243+
}
244+
245+
try {
246+
await db.frameworkEditorControlTemplate.update({
247+
where: { id: controlId },
248+
data: {
249+
name: data.name,
250+
description: data.description,
251+
},
252+
});
253+
revalidatePath(`/controls/${controlId}`);
254+
revalidatePath('/controls');
255+
} catch (error) {
256+
console.error("Error updating control details:", error);
257+
throw new Error("Failed to update control details.");
258+
}
259+
}
260+
261+
/**
262+
* Deletes a control template.
263+
*/
264+
export async function deleteControl(controlId: string): Promise<void> {
265+
if (!controlId) {
266+
throw new Error("Control ID must be provided.");
267+
}
268+
try {
269+
// Note: Depending on DB constraints, you might need to disconnect relations first
270+
// if there's no onDelete: Cascade or similar set up for related items.
271+
await db.frameworkEditorControlTemplate.delete({
272+
where: { id: controlId },
273+
});
274+
revalidatePath(`/controls/${controlId}`); // Or just /controls if navigating away
275+
revalidatePath('/controls');
276+
} catch (error) {
277+
console.error("Error deleting control:", error);
278+
// Consider more specific error handling, e.g., if control not found or if relations prevent deletion
279+
throw new Error("Failed to delete control.");
280+
}
225281
}

0 commit comments

Comments
 (0)