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:
commit
82fab4dd4f
@ -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
|
||||||
|
@ -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: {
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -559,3 +559,4 @@ en:
|
|||||||
form_image_upload:
|
form_image_upload:
|
||||||
browse: "Browse"
|
browse: "Browse"
|
||||||
edit: "Edit"
|
edit: "Edit"
|
||||||
|
main_image: "Main image"
|
||||||
|
@ -559,3 +559,4 @@ fr:
|
|||||||
form_image_upload:
|
form_image_upload:
|
||||||
browse: "Parcourir"
|
browse: "Parcourir"
|
||||||
edit: "Modifier"
|
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.
|
# 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|
|
||||||
|
Loading…
Reference in New Issue
Block a user