// * Packages import { Link, useForm } from "@inertiajs/react"; import { route } from "ziggy-js"; import { FormEvent, ReactNode, useState } from "react"; import { MoreHorizontalIcon } from "lucide-react"; import type { VisitOptions } from "@inertiajs/core"; // * Components shadcn import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { DropdownMenuSeparator, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; // * 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 { DefaultModel, Views as ViewsType } from "@/back/types/global"; import { Views } from "@/lib/utils"; import { ToastActionElement, ToastProps } from "@/components/ui/toast"; 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; }; /** * Button for GET action. * @param action ViewsType[number] * @param modelRoute string * @param model DefaultModel * @returns Element */ function GetAction({ action, modelRoute, model, }: { action: ViewsType[number]; modelRoute: string; model: DefaultModel; }) { return ( {str(useTrans(`crud.actions.${action.label}`)) .ucFirst() .value()} ); } /** * Action call on model. * @param event FormEvent * @param action ViewsType[number] * @param modelRoute string * @param model DefaultModel * @param setOpen (value: boolean) => void * @param translations Translations * @param toast */ const actionOnModel = ( /* eslint-disable no-unused-vars */ event: FormEvent, action: ViewsType[number], modelRoute: string, model: DefaultModel, 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, /* eslint-enable no-unused-vars */ ) => { 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: 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]; 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 }), }); }; /** * Button for POST action. * @param action ViewsType[number] * @param modelRoute string * @param model DefaultModel * @param setOpen (value: boolean) => void * @returns Element */ function PostAction({ action, modelRoute, model, setOpen, }: { action: ViewsType[number]; modelRoute: string; model: DefaultModel; // eslint-disable-next-line no-unused-vars setOpen: (value: boolean) => void; }) { const { delete: destroy, patch } = useForm(); 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 = "back.yes_restore"; } else if (isArchiveAction) { translationKeyBtnConfirm = "back.yes_archive"; } else { translationKeyBtnConfirm = "back.yes_delete"; } const btnConfirm = useTrans(translationKeyBtnConfirm); let translationKeyBtnDesc = "back.lost_data"; if (isRestoreAction) { translationKeyBtnDesc = "back.restore_data"; } else if (isArchiveAction) { translationKeyBtnDesc = "back.trash_data"; } const btnDesc = useTrans(translationKeyBtnDesc); // Translations for actionOnModel. const translations: Translations = { successfulArchivation: useTrans("crud.messages.successful_archivation"), successfulArchivationDetails: useTrans( "crud.messages.successful_archivation_details", ), canceledArchivation: useTrans("crud.messages.canceled_archivation"), canceledArchivationDetails: useTrans( "crud.messages.canceled_archivation_details", ), successfulRestoration: useTrans("crud.messages.successful_restoration"), successfulRestorationDetails: useTrans( "crud.messages.successful_restoration_details", ), canceledRestoration: useTrans("crud.messages.canceled_restoration"), canceledRestorationDetails: useTrans( "crud.messages.canceled_restoration_details", ), successfulDeletion: useTrans("crud.messages.successful_deletion"), successfulDeletionDetails: useTrans( "crud.messages.successful_deletion_details", ), canceledDeletion: useTrans("crud.messages.canceled_deletion"), canceledDeletionDetails: useTrans( "crud.messages.canceled_deletion_details", ), }; return ( {str(useTrans(`crud.actions.${action.label}`)) .ucFirst() .value()} {useTrans("back.are_you_sure")} {btnDesc} {useTrans("back.no_cancel")} actionOnModel( event, action, modelRoute, model, setOpen, translations, toast, destroy, patch, ) } className="w-fit mb-2 sm:mb-0" > {btnConfirm} ); } interface ModelActionsProps { actions?: ViewsType; modelRoute: string; model: DefaultModel; 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], ); /** * Get all action buttons. * @returns Element[] */ const AllBtnActions = () => { 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} ); }); }; return ( {!inDialog && modelUpdatedAt.length > 0 && ( {messageTrans} {formattedDate} )} {Boolean(allowedActions.length) && ( {inDialog ? ( ) : ( )} )} ); }
{messageTrans}