Skip to content

Commit

Permalink
Merge pull request #3659 from LiteFarmOrg/LF-4631_b/Implement_sensor_…
Browse files Browse the repository at this point in the history
…addition_UI_flow

LF-4631 (1): Implement sensor addition UI flow
  • Loading branch information
antsgar authored Jan 30, 2025
2 parents 1e6097f + 421f31e commit fb0f3e3
Show file tree
Hide file tree
Showing 12 changed files with 406 additions and 5 deletions.
1 change: 1 addition & 0 deletions packages/webapp/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"EDITING": "Editing...",
"ENTER_VALUE": "Enter value",
"EXPORT": "Export",
"FETCHING_YOUR_DATA": "Sit back, we’re fetching your {{dataName}} data",
"FINISH": "Finish",
"FROM": "from",
"GO_BACK": "Go back",
Expand Down
1 change: 1 addition & 0 deletions packages/webapp/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1785,6 +1785,7 @@
"DAYS_AGO": "{{time}} day(s) ago",
"DEPTH": "Depth",
"DETAIL": {
"ADD_SENSORS": "Add sensors",
"BRAND": "Brand",
"BRAND_TOOLTIP": "Brands that LiteFarm can integrate with are shown below. If you would no longer like to use this sensor brand, try retiring this sensor instead.",
"DEPTH": "Depth",
Expand Down
4 changes: 2 additions & 2 deletions packages/webapp/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ import { NotistackSnackbar } from './containers/Snackbar/NotistackSnackbar';
import { OfflineDetector } from './containers/hooks/useOfflineDetector/OfflineDetector';
import styles from './styles.module.scss';
import Routes from './routes';
import { ANIMALS_URL } from './util/siteMapConstants';
import { ANIMALS_URL, MAP_URL } from './util/siteMapConstants';

function App() {
const [isCompactSideMenu, setIsCompactSideMenu] = useState(false);
const [isFeedbackSurveyOpen, setFeedbackSurveyOpen] = useState(false);
const FULL_WIDTH_ROUTES = ['/map', ANIMALS_URL];
const FULL_WIDTH_ROUTES = [MAP_URL, ANIMALS_URL];
const isFullWidth = FULL_WIDTH_ROUTES.some((path) => matchPath(history.location.pathname, path));

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2025 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 } from 'react';
import Icon from '../../Icons';
import TextButton from '../Button/TextButton';
import { ReactComponent as XIcon } from '../../../assets/images/x-icon.svg';
import styles from './styles.module.scss';

interface HeaderWithBackAndCloseProps {
title: ReactNode;
onCancel?: () => void;
onGoBack?: () => void;
}

const HeaderWithBackAndClose = ({ title, onCancel, onGoBack }: HeaderWithBackAndCloseProps) => {
return (
<div className={styles.headerWrapper}>
<div className={styles.container}>
<div className={styles.leftContainer}>
{onGoBack && (
<TextButton onClick={onGoBack}>
<Icon iconName="CHEVRON_LEFT" className={styles.backButton} />
</TextButton>
)}
<div className={typeof title === 'string' ? styles.textTitle : ''}>{title}</div>
</div>
{onCancel && (
<TextButton className={styles.closeButtonContainer} onClick={onCancel}>
<XIcon className={styles.closeButton} />
</TextButton>
)}
</div>
</div>
);
};

export default HeaderWithBackAndClose;
38 changes: 38 additions & 0 deletions packages/webapp/src/components/Form/ContextForm/Loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2025 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 { useTranslation } from 'react-i18next';
import Spinner from '../../Spinner';
import styles from './styles.module.scss';

interface LoadingProps {
dataName?: string;
}

const Loading = ({ dataName = '' }: LoadingProps) => {
const { t } = useTranslation(['translation', 'common']);

return (
<div className={styles.loadingScreen}>
<div>
<Spinner />
</div>
<div className={styles.loadingText}>{t('common:LOADING')}</div>
<div className={styles.loadingMessage}>{t('common:FETCHING_YOUR_DATA', { dataName })}</div>
</div>
);
};

export default Loading;
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,18 @@ import FloatingContainer from '../../FloatingContainer';
import FormNavigationButtons from '../FormNavigationButtons';
import FixedHeaderContainer from '../../Animals/FixedHeaderContainer';
import CancelFlowModal from '../../Modals/CancelFlowModal';
import Loading from './Loading';
import styles from './styles.module.scss';

interface WithStepperProgressBarProps {
children: ReactNode;
history: History;
steps: { formContent: ReactNode; title: string }[];
steps: {
formContent: ReactNode;
title: string;
onContinueAction?: (values: any) => Promise<void>;
dataName?: string;
}[];
activeStepIndex: number;
cancelModalTitle: string;
isCompactSideMenu: boolean;
Expand All @@ -59,6 +65,7 @@ interface WithStepperProgressBarProps {
setIsEditing?: React.Dispatch<React.SetStateAction<boolean>>;
showCancelFlow?: boolean;
setShowCancelFlow?: React.Dispatch<React.SetStateAction<boolean>>;
headerComponent?: ((props: HeaderProps) => JSX.Element) | null;
}

export const WithStepperProgressBar = ({
Expand All @@ -84,12 +91,14 @@ export const WithStepperProgressBar = ({
setIsEditing,
showCancelFlow,
setShowCancelFlow,
headerComponent = StepperProgressBar,
}: WithStepperProgressBarProps) => {
const [transition, setTransition] = useState<{ unblock?: () => void; retry?: () => void }>({
unblock: undefined,
retry: undefined,
});
const [isSaving, setIsSaving] = useState(false);
const [isLoading, setIsLoading] = useState(false);

const isSummaryPage = hasSummaryWithinForm && activeStepIndex === steps.length - 1;
const isSingleStep = steps.length === 1;
Expand All @@ -107,6 +116,13 @@ export const WithStepperProgressBar = ({
return () => unblock();
}, [isSummaryPage, isDirty, history]);

useEffect(() => {
// Reset loading state whenever the step changes
if (isLoading) {
setIsLoading(false);
}
}, [activeStepIndex]);

const isFinalStep =
(!hasSummaryWithinForm && activeStepIndex === steps.length - 1) ||
(hasSummaryWithinForm && activeStepIndex === steps.length - 2);
Expand All @@ -123,6 +139,20 @@ export const WithStepperProgressBar = ({
};

const onContinue = async () => {
const { onContinueAction } = steps[activeStepIndex];

if (onContinueAction) {
setIsLoading(true);
try {
// Execute the custom action for the current step before proceeding to the next one
await onContinueAction(getValues());
} catch (error) {
console.error(error);
setIsLoading(false);
return;
}
}

if (isFinalStep) {
setIsSaving(true);
await handleSubmit((data: FieldValues) => onSave(data, onSuccess, setFormResultData))();
Expand All @@ -149,13 +179,20 @@ export const WithStepperProgressBar = ({
setShowCancelFlow?.(false);
};

if (isLoading) {
return <Loading dataName={steps[activeStepIndex].dataName} />;
}

return (
<StepperProgressBarWrapper
isSingleStep={isSingleStep}
{...stepperProgressBarConfig}
title={stepperProgressBarTitle}
steps={steps.map(({ title }) => title)}
activeStep={activeStepIndex}
onGoBack={onGoBack}
onCancel={onCancel}
headerComponent={headerComponent}
>
<div className={styles.contentWrapper}>{children}</div>
{shouldShowFormNavigationButtons && (
Expand All @@ -181,22 +218,29 @@ export const WithStepperProgressBar = ({
);
};

type StepperProgressBarWrapperProps = StepperProgressBarProps & {
type HeaderProps = StepperProgressBarProps & {
onGoBack: () => void;
onCancel: () => void;
};

type StepperProgressBarWrapperProps = HeaderProps & {
children: ReactNode;
isSingleStep: boolean;
headerComponent: ((props: HeaderProps) => JSX.Element) | null;
};

const StepperProgressBarWrapper = ({
children,
isSingleStep,
headerComponent,
...stepperProgressBarProps
}: StepperProgressBarWrapperProps) => {
if (isSingleStep) {
return <>{children}</>;
}

return (
<FixedHeaderContainer header={<StepperProgressBar {...stepperProgressBarProps} />}>
<FixedHeaderContainer header={headerComponent && headerComponent(stepperProgressBarProps)}>
{children}
</FixedHeaderContainer>
);
Expand Down
80 changes: 80 additions & 0 deletions packages/webapp/src/components/Form/ContextForm/styles.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,83 @@
padding-bottom: 104px; // button height 72px + button top 16px + button bottom 16px
}
}

.loadingScreen {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

.loadingText {
color: #000;
font-size: 16px;
font-weight: 700;
line-height: 24px;
margin-bottom: 16px;
}

.loadingMessage {
color: #000;
font-size: 16px;
line-height: 24px;
}

/*----------------------------------------
HeaderWithBackAndClose
----------------------------------------*/
.headerWrapper {
width: 100%;
border-bottom: 1px solid var(--Colors-Neutral-Neutral-50);
box-shadow: 0px 1px 0px 0px rgba(0, 0, 0, 0.05);

.container {
max-width: 1024px;
margin: 0 auto;
display: flex;
justify-content: space-between;
padding: 16px 8px 12px 8px;
}

.leftContainer {
display: flex;
align-items: center;
gap: 8px;
}

.backButton {
padding: 0;
min-width: 24px;
min-height: 24px;
background-color: transparent;

svg {
width: 24px;
height: 24px;
}

path {
stroke: var(--Colors-Neutral-Neutral-600);
stroke-width: 2px;
}
}

.textTitle {
font-size: 16px;
letter-spacing: -0.352px;
}

.closeButtonContainer {
display: flex;
justify-content: center;
align-items: center;
width: 24px;
height: 24px;
}

.closeButton {
width: 12px;
height: 12px;
}
}
Loading

0 comments on commit fb0f3e3

Please sign in to comment.