From f345fc2443054c928ecd25d420c26d4c7d50d5f1 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Wed, 3 Aug 2022 20:16:21 +0200 Subject: [PATCH] add is_main to product image --- app/controllers/api/products_controller.rb | 2 +- app/frontend/src/javascript/api/product.ts | 2 + .../components/form/form-image-upload.tsx | 36 +++++++++-- .../components/store/product-form.tsx | 64 +++++++++++++++++-- app/frontend/src/javascript/models/product.ts | 7 +- app/views/api/products/_product.json.jbuilder | 4 +- config/locales/app.shared.en.yml | 1 + config/locales/app.shared.fr.yml | 1 + .../20220803091913_add_is_main_to_assets.rb | 5 ++ db/schema.rb | 3 +- 10 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 db/migrate/20220803091913_add_is_main_to_assets.rb diff --git a/app/controllers/api/products_controller.rb b/app/controllers/api/products_controller.rb index b21e65be1..65a13eeb0 100644 --- a/app/controllers/api/products_controller.rb +++ b/app/controllers/api/products_controller.rb @@ -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 diff --git a/app/frontend/src/javascript/api/product.ts b/app/frontend/src/javascript/api/product.ts index c89a32642..a3c0a4e92 100644 --- a/app/frontend/src/javascript/api/product.ts +++ b/app/frontend/src/javascript/api/product.ts @@ -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 = 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 = await apiClient.patch(`/api/products/${product.id}`, data, { headers: { diff --git a/app/frontend/src/javascript/components/form/form-image-upload.tsx b/app/frontend/src/javascript/components/form/form-image-upload.tsx index 76bde2632..9214054b6 100644 --- a/app/frontend/src/javascript/components/form/form-image-upload.tsx +++ b/app/frontend/src/javascript/components/form/form-image-upload.tsx @@ -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 extends FormComponent, AbstractFormItemProps { @@ -21,19 +22,25 @@ interface FormImageUploadProps extends FormComponent 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 = ({ id, register, defaultImage, className, rules, disabled, error, warning, formState, onFileChange, onFileRemove, accept, setValue, size }: FormImageUploadProps) => { +export const FormImageUpload = ({ id, register, defaultImage, className, rules, disabled, error, warning, formState, onFileChange, onFileRemove, accept, setValue, size, onFileIsMain, mainOption = false }: FormImageUploadProps) => { const { t } = useTranslation('shared'); const [file, setFile] = useState(defaultImage); const [image, setImage] = useState(defaultImage.attachment_url); + useEffect(() => { + setFile(defaultImage); + }, [defaultImage]); + /** * Check if image is selected */ @@ -53,8 +60,13 @@ export const FormImageUpload = ({ id, register }; reader.readAsDataURL(f); setFile({ + ...file, attachment_name: f.name }); + setValue( + `${id}[attachment_name]` as Path, + f.name as UnpackNestedValue>> + ); setValue( `${id}[_destroy]` as Path, false as UnpackNestedValue>> @@ -80,12 +92,22 @@ export const FormImageUpload = ({ id, register null as UnpackNestedValue>> ); 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, + true as UnpackNestedValue>> + ); + onFileIsMain(); + } + // Compose classnames from props const classNames = [ `${className || ''}` @@ -114,6 +136,12 @@ export const FormImageUpload = ({ id, register {hasImage() && } className="delete-image" />} + {mainOption && +
+ + +
+ } ); }; diff --git a/app/frontend/src/javascript/components/store/product-form.tsx b/app/frontend/src/javascript/components/store/product-form.tsx index e2139568f..dc6fe7c70 100644 --- a/app/frontend/src/javascript/components/store/product-form.tsx +++ b/app/frontend/src/javascript/components/store/product-form.tsx @@ -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 = ({ 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 = ({ 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 = ({ product, title, onSucc setValue={setValue} formState={formState} className={image._destroy ? 'hidden' : ''} + mainOption={true} onFileRemove={handleRemoveProductImage(i)} + onFileIsMain={handleSetMainImage(i)} /> ))} diff --git a/app/frontend/src/javascript/models/product.ts b/app/frontend/src/javascript/models/product.ts index 471e00248..57aa51809 100644 --- a/app/frontend/src/javascript/models/product.ts +++ b/app/frontend/src/javascript/models/product.ts @@ -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 }> } diff --git a/app/views/api/products/_product.json.jbuilder b/app/views/api/products/_product.json.jbuilder index 9b9a99336..37d9fca9b 100644 --- a/app/views/api/products/_product.json.jbuilder +++ b/app/views/api/products/_product.json.jbuilder @@ -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 diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index 3fc1e941e..f5c3abaca 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -559,3 +559,4 @@ en: form_image_upload: browse: "Browse" edit: "Edit" + main_image: "Main image" diff --git a/config/locales/app.shared.fr.yml b/config/locales/app.shared.fr.yml index 139728a02..6f2cfa3d8 100644 --- a/config/locales/app.shared.fr.yml +++ b/config/locales/app.shared.fr.yml @@ -559,3 +559,4 @@ fr: form_image_upload: browse: "Parcourir" edit: "Modifier" + main_image: "Visuel principal" diff --git a/db/migrate/20220803091913_add_is_main_to_assets.rb b/db/migrate/20220803091913_add_is_main_to_assets.rb new file mode 100644 index 000000000..3201a80a5 --- /dev/null +++ b/db/migrate/20220803091913_add_is_main_to_assets.rb @@ -0,0 +1,5 @@ +class AddIsMainToAssets < ActiveRecord::Migration[5.2] + def change + add_column :assets, :is_main, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 1222e9bcf..864a474d7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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|