import type { VisitOptions } from "@inertiajs/core"; import { Link, useForm, usePage } from "@inertiajs/react"; import { MoreHorizontalIcon } from "lucide-react"; import { useState, type Dispatch, type ReactNode, type SetStateAction, type SubmitEvent, } from "react"; import Views from "@/back/lib/actions"; import type { ConfigBackend, DefaultModel, Views as ViewsType, } from "@/back/types/global"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { type ToastActionElement, type ToastProps, } from "@/components/ui/toast"; import str from "@/hooks/use-string"; import { useToast } from "@/hooks/use-toast"; import trans from "@/hooks/use-translations"; import { formatDate } from "@/lib/utils"; interface Translations { successfulArchivation: string; successfulArchivationDetails: string; canceledArchivation: string; canceledArchivationDetails: string; successfulRestoration: string; successfulRestorationDetails: string; canceledRestoration: string; canceledRestorationDetails: string; successfulDeletion: string; successfulDeletionDetails: string; canceledDeletion: string; canceledDeletionDetails: string; } type ToasterToast = ToastProps & { id: string; title?: ReactNode; description?: ReactNode; action?: ToastActionElement; }; type ActionableModel = Omit & { id: number | string; slug?: unknown; }; /** * Button for GET action. * @param action ViewsType[number] * @param modelRoute string * @param model ActionableModel * @returns Element */ function GetAction({ action, modelRoute, model, routes, }: Readonly<{ action: ViewsType[number]; modelRoute: string; model: ActionableModel; routes: ConfigBackend["routes"]; }>) { const actionLabelTrans = str( trans(`uwil-backend::crud.actions.${action.label}`), ) .ucFirst() .value(); const isFrontView = action.value === "show" && ["products", "recipes"].includes(modelRoute); if (isFrontView) { const routeName = `front.${modelRoute}.show`; const slug = typeof model.slug === "string" ? model.slug : model.id; return ( ); } return ( ); } /** * Action call on model. * @param event SubmitEvent * @param action ViewsType[number] * @param modelRoute string * @param model ActionableModel * @param setOpen (value: boolean) => void * @param translations Translations * @param toast */ const actionOnModel = ( event: SubmitEvent, action: ViewsType[number], nameRoute: string, modelRoute: string, model: ActionableModel, setOpen: (value: boolean) => void, translations: Translations, toast: ({ ...props }: Omit) => { id: string; dismiss: () => void; update: (propsChild: ToasterToast) => void; }, destroy: (url: string, options?: VisitOptions) => void, patch: (url: string, options?: VisitOptions) => void, ) => { event.preventDefault(); const routeAction = route( `${nameRoute}${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: translations.successfulArchivation, description: translations.successfulArchivationDetails, }, error: { title: translations.canceledArchivation, description: translations.canceledArchivationDetails, }, }, [Views.RESTORATION.label]: { method: patch, success: { title: translations.successfulRestoration, description: translations.successfulRestorationDetails, }, error: { title: translations.canceledRestoration, description: translations.canceledRestorationDetails, }, }, [Views.DELETION.label || Views.FORCEDELETION.label]: { method: destroy, success: { title: translations.successfulDeletion, description: translations.successfulDeletionDetails, }, error: { title: translations.canceledDeletion, description: translations.canceledDeletionDetails, }, }, }; // Check if route exist. const config = actionMap[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 }), }); }; /** * Button for POST action. * @param action ViewsType[number] * @param modelRoute string * @param model ActionableModel * @param setOpen (value: boolean) => void * @returns Element */ function PostAction({ action, modelRoute, model, setOpen, }: Readonly<{ action: ViewsType[number]; modelRoute: string; model: ActionableModel; setOpen: (value: boolean) => void; }>) { const { delete: destroy, patch } = useForm(); const { name: nameRoute } = (usePage().props.configBackend as ConfigBackend) .routes; const isRestoreAction = action.label === Views.RESTORATION.label; const isArchiveAction = action.label === Views.ARCHIVATION.label; const btnVariantType = isRestoreAction ? "ghost" : "destructive"; let translationKeyBtnConfirm: string; const { toast } = useToast(); if (isRestoreAction) { translationKeyBtnConfirm = "uwil-backend::back.yes_restore"; } else if (isArchiveAction) { translationKeyBtnConfirm = "uwil-backend::back.yes_archive"; } else { translationKeyBtnConfirm = "uwil-backend::back.yes_delete"; } const btnConfirm = trans(translationKeyBtnConfirm); let translationKeyBtnDesc = "uwil-backend::back.lost_data"; if (isRestoreAction) { translationKeyBtnDesc = "uwil-backend::back.restore_data"; } else if (isArchiveAction) { translationKeyBtnDesc = "uwil-backend::back.trash_data"; } const btnDesc = trans(translationKeyBtnDesc); // Translations for actionOnModel. const translations: Translations = { successfulArchivation: trans( "uwil-backend::crud.messages.successful_archivation", ), successfulArchivationDetails: trans( "uwil-backend::crud.messages.successful_archivation_details", ), canceledArchivation: trans( "uwil-backend::crud.messages.canceled_archivation", ), canceledArchivationDetails: trans( "uwil-backend::crud.messages.canceled_archivation_details", ), successfulRestoration: trans( "uwil-backend::crud.messages.successful_restoration", ), successfulRestorationDetails: trans( "uwil-backend::crud.messages.successful_restoration_details", ), canceledRestoration: trans( "uwil-backend::crud.messages.canceled_restoration", ), canceledRestorationDetails: trans( "uwil-backend::crud.messages.canceled_restoration_details", ), successfulDeletion: trans( "uwil-backend::crud.messages.successful_deletion", ), successfulDeletionDetails: trans( "uwil-backend::crud.messages.successful_deletion_details", ), canceledDeletion: trans("uwil-backend::crud.messages.canceled_deletion"), canceledDeletionDetails: trans( "uwil-backend::crud.messages.canceled_deletion_details", ), }; return ( {trans("uwil-backend::back.are_you_sure")} {btnDesc}
{ actionOnModel( event, action, nameRoute, modelRoute, model, setOpen, translations, toast, destroy, patch, ); }} className="w-fit mb-2 sm:mb-0" >
); } /** * Get all action buttons. * @returns Element[] */ const AllBtnActions = ({ allowedActions, model, modelRoute, routes, inDialog, setOpen, }: { allowedActions: ViewsType; model: ActionableModel; modelRoute: string; routes: ConfigBackend["routes"]; inDialog: boolean; setOpen: Dispatch>; }) => { const postActionValues = new Set([ Views.ARCHIVATION.value, Views.RESTORATION.value, Views.DELETION.value, Views.FORCEDELETION.value, ]); return allowedActions.map((action) => { const isPostAction = postActionValues.has(action.value); const showSeparator = inDialog && isPostAction && action.value !== allowedActions[0].value; const content = isPostAction ? ( <> {showSeparator && } ) : ( ); return (
{content}
); }); }; interface ModelActionsProps { actions?: ViewsType; modelRoute: string; model: ActionableModel; modelUpdatedAt?: string; inDialog?: boolean; message?: string; } export default function ModelActions({ actions = [], modelRoute, model, modelUpdatedAt = "", inDialog = false, message = "uwil-backend::back.model_action_updated_at", }: Readonly) { const { routes } = usePage().props.configBackend as ConfigBackend; const [open, setOpen] = useState(false); const messageTrans = trans(message); const formattedDate = formatDate(modelUpdatedAt); const allowedActions = actions.filter( (action) => model.policies[action.policy as keyof typeof model.policies], ); return (
{!inDialog && modelUpdatedAt.length > 0 && (

{messageTrans}

{formattedDate}
)} {Boolean(allowedActions.length) && (
{inDialog ? ( ) : ( )}
)}
); }