Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LF-4378 Readonly/Edit animal details framework - add form framework for animal details #3489

Draft
wants to merge 8 commits into
base: integration
Choose a base branch
from
3 changes: 3 additions & 0 deletions packages/webapp/src/components/Animals/Inventory/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const PureAnimalInventory = ({
onSelectInventory,
handleSelectAllClick,
selectedIds,
onRowClick,
totalInventoryCount,
isFilterActive,
clearFilters,
Expand All @@ -67,6 +68,7 @@ const PureAnimalInventory = ({
searchProps: SearchProps;
onSelectInventory: (event: ChangeEvent<HTMLInputElement>, row: AnimalInventory) => void;
handleSelectAllClick: (event: ChangeEvent<HTMLInputElement>) => void;
onRowClick: (event: ChangeEvent, row: AnimalInventory) => void;
selectedIds: string[];
totalInventoryCount: number;
isFilterActive: boolean;
Expand Down Expand Up @@ -139,6 +141,7 @@ const PureAnimalInventory = ({
maxHeight={tableMaxHeight}
spacerRowHeight={isDesktop ? 96 : 120}
headerClass={styles.headerClass}
onRowClick={onRowClick}
/>
) : (
<NoSearchResults
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { ClickAwayListener } from '@mui/material';
import Layout from '../../Layout';
import MultiStepPageTitle from '../../PageTitle/MultiStepPageTitle';

interface WithPageTitleProps {
interface MultiStepWithPageTitleProps {
children: ReactNode;
steps: { formContent: ReactNode; title: string }[];
activeStepIndex: number;
Expand All @@ -27,14 +27,14 @@ interface WithPageTitleProps {
onCancel: () => void;
}

export const WithPageTitle = ({
export const MultiStepWithPageTitle = ({
children,
steps,
activeStepIndex,
cancelModalTitle,
onGoBack,
onCancel,
}: WithPageTitleProps) => {
}: MultiStepWithPageTitleProps) => {
const [showConfirmCancelModal, setShowConfirmCancelModal] = useState(false);

const progressBarValue = useMemo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import FixedHeaderContainer from '../../Animals/FixedHeaderContainer';
import CancelFlowModal from '../../Modals/CancelFlowModal';
import styles from './styles.module.scss';

interface WithStepperProgressBarProps {
interface MultiStepWithStepperProgressBarProps {
children: ReactNode;
history: History;
steps: { formContent: ReactNode; title: string }[];
Expand All @@ -49,7 +49,7 @@ interface WithStepperProgressBarProps {
setFormResultData: (data: any) => void;
}

export const WithStepperProgressBar = ({
export const MultiStepWithStepperProgressBar = ({
children,
history,
steps,
Expand All @@ -66,7 +66,7 @@ export const WithStepperProgressBar = ({
handleSubmit,
formState: { isValid, isDirty },
setFormResultData,
}: WithStepperProgressBarProps) => {
}: MultiStepWithStepperProgressBarProps) => {
const [transition, setTransition] = useState<{ unblock?: () => void; retry?: () => void }>({
unblock: undefined,
retry: undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Copyright 2024 LiteFarm.org
* This file is part of LiteFarm.
*
* LiteFarm is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LiteFarm is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
*/

import { ReactNode, useEffect, useState } from 'react';
import { UseFormHandleSubmit, FieldValues, FormState } from 'react-hook-form';
import { History } from 'history';
import FloatingContainer from '../../FloatingContainer';
import FormNavigationButtons from '../FormNavigationButtons';
import CancelFlowModal from '../../Modals/CancelFlowModal';
import styles from './styles.module.scss';

interface WithReadonlyEditProps {
children: ReactNode;
history: History;
steps: { formContent: ReactNode; title: string }[];
activeStepIndex: number;
cancelModalTitle: string;
isCompactSideMenu: boolean;
hasSummaryWithinForm: boolean;
onSave: (
data: FieldValues,
onGoForward: () => void,
setFormResultData?: (data: any) => void,
) => void;
onGoBack: () => void;
onCancel: () => void;
onGoForward: () => void;
formState: FormState<FieldValues>;
handleSubmit: UseFormHandleSubmit<FieldValues>;
setFormResultData: (data: any) => void;
isEditing: boolean;
setIsEditing: React.Dispatch<React.SetStateAction<boolean>>;
checkIsFormDirty: boolean;
setCheckIsFormDirty: React.Dispatch<React.SetStateAction<boolean>>;
}

export const WithReadonlyEdit = ({
children,
history,
steps,
activeStepIndex,
cancelModalTitle,
isCompactSideMenu,
hasSummaryWithinForm,
onSave,
onGoBack,
onCancel,
onGoForward,
handleSubmit,
formState: { isValid, isDirty },
setFormResultData,
isEditing,
setIsEditing,
checkIsFormDirty,
setCheckIsFormDirty,
}: WithReadonlyEditProps) => {
const [transition, setTransition] = useState<{ unblock?: () => void; retry?: () => void }>({
unblock: undefined,
retry: undefined,
});

const isSummaryPage = hasSummaryWithinForm && activeStepIndex === steps.length - 1;

// Block the page transition
// https://github.com/remix-run/history/blob/dev/docs/blocking-transitions.md
useEffect(() => {
if (isSummaryPage || !isDirty) {
return;
}
const unblock = history.block((tx) => {
setTransition({ unblock, retry: tx.retry });
});

return () => unblock();
}, [isSummaryPage, isDirty, history]);

// Also manage the confirmation modal manually
const [showConfirmationModal, setShowConfirmationModal] = useState(false);

useEffect(() => {
if (isEditing && isDirty && checkIsFormDirty) {
setShowConfirmationModal(true);
} else if (isEditing && !isDirty && checkIsFormDirty) {
setIsEditing(false);
setCheckIsFormDirty(false);
}
}, [checkIsFormDirty]);

const isFinalStep =
(!hasSummaryWithinForm && activeStepIndex === steps.length - 1) ||
(hasSummaryWithinForm && activeStepIndex === steps.length - 2);

const onContinue = () => {
if (isFinalStep) {
handleSubmit((data: FieldValues) => onSave(data, onGoForward, setFormResultData))();
return;
}
onGoForward();
};

const handleDismissModal = () => {
setTransition({ unblock: undefined, retry: undefined });
setShowConfirmationModal(false);
setCheckIsFormDirty(false); // to re-trigger check when cancelling again
};

const handleCancel = () => {
try {
transition.unblock?.();
transition.retry?.();
} catch (e) {
console.error(`Error during canceling ${cancelModalTitle}: ${e}`);
}
setShowConfirmationModal(false);
setIsEditing(false);
setCheckIsFormDirty(false);
};

return (
<>
<div className={styles.contentWrapper}>{children}</div>
{isEditing && (
<FloatingContainer isCompactSideMenu={isCompactSideMenu}>
<FormNavigationButtons
onContinue={onContinue}
onCancel={onCancel}
onPrevious={onGoBack}
isFirstStep={!activeStepIndex}
isFinalStep={isFinalStep}
isDisabled={!isValid}
/>
</FloatingContainer>
)}
{(transition.unblock || showConfirmationModal) && (
<CancelFlowModal
flow={cancelModalTitle}
dismissModal={handleDismissModal}
handleCancel={handleCancel}
/>
)}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,28 @@
import { useState, useMemo } from 'react';
import PropTypes from 'prop-types';
import { FormProvider, useForm } from 'react-hook-form';
import { WithPageTitle } from './WithPageTitle';
import { WithStepperProgressBar } from './WithStepperProgressBar';
import { MultiStepWithPageTitle } from './MultiStepWithPageTitle';
import { MultiStepWithStepperProgressBar } from './MultiStepWithStepperProgressBar';
import { WithReadonlyEdit } from './SingleStepWithReadonlyEdit';

export const VARIANT = {
PAGE_TITLE: 'page_title',
STEPPER_PROGRESS_BAR: 'stepper_progress_bar',
READONLY_EDIT: 'readonly_edit',
};

const components = {
[VARIANT.PAGE_TITLE]: (props) => <WithPageTitle {...props} />,
[VARIANT.STEPPER_PROGRESS_BAR]: (props) => <WithStepperProgressBar {...props} />,
[VARIANT.PAGE_TITLE]: (props) => <MultiStepWithPageTitle {...props} />,
[VARIANT.STEPPER_PROGRESS_BAR]: (props) => <MultiStepWithStepperProgressBar {...props} />,
[VARIANT.READONLY_EDIT]: (props) => <WithReadonlyEdit {...props} />,
};

export const MultiStepForm = ({
export const ContextForm = ({
history,
getSteps,
defaultFormValues,
variant = VARIANT.PAGE_TITLE,
isEditing = false,
...props
}) => {
const [activeStepIndex, setActiveStepIndex] = useState(0);
Expand Down Expand Up @@ -83,6 +87,7 @@ export const MultiStepForm = ({
onCancel={onCancel}
onGoForward={onGoForward}
setFormResultData={setFormResultData}
isEditing={isEditing}
{...form}
{...props}
>
Expand All @@ -92,13 +97,14 @@ export const MultiStepForm = ({
form={form}
formResultData={formResultData}
history={history}
isEditing={isEditing}
/>
</FormProvider>
</Component>
);
};

MultiStepForm.propTypes = {
ContextForm.propTypes = {
variant: PropTypes.oneOf(Object.values(VARIANT)),
history: PropTypes.object,
getSteps: PropTypes.func,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Semibold } from '../../Typography';
import styles from '../styles.module.scss';
import clsx from 'clsx';

interface Tab {
label: string;
key: string;
}

interface StateTabProps {
tabs: Tab[];
state: string;
setState: React.Dispatch<React.SetStateAction<string>>;
className?: string;
}

/**
* A version of RouterTab that toggles an active tab held in parent state, rather than using path changes.
*
Expand All @@ -18,8 +31,8 @@ import clsx from 'clsx';
* @returns {React.Component} The rendered StateTab component.
*/

export default function StateTab({ tabs, state, setState, className = '' }) {
const isSelected = (key) => state === key;
const StateTab = ({ tabs, state, setState, className = '' }: StateTabProps) => {
const isSelected = (key: string) => state === key;
return (
<div className={clsx(styles.container, className)}>
{tabs.map((tab, index) => (
Expand All @@ -34,16 +47,18 @@ export default function StateTab({ tabs, state, setState, className = '' }) {
))}
</div>
);
}
};

StateTab.propTypes = {
tabs: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
key: PropTypes.string.isRequired,
}),
}).isRequired,
).isRequired,
state: PropTypes.string.isRequired,
setState: PropTypes.func.isRequired,
className: PropTypes.string,
};

export default StateTab;
1 change: 1 addition & 0 deletions packages/webapp/src/components/Typography/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type TypographyProps = {
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
children?: ReactNode;
className?: string;
id?: string;
};

export const Underlined = ({
Expand Down
4 changes: 2 additions & 2 deletions packages/webapp/src/containers/Animals/AddAnimals/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { useDispatch } from 'react-redux';
import { History } from 'history';
import { useMediaQuery } from '@mui/material';
import theme from '../../../assets/theme';
import { MultiStepForm, VARIANT } from '../../../components/Form/MultiStepForm';
import { ContextForm, VARIANT } from '../../../components/Form/ContextForm/';
import AddAnimalBasics, { animalBasicsDefaultValues } from './AddAnimalBasics';
import AddAnimalDetails from './AddAnimalDetails';
import AddAnimalSummary from './AddAnimalSummary';
Expand Down Expand Up @@ -123,7 +123,7 @@ function AddAnimals({ isCompactSideMenu, history }: AddAnimalsProps) {
};

return (
<MultiStepForm
<ContextForm
stepperProgressBarTitle={isMobile && t('ADD_ANIMAL.ADD_ANIMALS_TITLE')}
stepperProgressBarConfig={stepperProgressBarConfig}
onSave={onSave}
Expand Down
Loading
Loading