1
0
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:
Sylvain 2022-10-28 17:13:41 +02:00 committed by Sylvain
parent a97e08b43b
commit 20e50bda13
26 changed files with 402 additions and 222 deletions

View File

@ -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} />

View File

@ -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') {

View File

@ -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'
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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} />
);
};

View File

@ -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']));

View File

@ -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>
);
};

View File

@ -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">

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -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,

View File

@ -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,

View File

@ -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";

View File

@ -5,6 +5,7 @@
&-header {
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.8rem;

View File

@ -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;
}
}
}

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -13,6 +13,7 @@
.form-item-field {
display: flex;
flex-wrap: wrap;
padding: 16px;
background-color: white;
& > *:not(:first-child) {

View File

@ -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; }
}
}
}

View File

@ -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>

View 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

View File

@ -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

View File

@ -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

View File

@ -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"