1
0
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:
Sylvain 2023-03-09 15:47:12 +01:00
parent 0f142680b8
commit 2b8a7008bd
9 changed files with 131 additions and 126 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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