280 lines
10 KiB
TypeScript
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>
|
|
);
|
|
}
|