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

Merge remote-tracking branch 'origin/product_store' into product_store-inte

This commit is contained in:
vincent 2022-08-04 09:15:27 +02:00
commit 82fab4dd4f
10 changed files with 120 additions and 23 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';
@ -14,7 +14,8 @@ import { Trash } from 'phosphor-react';
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> {
@ -22,19 +23,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
*/ */
@ -54,8 +61,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>>>
@ -81,7 +93,6 @@ 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();
} }
@ -92,6 +103,17 @@ export const FormImageUpload = <TFieldValues extends FieldValues>({ id, register
*/ */
const placeholder = (): string => hasImage() ? t('app.shared.form_image_upload.edit') : t('app.shared.form_image_upload.browse'); const placeholder = (): string => hasImage() ? t('app.shared.form_image_upload.edit') : t('app.shared.form_image_upload.browse');
/**
* 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 || ''}`
@ -117,6 +139,12 @@ export const FormImageUpload = <TFieldValues extends FieldValues>({ id, register
placeholder={placeholder()}/> placeholder={placeholder()}/>
{hasImage() && <FabButton onClick={onRemoveFile} icon={<Trash size={20} weight="fill" />} className="is-main" />} {hasImage() && <FabButton onClick={onRemoveFile} icon={<Trash size={20} weight="fill" />} className="is-main" />}
</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';
@ -145,7 +145,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
}));
}; };
/** /**
@ -156,7 +158,59 @@ 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);
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
};
}));
} }
}; };
}; };
@ -241,15 +295,17 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
<div className="list"> <div className="list">
{output.product_images_attributes.map((image, i) => ( {output.product_images_attributes.map((image, i) => (
<FormImageUpload key={i} <FormImageUpload key={i}
defaultImage={image} defaultImage={image}
id={`product_images_attributes[${i}]`} id={`product_images_attributes[${i}]`}
accept="image/*" accept="image/*"
size="small" size="small"
register={register} register={register}
setValue={setValue} setValue={setValue}
formState={formState} formState={formState}
className={image._destroy ? 'hidden' : ''} className={image._destroy ? 'hidden' : ''}
onFileRemove={handleRemoveProductImage(i)} mainOption={true}
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|