mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-11-29 10:24:20 +01:00
Merge remote-tracking branch 'origin/product_store' into product_store-inte
This commit is contained in:
commit
82fab4dd4f
@ -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
|
||||
|
@ -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: {
|
||||
|
@ -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';
|
||||
@ -14,7 +14,8 @@ import { Trash } from 'phosphor-react';
|
||||
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> {
|
||||
@ -22,19 +23,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
|
||||
*/
|
||||
@ -54,8 +61,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>>>
|
||||
@ -81,7 +93,6 @@ export const FormImageUpload = <TFieldValues extends FieldValues>({ id, register
|
||||
null as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
|
||||
);
|
||||
setFile(null);
|
||||
setImage(null);
|
||||
if (typeof onFileRemove === 'function') {
|
||||
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');
|
||||
|
||||
/**
|
||||
* 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 || ''}`
|
||||
@ -117,6 +139,12 @@ export const FormImageUpload = <TFieldValues extends FieldValues>({ id, register
|
||||
placeholder={placeholder()}/>
|
||||
{hasImage() && <FabButton onClick={onRemoveFile} icon={<Trash size={20} weight="fill" />} className="is-main" />}
|
||||
</div>
|
||||
{mainOption &&
|
||||
<div>
|
||||
<input type="radio" checked={!!file?.is_main} onChange={setMainImage} />
|
||||
<label>{t('app.shared.form_image_upload.main_image')}</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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';
|
||||
@ -145,7 +145,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
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
@ -156,7 +158,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
|
||||
};
|
||||
}));
|
||||
}
|
||||
};
|
||||
};
|
||||
@ -241,15 +295,17 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
|
||||
<div className="list">
|
||||
{output.product_images_attributes.map((image, i) => (
|
||||
<FormImageUpload key={i}
|
||||
defaultImage={image}
|
||||
id={`product_images_attributes[${i}]`}
|
||||
accept="image/*"
|
||||
size="small"
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
formState={formState}
|
||||
className={image._destroy ? 'hidden' : ''}
|
||||
onFileRemove={handleRemoveProductImage(i)}
|
||||
defaultImage={image}
|
||||
id={`product_images_attributes[${i}]`}
|
||||
accept="image/*"
|
||||
size="small"
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
formState={formState}
|
||||
className={image._destroy ? 'hidden' : ''}
|
||||
mainOption={true}
|
||||
onFileRemove={handleRemoveProductImage(i)}
|
||||
onFileIsMain={handleSetMainImage(i)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -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
|
||||
}>
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -559,3 +559,4 @@ en:
|
||||
form_image_upload:
|
||||
browse: "Browse"
|
||||
edit: "Edit"
|
||||
main_image: "Main image"
|
||||
|
@ -559,3 +559,4 @@ fr:
|
||||
form_image_upload:
|
||||
browse: "Parcourir"
|
||||
edit: "Modifier"
|
||||
main_image: "Visuel principal"
|
||||
|
5
db/migrate/20220803091913_add_is_main_to_assets.rb
Normal file
5
db/migrate/20220803091913_add_is_main_to_assets.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class AddIsMainToAssets < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :assets, :is_main, :boolean
|
||||
end
|
||||
end
|
@ -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|
|
||||
|
Loading…
Reference in New Issue
Block a user