feat(duplicate): add product and recipe duplication functionality
This commit introduces the ability to duplicate products and recipes, including handling of associated files and unique naming conventions.
This commit is contained in:
parent
531a5c626a
commit
db5c95f9a1
16 changed files with 408 additions and 132 deletions
111
app/Cqs/Actions/Products/Duplicate/DuplicateProduct.php
Normal file
111
app/Cqs/Actions/Products/Duplicate/DuplicateProduct.php
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Cqs\Actions\Products\Duplicate;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Lib\PublicS3Disk;
|
||||
use App\Models\Product;
|
||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* phpcs:disable Generic.Files.LineLength.TooLong
|
||||
*
|
||||
* @implements \App\Contracts\Action<\App\Cqs\Actions\Products\Duplicate\DuplicateProductInput, \Domain\Models\ValueObjects\Entities\ProductId|\App\Cqs\Actions\Products\Duplicate\FailedToDuplicateProduct>
|
||||
* phpcs:enable
|
||||
*/
|
||||
final class DuplicateProduct implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PublicS3Disk $s3Disk,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \App\Cqs\Actions\Products\Duplicate\DuplicateProductInput $input
|
||||
*/
|
||||
public function __invoke(ActionInput $input): ProductId | FailedToDuplicateProduct
|
||||
{
|
||||
Assert::isInstanceOf($input, DuplicateProductInput::class);
|
||||
|
||||
$original = Product::withTrashed()->findOrFail((string) $input->productId->value);
|
||||
|
||||
$clone = $original->replicate();
|
||||
$clone->name = $this->generateUniqueName($original->name);
|
||||
$clone->slug = Str::slug($clone->name);
|
||||
|
||||
$duplicatedPicture = $this->duplicateFile($original->picture);
|
||||
if ($duplicatedPicture !== null) {
|
||||
$clone->picture = $duplicatedPicture;
|
||||
}
|
||||
$clone->pdf = $this->duplicateFile($original->pdf);
|
||||
|
||||
if (! $clone->save()) {
|
||||
return new FailedToDuplicateProduct();
|
||||
}
|
||||
|
||||
return new ProductId(Uuid::fromString($clone->id));
|
||||
}
|
||||
|
||||
private function generateUniqueName(string $originalName): string
|
||||
{
|
||||
$rootName = $originalName;
|
||||
if (preg_match('/^(.*?)(?: \(Copie(?: (\d+))?\))?$/', $originalName, $matches)) {
|
||||
$rootName = $matches[1];
|
||||
}
|
||||
|
||||
$baseName = "{$rootName} (Copie)";
|
||||
if (
|
||||
! Product::withTrashed()
|
||||
->where('name', $baseName)
|
||||
->orWhere('slug', Str::slug($baseName))
|
||||
->exists()
|
||||
) {
|
||||
return $baseName;
|
||||
}
|
||||
|
||||
$i = 2;
|
||||
while (
|
||||
Product::withTrashed()
|
||||
->where('name', "{$rootName} (Copie {$i})")
|
||||
->orWhere('slug', Str::slug("{$rootName} (Copie {$i})"))
|
||||
->exists()
|
||||
) {
|
||||
$i++;
|
||||
}
|
||||
|
||||
return "{$rootName} (Copie {$i})";
|
||||
}
|
||||
|
||||
private function duplicateFile(?StorageFilePath $filePath): ?StorageFilePath
|
||||
{
|
||||
if ($filePath === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sourcePath = $filePath->value;
|
||||
$disk = Storage::disk($this->s3Disk->disk->value);
|
||||
|
||||
if (! $disk->exists($sourcePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$extension = pathinfo($sourcePath, PATHINFO_EXTENSION);
|
||||
$dirname = pathinfo($sourcePath, PATHINFO_DIRNAME);
|
||||
$newFilename = Str::uuid()->toString() . ($extension !== '' ? '.' . $extension : '');
|
||||
$destinationPath = ($dirname !== '.' && $dirname !== '') ? "{$dirname}/{$newFilename}" : $newFilename;
|
||||
|
||||
if (! $disk->copy($sourcePath, $destinationPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new StorageFilePath($destinationPath);
|
||||
}
|
||||
}
|
||||
16
app/Cqs/Actions/Products/Duplicate/DuplicateProductInput.php
Normal file
16
app/Cqs/Actions/Products/Duplicate/DuplicateProductInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Cqs\Actions\Products\Duplicate;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
||||
|
||||
final readonly class DuplicateProductInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public ProductId $productId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Cqs\Actions\Products\Duplicate;
|
||||
|
||||
final readonly class FailedToDuplicateProduct
|
||||
{
|
||||
}
|
||||
114
app/Cqs/Actions/Recipes/Duplicate/DuplicateRecipe.php
Normal file
114
app/Cqs/Actions/Recipes/Duplicate/DuplicateRecipe.php
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Cqs\Actions\Recipes\Duplicate;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Lib\PublicS3Disk;
|
||||
use App\Models\Recipe;
|
||||
use Domain\Models\ValueObjects\Entities\RecipeId;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* phpcs:disable Generic.Files.LineLength.TooLong
|
||||
*
|
||||
* @implements \App\Contracts\Action<\App\Cqs\Actions\Recipes\Duplicate\DuplicateRecipeInput, \Domain\Models\ValueObjects\Entities\RecipeId|\App\Cqs\Actions\Recipes\Duplicate\FailedToDuplicateRecipe>
|
||||
* phpcs:enable
|
||||
*/
|
||||
final class DuplicateRecipe implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PublicS3Disk $s3Disk,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \App\Cqs\Actions\Recipes\Duplicate\DuplicateRecipeInput $input
|
||||
*/
|
||||
public function __invoke(ActionInput $input): RecipeId | FailedToDuplicateRecipe
|
||||
{
|
||||
Assert::isInstanceOf($input, DuplicateRecipeInput::class);
|
||||
|
||||
$original = Recipe::withTrashed()->with('products')->findOrFail((string) $input->recipeId->value);
|
||||
|
||||
$clone = $original->replicate();
|
||||
$clone->name = $this->generateUniqueName($original->name);
|
||||
$clone->slug = Str::slug($clone->name);
|
||||
|
||||
$duplicatedPicture = $this->duplicateFile($original->picture);
|
||||
if ($duplicatedPicture !== null) {
|
||||
$clone->picture = $duplicatedPicture;
|
||||
}
|
||||
|
||||
if (! $clone->save()) {
|
||||
return new FailedToDuplicateRecipe();
|
||||
}
|
||||
|
||||
$productIds = $original->products->pluck('id')->all();
|
||||
if (! empty($productIds)) {
|
||||
$clone->products()->sync($productIds);
|
||||
}
|
||||
|
||||
return new RecipeId(Uuid::fromString($clone->id));
|
||||
}
|
||||
|
||||
private function generateUniqueName(string $originalName): string
|
||||
{
|
||||
$rootName = $originalName;
|
||||
if (preg_match('/^(.*?)(?: \(Copie(?: (\d+))?\))?$/', $originalName, $matches)) {
|
||||
$rootName = $matches[1];
|
||||
}
|
||||
|
||||
$baseName = "{$rootName} (Copie)";
|
||||
if (
|
||||
! Recipe::withTrashed()
|
||||
->where('name', $baseName)
|
||||
->orWhere('slug', Str::slug($baseName))
|
||||
->exists()
|
||||
) {
|
||||
return $baseName;
|
||||
}
|
||||
|
||||
$i = 2;
|
||||
while (
|
||||
Recipe::withTrashed()
|
||||
->where('name', "{$rootName} (Copie {$i})")
|
||||
->orWhere('slug', Str::slug("{$rootName} (Copie {$i})"))->exists()
|
||||
) {
|
||||
$i++;
|
||||
}
|
||||
|
||||
return "{$rootName} (Copie {$i})";
|
||||
}
|
||||
|
||||
private function duplicateFile(?StorageFilePath $filePath): ?StorageFilePath
|
||||
{
|
||||
if ($filePath === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sourcePath = $filePath->value;
|
||||
$disk = Storage::disk($this->s3Disk->disk->value);
|
||||
|
||||
if (! $disk->exists($sourcePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$extension = pathinfo($sourcePath, PATHINFO_EXTENSION);
|
||||
$dirname = pathinfo($sourcePath, PATHINFO_DIRNAME);
|
||||
$newFilename = Str::uuid()->toString() . ($extension !== '' ? '.' . $extension : '');
|
||||
$destinationPath = ($dirname !== '.' && $dirname !== '') ? "{$dirname}/{$newFilename}" : $newFilename;
|
||||
|
||||
if (! $disk->copy($sourcePath, $destinationPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new StorageFilePath($destinationPath);
|
||||
}
|
||||
}
|
||||
16
app/Cqs/Actions/Recipes/Duplicate/DuplicateRecipeInput.php
Normal file
16
app/Cqs/Actions/Recipes/Duplicate/DuplicateRecipeInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Cqs\Actions\Recipes\Duplicate;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use Domain\Models\ValueObjects\Entities\RecipeId;
|
||||
|
||||
final readonly class DuplicateRecipeInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public RecipeId $recipeId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Cqs\Actions\Recipes\Duplicate;
|
||||
|
||||
final readonly class FailedToDuplicateRecipe
|
||||
{
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ use App\Contracts\Executor;
|
|||
use App\Cqs\Actions\Products\Create\CreateProduct;
|
||||
use App\Cqs\Actions\Products\Delete\DeleteProduct;
|
||||
use App\Cqs\Actions\Products\Delete\DeleteProductInput;
|
||||
use App\Cqs\Actions\Products\Duplicate\DuplicateProduct;
|
||||
use App\Cqs\Actions\Products\Duplicate\DuplicateProductInput;
|
||||
use App\Cqs\Actions\Products\ForceDelete\ForceDeleteProduct;
|
||||
use App\Cqs\Actions\Products\ForceDelete\ForceDeleteProductInput;
|
||||
use App\Cqs\Actions\Products\Restore\RestoreProduct;
|
||||
|
|
@ -140,11 +142,22 @@ class ProductController extends BackendController
|
|||
};
|
||||
}
|
||||
|
||||
public function duplicate(ProductDto $product): Response
|
||||
public function duplicate(ProductDto $product, DuplicateProduct $duplicateProduct): RedirectResponse
|
||||
{
|
||||
$model = Product::withTrashed()->findOrFail($product->id);
|
||||
$this->authorize('duplicate', $product);
|
||||
|
||||
return $this->create($model->replicate());
|
||||
$result = $this->executor->execute($duplicateProduct, new DuplicateProductInput($product->getId()));
|
||||
|
||||
return match (true) {
|
||||
$result instanceof ProductId => redirect()->route('back.products.edit', $result)
|
||||
->with('success', trans('back.has_been_duplicated', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.product', 1)),
|
||||
])),
|
||||
default => redirect()->back()
|
||||
->with('error', trans('back.cannot_be_duplicated', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.product', 1)),
|
||||
])),
|
||||
};
|
||||
}
|
||||
|
||||
public function restore(string $id, RestoreProduct $restoreProduct): RedirectResponse
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ use App\Contracts\QueryInputEmpty;
|
|||
use App\Cqs\Actions\Recipes\Create\CreateRecipe;
|
||||
use App\Cqs\Actions\Recipes\Delete\DeleteRecipe;
|
||||
use App\Cqs\Actions\Recipes\Delete\DeleteRecipeInput;
|
||||
use App\Cqs\Actions\Recipes\Duplicate\DuplicateRecipe;
|
||||
use App\Cqs\Actions\Recipes\Duplicate\DuplicateRecipeInput;
|
||||
use App\Cqs\Actions\Recipes\ForceDelete\ForceDeleteRecipe;
|
||||
use App\Cqs\Actions\Recipes\ForceDelete\ForceDeleteRecipeInput;
|
||||
use App\Cqs\Actions\Recipes\Restore\RestoreRecipe;
|
||||
|
|
@ -156,13 +158,22 @@ class RecipeController extends BackendController
|
|||
};
|
||||
}
|
||||
|
||||
public function duplicate(RecipeDto $recipe, GetProductsSummariesQuery $getProductsSummaries): Response
|
||||
public function duplicate(RecipeDto $recipe, DuplicateRecipe $duplicateRecipe): RedirectResponse
|
||||
{
|
||||
$this->authorize('duplicate', $recipe);
|
||||
|
||||
$model = Recipe::withTrashed()->findOrFail($recipe->id);
|
||||
$result = $this->executor->execute($duplicateRecipe, new DuplicateRecipeInput($recipe->getId()));
|
||||
|
||||
return $this->create($model->replicate(), $getProductsSummaries);
|
||||
return match (true) {
|
||||
$result instanceof RecipeId => redirect()->route('back.recipes.edit', $result)
|
||||
->with('success', trans('back.has_been_duplicated', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.recipe', 1)),
|
||||
])),
|
||||
default => redirect()->back()
|
||||
->with('error', trans('back.cannot_be_duplicated', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.recipe', 1)),
|
||||
])),
|
||||
};
|
||||
}
|
||||
|
||||
public function restore(string $id, RestoreRecipe $restoreRecipe): RedirectResponse
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
// "dashboard" => "Tableau de bord",
|
||||
'has_been_duplicated' => ':model has been duplicated',
|
||||
'cannot_be_duplicated' => ':model cannot be duplicated',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
// "dashboard" => "Tableau de bord",
|
||||
'has_been_duplicated' => ':model a été dupliqué',
|
||||
'cannot_be_duplicated' => ':model ne peut être dupliqué',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import {
|
|||
ZoomOutIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type Dispatch,
|
||||
|
|
@ -25,7 +24,6 @@ import { type FileWithPath } from "react-dropzone";
|
|||
import ReactCrop, { type Crop, type PixelCrop } from "react-image-crop";
|
||||
import "react-image-crop/dist/ReactCrop.css";
|
||||
|
||||
import type { ConfigBackend } from "@/back/types/global";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Collapsible,
|
||||
|
|
@ -87,18 +85,14 @@ export default function ImageCropper({
|
|||
reset,
|
||||
setIsReset,
|
||||
isReset,
|
||||
url,
|
||||
finalFileName,
|
||||
}: Readonly<ImageCropperProps>) {
|
||||
const { routes } = usePage().props.configBackend as ConfigBackend;
|
||||
const { errors } = usePage().props;
|
||||
|
||||
const aspect = 1;
|
||||
const { isDesktop } = useDeviceType();
|
||||
const imgRef = useRef<HTMLImageElement | null>(null);
|
||||
|
||||
const isDuplicating = route().current(`${routes.name}*.duplicate`);
|
||||
|
||||
// Zone of the crop in %.
|
||||
const [cropZone, setCropZone] = useState<Crop>();
|
||||
const [croppedImage, setCroppedImage] = useState<string>("");
|
||||
|
|
@ -231,34 +225,6 @@ export default function ImageCropper({
|
|||
return canvas.toDataURL("image/png");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate base64 from an url.
|
||||
* @param targetUrl string
|
||||
* @param callback (base64: string | ArrayBuffer | null) => void
|
||||
* @param errorCallback (error: string) => void
|
||||
*/
|
||||
function urlToBase64(
|
||||
targetUrl: string,
|
||||
|
||||
callback: (base64: string | ArrayBuffer | null) => void,
|
||||
|
||||
errorCallback?: (error: string) => void,
|
||||
): void {
|
||||
fetch(targetUrl)
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onloadend = () => {
|
||||
callback(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (errorCallback) errorCallback(String(error));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate file from the base64.
|
||||
* @param dataUrl base64 string
|
||||
|
|
@ -302,28 +268,6 @@ export default function ImageCropper({
|
|||
setIsDragging(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!value || !isDuplicating) return;
|
||||
const rawPath = typeof value === "object" ? value?.name : value;
|
||||
if (!rawPath) return;
|
||||
const pathFileBase = /^https?:\/\//i.test(rawPath)
|
||||
? rawPath
|
||||
: `${url}/${rawPath.replace(/^\/+/, "")}`;
|
||||
urlToBase64(
|
||||
pathFileBase,
|
||||
(base64) => {
|
||||
if (typeof base64 === "string") {
|
||||
const file = base64ToFile(base64);
|
||||
setData(name, file);
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
console.error(err);
|
||||
},
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Crop the image with all options.
|
||||
* @return void
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { MoreHorizontalIcon } from "lucide-react";
|
|||
import {
|
||||
useState,
|
||||
type Dispatch,
|
||||
type FormEvent,
|
||||
type ReactNode,
|
||||
type SetStateAction,
|
||||
type SubmitEvent,
|
||||
|
|
@ -352,6 +353,56 @@ function PostAction({
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Button for DUPLICATE POST action.
|
||||
* @param action ViewsType[number]
|
||||
* @param modelRoute string
|
||||
* @param model ActionableModel
|
||||
* @param routes ConfigBackend["routes"]
|
||||
* @param setOpen Dispatch<SetStateAction<boolean>>
|
||||
* @returns Element
|
||||
*/
|
||||
function DuplicateAction({
|
||||
action,
|
||||
modelRoute,
|
||||
model,
|
||||
routes,
|
||||
setOpen,
|
||||
}: Readonly<{
|
||||
action: ViewsType[number];
|
||||
modelRoute: string;
|
||||
model: ActionableModel;
|
||||
routes: ConfigBackend["routes"];
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}>) {
|
||||
const { post, processing } = useForm();
|
||||
const actionLabelTrans = str(
|
||||
trans(`uwil-backend::crud.actions.${action.label}`),
|
||||
)
|
||||
.ucFirst()
|
||||
.value();
|
||||
|
||||
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setOpen(false);
|
||||
post(route(`${routes.name}${modelRoute}.${action.value}`, model.id));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="w-full">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="ghost"
|
||||
className="w-full"
|
||||
disabled={processing}
|
||||
>
|
||||
<action.icon />
|
||||
{actionLabelTrans}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all action buttons.
|
||||
* @returns Element[]
|
||||
|
|
@ -379,28 +430,44 @@ const AllBtnActions = ({
|
|||
]);
|
||||
|
||||
return allowedActions.map((action) => {
|
||||
const isDuplicateAction = action.value === Views.DUPLICATION.value;
|
||||
const isPostAction = postActionValues.has(action.value);
|
||||
const showSeparator =
|
||||
inDialog && isPostAction && action.value !== allowedActions[0].value;
|
||||
|
||||
const content = isPostAction ? (
|
||||
<>
|
||||
{showSeparator && <DropdownMenuSeparator />}
|
||||
<PostAction
|
||||
let content: ReactNode;
|
||||
if (isDuplicateAction) {
|
||||
content = (
|
||||
<DuplicateAction
|
||||
action={action}
|
||||
model={model}
|
||||
modelRoute={modelRoute}
|
||||
routes={routes}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<GetAction
|
||||
action={action}
|
||||
model={model}
|
||||
modelRoute={modelRoute}
|
||||
routes={routes}
|
||||
/>
|
||||
);
|
||||
);
|
||||
} else if (isPostAction) {
|
||||
content = (
|
||||
<>
|
||||
{showSeparator && <DropdownMenuSeparator />}
|
||||
<PostAction
|
||||
action={action}
|
||||
model={model}
|
||||
modelRoute={modelRoute}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<GetAction
|
||||
action={action}
|
||||
model={model}
|
||||
modelRoute={modelRoute}
|
||||
routes={routes}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={`${action.value}-${action.label}`} className="w-full">
|
||||
|
|
|
|||
|
|
@ -7,11 +7,7 @@ import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout";
|
|||
import { Actions, Methods } from "@/back/lib/enums";
|
||||
import Form from "@/back/Pages/Products/Form";
|
||||
import { SingleResource } from "@/back/types/global";
|
||||
import {
|
||||
RawProductModel,
|
||||
validateAsProductModel,
|
||||
validateAsRawProductModel,
|
||||
} from "@/back/types/products";
|
||||
import { RawProductModel, validateAsProductModel } from "@/back/types/products";
|
||||
import usePermissions from "@/hooks/use-permissions";
|
||||
import trans from "@/hooks/use-translations";
|
||||
|
||||
|
|
@ -30,11 +26,7 @@ export default function Create({
|
|||
? data.data.length > 0
|
||||
: Object.keys(data.data).length > 0);
|
||||
if (hasData) {
|
||||
if (route().current(`back.products.duplicate`)) {
|
||||
product = validateAsRawProductModel(data.data);
|
||||
} else {
|
||||
product = validateAsProductModel(data.data);
|
||||
}
|
||||
product = validateAsProductModel(data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Zod validation failed, falling back to empty model.", error);
|
||||
|
|
@ -42,18 +34,13 @@ export default function Create({
|
|||
|
||||
// Translations.
|
||||
const transBackToList = trans("uwil-backend::back.crud_back_to_list");
|
||||
const transTitle = trans(
|
||||
route().current(`back.products.duplicate`)
|
||||
? "uwil-backend::crud.helpers.duplication_model"
|
||||
: "uwil-backend::crud.helpers.creation_model",
|
||||
{
|
||||
params: {
|
||||
model: trans(`models.classes.product`, {
|
||||
choice: 1,
|
||||
}),
|
||||
},
|
||||
const transTitle = trans("uwil-backend::crud.helpers.creation_model", {
|
||||
params: {
|
||||
model: trans(`models.classes.product`, {
|
||||
choice: 1,
|
||||
}),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const beforeNode = () => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -5,11 +5,7 @@ import AuthenticatedLayout from "@/back/Layouts/AuthenticatedLayout";
|
|||
import { Actions, Methods } from "@/back/lib/enums";
|
||||
import Form from "@/back/Pages/Recipes/Form";
|
||||
import { SingleResource } from "@/back/types/global";
|
||||
import {
|
||||
RawRecipeModel,
|
||||
validateAsRawRecipeModel,
|
||||
validateAsRecipeModel,
|
||||
} from "@/back/types/recipes";
|
||||
import { RawRecipeModel, validateAsRecipeModel } from "@/back/types/recipes";
|
||||
import usePermissions from "@/hooks/use-permissions";
|
||||
import trans, { UseTransOptions } from "@/hooks/use-translations";
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
|
|
@ -29,29 +25,20 @@ export default function Create({
|
|||
? data.data.length > 0
|
||||
: Object.keys(data.data).length > 0);
|
||||
if (hasData) {
|
||||
if (route().current(`back.recipes.duplicate`)) {
|
||||
recipe = validateAsRawRecipeModel(data.data);
|
||||
} else {
|
||||
recipe = validateAsRecipeModel(data.data);
|
||||
}
|
||||
recipe = validateAsRecipeModel(data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Zod validation failed, falling back to empty model.", error);
|
||||
}
|
||||
|
||||
const transBackToList = trans("uwil-backend::back.crud_back_to_list");
|
||||
const title = trans(
|
||||
route().current(`back.recipes.duplicate`)
|
||||
? "uwil-backend::crud.helpers.duplication_model"
|
||||
: "uwil-backend::crud.helpers.creation_model",
|
||||
{
|
||||
params: {
|
||||
model: trans(`models.classes.recipe`, {
|
||||
choice: 1,
|
||||
}),
|
||||
},
|
||||
} as UseTransOptions,
|
||||
);
|
||||
const title = trans("uwil-backend::crud.helpers.creation_model", {
|
||||
params: {
|
||||
model: trans(`models.classes.recipe`, {
|
||||
choice: 1,
|
||||
}),
|
||||
},
|
||||
} as UseTransOptions);
|
||||
|
||||
const beforeNode = () => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;
|
|||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('recipes/trash', [RecipeController::class, 'index'])->name('recipes.trash');
|
||||
Route::get('recipes/{recipe}/duplicate', [RecipeController::class, 'duplicate'])->name('recipes.duplicate');
|
||||
Route::post('recipes/{recipe}/duplicate', [RecipeController::class, 'duplicate'])->name('recipes.duplicate');
|
||||
Route::patch('recipes/{id}/restore', [RecipeController::class, 'restore'])->name('recipes.restore');
|
||||
Route::delete('recipes/{id}/forceDestroy', [RecipeController::class, 'forceDestroy'])->name('recipes.forceDestroy');
|
||||
Route::resource('recipes', RecipeController::class)
|
||||
|
|
@ -16,7 +16,7 @@ Route::resource('recipes', RecipeController::class)
|
|||
->middleware(['htmlsanitize:description,details,ingredients_list,preparation_steps,tips_and_advices', HandlePrecognitiveRequests::class]);
|
||||
|
||||
Route::get('products/trash', [ProductController::class, 'index'])->name('products.trash');
|
||||
Route::get('products/{product}/duplicate', [ProductController::class, 'duplicate'])->name('products.duplicate');
|
||||
Route::post('products/{product}/duplicate', [ProductController::class, 'duplicate'])->name('products.duplicate');
|
||||
Route::patch('products/{id}/restore', [ProductController::class, 'restore'])->name('products.restore');
|
||||
Route::delete('products/{id}/forceDestroy', [ProductController::class, 'forceDestroy'])->name('products.forceDestroy');
|
||||
Route::resource('products', ProductController::class)
|
||||
|
|
|
|||
|
|
@ -38,11 +38,6 @@ Breadcrumbs::for('back.products.edit', function (Generator $trail) {
|
|||
);
|
||||
});
|
||||
|
||||
Breadcrumbs::for('back.products.duplicate', function (Generator $trail) {
|
||||
$trail->parent('back.products.index');
|
||||
$trail->push(Str::ucfirst(trans('uwil-backend::crud.actions.duplicate')));
|
||||
});
|
||||
|
||||
Breadcrumbs::for('back.products.trash', function (Generator $trail) {
|
||||
$trail->parent('back.products.index');
|
||||
$trail->push(Str::ucfirst(trans('uwil-backend::back.archives')));
|
||||
|
|
@ -78,11 +73,6 @@ Breadcrumbs::for('back.recipes.edit', function (Generator $trail) {
|
|||
);
|
||||
});
|
||||
|
||||
Breadcrumbs::for('back.recipes.duplicate', function (Generator $trail) {
|
||||
$trail->parent('back.recipes.index');
|
||||
$trail->push(Str::ucfirst(trans('uwil-backend::crud.actions.duplicate')));
|
||||
});
|
||||
|
||||
Breadcrumbs::for('back.recipes.trash', function (Generator $trail) {
|
||||
$trail->parent('back.recipes.index');
|
||||
$trail->push(Str::ucfirst(trans('uwil-backend::back.archives')));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue