1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-17 11:54:22 +01:00

(feat) clone a product

This commit is contained in:
Du Peng 2022-10-11 18:53:12 +02:00
parent 7700737cf3
commit ef9a5c22bb
11 changed files with 153 additions and 4 deletions

View File

@ -4,7 +4,7 @@
# Products are used in store
class API::ProductsController < API::ApiController
before_action :authenticate_user!, except: %i[index show]
before_action :set_product, only: %i[update destroy]
before_action :set_product, only: %i[update clone destroy]
def index
@products = ProductService.list(params, current_user)
@ -35,6 +35,17 @@ class API::ProductsController < API::ApiController
end
end
def clone
authorize @product
@product = ProductService.clone(@product, product_params)
if @product.save
render status: :ok
else
render json: @product.errors.full_messages, status: :unprocessable_entity
end
end
def destroy
authorize @product
ProductService.destroy(@product)

View File

@ -91,6 +91,13 @@ export default class ProductAPI {
return res?.data;
}
static async clone (product: Product, data: Product): Promise<Product> {
const res: AxiosResponse<Product> = await apiClient.put(`/api/products/${product.id}/clone`, {
product: data
});
return res?.data;
}
static async destroy (productId: number): Promise<void> {
const res: AxiosResponse<void> = await apiClient.delete(`/api/products/${productId}`);
return res?.data;

View File

@ -0,0 +1,70 @@
/* eslint-disable */
import React, { useState, useEffect } from 'react';
import { SubmitHandler, useForm, useWatch } from 'react-hook-form';
import { FormInput } from '../form/form-input';
import { FormSwitch } from '../form/form-switch';
import { useTranslation } from 'react-i18next';
import { FabModal, ModalSize } from '../base/fab-modal';
import { Product } from '../../models/product';
import ProductAPI from '../../api/product';
interface CloneProductModalProps {
isOpen: boolean,
toggleModal: () => void,
onSuccess: (product: Product) => void,
onError: (message: string) => void,
product: Product,
}
/**
* Modal dialog to clone a product
*/
export const CloneProductModal: React.FC<CloneProductModalProps> = ({ isOpen, toggleModal, onSuccess, onError, product }) => {
const { t } = useTranslation('admin');
const { handleSubmit, register, control, formState, reset } = useForm<Product>({ defaultValues: {
name: product.name,
sku: product.sku,
is_active: false,
} });
/**
* Call product clone api
*/
const handleClone: SubmitHandler<Product> = (data: Product) => {
ProductAPI.clone(product, data).then((res) => {
reset(res);
onSuccess(res);
}).catch(onError);
};
return (
<FabModal title={t('app.admin.store.clone_product_model.clone_product')}
isOpen={isOpen}
toggleModal={toggleModal}
width={ModalSize.medium}
confirmButton={t('app.admin.store.clone_product_model.clone')}
onConfirm={handleSubmit(handleClone)}>
<form className="clone-product-form" onSubmit={handleSubmit(handleClone)}>
<FormInput id="name"
register={register}
rules={{ required: true }}
formState={formState}
label={t('app.admin.store.product_form.name')}
className="span-12" />
<FormInput id="sku"
register={register}
formState={formState}
label={t('app.admin.store.product_form.sku')}
className="span-12" />
{product.is_active &&
<FormSwitch control={control}
id="is_active"
formState={formState}
label={t('app.admin.store.product_form.is_show_in_store')}
tooltip={t('app.admin.store.product_form.active_price_info')}
className='span-12' />
}
</form>
</FabModal>
);
};

View File

@ -33,10 +33,17 @@ const EditProduct: React.FC<EditProductProps> = ({ productId, onSuccess, onError
/**
* Success to save product and return to product list
* or
* Success to clone product and return to new product
*/
const saveProductSuccess = () => {
onSuccess(t('app.admin.store.edit_product.successfully_updated'));
window.location.href = '/#!/admin/store/products';
const saveProductSuccess = (data: Product) => {
if (data.id === product.id) {
onSuccess(t('app.admin.store.edit_product.successfully_updated'));
window.location.href = '/#!/admin/store/products';
} else {
onSuccess(t('app.admin.store.edit_product.successfully_cloned'));
window.location.href = `/#!/admin/store/products/${data.id}/edit`;
}
};
if (product) {

View File

@ -19,6 +19,7 @@ import MachineAPI from '../../api/machine';
import ProductAPI from '../../api/product';
import { Plus } from 'phosphor-react';
import { ProductStockForm } from './product-stock-form';
import { CloneProductModal } from './clone-product-modal';
import ProductLib from '../../lib/product';
import { UnsavedFormAlert } from '../form/unsaved-form-alert';
import { UIRouter } from '@uirouter/angularjs';
@ -54,6 +55,7 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
const [productCategories, setProductCategories] = useState<selectOption[]>([]);
const [machines, setMachines] = useState<checklistOption[]>([]);
const [stockTab, setStockTab] = useState<boolean>(false);
const [openCloneModal, setOpenCloneModal] = useState<boolean>(false);
useEffect(() => {
ProductCategoryAPI.index().then(data => {
@ -136,6 +138,13 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
}
};
/**
* Toggle clone product modal
*/
const toggleCloneModal = () => {
setOpenCloneModal(!openCloneModal);
};
/**
* Add new product file
*/
@ -235,6 +244,12 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
<header>
<h2>{title}</h2>
<div className="grpBtn">
{product.id &&
<>
<FabButton className="main-action-btn" onClick={toggleCloneModal}>{t('app.admin.store.product_form.clone')}</FabButton>
<CloneProductModal isOpen={openCloneModal} toggleModal={toggleCloneModal} product={product} onSuccess={onSuccess} onError={onError} />
</>
}
<FabButton className="main-action-btn" onClick={handleSubmit(saveProduct)}>{t('app.admin.store.product_form.save')}</FabButton>
</div>
</header>

View File

@ -10,6 +10,10 @@ class ProductPolicy < ApplicationPolicy
user.privileged?
end
def clone?
user.privileged?
end
def destroy?
user.privileged?
end

View File

@ -67,6 +67,27 @@ class ProductService
product
end
def clone(product, product_params)
new_product = product.dup
new_product.name = product_params[:name]
new_product.sku = product_params[:sku]
new_product.is_active = product_params[:is_active]
new_product.stock['internal'] = 0
new_product.stock['external'] = 0
new_product.machine_ids = product.machine_ids
new_product.machine_ids = product.machine_ids
product.product_images.each do |image|
pi = new_product.product_images.build
pi.is_main = image.is_main
pi.attachment = File.open(image.attachment.file.file)
end
product.product_files.each do |file|
pf = new_product.product_files.build
pf.attachment = File.open(file.attachment.file.file)
end
new_product
end
def destroy(product)
used_in_order = OrderItem.joins(:order).where.not('orders.state' => 'cart')
.exists?(orderable: product)

View File

@ -0,0 +1,3 @@
# frozen_string_literal: true
json.partial! 'api/products/product', product: @product

View File

@ -2003,6 +2003,7 @@ en:
successfully_created: "The new product has been created."
edit_product:
successfully_updated: "The product has been updated."
successfully_cloned: "The product has been cloned."
product_form:
product_parameters: "Product parameters"
stock_management: "Stock management"
@ -2029,6 +2030,7 @@ en:
product_images_info: "<strong>Advice</strong></br>We advise you to use a square format, JPG or PNG. For JPG, please use white for the background colour. The main visual will be the first presented in the product sheet."
add_product_image: "Add an image"
save: "Save"
clone: "Clone"
product_stock_form:
stock_up_to_date: "Stock up to date"
date_time: "{DATE} - {TIME}"
@ -2072,6 +2074,9 @@ en:
damaged: "Damaged product"
other_in: "Other (in)"
other_out: "Other (out)"
clone_product_model:
clone_product: "Clone the product"
clone: "Clone"
orders:
heading: "Orders"
create_order: "Create an order"

View File

@ -2003,6 +2003,7 @@ fr:
successfully_created: "Le nouveau produit a été créé."
edit_product:
successfully_updated: "Le produit a été modifié."
successfully_cloned: "Le produit a été dupliqué."
product_form:
product_parameters: "Paramètres du produit"
stock_management: "Gestion des stocks"
@ -2029,6 +2030,7 @@ fr:
product_images_info: "<strong>Conseil</strong></br>Nous vous conseillons d'utiliser un format carré, JPG ou PNG. Pour le JPG, veuillez utiliser le blanc pour la couleur de fond. Le visuel principal sera le premier présenté dans la fiche produit."
add_product_image: "Ajouter une image"
save: "Enregistrer"
clone: "Dupliquer"
product_stock_form:
stock_up_to_date: "Stock à jour"
date_time: "{DATE} - {TIME}"
@ -2072,6 +2074,9 @@ fr:
damaged: "Produit endommagé"
other_in: "Autre (entrant)"
other_out: "Autre (sortant)"
clone_product_model:
clone_product: "Dupliquer le produit"
clone: "Dupliquer"
orders:
heading: "Commandes"
create_order: "Créer une commande"

View File

@ -155,6 +155,7 @@ Rails.application.routes.draw do
end
resources :products do
put 'clone', on: :member
get 'stock_movements', on: :member
end
resources :cart, only: %i[create] do