mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-20 14:54:15 +01:00
(bug) fix file/image input components
This commit is contained in:
parent
a97e08b43b
commit
20e50bda13
@ -101,9 +101,9 @@ const FabTextEditor: React.ForwardRefRenderFunction<FabTextEditorRef, FabTextEdi
|
||||
<div className={`fab-text-editor ${disabled ? 'is-disabled' : ''}`}>
|
||||
<MenuBar editor={editor} heading={heading} bulletList={bulletList} blockquote={blockquote} video={video} image={image} link={link} disabled={disabled} />
|
||||
<EditorContent editor={editor} />
|
||||
<div className="fab-text-editor-character-count">
|
||||
{limit && <div className="fab-text-editor-character-count">
|
||||
{editor?.storage.characterCount.characters()} / {limit}
|
||||
</div>
|
||||
</div>}
|
||||
{error &&
|
||||
<div className="fab-text-editor-error">
|
||||
<WarningOctagon size={24} />
|
||||
|
@ -45,7 +45,7 @@ export const FormFileUpload = <TFieldValues extends FieldValues>({ id, register,
|
||||
attachment_name: f.name
|
||||
});
|
||||
setValue(
|
||||
`${id}[_destroy]` as Path<TFieldValues>,
|
||||
`${id}._destroy` as Path<TFieldValues>,
|
||||
false as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
|
||||
);
|
||||
if (typeof onFileChange === 'function') {
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Path } from 'react-hook-form';
|
||||
import { Path, Controller } from 'react-hook-form';
|
||||
import { UnpackNestedValue, UseFormSetValue } from 'react-hook-form/dist/types/form';
|
||||
import { FieldPathValue } from 'react-hook-form/dist/types/path';
|
||||
import { FieldPath, FieldPathValue } from 'react-hook-form/dist/types/path';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { FormInput } from './form-input';
|
||||
import { FormComponent } from '../../models/form-component';
|
||||
import { FormComponent, FormControlledComponent } from '../../models/form-component';
|
||||
import { AbstractFormItemProps } from './abstract-form-item';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import noImage from '../../../../images/no_image.png';
|
||||
@ -13,7 +13,7 @@ import { Trash } from 'phosphor-react';
|
||||
import { ImageType } from '../../models/file';
|
||||
import FileUploadLib from '../../lib/file-upload';
|
||||
|
||||
interface FormImageUploadProps<TFieldValues> extends FormComponent<TFieldValues>, AbstractFormItemProps<TFieldValues> {
|
||||
interface FormImageUploadProps<TFieldValues, TContext extends object> extends FormComponent<TFieldValues>, FormControlledComponent<TFieldValues, TContext>, AbstractFormItemProps<TFieldValues> {
|
||||
setValue: UseFormSetValue<TFieldValues>,
|
||||
defaultImage?: ImageType,
|
||||
accept?: string,
|
||||
@ -21,13 +21,13 @@ interface FormImageUploadProps<TFieldValues> extends FormComponent<TFieldValues>
|
||||
mainOption?: boolean,
|
||||
onFileChange?: (value: ImageType) => void,
|
||||
onFileRemove?: () => void,
|
||||
onFileIsMain?: () => void,
|
||||
onFileIsMain?: (setIsMain: () => void) => 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, onFileIsMain, mainOption = false }: FormImageUploadProps<TFieldValues>) => {
|
||||
export const FormImageUpload = <TFieldValues extends FieldValues, TContext extends object>({ id, label, register, control, defaultImage, className, rules, disabled, error, warning, formState, onFileChange, onFileRemove, accept, setValue, size, onFileIsMain, mainOption = false }: FormImageUploadProps<TFieldValues, TContext>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const [file, setFile] = useState<ImageType>(defaultImage);
|
||||
@ -60,12 +60,11 @@ export const FormImageUpload = <TFieldValues extends FieldValues>({ id, register
|
||||
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>>>
|
||||
id as Path<TFieldValues>,
|
||||
{
|
||||
attachment_name: f.name,
|
||||
_destroy: false
|
||||
} as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
|
||||
);
|
||||
if (typeof onFileChange === 'function') {
|
||||
onFileChange({ attachment_name: f.name });
|
||||
@ -85,44 +84,41 @@ 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 || ''}`
|
||||
].join(' ');
|
||||
|
||||
return (
|
||||
<div className={`form-image-upload form-image-upload--${size} ${classNames}`}>
|
||||
<div className={`form-image-upload form-image-upload--${size} ${label ? 'with-label' : ''} ${classNames}`}>
|
||||
<div className={`image image--${size}`}>
|
||||
<img src={image || noImage} />
|
||||
<img src={hasImage() ? image : noImage} alt={file?.attachment_name || 'no image'} />
|
||||
</div>
|
||||
<div className="actions">
|
||||
{mainOption &&
|
||||
<label className='fab-button'>
|
||||
{t('app.shared.form_image_upload.main_image')}
|
||||
<input type="radio" checked={!!file?.is_main} onChange={setMainImage} />
|
||||
<Controller name={`${id}.is_main` as FieldPath<TFieldValues>}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) =>
|
||||
<input id={`${id}.is_main`}
|
||||
type="radio"
|
||||
checked={value}
|
||||
onChange={() => { onFileIsMain(onChange); }} />
|
||||
} />
|
||||
</label>
|
||||
}
|
||||
<FormInput className="image-file-input"
|
||||
type="file"
|
||||
accept={accept}
|
||||
register={register}
|
||||
label={label}
|
||||
formState={formState}
|
||||
rules={rules}
|
||||
disabled={disabled}
|
||||
error={error}
|
||||
warning={warning}
|
||||
id={`${id}[attachment_files]`}
|
||||
id={`${id}.attachment_files`}
|
||||
onChange={onFileSelected}
|
||||
placeholder={placeholder()}/>
|
||||
{hasImage() && <FabButton onClick={onRemoveFile} icon={<Trash size={20} weight="fill" />} className="is-main" />}
|
||||
@ -130,3 +126,7 @@ export const FormImageUpload = <TFieldValues extends FieldValues>({ id, register
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FormImageUpload.defaultProps = {
|
||||
size: 'medium'
|
||||
};
|
||||
|
@ -0,0 +1,48 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { FormFileUpload } from './form-file-upload';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { Plus } from 'phosphor-react';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { FormComponent, FormControlledComponent } from '../../models/form-component';
|
||||
import { AbstractFormItemProps } from './abstract-form-item';
|
||||
import { UseFormSetValue } from 'react-hook-form/dist/types/form';
|
||||
import { ArrayPath, FieldArray, useFieldArray } from 'react-hook-form';
|
||||
import { FileType } from '../../models/file';
|
||||
import { UnpackNestedValue } from 'react-hook-form/dist/types';
|
||||
|
||||
interface FormMultiFileUploadProps<TFieldValues, TContext extends object> extends FormComponent<TFieldValues>, FormControlledComponent<TFieldValues, TContext>, AbstractFormItemProps<TFieldValues> {
|
||||
setValue: UseFormSetValue<TFieldValues>,
|
||||
addButtonLabel: ReactNode,
|
||||
accept: string
|
||||
}
|
||||
|
||||
/**
|
||||
* This component allows to upload multiple files, in forms managed by react-hook-form.
|
||||
*/
|
||||
export const FormMultiFileUpload = <TFieldValues extends FieldValues, TContext extends object>({ id, className, register, control, setValue, formState, addButtonLabel, accept }: FormMultiFileUploadProps<TFieldValues, TContext>) => {
|
||||
const { fields, append, remove } = useFieldArray({ control, name: id as ArrayPath<TFieldValues> });
|
||||
|
||||
return (
|
||||
<div className={`form-multi-file-upload ${className || ''}`}>
|
||||
<div className="list">
|
||||
{fields.map((field: FileType, index) => (
|
||||
<FormFileUpload key={field.id}
|
||||
defaultFile={field}
|
||||
id={`${id}.${index}`}
|
||||
accept={accept}
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
formState={formState}
|
||||
className={field._destroy ? 'hidden' : ''}
|
||||
onFileRemove={() => remove(index)}/>
|
||||
))}
|
||||
</div>
|
||||
<FabButton
|
||||
onClick={() => append({ _destroy: false } as UnpackNestedValue<FieldArray<TFieldValues, ArrayPath<TFieldValues>>>)}
|
||||
className='is-secondary'
|
||||
icon={<Plus size={24} />}>
|
||||
{addButtonLabel}
|
||||
</FabButton>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,102 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { FormComponent, FormControlledComponent } from '../../models/form-component';
|
||||
import { AbstractFormItemProps } from './abstract-form-item';
|
||||
import { UseFormSetValue } from 'react-hook-form/dist/types/form';
|
||||
import { ArrayPath, FieldArray, Path, useFieldArray, useWatch } from 'react-hook-form';
|
||||
import { FormImageUpload } from './form-image-upload';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { Plus } from 'phosphor-react';
|
||||
import { ImageType } from '../../models/file';
|
||||
import { UnpackNestedValue } from 'react-hook-form/dist/types';
|
||||
import { FieldPathValue } from 'react-hook-form/dist/types/path';
|
||||
|
||||
interface FormMultiImageUploadProps<TFieldValues, TContext extends object> extends FormComponent<TFieldValues>, FormControlledComponent<TFieldValues, TContext>, AbstractFormItemProps<TFieldValues> {
|
||||
setValue: UseFormSetValue<TFieldValues>,
|
||||
addButtonLabel: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* This component allows to upload multiple images, in forms managed by react-hook-form.
|
||||
*/
|
||||
export const FormMultiImageUpload = <TFieldValues extends FieldValues, TContext extends object>({ id, className, register, control, setValue, formState, addButtonLabel }: FormMultiImageUploadProps<TFieldValues, TContext>) => {
|
||||
const { fields, append, remove } = useFieldArray({ control, name: id as ArrayPath<TFieldValues> });
|
||||
const output = useWatch({ control, name: id as Path<TFieldValues> });
|
||||
|
||||
/**
|
||||
* Add new image, set as main if it is the first
|
||||
*/
|
||||
const addImage = () => {
|
||||
append({
|
||||
is_main: output.filter(i => i.is_main).length === 0,
|
||||
_destroy: false
|
||||
} as UnpackNestedValue<FieldArray<TFieldValues, ArrayPath<TFieldValues>>>);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove an image and set the first image as the new main image if the provided was main
|
||||
*/
|
||||
const handleRemoveImage = (image: ImageType, index: number) => {
|
||||
return () => {
|
||||
if (image.is_main && output.length > 1) {
|
||||
setValue(
|
||||
`${id}.${index === 0 ? 1 : 0}.is_main` as Path<TFieldValues>,
|
||||
true as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
|
||||
);
|
||||
}
|
||||
if (typeof image.id === 'string') {
|
||||
remove(index);
|
||||
} else {
|
||||
setValue(
|
||||
`${id}.${index}._destroy` as Path<TFieldValues>,
|
||||
true as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the image at the given index as the new main image, and unset the current main image
|
||||
*/
|
||||
const handleSetMainImage = (index: number) => {
|
||||
return (setNewImageValue) => {
|
||||
const mainImageIndex = output.findIndex(i => i.is_main && i !== index);
|
||||
if (mainImageIndex > -1) {
|
||||
setValue(
|
||||
`${id}.${mainImageIndex}.is_main` as Path<TFieldValues>,
|
||||
false as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
|
||||
);
|
||||
}
|
||||
setNewImageValue(true);
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`form-multi-image-upload ${className || ''}`}>
|
||||
<div className="list">
|
||||
{fields.map((field: ImageType, index) => (
|
||||
<FormImageUpload key={field.id}
|
||||
defaultImage={field}
|
||||
id={`${id}.${index}`}
|
||||
accept="image/*"
|
||||
size="small"
|
||||
register={register}
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
formState={formState}
|
||||
className={field._destroy ? 'hidden' : ''}
|
||||
onFileRemove={handleRemoveImage(field, index)}
|
||||
onFileIsMain={handleSetMainImage(index)}
|
||||
mainOption
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<FabButton
|
||||
onClick={addImage}
|
||||
className='is-secondary'
|
||||
icon={<Plus size={24} />}>
|
||||
{addButtonLabel}
|
||||
</FabButton>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -44,12 +44,12 @@ const MachineCard: React.FC<MachineCardProps> = ({ user, machine, onShowMachine,
|
||||
* Return the machine's picture or a placeholder
|
||||
*/
|
||||
const machinePicture = (): ReactNode => {
|
||||
if (!machine.machine_image) {
|
||||
if (!machine.machine_image_attributes) {
|
||||
return <div className="machine-picture no-picture" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="machine-picture" style={{ backgroundImage: `url(${machine.machine_image}), url('/default-image.png')` }} onClick={handleShowMachine} />
|
||||
<div className="machine-picture" style={{ backgroundImage: `url(${machine.machine_image_attributes.attachment_url}), url('/default-image.png')` }} onClick={handleShowMachine} />
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { SubmitHandler, useForm, useWatch } from 'react-hook-form';
|
||||
import { Machine } from '../../models/machine';
|
||||
import MachineAPI from '../../api/machine';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FormInput } from '../form/form-input';
|
||||
import { FormImageUpload } from '../form/form-image-upload';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { Loader } from '../base/loader';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { ErrorBoundary } from '../base/error-boundary';
|
||||
import { FormRichText } from '../form/form-rich-text';
|
||||
import { FormSwitch } from '../form/form-switch';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
interface MachineFormProps {
|
||||
action: 'create' | 'update',
|
||||
machine?: Machine,
|
||||
onError: (message: string) => void,
|
||||
onSuccess: (message: string) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* Form to edit or create machines
|
||||
*/
|
||||
export const MachineForm: React.FC<MachineFormProps> = ({ action, machine, onError, onSuccess }) => {
|
||||
const { handleSubmit, register, control, setValue, formState } = useForm<Machine>({ defaultValues: { ...machine } });
|
||||
const output = useWatch<Machine>({ control });
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
/**
|
||||
* Callback triggered when the user validates the machine form: handle create or update
|
||||
*/
|
||||
const onSubmit: SubmitHandler<Machine> = (data: Machine) => {
|
||||
MachineAPI[action](data).then(() => {
|
||||
onSuccess(t(`app.admin.machine_form.${action}_success`));
|
||||
}).catch(error => {
|
||||
onError(error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="machine-form" onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormInput register={register} id="name"
|
||||
formState={formState}
|
||||
rules={{ required: true }}
|
||||
label={t('app.admin.machine_form.name')} />
|
||||
<FormImageUpload setValue={setValue}
|
||||
register={register}
|
||||
control={control}
|
||||
formState={formState}
|
||||
rules={{ required: true }}
|
||||
id="machine_image_attributes"
|
||||
accept="image/*"
|
||||
defaultImage={output.machine_image_attributes}
|
||||
label={t('app.admin.machine_form.illustration')} />
|
||||
<FormRichText control={control}
|
||||
id="description"
|
||||
rules={{ required: true }}
|
||||
label={t('app.admin.machine_form.description')}
|
||||
limit={null}
|
||||
heading bulletList blockquote link video image />
|
||||
<FormRichText control={control}
|
||||
id="spec"
|
||||
rules={{ required: true }}
|
||||
label={t('app.admin.machine_form.technical_specifications')}
|
||||
limit={null}
|
||||
heading bulletList blockquote link video image />
|
||||
|
||||
<FormSwitch control={control}
|
||||
id="disabled"
|
||||
label={t('app.admin.machine_form.disable_machine')}
|
||||
tooltip={t('app.admin.machine_form.disabled_help')} />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const MachineFormWrapper: React.FC<MachineFormProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<ErrorBoundary>
|
||||
<MachineForm {...props} />
|
||||
</ErrorBoundary>
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('machineForm', react2angular(MachineFormWrapper, ['action', 'machine', 'onError', 'onSuccess']));
|
@ -100,10 +100,10 @@ export const MachinesList: React.FC<MachinesListProps> = ({ onError, onSuccess,
|
||||
);
|
||||
};
|
||||
|
||||
const MachinesListWrapper: React.FC<MachinesListProps> = ({ user, onError, onSuccess, onShowMachine, onReserveMachine, onLoginRequested, onEnrollRequested, canProposePacks }) => {
|
||||
const MachinesListWrapper: React.FC<MachinesListProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<MachinesList user={user} onError={onError} onSuccess={onSuccess} onShowMachine={onShowMachine} onReserveMachine={onReserveMachine} onLoginRequested={onLoginRequested} onEnrollRequested={onEnrollRequested} canProposePacks={canProposePacks}/>
|
||||
<MachinesList {...props} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
@ -10,20 +10,19 @@ import { FormSwitch } from '../form/form-switch';
|
||||
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 { FabButton } from '../base/fab-button';
|
||||
import { FabAlert } from '../base/fab-alert';
|
||||
import ProductCategoryAPI from '../../api/product-category';
|
||||
import MachineAPI from '../../api/machine';
|
||||
import ProductAPI from '../../api/product';
|
||||
import { Plus } from 'phosphor-react';
|
||||
import { ProductStockForm } from './product-stock-form';
|
||||
import { CloneProductModal } from './clone-product-modal';
|
||||
import ProductLib from '../../lib/product';
|
||||
import { UnsavedFormAlert } from '../form/unsaved-form-alert';
|
||||
import { UIRouter } from '@uirouter/angularjs';
|
||||
import { SelectOption, ChecklistOption } from '../../models/select';
|
||||
import { FormMultiFileUpload } from '../form/form-multi-file-upload';
|
||||
import { FormMultiImageUpload } from '../form/form-multi-image-upload';
|
||||
|
||||
interface ProductFormProps {
|
||||
product: Product,
|
||||
@ -149,101 +148,6 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
|
||||
setOpenCloneModal(!openCloneModal);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add new product file
|
||||
*/
|
||||
const addProductFile = () => {
|
||||
setValue('product_files_attributes', output.product_files_attributes.concat({}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a product file
|
||||
*/
|
||||
const handleRemoveProductFile = (i: number) => {
|
||||
return () => {
|
||||
const productFile = output.product_files_attributes[i];
|
||||
if (!productFile.id) {
|
||||
output.product_files_attributes.splice(i, 1);
|
||||
setValue('product_files_attributes', output.product_files_attributes);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Add new product image
|
||||
*/
|
||||
const addProductImage = () => {
|
||||
setValue('product_images_attributes', output.product_images_attributes.concat({
|
||||
is_main: output.product_images_attributes.filter(i => i.is_main).length === 0
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a product image
|
||||
*/
|
||||
const handleRemoveProductImage = (i: number) => {
|
||||
return () => {
|
||||
const productImage = output.product_images_attributes[i];
|
||||
if (!productImage.id) {
|
||||
output.product_images_attributes.splice(i, 1);
|
||||
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,
|
||||
is_main: i === k ? false : image.is_main,
|
||||
_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
|
||||
};
|
||||
}));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<header>
|
||||
@ -336,31 +240,12 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
|
||||
<FabAlert level="warning">
|
||||
<HtmlTranslate trKey="app.admin.store.product_form.product_images_info" />
|
||||
</FabAlert>
|
||||
<div className="product-images">
|
||||
<div className="list">
|
||||
{output.product_images_attributes.map((image, i) => (
|
||||
<FormImageUpload key={i}
|
||||
defaultImage={image}
|
||||
id={`product_images_attributes[${i}]`}
|
||||
accept="image/*"
|
||||
size="small"
|
||||
<FormMultiImageUpload setValue={setValue}
|
||||
addButtonLabel={t('app.admin.store.product_form.add_product_image')}
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
formState={formState}
|
||||
className={image._destroy ? 'hidden' : ''}
|
||||
mainOption={true}
|
||||
onFileRemove={handleRemoveProductImage(i)}
|
||||
onFileIsMain={handleSetMainImage(i)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<FabButton
|
||||
onClick={addProductImage}
|
||||
className='is-secondary'
|
||||
icon={<Plus size={24} />}>
|
||||
{t('app.admin.store.product_form.add_product_image')}
|
||||
</FabButton>
|
||||
</div>
|
||||
control={control}
|
||||
id="product_images_attributes"
|
||||
className="product-images" />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
@ -413,27 +298,13 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
|
||||
<FabAlert level="warning">
|
||||
<HtmlTranslate trKey="app.admin.store.product_form.product_files_info" />
|
||||
</FabAlert>
|
||||
<div className="product-documents">
|
||||
<div className="list">
|
||||
{output.product_files_attributes.map((file, i) => (
|
||||
<FormFileUpload key={i}
|
||||
defaultFile={file}
|
||||
id={`product_files_attributes[${i}]`}
|
||||
accept="application/pdf"
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
formState={formState}
|
||||
className={file._destroy ? 'hidden' : ''}
|
||||
onFileRemove={handleRemoveProductFile(i)}/>
|
||||
))}
|
||||
</div>
|
||||
<FabButton
|
||||
onClick={addProductFile}
|
||||
className='is-secondary'
|
||||
icon={<Plus size={24} />}>
|
||||
{t('app.admin.store.product_form.add_product_file')}
|
||||
</FabButton>
|
||||
</div>
|
||||
<FormMultiFileUpload setValue={setValue}
|
||||
addButtonLabel={t('app.admin.store.product_form.add_product_file')}
|
||||
control={control}
|
||||
accept="application/pdf"
|
||||
register={register}
|
||||
id="product_files_attributes"
|
||||
className="product-documents" />
|
||||
</div>
|
||||
|
||||
<div className="main-actions">
|
||||
|
@ -263,7 +263,21 @@ Application.Controllers.controller('EditMachineController', ['$scope', '$state',
|
||||
$scope.method = 'put';
|
||||
|
||||
// Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list
|
||||
$scope.machine = machinePromise;
|
||||
$scope.machine = cleanMachine(machinePromise);
|
||||
|
||||
/**
|
||||
* Shows an error message forwarded from a child component
|
||||
*/
|
||||
$scope.onError = function (message) {
|
||||
growl.error(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a success message forwarded from a child react components
|
||||
*/
|
||||
$scope.onSuccess = function (message) {
|
||||
growl.success(message)
|
||||
}
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
@ -277,6 +291,13 @@ Application.Controllers.controller('EditMachineController', ['$scope', '$state',
|
||||
return new MachinesController($scope, $state);
|
||||
};
|
||||
|
||||
// prepare the machine for the react-hook-form
|
||||
function cleanMachine (machine) {
|
||||
delete machine.$promise;
|
||||
delete machine.$resolved;
|
||||
return machine;
|
||||
}
|
||||
|
||||
// !!! MUST BE CALLED AT THE END of the controller
|
||||
return initialize();
|
||||
}
|
||||
|
@ -9,12 +9,12 @@ export default class FileUploadLib {
|
||||
public static onRemoveFile<TFieldValues extends FieldValues> (file: FileType, id: string, setFile: Dispatch<SetStateAction<FileType>>, setValue: UseFormSetValue<TFieldValues>, onFileRemove: () => void) {
|
||||
if (file?.id) {
|
||||
setValue(
|
||||
`${id}[_destroy]` as Path<TFieldValues>,
|
||||
`${id}._destroy` as Path<TFieldValues>,
|
||||
true as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
|
||||
);
|
||||
}
|
||||
setValue(
|
||||
`${id}[attachment_files]` as Path<TFieldValues>,
|
||||
`${id}.attachment_files` as Path<TFieldValues>,
|
||||
null as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
|
||||
);
|
||||
setFile(null);
|
||||
@ -24,6 +24,6 @@ export default class FileUploadLib {
|
||||
}
|
||||
|
||||
public static hasFile (file: FileType): boolean {
|
||||
return !!file?.attachment_name;
|
||||
return file?.attachment_name && !file?._destroy;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
export interface FileType {
|
||||
id?: number,
|
||||
id?: number|string,
|
||||
attachment_name?: string,
|
||||
attachment_url?: string
|
||||
attachment_url?: string,
|
||||
_destroy?: boolean
|
||||
}
|
||||
|
||||
export interface ImageType extends FileType {
|
||||
|
@ -1,23 +1,20 @@
|
||||
import { Reservation } from './reservation';
|
||||
import { ApiFilter } from './api';
|
||||
import { FileType } from './file';
|
||||
|
||||
export interface MachineIndexFilter extends ApiFilter {
|
||||
disabled: boolean,
|
||||
}
|
||||
|
||||
export interface Machine {
|
||||
id: number,
|
||||
id?: number,
|
||||
name: string,
|
||||
description?: string,
|
||||
spec?: string,
|
||||
disabled: boolean,
|
||||
slug: string,
|
||||
machine_image: string,
|
||||
machine_files_attributes?: Array<{
|
||||
id: number,
|
||||
attachment: string,
|
||||
attachment_url: string
|
||||
}>,
|
||||
machine_image_attributes: FileType,
|
||||
machine_files_attributes?: Array<FileType>,
|
||||
trainings?: Array<{
|
||||
id: number,
|
||||
name: string,
|
||||
|
@ -73,8 +73,8 @@ export const stockMovementAllReasons = [...stockMovementInReasons, ...stockMovem
|
||||
export type StockMovementReason = typeof stockMovementAllReasons[number];
|
||||
|
||||
export interface Stock {
|
||||
internal: number,
|
||||
external: number,
|
||||
internal?: number,
|
||||
external?: number,
|
||||
}
|
||||
|
||||
export type ProductsIndex = PaginatedIndex<Product>;
|
||||
@ -99,21 +99,21 @@ export interface StockMovementIndexFilter extends ApiFilter {
|
||||
|
||||
export interface Product {
|
||||
id?: number,
|
||||
name: string,
|
||||
slug: string,
|
||||
name?: string,
|
||||
slug?: string,
|
||||
sku?: string,
|
||||
description?: string,
|
||||
is_active: boolean,
|
||||
is_active?: boolean,
|
||||
product_category_id?: number,
|
||||
is_active_price?: boolean,
|
||||
amount?: number,
|
||||
quantity_min?: number,
|
||||
stock: Stock,
|
||||
low_stock_alert: boolean,
|
||||
stock?: Stock,
|
||||
low_stock_alert?: boolean,
|
||||
low_stock_threshold?: number,
|
||||
machine_ids: number[],
|
||||
machine_ids?: number[],
|
||||
created_at?: TDateISO,
|
||||
product_files_attributes: Array<{
|
||||
product_files_attributes?: Array<{
|
||||
id?: number,
|
||||
attachment?: File,
|
||||
attachment_files?: FileList,
|
||||
@ -121,7 +121,7 @@ export interface Product {
|
||||
attachment_url?: string,
|
||||
_destroy?: boolean
|
||||
}>,
|
||||
product_images_attributes: Array<{
|
||||
product_images_attributes?: Array<{
|
||||
id?: number,
|
||||
attachment?: File,
|
||||
attachment_files?: FileList,
|
||||
|
@ -41,6 +41,8 @@
|
||||
@import "modules/events/event";
|
||||
@import "modules/form/abstract-form-item";
|
||||
@import "modules/form/form-input";
|
||||
@import "modules/form/form-multi-file-upload";
|
||||
@import "modules/form/form-multi-image-upload";
|
||||
@import "modules/form/form-rich-text";
|
||||
@import "modules/form/form-select";
|
||||
@import "modules/form/form-switch";
|
||||
|
@ -5,6 +5,7 @@
|
||||
&-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.8rem;
|
||||
|
@ -14,6 +14,12 @@
|
||||
@include base;
|
||||
}
|
||||
|
||||
&.with-label {
|
||||
margin-top: 2.6rem;
|
||||
position: relative;
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
.image {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
@ -51,5 +57,11 @@
|
||||
.image-file-input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.form-item-header {
|
||||
position: absolute;
|
||||
top: -1.5em;
|
||||
left: 0;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
.form-multi-file-upload {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.list {
|
||||
margin-bottom: 2.4rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(440px, 1fr));
|
||||
gap: 2.4rem;
|
||||
}
|
||||
button { margin-left: auto; }
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
.form-multi-image-upload {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.list {
|
||||
margin-bottom: 2.4rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(440px, 1fr));
|
||||
gap: 2.4rem;
|
||||
}
|
||||
button { margin-left: auto; }
|
||||
}
|
@ -13,6 +13,7 @@
|
||||
|
||||
.form-item-field {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 16px;
|
||||
background-color: white;
|
||||
& > *:not(:first-child) {
|
||||
|
@ -50,7 +50,7 @@
|
||||
flex: 1 1 320px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.header-switch {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -65,18 +65,4 @@
|
||||
gap: 0 3.2rem;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.product-images,
|
||||
.product-documents {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.list {
|
||||
margin-bottom: 2.4rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(440px, 1fr));
|
||||
gap: 2.4rem;
|
||||
}
|
||||
button { margin-left: auto; }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,11 @@
|
||||
|
||||
<div class="row no-gutter">
|
||||
<div class="col-sm-12 col-md-12 col-lg-9 b-r-lg nopadding">
|
||||
<!-- <div class="panel panel-default bg-light m-lg">-->
|
||||
<!-- <div class="panel-body m-r">-->
|
||||
<!-- <machine-form action="'update'" machine="machine" on-error="onError" on-success="onSuccess"></machine-form>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<ng-include src="'/machines/_form.html'"></ng-include>
|
||||
</div>
|
||||
</div>
|
||||
|
11
app/views/api/machines/_machine.json.jbuilder
Normal file
11
app/views/api/machines/_machine.json.jbuilder
Normal file
@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! machine, :id, :name, :slug, :disabled
|
||||
|
||||
if machine.machine_image
|
||||
json.machine_image_attributes do
|
||||
json.id machine.machine_image.id
|
||||
json.attachment_name machine.machine_image.attachment_identifier
|
||||
json.attachment_url machine.machine_image.attachment.url
|
||||
end
|
||||
end
|
@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.array!(@machines) do |machine|
|
||||
json.extract! machine, :id, :name, :slug, :disabled
|
||||
|
||||
json.machine_image machine.machine_image.attachment.medium.url if machine.machine_image
|
||||
json.partial! 'api/machines/machine', machine: machine
|
||||
end
|
||||
|
@ -1,10 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! @machine, :id, :name, :description, :spec, :disabled, :slug
|
||||
json.machine_image @machine.machine_image.attachment.large.url if @machine.machine_image
|
||||
json.partial! 'api/machines/machine', machine: @machine
|
||||
json.extract! @machine, :description, :spec
|
||||
|
||||
json.machine_files_attributes @machine.machine_files do |f|
|
||||
json.id f.id
|
||||
json.attachment f.attachment_identifier
|
||||
json.attachment_name f.attachment_identifier
|
||||
json.attachment_url f.attachment_url
|
||||
end
|
||||
json.trainings @machine.trainings.each, :id, :name, :disabled
|
||||
|
@ -1,6 +1,18 @@
|
||||
en:
|
||||
app:
|
||||
admin:
|
||||
machine_form:
|
||||
name: "Name"
|
||||
illustration: "Visual"
|
||||
add_an_illustration: "Add a visual"
|
||||
description: "Description"
|
||||
technical_specifications: "Technical specifications"
|
||||
attached_files_pdf: "Attached files (pdf)"
|
||||
attach_a_file: "Attach a file"
|
||||
add_an_attachment: "Add an attachment"
|
||||
disable_machine: "Disable machine"
|
||||
disabled_help: "When disabled, the machine won't be reservable and won't appear by default in the machine list."
|
||||
validate_your_machine: "Validate your machine"
|
||||
#add a new machine
|
||||
machines_new:
|
||||
declare_a_new_machine: "Declare a new machine"
|
||||
|
Loading…
x
Reference in New Issue
Block a user