ldc-groupe/resources/ts/back/Components/ModelActions.tsx
2026-01-21 12:32:04 +01:00

280 lines
10 KiB
TypeScript

// * Packages
import { Link, useForm } from "@inertiajs/react";
import { route } from "ziggy-js";
import { FormEvent, useState } from "react";
import { MoreHorizontalIcon } from "lucide-react";
// * Components shadcn
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
// * Other
import { useFormatedDate } from "@/hooks/use-formated-value";
import { useToast } from "@/hooks/use-toast";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import { Model, Views as ViewsType } from "@/back/types/global";
import { Views } from "@/lib/utils";
interface ModelActionsProps {
actions?: ViewsType;
modelRoute: string;
model: Model;
modelUpdatedAt?: string;
inDialog?: boolean;
message?: string;
}
export default function ModelActions({
actions = [],
modelRoute,
model,
modelUpdatedAt = "",
inDialog = false,
message = "back.model_action_updated_at",
}: ModelActionsProps) {
const [open, setOpen] = useState(false);
const messageTrans = useTrans(message);
const formattedDate = useFormatedDate(modelUpdatedAt);
const allowedActions = actions.filter((action) => model.policies[action.policy as keyof typeof model.policies])
const { delete: destroy, patch } = useForm();
const { toast } = useToast();
// Translations.
const successfulArchivation = useTrans('crud.messages.successful_archivation');
const successfulArchivationDetails = useTrans("crud.messages.successful_archivation_details");
const canceledArchivation = useTrans('crud.messages.canceled_archivation');
const canceledArchivationDetails = useTrans("crud.messages.canceled_archivation_details");
const successfulRestoration = useTrans('crud.messages.successful_restoration');
const successfulRestorationDetails = useTrans("crud.messages.successful_restoration_details");
const canceledRestoration = useTrans('crud.messages.canceled_restoration');
const canceledRestorationDetails = useTrans("crud.messages.canceled_restoration_details");
const successfulDeletion = useTrans('crud.messages.successful_deletion');
const successfulDeletionDetails = useTrans("crud.messages.successful_deletion_details");
const canceledDeletion = useTrans('crud.messages.canceled_deletion');
const canceledDeletionDetails = useTrans("crud.messages.canceled_deletion_details");
/**
* Action call on model.
* @param event FormEvent<HTMLFormElement>
* @param action ViewsType[number]
*/
const actionOnModel = (event: FormEvent<HTMLFormElement>, action: ViewsType[number]) => {
event.preventDefault();
const routeAction = route(`back.${modelRoute}.${action.value}`, model.id);
// All post actions mapping.
const actionMap: Record<ViewsType[number]["label"], {
method: typeof destroy | typeof patch;
success: { title: string; description: string };
error: { title: string; description: string };
}> = {
[Views.ARCHIVATION.label]: {
method: destroy,
success: {
title: successfulArchivation,
description: successfulArchivationDetails,
},
error: {
title: canceledArchivation,
description: canceledArchivationDetails,
},
},
[Views.RESTORATION.label]: {
method: patch,
success: {
title: successfulRestoration,
description: successfulRestorationDetails,
},
error: {
title: canceledRestoration,
description: canceledRestorationDetails,
},
},
[Views.DELETION.label || Views.FORCEDELETION.label]: {
method: destroy,
success: {
title: successfulDeletion,
description: successfulDeletionDetails,
},
error: {
title: canceledDeletion,
description: canceledDeletionDetails,
},
},
};
// Check if route exist.
const config = actionMap[action.label];
if (!config) {
throw new Error(`Unknown action: ${action.label}`);
}
// Close the dropdown.
setOpen(false);
// Execute the call.
config.method(routeAction, {
preserveScroll: true,
onSuccess: () => toast(config.success),
onError: () => toast({ variant: "destructive", ...config.error }),
});
};
/**
* Get all action buttons.
* @returns Element[]
*/
const AllBtnActions = () => {
return allowedActions.map((action, index) => (
<div key={index} className="w-full">
{
action.value === Views.ARCHIVATION.value
|| action.value === Views.RESTORATION.value
|| action.value === Views.DELETION.value
|| action.value === Views.FORCEDELETION.value
? inDialog && index != 0 ? (
<>
<DropdownMenuSeparator />
<PostAction action={action} />
</>
) : <PostAction action={action} />
: <GetAction action={action} />
}
</div>
))
}
/**
* Button for POST action.
* @param action ViewsType[number]
* @returns Element
*/
const PostAction = ({ action }: { action: ViewsType[number] }) => {
const isRestoreAction = action.label === Views.RESTORATION.label;
const isArchiveAction = action.label === Views.ARCHIVATION.label;
const btnVariantType = isRestoreAction ? "ghost" : "destructive";
const btnConfirm = useTrans(isRestoreAction ? "back.yes_restore" : isArchiveAction ? "back.yes_archive" : "back.yes_delete");
const btnDesc = useTrans(isRestoreAction ? "back.restore_data" : isArchiveAction ? "back.trash_data" : "back.lost_data");
return (
<Dialog>
<DialogTrigger asChild>
<Button
variant={btnVariantType}
className="w-full"
>
<action.icon />
{str(useTrans(`crud.actions.${action.label}`)).ucFirst().value()}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
{useTrans("back.are_you_sure")}
</DialogTitle>
<DialogDescription>
{btnDesc}
</DialogDescription>
</DialogHeader>
<DialogFooter className="items-center sm:justify-start">
<DialogClose asChild>
<Button variant="secondary">
{useTrans("back.no_cancel")}
</Button>
</DialogClose>
<form
onSubmit={(event) => actionOnModel(event, action)}
className="w-fit mb-2 sm:mb-0"
>
<Button
type="submit"
variant={btnVariantType}
>
{btnConfirm}
</Button>
</form>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
/**
* Button for GET action.
* @param action ViewsType[number]
* @returns Element
*/
const GetAction = ({ action }: { action: ViewsType[number] }) => (
<Button
asChild
variant="ghost"
className="w-full"
>
<Link
href={route(`back.${modelRoute}.${action.value}`, model.id)}
className="w-full"
>
<action.icon />
{str(useTrans(`crud.actions.${action.label}`)).ucFirst().value()}
</Link>
</Button>
)
return (
<div className={`flex justify-between items-center ${
!inDialog ? "flex-col xl:flex-row mb-4" : "flex-col"
}`}>
{!inDialog && modelUpdatedAt.length > 0 && (
<div className="flex justify-start items-center">
<p className="text-center xl:text-left">{messageTrans}</p>
<Badge variant="secondary" className="ms-1 mt-1">
{formattedDate}
</Badge>
</div>
)}
{Boolean(allowedActions.length) && (
<div className={`flex justify-between items-center ${
!inDialog ? "flex-col sm:flex-row mt-4 xl:mt-0 gap-1.5" : "flex-col w-full"
}`}>
{inDialog ? (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="secondary"
size="icon"
>
<MoreHorizontalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
side="right"
align="start"
>
<AllBtnActions />
</DropdownMenuContent>
</DropdownMenu>
) : (
<AllBtnActions />
)}
</div>
)}
</div>
);
}