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,
machine_ids: [],
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

View File

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

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Path } from 'react-hook-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 {
id?: number,
attachment_name?: string,
attachment_url?: string
attachment_url?: string,
is_main?: boolean
}
interface FormImageUploadProps<TFieldValues> extends FormComponent<TFieldValues>, AbstractFormItemProps<TFieldValues> {
@ -21,19 +22,25 @@ interface FormImageUploadProps<TFieldValues> extends FormComponent<TFieldValues>
defaultImage?: ImageType,
accept?: string,
size?: 'small' | 'large'
mainOption?: boolean,
onFileChange?: (value: ImageType) => void,
onFileRemove?: () => void,
onFileIsMain?: () => void,
}
/**
* 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 [file, setFile] = useState<ImageType>(defaultImage);
const [image, setImage] = useState<string | ArrayBuffer>(defaultImage.attachment_url);
useEffect(() => {
setFile(defaultImage);
}, [defaultImage]);
/**
* Check if image is selected
*/
@ -53,8 +60,13 @@ export const FormImageUpload = <TFieldValues extends FieldValues>({ id, register
};
reader.readAsDataURL(f);
setFile({
...file,
attachment_name: f.name
});
setValue(
`${id}[attachment_name]` as Path<TFieldValues>,
f.name as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
);
setValue(
`${id}[_destroy]` as 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>>>
);
setFile(null);
setImage(null);
if (typeof onFileRemove === 'function') {
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
const classNames = [
`${className || ''}`
@ -114,6 +136,12 @@ export const FormImageUpload = <TFieldValues extends FieldValues>({ id, register
</FabButton>
{hasImage() && <FabButton onClick={onRemoveFile} icon={<i className="fa fa-trash-o"/>} className="delete-image" />}
</div>
{mainOption &&
<div>
<input type="radio" checked={!!file?.is_main} onChange={setMainImage} />
<label>{t('app.shared.form_image_upload.main_image')}</label>
</div>
}
</div>
);
};

View File

@ -1,5 +1,5 @@
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 slugify from 'slugify';
import _ from 'lodash';
@ -11,7 +11,7 @@ import { FormSelect } from '../form/form-select';
import { FormChecklist } from '../form/form-checklist';
import { FormRichText } from '../form/form-rich-text';
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 { FabAlert } from '../base/fab-alert';
import ProductCategoryAPI from '../../api/product-category';
@ -144,7 +144,9 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
* Add new product image
*/
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,7 +157,59 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
const productImage = output.product_images_attributes[i];
if (!productImage.id) {
output.product_images_attributes.splice(i, 1);
setValue('product_images_attributes', output.product_images_attributes);
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);
}
} 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}
formState={formState}
className={image._destroy ? 'hidden' : ''}
mainOption={true}
onFileRemove={handleRemoveProductImage(i)}
onFileIsMain={handleSetMainImage(i)}
/>
))}
</div>

View File

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

View File

@ -1,6 +1,7 @@
# 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.product_files_attributes product.product_files do |f|
json.id f.id
@ -11,4 +12,5 @@ json.product_images_attributes product.product_images do |f|
json.id f.id
json.attachment_name f.attachment_identifier
json.attachment_url f.attachment_url
json.is_main f.is_main
end

View File

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

View File

@ -559,3 +559,4 @@ fr:
form_image_upload:
browse: "Parcourir"
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.
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
enable_extension "fuzzystrmatch"
@ -70,6 +70,7 @@ ActiveRecord::Schema.define(version: 2022_07_20_135828) do
t.string "type"
t.datetime "created_at"
t.datetime "updated_at"
t.boolean "is_main"
end
create_table "auth_provider_mappings", id: :serial, force: :cascade do |t|