1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-28 09:24:24 +01:00

add is_main to product image

This commit is contained in:
Du Peng 2022-08-03 20:16:21 +02:00
parent 8f38ff79d7
commit f345fc2443
10 changed files with 111 additions and 14 deletions

View File

@ -65,6 +65,6 @@ class API::ProductsController < API::ApiController
:low_stock_alert, :low_stock_threshold, :low_stock_alert, :low_stock_threshold,
machine_ids: [], machine_ids: [],
product_files_attributes: %i[id attachment _destroy], product_files_attributes: %i[id attachment _destroy],
product_images_attributes: %i[id attachment _destroy]) product_images_attributes: %i[id attachment is_main _destroy])
end end
end end

View File

@ -32,6 +32,7 @@ export default class ProductAPI {
product.product_images_attributes?.forEach((image, i) => { product.product_images_attributes?.forEach((image, i) => {
if (image?.attachment_files && image?.attachment_files[0]) { if (image?.attachment_files && image?.attachment_files[0]) {
data.set(`product[product_images_attributes][${i}][attachment]`, image.attachment_files[0]); data.set(`product[product_images_attributes][${i}][attachment]`, image.attachment_files[0]);
data.set(`product[product_images_attributes][${i}][is_main]`, (!!image.is_main).toString());
} }
}); });
const res: AxiosResponse<Product> = await apiClient.post('/api/products', data, { const res: AxiosResponse<Product> = await apiClient.post('/api/products', data, {
@ -73,6 +74,7 @@ export default class ProductAPI {
if (image?._destroy) { if (image?._destroy) {
data.set(`product[product_images_attributes][${i}][_destroy]`, image._destroy.toString()); data.set(`product[product_images_attributes][${i}][_destroy]`, image._destroy.toString());
} }
data.set(`product[product_images_attributes][${i}][is_main]`, (!!image.is_main).toString());
}); });
const res: AxiosResponse<Product> = await apiClient.patch(`/api/products/${product.id}`, data, { const res: AxiosResponse<Product> = await apiClient.patch(`/api/products/${product.id}`, data, {
headers: { headers: {

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Path } from 'react-hook-form'; import { Path } from 'react-hook-form';
import { UnpackNestedValue, UseFormSetValue } from 'react-hook-form/dist/types/form'; import { UnpackNestedValue, UseFormSetValue } from 'react-hook-form/dist/types/form';
@ -13,7 +13,8 @@ import noAvatar from '../../../../images/no_avatar.png';
export interface ImageType { export interface ImageType {
id?: number, id?: number,
attachment_name?: string, attachment_name?: string,
attachment_url?: string attachment_url?: string,
is_main?: boolean
} }
interface FormImageUploadProps<TFieldValues> extends FormComponent<TFieldValues>, AbstractFormItemProps<TFieldValues> { interface FormImageUploadProps<TFieldValues> extends FormComponent<TFieldValues>, AbstractFormItemProps<TFieldValues> {
@ -21,19 +22,25 @@ interface FormImageUploadProps<TFieldValues> extends FormComponent<TFieldValues>
defaultImage?: ImageType, defaultImage?: ImageType,
accept?: string, accept?: string,
size?: 'small' | 'large' size?: 'small' | 'large'
mainOption?: boolean,
onFileChange?: (value: ImageType) => void, onFileChange?: (value: ImageType) => void,
onFileRemove?: () => void, onFileRemove?: () => void,
onFileIsMain?: () => void,
} }
/** /**
* This component allows to upload image, in forms managed by react-hook-form. * This component allows to upload image, in forms managed by react-hook-form.
*/ */
export const FormImageUpload = <TFieldValues extends FieldValues>({ id, register, defaultImage, className, rules, disabled, error, warning, formState, onFileChange, onFileRemove, accept, setValue, size }: FormImageUploadProps<TFieldValues>) => { export const FormImageUpload = <TFieldValues extends FieldValues>({ id, register, defaultImage, className, rules, disabled, error, warning, formState, onFileChange, onFileRemove, accept, setValue, size, onFileIsMain, mainOption = false }: FormImageUploadProps<TFieldValues>) => {
const { t } = useTranslation('shared'); const { t } = useTranslation('shared');
const [file, setFile] = useState<ImageType>(defaultImage); const [file, setFile] = useState<ImageType>(defaultImage);
const [image, setImage] = useState<string | ArrayBuffer>(defaultImage.attachment_url); const [image, setImage] = useState<string | ArrayBuffer>(defaultImage.attachment_url);
useEffect(() => {
setFile(defaultImage);
}, [defaultImage]);
/** /**
* Check if image is selected * Check if image is selected
*/ */
@ -53,8 +60,13 @@ export const FormImageUpload = <TFieldValues extends FieldValues>({ id, register
}; };
reader.readAsDataURL(f); reader.readAsDataURL(f);
setFile({ setFile({
...file,
attachment_name: f.name attachment_name: f.name
}); });
setValue(
`${id}[attachment_name]` as Path<TFieldValues>,
f.name as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
);
setValue( setValue(
`${id}[_destroy]` as Path<TFieldValues>, `${id}[_destroy]` as Path<TFieldValues>,
false as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>> false as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
@ -80,12 +92,22 @@ export const FormImageUpload = <TFieldValues extends FieldValues>({ id, register
null as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>> null as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
); );
setFile(null); setFile(null);
setImage(null);
if (typeof onFileRemove === 'function') { if (typeof onFileRemove === 'function') {
onFileRemove(); onFileRemove();
} }
} }
/**
* Callback triggered when the user set the image is main
*/
function setMainImage () {
setValue(
`${id}[is_main]` as Path<TFieldValues>,
true as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
);
onFileIsMain();
}
// Compose classnames from props // Compose classnames from props
const classNames = [ const classNames = [
`${className || ''}` `${className || ''}`
@ -114,6 +136,12 @@ export const FormImageUpload = <TFieldValues extends FieldValues>({ id, register
</FabButton> </FabButton>
{hasImage() && <FabButton onClick={onRemoveFile} icon={<i className="fa fa-trash-o"/>} className="delete-image" />} {hasImage() && <FabButton onClick={onRemoveFile} icon={<i className="fa fa-trash-o"/>} className="delete-image" />}
</div> </div>
{mainOption &&
<div>
<input type="radio" checked={!!file?.is_main} onChange={setMainImage} />
<label>{t('app.shared.form_image_upload.main_image')}</label>
</div>
}
</div> </div>
); );
}; };

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch, Path } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import slugify from 'slugify'; import slugify from 'slugify';
import _ from 'lodash'; import _ from 'lodash';
@ -11,7 +11,7 @@ import { FormSelect } from '../form/form-select';
import { FormChecklist } from '../form/form-checklist'; import { FormChecklist } from '../form/form-checklist';
import { FormRichText } from '../form/form-rich-text'; import { FormRichText } from '../form/form-rich-text';
import { FormFileUpload } from '../form/form-file-upload'; import { FormFileUpload } from '../form/form-file-upload';
import { FormImageUpload } from '../form/form-image-upload'; import { FormImageUpload, ImageType } from '../form/form-image-upload';
import { FabButton } from '../base/fab-button'; import { FabButton } from '../base/fab-button';
import { FabAlert } from '../base/fab-alert'; import { FabAlert } from '../base/fab-alert';
import ProductCategoryAPI from '../../api/product-category'; import ProductCategoryAPI from '../../api/product-category';
@ -144,7 +144,9 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
* Add new product image * Add new product image
*/ */
const addProductImage = () => { const addProductImage = () => {
setValue('product_images_attributes', output.product_images_attributes.concat({})); setValue('product_images_attributes', output.product_images_attributes.concat({
is_main: output.product_images_attributes.length === 0
}));
}; };
/** /**
@ -155,8 +157,60 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
const productImage = output.product_images_attributes[i]; const productImage = output.product_images_attributes[i];
if (!productImage.id) { if (!productImage.id) {
output.product_images_attributes.splice(i, 1); output.product_images_attributes.splice(i, 1);
if (productImage.is_main) {
setValue('product_images_attributes', output.product_images_attributes.map((image, k) => {
if (k === 0) {
return {
...image,
is_main: true
};
}
return image;
}));
} else {
setValue('product_images_attributes', output.product_images_attributes); setValue('product_images_attributes', output.product_images_attributes);
} }
} else {
if (productImage.is_main) {
let mainImage = false;
setValue('product_images_attributes', output.product_images_attributes.map((image, k) => {
if (i !== k && !mainImage) {
mainImage = true;
return {
...image,
_destroy: i === k,
is_main: true
};
}
return {
...image,
_destroy: i === k
};
}));
}
}
};
};
/**
* Remove main image in others product images
*/
const handleSetMainImage = (i: number) => {
return () => {
if (output.product_images_attributes.length > 1) {
setValue('product_images_attributes', output.product_images_attributes.map((image, k) => {
if (i !== k) {
return {
...image,
is_main: false
};
}
return {
...image,
is_main: true
};
}));
}
}; };
}; };
@ -246,7 +300,9 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
setValue={setValue} setValue={setValue}
formState={formState} formState={formState}
className={image._destroy ? 'hidden' : ''} className={image._destroy ? 'hidden' : ''}
mainOption={true}
onFileRemove={handleRemoveProductImage(i)} onFileRemove={handleRemoveProductImage(i)}
onFileIsMain={handleSetMainImage(i)}
/> />
))} ))}
</div> </div>

View File

@ -27,7 +27,7 @@ export interface Product {
attachment?: File, attachment?: File,
attachment_files?: FileList, attachment_files?: FileList,
attachment_name?: string, attachment_name?: string,
attachment_url?: string attachment_url?: string,
_destroy?: boolean _destroy?: boolean
}>, }>,
product_images_attributes: Array<{ product_images_attributes: Array<{
@ -35,7 +35,8 @@ export interface Product {
attachment?: File, attachment?: File,
attachment_files?: FileList, attachment_files?: FileList,
attachment_name?: string, attachment_name?: string,
attachment_url?: string attachment_url?: string,
_destroy?: boolean _destroy?: boolean,
is_main?: boolean
}> }>
} }

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
json.extract! product, :id, :name, :slug, :sku, :description, :is_active, :product_category_id, :quantity_min, :stock, :low_stock_alert, :low_stock_threshold, :machine_ids json.extract! product, :id, :name, :slug, :sku, :description, :is_active, :product_category_id, :quantity_min, :stock, :low_stock_alert,
:low_stock_threshold, :machine_ids
json.amount product.amount / 100.0 if product.amount.present? json.amount product.amount / 100.0 if product.amount.present?
json.product_files_attributes product.product_files do |f| json.product_files_attributes product.product_files do |f|
json.id f.id json.id f.id
@ -11,4 +12,5 @@ json.product_images_attributes product.product_images do |f|
json.id f.id json.id f.id
json.attachment_name f.attachment_identifier json.attachment_name f.attachment_identifier
json.attachment_url f.attachment_url json.attachment_url f.attachment_url
json.is_main f.is_main
end end

View File

@ -559,3 +559,4 @@ en:
form_image_upload: form_image_upload:
browse: "Browse" browse: "Browse"
edit: "Edit" edit: "Edit"
main_image: "Main image"

View File

@ -559,3 +559,4 @@ fr:
form_image_upload: form_image_upload:
browse: "Parcourir" browse: "Parcourir"
edit: "Modifier" edit: "Modifier"
main_image: "Visuel principal"

View File

@ -0,0 +1,5 @@
class AddIsMainToAssets < ActiveRecord::Migration[5.2]
def change
add_column :assets, :is_main, :boolean
end
end

View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2022_07_20_135828) do ActiveRecord::Schema.define(version: 2022_08_03_091913) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "fuzzystrmatch" enable_extension "fuzzystrmatch"
@ -70,6 +70,7 @@ ActiveRecord::Schema.define(version: 2022_07_20_135828) do
t.string "type" t.string "type"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.boolean "is_main"
end end
create_table "auth_provider_mappings", id: :serial, force: :cascade do |t| create_table "auth_provider_mappings", id: :serial, force: :cascade do |t|