mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-20 14:54:15 +01:00
(quality) use a single component for unsaved list
This commit is contained in:
parent
0f142680b8
commit
2b8a7008bd
@ -2,7 +2,7 @@ import { FieldArrayWithId, UseFieldArrayRemove } from 'react-hook-form/dist/type
|
||||
import { UseFormRegister } from 'react-hook-form';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactNode } from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { X } from 'phosphor-react';
|
||||
import { FormInput } from './form-input';
|
||||
import { FieldArrayPath } from 'react-hook-form/dist/types/path';
|
||||
@ -14,15 +14,18 @@ interface FormUnsavedListProps<TFieldValues, TFieldArrayName extends FieldArrayP
|
||||
className?: string,
|
||||
title: string,
|
||||
shouldRenderField?: (field: FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>) => boolean,
|
||||
formAttributeName: string,
|
||||
renderFieldAttribute: (field: FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>, attribute: string) => ReactNode,
|
||||
renderField: (field: FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>) => ReactNode,
|
||||
formAttributeName: `${string}_attributes`,
|
||||
formAttributes: Array<keyof FieldArrayWithId<TFieldValues, TFieldArrayName>>,
|
||||
saveReminderLabel?: string | ReactNode,
|
||||
cancelLabel?: string | ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* This component render a list of unsaved attributes, created elsewhere than in the form (e.g. in a modal dialog)
|
||||
* and pending for the form to be saved.
|
||||
*/
|
||||
export const FormUnsavedList = <TFieldValues extends FieldValues = FieldValues, TFieldArrayName extends FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>, TKeyName extends string = 'id'>({ fields, remove, register, className, title, shouldRenderField, formAttributeName, renderFieldAttribute }: FormUnsavedListProps<TFieldValues, TFieldArrayName, TKeyName>) => {
|
||||
export const FormUnsavedList = <TFieldValues extends FieldValues = FieldValues, TFieldArrayName extends FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>, TKeyName extends string = 'id'>({ fields, remove, register, className, title, shouldRenderField = () => true, renderField, formAttributeName, formAttributes, saveReminderLabel, cancelLabel }: FormUnsavedListProps<TFieldValues, TFieldArrayName, TKeyName>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
/**
|
||||
@ -31,25 +34,26 @@ export const FormUnsavedList = <TFieldValues extends FieldValues = FieldValues,
|
||||
const renderUnsavedField = (field: FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>, index: number): ReactNode => {
|
||||
return (
|
||||
<div key={index} className="unsaved-field">
|
||||
{Object.keys(field).map(attribute => (
|
||||
<div className="grp" key={index}>
|
||||
{renderFieldAttribute(field, attribute)}
|
||||
<FormInput id={`${formAttributeName}.${index}.${attribute}`} register={register} type="hidden" />
|
||||
</div>
|
||||
))}
|
||||
{renderField(field)}
|
||||
<p className="cancel-action" onClick={() => remove(index)}>
|
||||
{t('app.shared.form_unsaved_list.cancel')}
|
||||
{cancelLabel || t('app.shared.form_unsaved_list.cancel')}
|
||||
<X size={20} />
|
||||
</p>
|
||||
{formAttributes.map((attribute, attrIndex) => (
|
||||
<FormInput key={attrIndex} id={`${formAttributeName}.${index}.${attribute}`} register={register} type="hidden" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (fields.filter(shouldRenderField).length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className={`form-unsaved-list ${className || ''}`}>
|
||||
<span className="title">{title}</span>
|
||||
<span className="save-notice">{t('app.shared.form_unsaved_list.save_reminder')}</span>
|
||||
<span className="save-notice">{saveReminderLabel || t('app.shared.form_unsaved_list.save_reminder')}</span>
|
||||
{fields.map((field, index) => {
|
||||
if (typeof shouldRenderField === 'function' && !shouldRenderField(field)) return false;
|
||||
if (!shouldRenderField(field)) return false;
|
||||
return renderUnsavedField(field, index);
|
||||
}).filter(Boolean)}
|
||||
</div>
|
||||
|
@ -3,11 +3,10 @@ import { Control, FormState } from 'react-hook-form/dist/types/form';
|
||||
import { FormSwitch } from '../form/form-switch';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { PencilSimple, Trash, X } from 'phosphor-react';
|
||||
import { PencilSimple, Trash } from 'phosphor-react';
|
||||
import { PlanLimitModal } from './plan-limit-modal';
|
||||
import { Plan, PlanLimitation } from '../../models/plan';
|
||||
import { useFieldArray, UseFormRegister } from 'react-hook-form';
|
||||
import { FormInput } from '../form/form-input';
|
||||
import { Machine } from '../../models/machine';
|
||||
import { MachineCategory } from '../../models/machine-category';
|
||||
import MachineAPI from '../../api/machine';
|
||||
@ -56,34 +55,24 @@ export const PlanLimitForm = <TContext extends object> ({ register, control, for
|
||||
};
|
||||
|
||||
/**
|
||||
* Render an attribute of an unsaved limitation of use
|
||||
* Render an unsaved limitation of use
|
||||
*/
|
||||
const renderOngoingLimitAttribute = (limit: PlanLimitation, attribute: string): ReactNode => {
|
||||
switch (attribute) {
|
||||
case 'limitable_id':
|
||||
if (limit.limitable_type === 'MachineCategory') {
|
||||
return (
|
||||
<>
|
||||
<span>{t('app.admin.plan_limit_form.category')}</span>
|
||||
<p>{categories?.find(c => c.id === limit.limitable_id)?.name}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<span>{t('app.admin.plan_limit_form.machine')}</span>
|
||||
<p>{machines?.find(m => m.id === limit.limitable_id)?.name}</p>
|
||||
</>
|
||||
);
|
||||
case 'limit':
|
||||
return (
|
||||
<>
|
||||
<span>{t('app.admin.plan_limit_form.max_hours_per_day')}</span>
|
||||
<p>{limit.limit}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
const renderOngoingLimit = (limit: PlanLimitation): ReactNode => (
|
||||
<>
|
||||
{(limit.limitable_type === 'MachineCategory' && <div className="group">
|
||||
<span>{t('app.admin.plan_limit_form.category')}</span>
|
||||
<p>{categories?.find(c => c.id === limit.limitable_id)?.name}</p>
|
||||
</div>) ||
|
||||
<div className="group">
|
||||
<span>{t('app.admin.plan_limit_form.machine')}</span>
|
||||
<p>{machines?.find(m => m.id === limit.limitable_id)?.name}</p>
|
||||
</div>}
|
||||
<div className="group">
|
||||
<span>{t('app.admin.plan_limit_form.max_hours_per_day')}</span>
|
||||
<p>{limit.limit}</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="plan-limit-form">
|
||||
@ -142,10 +131,12 @@ export const PlanLimitForm = <TContext extends object> ({ register, control, for
|
||||
<FormUnsavedList fields={fields}
|
||||
remove={remove}
|
||||
register={register}
|
||||
title={t('app.admin.plan_limit_form.ongoing_limit')}
|
||||
title={t('app.admin.plan_limit_form.ongoing_limitations')}
|
||||
shouldRenderField={(limit: PlanLimitation) => limit.limitable_type === 'MachineCategory'}
|
||||
formAttributeName="plan_limitations_attributes"
|
||||
renderFieldAttribute={renderOngoingLimitAttribute} />
|
||||
formAttributes={['limitable_id', 'limit']}
|
||||
renderField={renderOngoingLimit}
|
||||
cancelLabel={t('app.admin.plan_limit_form.cancel')} />
|
||||
|
||||
<div className='plan-limit-list'>
|
||||
<p className="title">{t('app.admin.plan_limit_form.by_machine')}</p>
|
||||
@ -181,7 +172,10 @@ export const PlanLimitForm = <TContext extends object> ({ register, control, for
|
||||
title={t('app.admin.plan_limit_form.ongoing_limit')}
|
||||
shouldRenderField={(limit: PlanLimitation) => limit.limitable_type === 'Machine'}
|
||||
formAttributeName="plan_limitations_attributes"
|
||||
renderFieldAttribute={renderOngoingLimitAttribute} />
|
||||
formAttributes={['limitable_id', 'limit']}
|
||||
renderField={renderOngoingLimit}
|
||||
saveReminderLabel={t('app.admin.plan_limit_form.save_reminder')}
|
||||
cancelLabel={t('app.admin.plan_limit_form.cancel')} />
|
||||
|
||||
<PlanLimitModal isOpen={isOpen}
|
||||
machines={machines}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import Select from 'react-select';
|
||||
import { PencilSimple, X } from 'phosphor-react';
|
||||
import { PencilSimple } from 'phosphor-react';
|
||||
import { useFieldArray, UseFormRegister } from 'react-hook-form';
|
||||
import { Control, FormState, UseFormSetValue } from 'react-hook-form/dist/types/form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Product,
|
||||
Product, ProductStockMovement,
|
||||
stockMovementAllReasons, StockMovementIndex, StockMovementIndexFilter,
|
||||
StockMovementReason,
|
||||
StockType
|
||||
@ -20,6 +20,7 @@ import FormatLib from '../../lib/format';
|
||||
import ProductLib from '../../lib/product';
|
||||
import { useImmer } from 'use-immer';
|
||||
import { FabPagination } from '../base/fab-pagination';
|
||||
import { FormUnsavedList } from '../form/form-unsaved-list';
|
||||
|
||||
interface ProductStockFormProps<TContext extends object> {
|
||||
currentFormValues: Product,
|
||||
@ -159,6 +160,25 @@ export const ProductStockForm = <TContext extends object> ({ currentFormValues,
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Render an attribute of an unsaved stock movement
|
||||
*/
|
||||
const renderOngoingStockMovement = (movement: ProductStockMovement): ReactNode => (
|
||||
<>
|
||||
<div className="group">
|
||||
<p>{t(`app.admin.store.product_stock_form.type_${ProductLib.stockMovementType(movement.reason)}`)}</p>
|
||||
</div>
|
||||
<div className="group">
|
||||
<span>{t(`app.admin.store.product_stock_form.${movement.stock_type}`)}</span>
|
||||
<p>{ProductLib.absoluteStockMovement(movement.quantity, movement.reason)}</p>
|
||||
</div>
|
||||
<div className="group">
|
||||
<span>{t('app.admin.store.product_stock_form.reason')}</span>
|
||||
<p>{t(ProductLib.stockMovementReasonTrKey(movement.reason))}</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='product-stock-form'>
|
||||
<h4>{t('app.admin.store.product_stock_form.stock_up_to_date')}
|
||||
@ -181,33 +201,16 @@ export const ProductStockForm = <TContext extends object> ({ currentFormValues,
|
||||
<FabButton onClick={toggleModal} icon={<PencilSimple size={20} weight="fill" />} className="is-black">{t('app.admin.store.product_stock_form.edit')}</FabButton>
|
||||
</div>
|
||||
|
||||
{fields.length > 0 && <div className="ongoing-stocks">
|
||||
<span className="title">{t('app.admin.store.product_stock_form.ongoing_operations')}</span>
|
||||
<span className="save-notice">{t('app.admin.store.product_stock_form.save_reminder')}</span>
|
||||
{fields.map((newMovement, index) => (
|
||||
<div key={index} className="unsaved-stock-movement stock-item">
|
||||
<div className="group">
|
||||
<p>{t(`app.admin.store.product_stock_form.type_${ProductLib.stockMovementType(newMovement.reason)}`)}</p>
|
||||
</div>
|
||||
<div className="group">
|
||||
<span>{t(`app.admin.store.product_stock_form.${newMovement.stock_type}`)}</span>
|
||||
<p>{ProductLib.absoluteStockMovement(newMovement.quantity, newMovement.reason)}</p>
|
||||
</div>
|
||||
<div className="group">
|
||||
<span>{t('app.admin.store.product_stock_form.reason')}</span>
|
||||
<p>{t(ProductLib.stockMovementReasonTrKey(newMovement.reason))}</p>
|
||||
</div>
|
||||
<p className="cancel-action" onClick={() => remove(index)}>
|
||||
{t('app.admin.store.product_stock_form.cancel')}
|
||||
<X size={20} />
|
||||
</p>
|
||||
<FormInput id={`product_stock_movements_attributes.${index}.stock_type`} register={register}
|
||||
type="hidden" />
|
||||
<FormInput id={`product_stock_movements_attributes.${index}.quantity`} register={register} type="hidden" />
|
||||
<FormInput id={`product_stock_movements_attributes.${index}.reason`} register={register} type="hidden" />
|
||||
</div>
|
||||
))}
|
||||
</div>}
|
||||
<FormUnsavedList fields={fields}
|
||||
className="ongoing-stocks"
|
||||
remove={remove}
|
||||
register={register}
|
||||
title={t('app.admin.store.product_stock_form.ongoing_operations')}
|
||||
formAttributeName="product_stock_movements_attributes"
|
||||
formAttributes={['stock_type', 'quantity', 'reason']}
|
||||
renderField={renderOngoingStockMovement}
|
||||
saveReminderLabel={t('app.admin.store.product_stock_form.save_reminder')}
|
||||
cancelLabel={t('app.admin.store.product_stock_form.cancel')} />
|
||||
|
||||
<hr />
|
||||
|
||||
|
@ -62,6 +62,7 @@
|
||||
@import "modules/form/form-checklist";
|
||||
@import "modules/form/form-file-upload";
|
||||
@import "modules/form/form-image-upload";
|
||||
@import "modules/form/form-unsaved-list";
|
||||
@import "modules/group/change-group";
|
||||
@import "modules/invoices/invoices-settings-panel";
|
||||
@import "modules/invoices/vat-settings-modal";
|
||||
|
@ -0,0 +1,47 @@
|
||||
.form-unsaved-list {
|
||||
.save-notice {
|
||||
@include text-xs;
|
||||
margin-left: 1rem;
|
||||
color: var(--alert);
|
||||
}
|
||||
.unsaved-field {
|
||||
background-color: var(--gray-soft-light);
|
||||
border: 0;
|
||||
padding: 1.2rem;
|
||||
margin-top: 1rem;width: 100%;
|
||||
display: flex;
|
||||
gap: 4.8rem;
|
||||
justify-items: flex-start;
|
||||
align-items: center;
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
& > * { flex: 1 1 45%; }
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
@include text-base;
|
||||
}
|
||||
.title {
|
||||
@include text-base(600);
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
.group {
|
||||
span {
|
||||
@include text-xs;
|
||||
color: var(--gray-hard-light);
|
||||
}
|
||||
p { @include text-base(600); }
|
||||
}
|
||||
|
||||
.cancel-action {
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
svg {
|
||||
margin-left: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,32 +5,6 @@
|
||||
|
||||
section { @include layout-settings; }
|
||||
|
||||
.ongoing-limits {
|
||||
margin: 2.4rem 0;
|
||||
.save-notice {
|
||||
@include text-xs;
|
||||
margin-left: 1rem;
|
||||
color: var(--alert);
|
||||
}
|
||||
.unsaved-plan-limit {
|
||||
background-color: var(--gray-soft-light);
|
||||
border: 0;
|
||||
padding: 1.2rem;
|
||||
margin-top: 1rem;
|
||||
|
||||
.cancel-action {
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
svg {
|
||||
margin-left: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plan-limit-grp {
|
||||
header {
|
||||
@include header();
|
||||
@ -39,9 +13,11 @@
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.form-unsaved-list {
|
||||
margin-bottom: 6.4rem;
|
||||
}
|
||||
.plan-limit-list {
|
||||
max-height: 65vh;
|
||||
margin-bottom: 6.4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
|
@ -3,28 +3,6 @@
|
||||
|
||||
.ongoing-stocks {
|
||||
margin: 2.4rem 0;
|
||||
.save-notice {
|
||||
@include text-xs;
|
||||
margin-left: 1rem;
|
||||
color: var(--alert);
|
||||
}
|
||||
.unsaved-stock-movement {
|
||||
background-color: var(--gray-soft-light);
|
||||
border: 0;
|
||||
padding: 1.2rem;
|
||||
margin-top: 1rem;
|
||||
|
||||
.cancel-action {
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
svg {
|
||||
margin-left: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.store-list {
|
||||
margin-top: 2.4rem;
|
||||
|
@ -206,8 +206,7 @@ en:
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
max_hours_per_day: "Max. hours/day"
|
||||
ongoing_limit: "Ongoing settings"
|
||||
save_reminder: "Don't forget to save your settings"
|
||||
ongoing_limitations: "Ongoing limitations"
|
||||
cancel: "Cancel this limitation"
|
||||
plan_limit_modal:
|
||||
title: "Manage limitation of use"
|
||||
|
@ -540,3 +540,6 @@ en:
|
||||
show_reserved_uniq: "Show only slots with reservations"
|
||||
machine:
|
||||
machine_uncategorized: "Uncategorized machines"
|
||||
form_unsaved_list:
|
||||
save_reminder: "Do not forget to save your changes"
|
||||
cancel: "Cancel"
|
||||
|
Loading…
x
Reference in New Issue
Block a user