From e63c9ef9aa87a9c3fd0e93cf5f095710f1cd9c52 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Mon, 27 Jan 2025 14:47:46 -0800 Subject: [PATCH 01/41] LF-4691 Add translations --- packages/webapp/public/locales/en/common.json | 2 ++ packages/webapp/public/locales/en/translation.json | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/webapp/public/locales/en/common.json b/packages/webapp/public/locales/en/common.json index c0f2fc2963..e733c65ef6 100644 --- a/packages/webapp/public/locales/en/common.json +++ b/packages/webapp/public/locales/en/common.json @@ -47,6 +47,7 @@ "HERE": "here", "INVALID_DATE": "Invalid date", "LOADING": "Loading...", + "MANAGE_ENTITY": "Manage {{entity}}", "MARK_ABANDON": "Abandon", "MARK_COMPLETE": "Mark Complete", "MARK_COMPLETED": "Mark completed", @@ -80,6 +81,7 @@ "SAVE": "Save", "SAVE_CHANGES": "Save Changes", "SEARCH": "Search", + "SEE_ON_MAP": "See on map", "SELECT": "Select", "SELECT_ALL": "Select all", "SELECTED_COUNT": "{{count}} selected", diff --git a/packages/webapp/public/locales/en/translation.json b/packages/webapp/public/locales/en/translation.json index 4fa8609e16..efde9c6b19 100644 --- a/packages/webapp/public/locales/en/translation.json +++ b/packages/webapp/public/locales/en/translation.json @@ -1789,6 +1789,8 @@ "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", + "DEVICE_TYPE": "Device type", + "DITECTED_FIELD": "We detected the field location to be:", "EDIT": "Edit", "EXTERNAL_ID_TOOLTIP": "This id is used to uniquely identify this sensor in other, integrated systems and cannot be changed. If it is no longer being used, try retiring this sensor and adding a new one.", "EXTERNAL_IDENTIFIER": "External Identifier", @@ -1796,7 +1798,16 @@ "LONGITUDE": "Longitude", "MODEL": "Model", "NAME": "Sensor Name", - "RETIRE": "Retire" + "RETIRE": "Retire", + "SEE_FULL_SENSOR_SETUP": "See full sensor setup" + }, + "ESCI": { + "ACTIVE_CONNECTION": "You have an active ESCI connection", + "CONNECT_NEW_SENSOR": "Connect a new sensor setup from ESCI", + "CURRENT_SUPPORT": "We currently only support sensors from \"Ensemble scientific \"", + "ENSEMBLE_ESID": "Ensemble ESID", + "ENTER_ID": "Enter you ensemble scientific ID", + "ORGANIZATION_ID": "ESCI organisation ID" }, "EXTERNAL_IDENTIFIER": "External identifier", "HOURS_AGO": "{{time}} hour(s) ago", From 1eec16d79fa84ceba7473adab773e67c0ccc50b7 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Mon, 27 Jan 2025 14:51:56 -0800 Subject: [PATCH 02/41] LF-4691 Add esci logo --- .../src/assets/images/partners/esci_logo.png | Bin 0 -> 1756 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/webapp/src/assets/images/partners/esci_logo.png diff --git a/packages/webapp/src/assets/images/partners/esci_logo.png b/packages/webapp/src/assets/images/partners/esci_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..dbb33707c030eaa085e188ce387f74fc1398bcbf GIT binary patch literal 1756 zcmV<21|#{2P)xdZSRkMaKo!XpA@Sc}WWGV=4&R?VIrtgo*ZFu`EM88Ul&@@2|tJP+|>QAvpvrcbvn?C9{<)YNclN;opZhCZ*bu&c+W-VJJj z3k^FD1|UA*oR2?T%_paR##Qyr+i8>YrWGx{)>T*MiS}$v1IA#F*9$oJqYpMvD7GjZ zY$&Kd61ZsaalW~A|NeXCnAzL6JJn#&!F9c}vATLhXRuLT8h5BNcWjzWz1~L7ZJ_}W zP!1v#%(YtJ4RbQ{$|`a|h);u7^%G&Yzn|rD*&z<1_`FOU%6Ldnnp9-dnE;E>)n7|Uip-{O(p87Ar?ayb?6t#9?ZcV+wCW%J6)ra=PG z1@9mNzA&Nwx6?8}!HpKPnb0Lk0;L$I8`QLJ7$JgTqThA3&axWtG6ONqYKT0r;wzAV zS6eC%bu)n{Wzfm9pa>yJ(jEH!3jfh0!vcX{grT3JpUq5&BTY`{^^wL6%aiJ`B^kOZ z4B}=mKyrW;VuP7*;YnG%fv*CNBtfovi6(9*LtBt*)$Lz=dDDERKY5Opl$5Yoq$^A` zxl6m@No9W_V~e?fEhji9Am4OS7M zvO*+1{S2>-7Hh^eEvDM>QLxy)rW)vwb6jhnL>%eN= zAp7(>97|A$0yGze*)ighFi+*~N{mQK)=9k%?vOn8$z;wGJPZ?`v^WJwJeK5hFqs!l5XYDzx~N=kfD_DzZ=`zy(G35>B-(mk^}t* z-IENkvoJ11X>i@ZZ#XkYWhiTghTULMEnwRD8Y}z zL)4)rqSmACy3^jN%ZpDFy z5mccs-#BNG%Ph)PNDyO5bYM_~Vnr6igLEq7@EbBaroj zK}B%Zy+8r8z(P9JwflP}oof5q<%0wZWDw^AmTBK162G3D7b(%iVc<77BnJL}fvSu9 Date: Mon, 27 Jan 2025 14:52:15 -0800 Subject: [PATCH 03/41] LF-4691 Add color codes to colors.scss --- packages/webapp/src/assets/colors.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/webapp/src/assets/colors.scss b/packages/webapp/src/assets/colors.scss index c62c92fc1f..8cc411bd2e 100644 --- a/packages/webapp/src/assets/colors.scss +++ b/packages/webapp/src/assets/colors.scss @@ -76,6 +76,7 @@ --Colors-Primary-Primary-teal-50: #ebf5f4; --Colors-Primary-Primary-teal-100: #c0e1dd; + --Colors-Primary-Primary-teal-200: #a2d2cd; --Colors-Primary-Primary-teal-300: #78bdb6; --Colors-Primary-Primary-teal-400: #5db1a8; --Colors-Primary-Primary-teal-500: #359d92; @@ -109,6 +110,8 @@ --Btn-primary-hover: #e8a700; --Btn-primary-disabled: #e7ebf2; + --Colors-Backgrounds-Disabled-grey: #e7ebf2; + --Brand-Accents-colors-and-overlays-Accent-red: #eb6034; --Colors-Accent-Accent-yellow-50: #fff8e6; @@ -130,6 +133,7 @@ --Colors-Accent---singles-Purple-full: #8f26f0; --Form-focus: #89d1c7; + --Box-bg: #f5fafa; // Named in Figma but not in design system --Colors-Primary-green: #247360; From 8136adbb9aa83be69e4ab4d7416ac3484651796d Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Mon, 27 Jan 2025 16:15:52 -0800 Subject: [PATCH 04/41] LF-4691 Add Partners component --- .../src/components/Sensor/v2/Partners.tsx | 77 +++++++++++++++++++ .../components/Sensor/v2/styles.module.scss | 76 ++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 packages/webapp/src/components/Sensor/v2/Partners.tsx create mode 100644 packages/webapp/src/components/Sensor/v2/styles.module.scss diff --git a/packages/webapp/src/components/Sensor/v2/Partners.tsx b/packages/webapp/src/components/Sensor/v2/Partners.tsx new file mode 100644 index 0000000000..e83062d835 --- /dev/null +++ b/packages/webapp/src/components/Sensor/v2/Partners.tsx @@ -0,0 +1,77 @@ +/* + * 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 . + */ + +import { useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import Input from '../../Form/Input'; +import { Main } from '../../Typography'; +import EsciLogo from '../../../assets/images/partners/esci_logo.png'; +import styles from './styles.module.scss'; + +type PartnersProps = { + hasActiveConnection: { + esci: boolean; + }; +}; + +const PARTNERS = [{ name: 'Ensemble scientific', url: 'www.esci.io', logoPath: EsciLogo }]; + +const Partner = ({ name, url, logoPath }: { name: string; url: string; logoPath: string }) => { + return ( +
+
+ +
+
+
{name}
+ + {url} + +
+
+ ); +}; + +const Partners = ({ hasActiveConnection }: PartnersProps) => { + const { t } = useTranslation(); + const { register } = useFormContext(); + + return ( +
+
{t('SENSOR.ESCI.CURRENT_SUPPORT')}
+ {PARTNERS.map((data) => ( + + ))} + {hasActiveConnection.esci ? ( + <>{/* LF-4693 */} + ) : ( +
+
{t('SENSOR.ESCI.CONNECT_NEW_SENSOR')}
+
+ {/* @ts-ignore */} + +
+
+ )} +
+ ); +}; + +export default Partners; diff --git a/packages/webapp/src/components/Sensor/v2/styles.module.scss b/packages/webapp/src/components/Sensor/v2/styles.module.scss new file mode 100644 index 0000000000..62a16bcddc --- /dev/null +++ b/packages/webapp/src/components/Sensor/v2/styles.module.scss @@ -0,0 +1,76 @@ +/* + * 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 . + */ + +.wrapper { + padding: 34px 8px 0 8px; +} + +.lead { + color: #000; + margin-bottom: 16px; +} + +.partner { + display: flex; + gap: 8px; + padding-left: 8px; + + .logo { + display: flex; + width: 64px; + height: 64px; + justify-content: center; + align-items: center; + border-radius: 6px; + border: 1px solid var(--Colors-Primary-Primary-teal-200); + box-shadow: 0px 0.741px 0px 0px var(--Colors-Primary-Primary-teal-400); + } + + .info { + .name { + color: var(--Colors-Primary-Primary-teal-700); + font-size: 16px; + font-weight: 700; + line-height: 18px; + margin-bottom: 4px; + } + + .url { + color: #000; + font-family: 'Open Sans'; + font-size: 16px; + } + } +} + +// Sensor connection setup +.sensorSetup { + margin-top: 32px; + color: var(--Colors-Neutral-Neutral-400); + font-size: 16px; +} + +.idInputWrapper { + margin-top: 4px; + padding: 16px; + border-radius: 4px; + border: 1px solid var(--Colors-Backgrounds-Disabled-grey); + background: var(--Colors-Backgrounds-Background-color); + + h5 { + color: var(--Colors-Neutral-Neutral-500); + font-size: 16px; + } +} From 56207c15a086575208bada390c5ccb9738dc70dd Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Mon, 27 Jan 2025 16:16:36 -0800 Subject: [PATCH 05/41] LF-4691 Add Partners container --- .../PointDetails/SensorDetail/v2/Partners.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/Partners.tsx diff --git a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/Partners.tsx b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/Partners.tsx new file mode 100644 index 0000000000..5ee1bce2a7 --- /dev/null +++ b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/Partners.tsx @@ -0,0 +1,23 @@ +/* + * 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 . + */ +import PurePartners from '../../../../../components/Sensor/v2/Partners'; + +const Partners = () => { + // TODO: LF-4693 GET /farm_addon?integrating_partner_id=1 + + return ; +}; + +export default Partners; From c105c85c2bd1cacabda4483ba1deb7ae789133af Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Tue, 28 Jan 2025 10:40:44 -0800 Subject: [PATCH 06/41] LF-4691 Navigate to sensor connection flow on sensor menu click * remove sensor upload related code --- packages/webapp/src/containers/Map/index.jsx | 65 +------------------- 1 file changed, 3 insertions(+), 62 deletions(-) diff --git a/packages/webapp/src/containers/Map/index.jsx b/packages/webapp/src/containers/Map/index.jsx index 9b1cd76a00..a6c51ecaf2 100644 --- a/packages/webapp/src/containers/Map/index.jsx +++ b/packages/webapp/src/containers/Map/index.jsx @@ -4,25 +4,15 @@ import { useTranslation } from 'react-i18next'; import styles from './styles.module.scss'; import GoogleMap from 'google-map-react'; import { saveAs } from 'file-saver'; -import { - DEFAULT_ZOOM, - GMAPS_API_KEY, - isArea, - isLine, - locationEnum, - SENSOR_BULK_UPLOAD_SUCCESS, -} from './constants'; +import { DEFAULT_ZOOM, GMAPS_API_KEY, isArea, isLine, locationEnum } from './constants'; import { useDispatch, useSelector } from 'react-redux'; import { measurementSelector, userFarmSelector } from '../userFarmSlice'; import html2canvas from 'html2canvas'; import { sendMapToEmail, setSpotlightToShown, - bulkUploadSensorsInfoFile, getSensorReadings, getAllSensorReadingTypes, - resetBulkUploadSensorsInfoFile, - resetShowTransitionModalState, } from './saga'; import { canShowSuccessHeader, @@ -39,8 +29,6 @@ import DrawAreaModal from '../../components/Map/Modals/DrawArea'; import DrawLineModal from '../../components/Map/Modals/DrawLine'; import AdjustAreaModal from '../../components/Map/Modals/AdjustArea'; import AdjustLineModal from '../../components/Map/Modals/AdjustLine'; -import BulkSensorUploadModal from '../../components/Map/Modals/BulkSensorUploadModal'; -import BulkUploadTransitionModal from '../../components/Modals/BulkUploadTransitionModal'; import CustomZoom from '../../components/Map/CustomZoom'; import CustomCompass from '../../components/Map/CustomCompass'; import DrawingManager from '../../components/Map/DrawingManager'; @@ -65,10 +53,6 @@ import { setIsRedrawing, hookFormPersistIsRedrawingSelector, } from '../hooks/useHookFormPersist/hookFormPersistSlice'; -import { - bulkSensorsUploadSliceSelector, - bulkSensorsUploadReInit, -} from '../../containers/bulkSensorUploadSlice'; import LocationSelectionModal from './LocationSelectionModal'; import { useMaxZoom } from './useMaxZoom'; import { @@ -77,6 +61,7 @@ import { setMapAddDrawerShow, } from './mapAddDrawerSlice'; import clsx from 'clsx'; +import { POST_SENSOR_URL } from '../../util/siteMapConstants'; export default function Map({ history, isCompactSideMenu }) { const { farm_name, grid_points, is_admin, farm_id } = useSelector(userFarmSelector); @@ -88,7 +73,6 @@ export default function Map({ history, isCompactSideMenu }) { const dispatch = useDispatch(); const system = useSelector(measurementSelector); const overlayData = useSelector(hookFormPersistSelector); - const bulkSensorsUploadResponse = useSelector(bulkSensorsUploadSliceSelector); const [gMap, setGMap] = useState(null); const [gMaps, setGMaps] = useState(null); const [gMapBounds, setGMapBounds] = useState(null); @@ -130,23 +114,6 @@ export default function Map({ history, isCompactSideMenu }) { }; }, []); - useEffect(() => { - if (bulkSensorsUploadResponse?.isBulkUploadSuccessful && gMaps && gMap) { - getMaxZoom(gMaps, gMap); - setShowBulkSensorUploadModal(false); - } - }, [bulkSensorsUploadResponse?.isBulkUploadSuccessful, gMaps, gMap]); - - useEffect(() => { - setShowBulkSensorUploadModal(false); - }, [bulkSensorsUploadResponse?.showTransitionModal]); - - useEffect(() => { - if (history.location.state?.notification_type === SENSOR_BULK_UPLOAD_SUCCESS) { - dispatch(setMapFilterShowAll(farm_id)); - } - }, []); - const [ drawingState, { @@ -183,7 +150,6 @@ export default function Map({ history, isCompactSideMenu }) { const [showExportModal, setShowExportModal] = useState(false); const [showDrawAreaSpotlightModal, setShowDrawAreaSpotlightModal] = useState(false); const [showDrawLineSpotlightModal, setShowDrawLineSpotlightModal] = useState(false); - const [showBulkSensorUploadModal, setShowBulkSensorUploadModal] = useState(false); const getMapOptions = (maps) => { return { @@ -389,8 +355,7 @@ export default function Map({ history, isCompactSideMenu }) { setShowDrawLineSpotlightModal(true); } else if (locationType === locationEnum.sensor) { dispatch(showAddDrawer ? setMapAddDrawerHide(farm_id) : setMapAddDrawerShow(farm_id)); - setShowBulkSensorUploadModal(true); - dispatch(resetBulkUploadSensorsInfoFile()); + history.push(POST_SENSOR_URL); return; } isLineWithWidth(locationType) && dispatch(upsertFormData(initialLineData[locationType])); @@ -408,9 +373,6 @@ export default function Map({ history, isCompactSideMenu }) { const handleCloseSuccessHeader = () => { dispatch(canShowSuccessHeader(false)); setShowSuccessHeader(false); - if (bulkSensorsUploadResponse?.isBulkUploadSuccessful) { - dispatch(bulkSensorsUploadReInit()); - } }; useEffect(() => { @@ -462,11 +424,6 @@ export default function Map({ history, isCompactSideMenu }) { return lineTypesWithWidth.includes(type); }; - const dismissBulkSensorsUploadModal = () => { - setShowBulkSensorUploadModal(false); - dispatch(setMapAddDrawerShow(farm_id)); - }; - const { showAdjustAreaSpotlightModal, showAdjustLineSpotlightModal, @@ -615,22 +572,6 @@ export default function Map({ history, isCompactSideMenu }) { }} /> )} - {showBulkSensorUploadModal && ( - { - const payload = { file }; - dispatch(bulkUploadSensorsInfoFile(payload)); - }} - /> - )} - {(bulkSensorsUploadResponse?.showTransitionModal ?? false) && ( - { - dispatch(resetShowTransitionModalState()); - }} - /> - )} ); From d3cfdbc17a8361187d178e7a9c0f95733947e368 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Tue, 28 Jan 2025 10:42:30 -0800 Subject: [PATCH 07/41] LF-4691 Replace hard-coded post sensor URL with constant in Routes --- packages/webapp/src/routes/index.jsx | 215 ++++++++++++++------------- 1 file changed, 111 insertions(+), 104 deletions(-) diff --git a/packages/webapp/src/routes/index.jsx b/packages/webapp/src/routes/index.jsx index 7df29c0c11..757ecc31a7 100644 --- a/packages/webapp/src/routes/index.jsx +++ b/packages/webapp/src/routes/index.jsx @@ -29,6 +29,8 @@ import { chooseFarmFlowSelector } from '../containers/ChooseFarm/chooseFarmFlowS import useScrollToTop from '../containers/hooks/useScrollToTop'; import { useReduxSnackbar } from '../containers/Snackbar/useReduxSnackbar'; +import { POST_SENSOR_URL } from '../util/siteMapConstants'; + //dynamic imports const Home = React.lazy(() => import('../containers/Home')); const Account = React.lazy(() => import('../containers/Profile/Account')); @@ -54,85 +56,87 @@ const Prices = React.lazy(() => import('../containers/Insights/Prices')); const ExpiredTokenScreen = React.lazy(() => import('../containers/ExpiredTokenScreen')); const Map = React.lazy(() => import('../containers/Map')); const MapVideo = React.lazy(() => import('../components/Map/Videos')); -const PostFarmSiteBoundaryForm = React.lazy(() => - import( - '../containers/LocationDetails/AreaDetails/FarmSiteBoundaryDetailForm/PostFarmSiteBoundary' - ), +const PostFarmSiteBoundaryForm = React.lazy( + () => + import( + '../containers/LocationDetails/AreaDetails/FarmSiteBoundaryDetailForm/PostFarmSiteBoundary' + ), ); const FarmSiteBoundaryDetails = React.lazy(() => import('./FarmSiteBoundaryDetailsRoutes')); -const PostFieldForm = React.lazy(() => - import('../containers/LocationDetails/AreaDetails/FieldDetailForm/PostField'), +const PostFieldForm = React.lazy( + () => import('../containers/LocationDetails/AreaDetails/FieldDetailForm/PostField'), ); const FieldDetails = React.lazy(() => import('./FieldDetailsRoutes')); -const PostGardenForm = React.lazy(() => - import('../containers/LocationDetails/AreaDetails/GardenDetailForm/PostGarden'), +const PostGardenForm = React.lazy( + () => import('../containers/LocationDetails/AreaDetails/GardenDetailForm/PostGarden'), ); const GardenDetails = React.lazy(() => import('./GardenDetailsRoutes')); -const PostGateForm = React.lazy(() => - import('../containers/LocationDetails/PointDetails/GateDetailForm/PostGate'), +const PostGateForm = React.lazy( + () => import('../containers/LocationDetails/PointDetails/GateDetailForm/PostGate'), ); const GateDetails = React.lazy(() => import('./GateDetailsRoutes')); -const PostWaterValveForm = React.lazy(() => - import('../containers/LocationDetails/PointDetails/WaterValveDetailForm/PostWaterValve'), +const PostWaterValveForm = React.lazy( + () => import('../containers/LocationDetails/PointDetails/WaterValveDetailForm/PostWaterValve'), ); const WaterValveDetails = React.lazy(() => import('./WaterValveDetailsRoutes')); -const EditSensor = React.lazy(() => - import('../containers/LocationDetails/PointDetails/SensorDetail/EditSensor'), +const EditSensor = React.lazy( + () => import('../containers/LocationDetails/PointDetails/SensorDetail/EditSensor'), ); -const PostBarnForm = React.lazy(() => - import('../containers/LocationDetails/AreaDetails/BarnDetailForm/PostBarn'), +const PostBarnForm = React.lazy( + () => import('../containers/LocationDetails/AreaDetails/BarnDetailForm/PostBarn'), ); const BarnDetails = React.lazy(() => import('./BarnDetailsRoutes')); -const PostNaturalAreaForm = React.lazy(() => - import('../containers/LocationDetails/AreaDetails/NaturalAreaDetailForm/PostNaturalArea'), +const PostNaturalAreaForm = React.lazy( + () => import('../containers/LocationDetails/AreaDetails/NaturalAreaDetailForm/PostNaturalArea'), ); const NaturalAreaDetails = React.lazy(() => import('./NaturalAreaDetailsRoutes')); -const PostSurfaceWaterForm = React.lazy(() => - import('../containers/LocationDetails/AreaDetails/SurfaceWaterDetailForm/PostSurfaceWater'), +const PostSurfaceWaterForm = React.lazy( + () => import('../containers/LocationDetails/AreaDetails/SurfaceWaterDetailForm/PostSurfaceWater'), ); const SurfaceWaterDetails = React.lazy(() => import('./SurfaceWaterDetailsRoutes')); -const PostResidenceForm = React.lazy(() => - import('../containers/LocationDetails/AreaDetails/ResidenceDetailForm/PostResidence'), +const PostResidenceForm = React.lazy( + () => import('../containers/LocationDetails/AreaDetails/ResidenceDetailForm/PostResidence'), ); const ResidenceDetails = React.lazy(() => import('./ResidenceDetailsRoutes')); -const PostCeremonialForm = React.lazy(() => - import('../containers/LocationDetails/AreaDetails/CeremonialAreaDetailForm/PostCeremonialArea'), +const PostCeremonialForm = React.lazy( + () => + import('../containers/LocationDetails/AreaDetails/CeremonialAreaDetailForm/PostCeremonialArea'), ); const CeremonialAreaDetails = React.lazy(() => import('./CeremonialAreaDetailsRoutes')); -const PostGreenhouseForm = React.lazy(() => - import('../containers/LocationDetails/AreaDetails/GreenhouseDetailForm/PostGreenhouse'), +const PostGreenhouseForm = React.lazy( + () => import('../containers/LocationDetails/AreaDetails/GreenhouseDetailForm/PostGreenhouse'), ); const GreenhouseDetails = React.lazy(() => import('./GreenhouseDetailsRoutes')); const CropManagement = React.lazy(() => import('../containers/Crop/CropManagement')); const CropDetail = React.lazy(() => import('../containers/Crop/CropDetail/index')); -const PostFenceForm = React.lazy(() => - import('../containers/LocationDetails/LineDetails/FenceDetailForm/PostFence'), +const PostFenceForm = React.lazy( + () => import('../containers/LocationDetails/LineDetails/FenceDetailForm/PostFence'), ); const FenceDetails = React.lazy(() => import('./FenceDetailsRoutes')); -const PostBufferZoneForm = React.lazy(() => - import('../containers/LocationDetails/LineDetails/BufferZoneDetailForm/PostBufferZone'), +const PostBufferZoneForm = React.lazy( + () => import('../containers/LocationDetails/LineDetails/BufferZoneDetailForm/PostBufferZone'), ); const BufferZoneDetails = React.lazy(() => import('./BufferZoneDetailsRoutes')); -const PostWatercourseForm = React.lazy(() => - import('../containers/LocationDetails/LineDetails/WatercourseDetailForm/PostWatercourse'), +const PostWatercourseForm = React.lazy( + () => import('../containers/LocationDetails/LineDetails/WatercourseDetailForm/PostWatercourse'), ); const WatercourseDetails = React.lazy(() => import('./WatercourseDetailsRoutes')); -const PostSensorForm = React.lazy(() => - import('../containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor'), +const PostSensorForm = React.lazy( + () => import('../containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor'), ); const SensorDetails = React.lazy(() => import('./SensorDetailsRoutes')); @@ -142,34 +146,34 @@ const AddCrop = React.lazy(() => import('../containers/AddCropVariety/AddCropVar const EditCrop = React.lazy(() => import('../containers/EditCropVariety')); const ComplianceInfo = React.lazy(() => import('../containers/AddCropVariety/ComplianceInfo')); const AddNewCrop = React.lazy(() => import('../containers/AddNewCrop')); -const PlantingLocation = React.lazy(() => - import('../containers/Crop/AddManagementPlan/PlantingLocation'), +const PlantingLocation = React.lazy( + () => import('../containers/Crop/AddManagementPlan/PlantingLocation'), ); const Transplant = React.lazy(() => import('../containers/Crop/AddManagementPlan/Transplant')); const PlantingDate = React.lazy(() => import('../containers/Crop/AddManagementPlan/PlantingDate')); -const PlantingMethod = React.lazy(() => - import('../containers/Crop/AddManagementPlan/PlantingMethod'), +const PlantingMethod = React.lazy( + () => import('../containers/Crop/AddManagementPlan/PlantingMethod'), ); -const PlantInContainer = React.lazy(() => - import('../containers/Crop/AddManagementPlan/PlantInContainer'), +const PlantInContainer = React.lazy( + () => import('../containers/Crop/AddManagementPlan/PlantInContainer'), ); -const PlantBroadcast = React.lazy(() => - import('../containers/Crop/AddManagementPlan/BroadcastPlan'), +const PlantBroadcast = React.lazy( + () => import('../containers/Crop/AddManagementPlan/BroadcastPlan'), ); const BedPlan = React.lazy(() => import('../containers/Crop/AddManagementPlan/BedPlan/BedPlan')); -const BedPlanGuidance = React.lazy(() => - import('../containers/Crop/AddManagementPlan/BedPlan/BedPlanGuidance'), +const BedPlanGuidance = React.lazy( + () => import('../containers/Crop/AddManagementPlan/BedPlan/BedPlanGuidance'), ); -const ManagementPlanName = React.lazy(() => - import('../containers/Crop/AddManagementPlan/ManagementPlanName'), +const ManagementPlanName = React.lazy( + () => import('../containers/Crop/AddManagementPlan/ManagementPlanName'), ); const RowMethod = React.lazy(() => import('../containers/Crop/AddManagementPlan/RowMethod')); -const RowMethodGuidance = React.lazy(() => - import('../containers/Crop/AddManagementPlan/RowMethod/RowGuidance'), +const RowMethodGuidance = React.lazy( + () => import('../containers/Crop/AddManagementPlan/RowMethod/RowGuidance'), ); -const PlantedAlready = React.lazy(() => - import('../containers/Crop/AddManagementPlan/PlantedAlready'), +const PlantedAlready = React.lazy( + () => import('../containers/Crop/AddManagementPlan/PlantedAlready'), ); const Documents = React.lazy(() => import('../containers/Documents')); @@ -178,60 +182,63 @@ const EditDocument = React.lazy(() => import('../containers/Documents/Edit')); const AddDocument = React.lazy(() => import('../containers/Documents/Add')); const MainDocument = React.lazy(() => import('../containers/Documents/Main')); -const CertificationReportingPeriod = React.lazy(() => - import('../containers/Certifications/ReportingPeriod'), +const CertificationReportingPeriod = React.lazy( + () => import('../containers/Certifications/ReportingPeriod'), ); const CertificationSurvey = React.lazy(() => import('../containers/Certifications/Survey')); -const InterestedOrganic = React.lazy(() => - import('../containers/OrganicCertifierSurvey/InterestedOrganic/UpdateInterestedOrganic'), +const InterestedOrganic = React.lazy( + () => import('../containers/OrganicCertifierSurvey/InterestedOrganic/UpdateInterestedOrganic'), ); -const CertificationSelection = React.lazy(() => - import( - '../containers/OrganicCertifierSurvey/CertificationSelection/UpdateCertificationSelection' - ), +const CertificationSelection = React.lazy( + () => + import( + '../containers/OrganicCertifierSurvey/CertificationSelection/UpdateCertificationSelection' + ), ); -const CertifierSelectionMenu = React.lazy(() => - import( - '../containers/OrganicCertifierSurvey/CertifierSelectionMenu/UpdateCertifierSelectionMenu' - ), +const CertifierSelectionMenu = React.lazy( + () => + import( + '../containers/OrganicCertifierSurvey/CertifierSelectionMenu/UpdateCertifierSelectionMenu' + ), ); -const SetCertificationSummary = React.lazy(() => - import( - '../containers/OrganicCertifierSurvey/SetCertificationSummary/UpdateSetCertificationSummary' - ), +const SetCertificationSummary = React.lazy( + () => + import( + '../containers/OrganicCertifierSurvey/SetCertificationSummary/UpdateSetCertificationSummary' + ), ); -const RequestCertifier = React.lazy(() => - import('../containers/OrganicCertifierSurvey/RequestCertifier/UpdateRequestCertifier'), +const RequestCertifier = React.lazy( + () => import('../containers/OrganicCertifierSurvey/RequestCertifier/UpdateRequestCertifier'), ); -const ViewCertification = React.lazy(() => - import('../containers/OrganicCertifierSurvey/ViewCertification/ViewCertification'), +const ViewCertification = React.lazy( + () => import('../containers/OrganicCertifierSurvey/ViewCertification/ViewCertification'), ); const RenderSurvey = React.lazy(() => import('../containers/RenderSurvey/RenderSurvey')); const ExportDownload = React.lazy(() => import('../containers/ExportDownload')); -const ManagementTasks = React.lazy(() => - import('../containers/Crop/ManagementDetail/ManagementTasks'), +const ManagementTasks = React.lazy( + () => import('../containers/Crop/ManagementDetail/ManagementTasks'), ); -const ManagementDetails = React.lazy(() => - import('../containers/Crop/ManagementDetail/ManagementDetails'), +const ManagementDetails = React.lazy( + () => import('../containers/Crop/ManagementDetail/ManagementDetails'), ); -const EditManagementDetails = React.lazy(() => - import('../containers/Crop/ManagementDetail/EditManagementDetails'), +const EditManagementDetails = React.lazy( + () => import('../containers/Crop/ManagementDetail/EditManagementDetails'), ); -const CompleteManagementPlan = React.lazy(() => - import('../containers/Crop/CompleteManagementPlan/CompleteManagementPlan'), +const CompleteManagementPlan = React.lazy( + () => import('../containers/Crop/CompleteManagementPlan/CompleteManagementPlan'), ); -const AbandonManagementPlan = React.lazy(() => - import('../containers/Crop/CompleteManagementPlan/AbandonManagementPlan'), +const AbandonManagementPlan = React.lazy( + () => import('../containers/Crop/CompleteManagementPlan/AbandonManagementPlan'), ); const RepeatCropPlan = React.lazy(() => import('../containers/Crop/RepeatCropPlan')); -const RepeatCropPlanConfirmation = React.lazy(() => - import('../containers/Crop/RepeatCropPlan/Confirmation'), +const RepeatCropPlanConfirmation = React.lazy( + () => import('../containers/Crop/RepeatCropPlan/Confirmation'), ); const TaskAssignment = React.lazy(() => import('../containers/Task/TaskAssignment')); @@ -245,41 +252,41 @@ const Tasks = React.lazy(() => import('../containers/Task')); const ManageCustomTasks = React.lazy(() => import('../containers/Task/ManageCustomTasks')); const AddCustomTask = React.lazy(() => import('../containers/Task/AddCustomTask')); const TaskComplete = React.lazy(() => import('../containers/Task/TaskComplete')); -const HarvestCompleteQuantity = React.lazy(() => - import('../containers/Task/TaskComplete/HarvestComplete/Quantity'), +const HarvestCompleteQuantity = React.lazy( + () => import('../containers/Task/TaskComplete/HarvestComplete/Quantity'), ); -const HarvestUses = React.lazy(() => - import('../containers/Task/TaskComplete/HarvestComplete/HarvestUses'), +const HarvestUses = React.lazy( + () => import('../containers/Task/TaskComplete/HarvestComplete/HarvestUses'), ); const TaskCompleteStepOne = React.lazy(() => import('../containers/Task/TaskComplete/StepOne')); const TaskReadOnly = React.lazy(() => import('../containers/Task/TaskReadOnly')); const EditCustomTask = React.lazy(() => import('../containers/Task/EditCustomTask')); const TaskAbandon = React.lazy(() => import('../containers/Task/TaskAbandon')); const EditCustomTaskUpdate = React.lazy(() => import('../containers/Task/EditCustomTaskUpdate')); -const TaskTransplantMethod = React.lazy(() => - import('../containers/Task/TaskTransplantMethod/TaskTransplantMethod'), +const TaskTransplantMethod = React.lazy( + () => import('../containers/Task/TaskTransplantMethod/TaskTransplantMethod'), ); -const TaskBedMethod = React.lazy(() => - import('../containers/Task/TaskTransplantMethod/TaskBedMethod'), +const TaskBedMethod = React.lazy( + () => import('../containers/Task/TaskTransplantMethod/TaskBedMethod'), ); -const TaskBedGuidance = React.lazy(() => - import('../containers/Task/TaskTransplantMethod/TaskBedGuidance'), +const TaskBedGuidance = React.lazy( + () => import('../containers/Task/TaskTransplantMethod/TaskBedGuidance'), ); -const TaskRowMethod = React.lazy(() => - import('../containers/Task/TaskTransplantMethod/TaskRowMethod'), +const TaskRowMethod = React.lazy( + () => import('../containers/Task/TaskTransplantMethod/TaskRowMethod'), ); -const TaskRowGuidance = React.lazy(() => - import('../containers/Task/TaskTransplantMethod/TaskRowGuidance'), +const TaskRowGuidance = React.lazy( + () => import('../containers/Task/TaskTransplantMethod/TaskRowGuidance'), ); -const TaskContainerMethod = React.lazy(() => - import('../containers/Task/TaskTransplantMethod/TaskContainerMethod'), +const TaskContainerMethod = React.lazy( + () => import('../containers/Task/TaskTransplantMethod/TaskContainerMethod'), ); const Notification = React.lazy(() => import('../containers/Notification')); -const NotificationReadOnly = React.lazy(() => - import('../containers/Notification/NotificationReadOnly'), +const NotificationReadOnly = React.lazy( + () => import('../containers/Notification/NotificationReadOnly'), ); -const UnknownRecord = React.lazy(() => - import('../containers/ErrorHandler/UnknownRecord/UnknownRecord'), +const UnknownRecord = React.lazy( + () => import('../containers/ErrorHandler/UnknownRecord/UnknownRecord'), ); const Routes = ({ isCompactSideMenu, isFeedbackSurveyOpen, setFeedbackSurveyOpen }) => { @@ -536,7 +543,7 @@ const Routes = ({ isCompactSideMenu, isFeedbackSurveyOpen, setFeedbackSurveyOpen - + @@ -821,7 +828,7 @@ const Routes = ({ isCompactSideMenu, isFeedbackSurveyOpen, setFeedbackSurveyOpen - + From a8b47beaaee1093b55e6845a39095cac029537e4 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Tue, 28 Jan 2025 13:36:45 -0800 Subject: [PATCH 08/41] LF-4691 Add showPreviousButton prop to WithStepperProgressBar --- .../components/Form/ContextForm/WithStepperProgressBar.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/webapp/src/components/Form/ContextForm/WithStepperProgressBar.tsx b/packages/webapp/src/components/Form/ContextForm/WithStepperProgressBar.tsx index 34f4088362..4832fa03c8 100644 --- a/packages/webapp/src/components/Form/ContextForm/WithStepperProgressBar.tsx +++ b/packages/webapp/src/components/Form/ContextForm/WithStepperProgressBar.tsx @@ -66,6 +66,7 @@ interface WithStepperProgressBarProps { showCancelFlow?: boolean; setShowCancelFlow?: React.Dispatch>; headerComponent?: ((props: HeaderProps) => JSX.Element) | null; + showPreviousButton?: boolean; } export const WithStepperProgressBar = ({ @@ -92,6 +93,7 @@ export const WithStepperProgressBar = ({ showCancelFlow, setShowCancelFlow, headerComponent = StepperProgressBar, + showPreviousButton = true, }: WithStepperProgressBarProps) => { const [transition, setTransition] = useState<{ unblock?: () => void; retry?: () => void }>({ unblock: undefined, @@ -200,7 +202,7 @@ export const WithStepperProgressBar = ({ Date: Tue, 28 Jan 2025 13:55:59 -0800 Subject: [PATCH 09/41] LF-4691 Style organization ID input properly --- packages/webapp/src/components/Sensor/v2/Partners.tsx | 3 ++- packages/webapp/src/components/Sensor/v2/styles.module.scss | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/webapp/src/components/Sensor/v2/Partners.tsx b/packages/webapp/src/components/Sensor/v2/Partners.tsx index e83062d835..635b3fe58d 100644 --- a/packages/webapp/src/components/Sensor/v2/Partners.tsx +++ b/packages/webapp/src/components/Sensor/v2/Partners.tsx @@ -16,6 +16,7 @@ import { useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import Input from '../../Form/Input'; +import InputBaseLabel from '../../Form/InputBase/InputBaseLabel'; import { Main } from '../../Typography'; import EsciLogo from '../../../assets/images/partners/esci_logo.png'; import styles from './styles.module.scss'; @@ -60,9 +61,9 @@ const Partners = ({ hasActiveConnection }: PartnersProps) => {
{t('SENSOR.ESCI.CONNECT_NEW_SENSOR')}
+ {/* @ts-ignore */} Date: Wed, 29 Jan 2025 10:25:51 -0800 Subject: [PATCH 10/41] LF-4691 Use Partners component in PostSensor * send showPreviousButton prop to ContextForm --- .../SensorDetail/v2/PostSensor.tsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx index 3618dd8258..8bac7d2b46 100644 --- a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx +++ b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx @@ -16,6 +16,7 @@ import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { History } from 'history'; import { ContextForm, Variant } from '../../../../../components/Form/ContextForm'; +import Partners from './Partners'; import PageTitle from '../../../../../components/PageTitle/v2'; import { enqueueErrorSnackbar } from '../../../../Snackbar/snackbarSlice'; import styles from './styles.module.scss'; @@ -28,19 +29,21 @@ const PostSensor = ({ history }: PostSensorProps) => { const { t } = useTranslation(); const dispatch = useDispatch(); - const linkOrganizationId = async () => { + const linkOrganizationId = async (values: any) => { // TODO: POST /farm_addon // When failed: snackbar // Simulating the API call return new Promise((resolve, reject) => { setTimeout(() => { - // Successful - resolve(); - - // Failed - // reject(dispatch(enqueueErrorSnackbar('TODO: Error message'))); - }, 1500); + if (values.partner.organization_uuid === '1') { + // Successful + resolve(); + } else { + // Failed + reject(dispatch(enqueueErrorSnackbar('TODO: Failed to connect to ESCI'))); + } + }, 500); }); }; @@ -53,7 +56,7 @@ const PostSensor = ({ history }: PostSensorProps) => { const getFormSteps = () => [ { - FormContent: () =>
Partner selection view
, + FormContent: () => , onContinueAction: linkOrganizationId, }, { FormContent: () =>
ESCI devices view
}, @@ -74,6 +77,8 @@ const PostSensor = ({ history }: PostSensorProps) => { hasSummaryWithinForm={true} onSave={onSave} headerComponent={PageTitle} + showPreviousButton={false} + // TODO: Make sure LF-4704 is mreged before the release. Otherwise cancelModalTitle is required />
); From 07b5e2876f5b8b486e56a32c8014974135708481 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Wed, 29 Jan 2025 16:00:02 -0800 Subject: [PATCH 11/41] LF-4691 Impelment validation for ESCI organisation ID --- .../webapp/public/locales/en/translation.json | 3 ++- .../src/components/Sensor/v2/Partners.tsx | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/webapp/public/locales/en/translation.json b/packages/webapp/public/locales/en/translation.json index efde9c6b19..617688c80c 100644 --- a/packages/webapp/public/locales/en/translation.json +++ b/packages/webapp/public/locales/en/translation.json @@ -1807,7 +1807,8 @@ "CURRENT_SUPPORT": "We currently only support sensors from \"Ensemble scientific \"", "ENSEMBLE_ESID": "Ensemble ESID", "ENTER_ID": "Enter you ensemble scientific ID", - "ORGANIZATION_ID": "ESCI organisation ID" + "ORGANIZATION_ID": "ESCI organisation ID", + "ORGANIZATION_ID_ERROR": "Invalid Organisation ID" }, "EXTERNAL_IDENTIFIER": "External identifier", "HOURS_AGO": "{{time}} hour(s) ago", diff --git a/packages/webapp/src/components/Sensor/v2/Partners.tsx b/packages/webapp/src/components/Sensor/v2/Partners.tsx index 635b3fe58d..171926f710 100644 --- a/packages/webapp/src/components/Sensor/v2/Partners.tsx +++ b/packages/webapp/src/components/Sensor/v2/Partners.tsx @@ -15,7 +15,8 @@ import { useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import Input from '../../Form/Input'; +import { validate as uuidValidate } from 'uuid'; +import Input, { getInputErrors } from '../../Form/Input'; import InputBaseLabel from '../../Form/InputBase/InputBaseLabel'; import { Main } from '../../Typography'; import EsciLogo from '../../../assets/images/partners/esci_logo.png'; @@ -27,6 +28,10 @@ type PartnersProps = { }; }; +const validateUuidFormat = (value: string, errorMessage: string) => { + return uuidValidate(value) || errorMessage; +}; + const PARTNERS = [{ name: 'Ensemble scientific', url: 'www.esci.io', logoPath: EsciLogo }]; const Partner = ({ name, url, logoPath }: { name: string; url: string; logoPath: string }) => { @@ -47,7 +52,10 @@ const Partner = ({ name, url, logoPath }: { name: string; url: string; logoPath: const Partners = ({ hasActiveConnection }: PartnersProps) => { const { t } = useTranslation(); - const { register } = useFormContext(); + const { + register, + formState: { errors }, + } = useFormContext(); return (
@@ -66,7 +74,12 @@ const Partners = ({ hasActiveConnection }: PartnersProps) => { + validateUuidFormat(value, t('SENSOR.ESCI.ORGANIZATION_ID_ERROR')), + })} + errors={getInputErrors(errors, 'partner.organization_uuid')} />
From f67034daf053909b1c1a4e9a05ed38e96389943d Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Wed, 29 Jan 2025 16:01:23 -0800 Subject: [PATCH 12/41] LF-4691 Update ContextForm to accept formMode prop --- packages/webapp/src/components/Form/ContextForm/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/webapp/src/components/Form/ContextForm/index.tsx b/packages/webapp/src/components/Form/ContextForm/index.tsx index 9b9cdae4fb..5e840a29b0 100644 --- a/packages/webapp/src/components/Form/ContextForm/index.tsx +++ b/packages/webapp/src/components/Form/ContextForm/index.tsx @@ -14,7 +14,7 @@ */ import { useState, useMemo } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; +import { FormProvider, useForm, ValidationMode } from 'react-hook-form'; import { WithPageTitle } from './WithPageTitle'; import { WithStepperProgressBar } from './WithStepperProgressBar'; @@ -35,6 +35,7 @@ interface ContextFormProps { variant?: Variant; isEditing?: boolean; setIsEditing?: React.Dispatch>; + formMode?: keyof ValidationMode; [key: string]: any; } @@ -45,6 +46,7 @@ export const ContextForm = ({ variant = Variant.PAGE_TITLE, isEditing = true, setIsEditing, + formMode = 'onBlur', ...props }: ContextFormProps) => { const [activeStepIndex, setActiveStepIndex] = useState(0); @@ -53,7 +55,7 @@ export const ContextForm = ({ const [showCancelFlow, setShowCancelFlow] = useState(false); const form = useForm({ - mode: 'onBlur', + mode: formMode, defaultValues: defaultFormValues, }); From d5ef4a6f5dde9b76b421f1386484c14a975d0ac6 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Wed, 29 Jan 2025 16:09:49 -0800 Subject: [PATCH 13/41] LF-4691 Add formMode prop to ContextForm in PostSensor --- .../LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx index 8bac7d2b46..b15fa71824 100644 --- a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx +++ b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx @@ -78,6 +78,7 @@ const PostSensor = ({ history }: PostSensorProps) => { onSave={onSave} headerComponent={PageTitle} showPreviousButton={false} + formMode="onChange" // TODO: Make sure LF-4704 is mreged before the release. Otherwise cancelModalTitle is required /> From 6c9cb8f5916ec6f80b2cc6a35d4c083d5b9f6a09 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Wed, 29 Jan 2025 16:12:16 -0800 Subject: [PATCH 14/41] LF-4691 Correct organisation spelling --- packages/webapp/public/locales/en/translation.json | 6 +++--- packages/webapp/src/components/Sensor/v2/Partners.tsx | 8 ++++---- .../PointDetails/SensorDetail/v2/PostSensor.tsx | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/webapp/public/locales/en/translation.json b/packages/webapp/public/locales/en/translation.json index 617688c80c..927546f7bb 100644 --- a/packages/webapp/public/locales/en/translation.json +++ b/packages/webapp/public/locales/en/translation.json @@ -1806,9 +1806,9 @@ "CONNECT_NEW_SENSOR": "Connect a new sensor setup from ESCI", "CURRENT_SUPPORT": "We currently only support sensors from \"Ensemble scientific \"", "ENSEMBLE_ESID": "Ensemble ESID", - "ENTER_ID": "Enter you ensemble scientific ID", - "ORGANIZATION_ID": "ESCI organisation ID", - "ORGANIZATION_ID_ERROR": "Invalid Organisation ID" + "ENTER_ID": "Enter your ensemble scientific organisation ID", + "ORGANISATION_ID": "ESCI organisation ID", + "ORGANISATION_ID_ERROR": "Invalid Organisation ID" }, "EXTERNAL_IDENTIFIER": "External identifier", "HOURS_AGO": "{{time}} hour(s) ago", diff --git a/packages/webapp/src/components/Sensor/v2/Partners.tsx b/packages/webapp/src/components/Sensor/v2/Partners.tsx index 171926f710..af6a7386e1 100644 --- a/packages/webapp/src/components/Sensor/v2/Partners.tsx +++ b/packages/webapp/src/components/Sensor/v2/Partners.tsx @@ -72,14 +72,14 @@ const Partners = ({ hasActiveConnection }: PartnersProps) => { {/* @ts-ignore */} - validateUuidFormat(value, t('SENSOR.ESCI.ORGANIZATION_ID_ERROR')), + validateUuidFormat(value, t('SENSOR.ESCI.ORGANISATION_ID_ERROR')), })} - errors={getInputErrors(errors, 'partner.organization_uuid')} + errors={getInputErrors(errors, 'partner.organisation_uuid')} /> diff --git a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx index b15fa71824..6a2342e516 100644 --- a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx +++ b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx @@ -29,14 +29,14 @@ const PostSensor = ({ history }: PostSensorProps) => { const { t } = useTranslation(); const dispatch = useDispatch(); - const linkOrganizationId = async (values: any) => { + const linkOrganisationId = async (values: any) => { // TODO: POST /farm_addon // When failed: snackbar // Simulating the API call return new Promise((resolve, reject) => { setTimeout(() => { - if (values.partner.organization_uuid === '1') { + if (values.partner.organisation_uuid === '1') { // Successful resolve(); } else { @@ -57,13 +57,13 @@ const PostSensor = ({ history }: PostSensorProps) => { const getFormSteps = () => [ { FormContent: () => , - onContinueAction: linkOrganizationId, + onContinueAction: linkOrganisationId, }, { FormContent: () =>
ESCI devices view
}, ]; const defaultFormValues = { - partner: { integrating_partner_id: 1, organization_uuid: '' }, + partner: { integrating_partner_id: 1, organisation_uuid: '' }, }; return ( From 7a97ea80e7b0aecef2b9f6694673cbf3a51caba1 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Wed, 29 Jan 2025 16:28:39 -0800 Subject: [PATCH 15/41] LF-4691 Adjust property names --- packages/webapp/src/components/Sensor/v2/Partners.tsx | 4 ++-- .../PointDetails/SensorDetail/v2/PostSensor.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/webapp/src/components/Sensor/v2/Partners.tsx b/packages/webapp/src/components/Sensor/v2/Partners.tsx index af6a7386e1..085276fc58 100644 --- a/packages/webapp/src/components/Sensor/v2/Partners.tsx +++ b/packages/webapp/src/components/Sensor/v2/Partners.tsx @@ -74,12 +74,12 @@ const Partners = ({ hasActiveConnection }: PartnersProps) => { validateUuidFormat(value, t('SENSOR.ESCI.ORGANISATION_ID_ERROR')), })} - errors={getInputErrors(errors, 'partner.organisation_uuid')} + errors={getInputErrors(errors, 'partner.org_uuid')} /> diff --git a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx index 6a2342e516..a635d16a59 100644 --- a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx +++ b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx @@ -36,7 +36,7 @@ const PostSensor = ({ history }: PostSensorProps) => { // Simulating the API call return new Promise((resolve, reject) => { setTimeout(() => { - if (values.partner.organisation_uuid === '1') { + if (values.partner.org_uuid === '1') { // Successful resolve(); } else { @@ -63,7 +63,7 @@ const PostSensor = ({ history }: PostSensorProps) => { ]; const defaultFormValues = { - partner: { integrating_partner_id: 1, organisation_uuid: '' }, + partner: { addon_partner_id: 1, org_uuid: '' }, }; return ( From 9e9864e0870f7e9fb507a7c109d4bd52499fff22 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Thu, 30 Jan 2025 14:34:01 -0800 Subject: [PATCH 16/41] LF-4691 FarmAddon API setup for RTK query --- packages/webapp/src/apiConfig.js | 2 ++ packages/webapp/src/store/api/apiSlice.ts | 12 ++++++++++++ packages/webapp/src/store/api/types/index.ts | 5 +++++ 3 files changed, 19 insertions(+) diff --git a/packages/webapp/src/apiConfig.js b/packages/webapp/src/apiConfig.js index 74e1ae1e92..b385be27ad 100644 --- a/packages/webapp/src/apiConfig.js +++ b/packages/webapp/src/apiConfig.js @@ -91,6 +91,7 @@ export const soilAmendmentMethodsUrl = `${URI}/soil_amendment_methods`; export const soilAmendmentPurposesUrl = `${URI}/soil_amendment_purposes`; export const soilAmendmentFertiliserTypesUrl = `${URI}/soil_amendment_fertiliser_types`; export const productUrl = `${URI}/product`; +export const farmAddonUrl = `${URI}/farm_addon`; export const url = URI; @@ -152,5 +153,6 @@ export default { soilAmendmentPurposesUrl, soilAmendmentFertiliserTypesUrl, productUrl, + farmAddonUrl, url, }; diff --git a/packages/webapp/src/store/api/apiSlice.ts b/packages/webapp/src/store/api/apiSlice.ts index 6dcf2ffda0..8a7dadc224 100644 --- a/packages/webapp/src/store/api/apiSlice.ts +++ b/packages/webapp/src/store/api/apiSlice.ts @@ -35,6 +35,7 @@ import { url, animalMovementPurposesUrl, sensorUrl, + farmAddonUrl, } from '../../apiConfig'; import type { Animal, @@ -55,6 +56,7 @@ import type { AnimalUse, AnimalMovementPurpose, SensorData, + FarmAddon, } from './types'; export const api = createApi({ @@ -91,6 +93,7 @@ export const api = createApi({ 'SoilAmendmentFertiliserTypes', 'SoilAmendmentProduct', 'Sensors', + 'FarmAddon', ], endpoints: (build) => ({ // redux-toolkit.js.org/rtk-query/usage-with-typescript#typing-query-and-mutation-endpoints @@ -246,6 +249,14 @@ export const api = createApi({ keepUnusedDataFor: 60 * 60 * 24 * 365, // 1 year providesTags: ['Sensors'], }), + addFarmAddon: build.mutation({ + query: (body) => ({ + url: `${farmAddonUrl}`, + method: 'POST', + body, + }), + invalidatesTags: ['FarmAddon'], + }), }), }); @@ -277,4 +288,5 @@ export const { useAddSoilAmendmentProductMutation, useUpdateSoilAmendmentProductMutation, useGetSensorsQuery, + useAddFarmAddonMutation, } = api; diff --git a/packages/webapp/src/store/api/types/index.ts b/packages/webapp/src/store/api/types/index.ts index 577994987b..fb40807e53 100644 --- a/packages/webapp/src/store/api/types/index.ts +++ b/packages/webapp/src/store/api/types/index.ts @@ -270,3 +270,8 @@ export interface SensorData { sensors: Sensor[]; sensor_arrays: SensorArray[]; } + +export interface FarmAddon { + addon_partner_id: number; + org_uuid: string; +} From 49572a22b5abd82f1e9bbd49add46aa103de57d9 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Thu, 30 Jan 2025 14:35:08 -0800 Subject: [PATCH 17/41] LF-4691 Add/modify translations --- packages/webapp/public/locales/en/translation.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/webapp/public/locales/en/translation.json b/packages/webapp/public/locales/en/translation.json index 927546f7bb..95d24bf2d5 100644 --- a/packages/webapp/public/locales/en/translation.json +++ b/packages/webapp/public/locales/en/translation.json @@ -1808,7 +1808,8 @@ "ENSEMBLE_ESID": "Ensemble ESID", "ENTER_ID": "Enter your ensemble scientific organisation ID", "ORGANISATION_ID": "ESCI organisation ID", - "ORGANISATION_ID_ERROR": "Invalid Organisation ID" + "ORGANISATION_ID_ERROR": "Invalid Organisation ID", + "ORGANISATION_ID_GENERIC_ERROR": "Failed to connect to ESCI. Please try again later." }, "EXTERNAL_IDENTIFIER": "External identifier", "HOURS_AGO": "{{time}} hour(s) ago", From 66d9fdcbf119afb5ff5713b708d47fdbe882521f Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Thu, 30 Jan 2025 14:43:17 -0800 Subject: [PATCH 18/41] LF-4691 Integrate API POST /farm_addon --- .../SensorDetail/v2/PostSensor.tsx | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx index a635d16a59..4faf15df42 100644 --- a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx +++ b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx @@ -19,6 +19,7 @@ import { ContextForm, Variant } from '../../../../../components/Form/ContextForm import Partners from './Partners'; import PageTitle from '../../../../../components/PageTitle/v2'; import { enqueueErrorSnackbar } from '../../../../Snackbar/snackbarSlice'; +import { useAddFarmAddonMutation } from '../../../../../store/api/apiSlice'; import styles from './styles.module.scss'; interface PostSensorProps { @@ -29,22 +30,21 @@ const PostSensor = ({ history }: PostSensorProps) => { const { t } = useTranslation(); const dispatch = useDispatch(); - const linkOrganisationId = async (values: any) => { - // TODO: POST /farm_addon - // When failed: snackbar + const [addFarmAddon] = useAddFarmAddonMutation(); - // Simulating the API call - return new Promise((resolve, reject) => { - setTimeout(() => { - if (values.partner.org_uuid === '1') { - // Successful - resolve(); - } else { - // Failed - reject(dispatch(enqueueErrorSnackbar('TODO: Failed to connect to ESCI'))); - } - }, 500); - }); + const linkEsci = async (values: any) => { + const result = await addFarmAddon(values.partner); + + if ('error' in result) { + const isInvalidId = 'data' in result.error && result.error.data === 'Organisation not found'; + const errorMessage = isInvalidId + ? t('SENSOR.ESCI.ORGANISATION_ID_ERROR') + : t('SENSOR.ESCI.ORGANISATION_ID_GENERIC_ERROR'); + dispatch(enqueueErrorSnackbar(errorMessage)); + + // This error prevents the page transition in WithStepperProgressBar's onContinue + throw Error(isInvalidId ? 'Invalid ESCI organisation ID' : 'ESCI Connection Error'); + } }; const onSave = async (data: any, onSuccess: () => void) => { @@ -57,7 +57,7 @@ const PostSensor = ({ history }: PostSensorProps) => { const getFormSteps = () => [ { FormContent: () => , - onContinueAction: linkOrganisationId, + onContinueAction: linkEsci, }, { FormContent: () =>
ESCI devices view
}, ]; From 2e1932cd79706603f61be09e99d28d1c511c18f0 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Thu, 30 Jan 2025 15:31:49 -0800 Subject: [PATCH 19/41] LF-4691 Export useLazyGetSensorsQuery from apiSlice --- packages/webapp/src/store/api/apiSlice.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/webapp/src/store/api/apiSlice.ts b/packages/webapp/src/store/api/apiSlice.ts index 8a7dadc224..70429b8c54 100644 --- a/packages/webapp/src/store/api/apiSlice.ts +++ b/packages/webapp/src/store/api/apiSlice.ts @@ -288,5 +288,6 @@ export const { useAddSoilAmendmentProductMutation, useUpdateSoilAmendmentProductMutation, useGetSensorsQuery, + useLazyGetSensorsQuery, useAddFarmAddonMutation, } = api; From c1a33bd59a52269fd58ad0e40f954b0282728943 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Thu, 30 Jan 2025 15:32:58 -0800 Subject: [PATCH 20/41] LF-4691 'LazyGetSensors' in PostSensor onSave --- .../PointDetails/SensorDetail/v2/PostSensor.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx index 4faf15df42..0c57ab9202 100644 --- a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx +++ b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx @@ -19,7 +19,7 @@ import { ContextForm, Variant } from '../../../../../components/Form/ContextForm import Partners from './Partners'; import PageTitle from '../../../../../components/PageTitle/v2'; import { enqueueErrorSnackbar } from '../../../../Snackbar/snackbarSlice'; -import { useAddFarmAddonMutation } from '../../../../../store/api/apiSlice'; +import { useAddFarmAddonMutation, useLazyGetSensorsQuery } from '../../../../../store/api/apiSlice'; import styles from './styles.module.scss'; interface PostSensorProps { @@ -31,6 +31,7 @@ const PostSensor = ({ history }: PostSensorProps) => { const dispatch = useDispatch(); const [addFarmAddon] = useAddFarmAddonMutation(); + const [triggerGetSensors] = useLazyGetSensorsQuery(); const linkEsci = async (values: any) => { const result = await addFarmAddon(values.partner); @@ -48,9 +49,10 @@ const PostSensor = ({ history }: PostSensorProps) => { }; const onSave = async (data: any, onSuccess: () => void) => { - // TODO: GET devices with useLazyQuery. - // Once the data is returned, call onSuccess to navigate to the next view. + // Fetch sensors + await triggerGetSensors(); + // Proceed to sensors view onSuccess(); }; From 14897dd18e1b044ba631a61f85a5a515bdb5d4a7 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Thu, 30 Jan 2025 15:52:29 -0800 Subject: [PATCH 21/41] LF-4691 Add type for Add sensor form --- .../PointDetails/SensorDetail/v2/types.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/types.ts diff --git a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/types.ts b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/types.ts new file mode 100644 index 0000000000..e9c48d8766 --- /dev/null +++ b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/types.ts @@ -0,0 +1,27 @@ +/* + * 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 . + */ + +import { FarmAddon } from '../../../../../store/api/types'; + +export const PARTNER = 'partner'; + +export const FarmAddonField = { + ORG_UUID: 'org_uuid', + PARTNER_ID: 'addon_partner_id', +} as const; + +export type AddSensorsFormFields = { + [PARTNER]: FarmAddon; +}; From 38de118ea7096063f0f7ad48a31dc905f08f0d40 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Thu, 30 Jan 2025 15:53:50 -0800 Subject: [PATCH 22/41] LF-4691 Use constants and types for Add sensors form --- .../webapp/src/components/Sensor/v2/Partners.tsx | 13 ++++++++++--- .../PointDetails/SensorDetail/v2/PostSensor.tsx | 9 +++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/webapp/src/components/Sensor/v2/Partners.tsx b/packages/webapp/src/components/Sensor/v2/Partners.tsx index 085276fc58..1f19b73486 100644 --- a/packages/webapp/src/components/Sensor/v2/Partners.tsx +++ b/packages/webapp/src/components/Sensor/v2/Partners.tsx @@ -20,6 +20,11 @@ import Input, { getInputErrors } from '../../Form/Input'; import InputBaseLabel from '../../Form/InputBase/InputBaseLabel'; import { Main } from '../../Typography'; import EsciLogo from '../../../assets/images/partners/esci_logo.png'; +import { + AddSensorsFormFields, + FarmAddonField, + PARTNER, +} from '../../../containers/LocationDetails/PointDetails/SensorDetail/v2/types'; import styles from './styles.module.scss'; type PartnersProps = { @@ -50,12 +55,14 @@ const Partner = ({ name, url, logoPath }: { name: string; url: string; logoPath: ); }; +const ORG_UUID = `${PARTNER}.${FarmAddonField.ORG_UUID}` as const; + const Partners = ({ hasActiveConnection }: PartnersProps) => { const { t } = useTranslation(); const { register, formState: { errors }, - } = useFormContext(); + } = useFormContext(); return (
@@ -74,12 +81,12 @@ const Partners = ({ hasActiveConnection }: PartnersProps) => { validateUuidFormat(value, t('SENSOR.ESCI.ORGANISATION_ID_ERROR')), })} - errors={getInputErrors(errors, 'partner.org_uuid')} + errors={getInputErrors(errors, ORG_UUID)} />
diff --git a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx index 0c57ab9202..8e45ba1a21 100644 --- a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx +++ b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx @@ -20,6 +20,7 @@ import Partners from './Partners'; import PageTitle from '../../../../../components/PageTitle/v2'; import { enqueueErrorSnackbar } from '../../../../Snackbar/snackbarSlice'; import { useAddFarmAddonMutation, useLazyGetSensorsQuery } from '../../../../../store/api/apiSlice'; +import { type AddSensorsFormFields, FarmAddonField, PARTNER } from './types'; import styles from './styles.module.scss'; interface PostSensorProps { @@ -33,8 +34,8 @@ const PostSensor = ({ history }: PostSensorProps) => { const [addFarmAddon] = useAddFarmAddonMutation(); const [triggerGetSensors] = useLazyGetSensorsQuery(); - const linkEsci = async (values: any) => { - const result = await addFarmAddon(values.partner); + const linkEsci = async (values: AddSensorsFormFields) => { + const result = await addFarmAddon(values[PARTNER]); if ('error' in result) { const isInvalidId = 'data' in result.error && result.error.data === 'Organisation not found'; @@ -48,7 +49,7 @@ const PostSensor = ({ history }: PostSensorProps) => { } }; - const onSave = async (data: any, onSuccess: () => void) => { + const onSave = async (data: AddSensorsFormFields, onSuccess: () => void) => { // Fetch sensors await triggerGetSensors(); @@ -65,7 +66,7 @@ const PostSensor = ({ history }: PostSensorProps) => { ]; const defaultFormValues = { - partner: { addon_partner_id: 1, org_uuid: '' }, + [PARTNER]: { [FarmAddonField.PARTNER_ID]: 1, [FarmAddonField.ORG_UUID]: '' }, }; return ( From 728c6bfe8d444e6fda265adef353bae3de02728d Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 31 Jan 2025 12:53:29 -0800 Subject: [PATCH 23/41] LF-4691 Remove background-color: white from formWrapper --- packages/webapp/src/components/Sensor/v2/styles.module.scss | 1 + .../PointDetails/SensorDetail/v2/styles.module.scss | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webapp/src/components/Sensor/v2/styles.module.scss b/packages/webapp/src/components/Sensor/v2/styles.module.scss index 77d61fe121..c2d4a5ec81 100644 --- a/packages/webapp/src/components/Sensor/v2/styles.module.scss +++ b/packages/webapp/src/components/Sensor/v2/styles.module.scss @@ -36,6 +36,7 @@ border-radius: 6px; border: 1px solid var(--Colors-Primary-Primary-teal-200); box-shadow: 0px 0.741px 0px 0px var(--Colors-Primary-Primary-teal-400); + background-color: var(--White); } .info { diff --git a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/styles.module.scss b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/styles.module.scss index aa95ee5cc4..ccb1364fb8 100644 --- a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/styles.module.scss +++ b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/styles.module.scss @@ -15,6 +15,5 @@ .formWrapper { height: 100%; - background-color: var(--White); padding: 24px; } From 7eedc505fe2b98fc540bee51a386edffc10abba1 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 31 Jan 2025 14:31:39 -0800 Subject: [PATCH 24/41] LF-4691 Extract CSSLength type --- packages/webapp/src/components/FloatingContainer/index.tsx | 3 +-- packages/webapp/src/types/index.ts | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/webapp/src/components/FloatingContainer/index.tsx b/packages/webapp/src/components/FloatingContainer/index.tsx index fb35a1682b..c6f03a9119 100644 --- a/packages/webapp/src/components/FloatingContainer/index.tsx +++ b/packages/webapp/src/components/FloatingContainer/index.tsx @@ -14,10 +14,9 @@ */ import clsx from 'clsx'; +import { CSSLength } from '../../types'; import styles from './styles.module.scss'; -type CSSLength = `${number}px` | `${number}%` | `${number}vw` | `${number}vh` | 'auto'; - interface FloatingContainerProps { isCompactSideMenu: boolean; children: React.ReactNode; diff --git a/packages/webapp/src/types/index.ts b/packages/webapp/src/types/index.ts index 318ee7f60b..eaa1bebc1e 100644 --- a/packages/webapp/src/types/index.ts +++ b/packages/webapp/src/types/index.ts @@ -37,3 +37,5 @@ export interface Location { [key: string]: any; } + +export type CSSLength = `${number}px` | `${number}%` | `${number}vw` | `${number}vh` | 'auto'; From aa18bc5667cdf08fd170aec3091855f6d750d63b Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 31 Jan 2025 14:32:20 -0800 Subject: [PATCH 25/41] LF-4691 Fix translation --- packages/webapp/public/locales/en/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webapp/public/locales/en/translation.json b/packages/webapp/public/locales/en/translation.json index 95d24bf2d5..bdd67799de 100644 --- a/packages/webapp/public/locales/en/translation.json +++ b/packages/webapp/public/locales/en/translation.json @@ -1804,7 +1804,7 @@ "ESCI": { "ACTIVE_CONNECTION": "You have an active ESCI connection", "CONNECT_NEW_SENSOR": "Connect a new sensor setup from ESCI", - "CURRENT_SUPPORT": "We currently only support sensors from \"Ensemble scientific \"", + "CURRENT_SUPPORT": "We currently only support sensors from \"Ensemble scientific\"", "ENSEMBLE_ESID": "Ensemble ESID", "ENTER_ID": "Enter your ensemble scientific organisation ID", "ORGANISATION_ID": "ESCI organisation ID", From 5536d2accd6871177d5517cebe2c040945b9dfae Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 31 Jan 2025 14:42:25 -0800 Subject: [PATCH 26/41] LF-4691 Update routes and PostSensor to send isCompactSideMenu --- .../PointDetails/SensorDetail/v2/PostSensor.tsx | 4 +++- packages/webapp/src/routes/index.jsx | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx index 8e45ba1a21..7ed95517dd 100644 --- a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx +++ b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx @@ -25,9 +25,10 @@ import styles from './styles.module.scss'; interface PostSensorProps { history: History; + isCompactSideMenu: boolean; } -const PostSensor = ({ history }: PostSensorProps) => { +const PostSensor = ({ history, isCompactSideMenu }: PostSensorProps) => { const { t } = useTranslation(); const dispatch = useDispatch(); @@ -82,6 +83,7 @@ const PostSensor = ({ history }: PostSensorProps) => { headerComponent={PageTitle} showPreviousButton={false} formMode="onChange" + isCompactSideMenu={isCompactSideMenu} // TODO: Make sure LF-4704 is mreged before the release. Otherwise cancelModalTitle is required /> diff --git a/packages/webapp/src/routes/index.jsx b/packages/webapp/src/routes/index.jsx index 757ecc31a7..93ac5e9741 100644 --- a/packages/webapp/src/routes/index.jsx +++ b/packages/webapp/src/routes/index.jsx @@ -543,7 +543,13 @@ const Routes = ({ isCompactSideMenu, isFeedbackSurveyOpen, setFeedbackSurveyOpen - + ( + + )} + /> @@ -828,7 +834,13 @@ const Routes = ({ isCompactSideMenu, isFeedbackSurveyOpen, setFeedbackSurveyOpen - + ( + + )} + /> From 3ab99694f3a2c6405b470bbe9332e6e2997a27bb Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 31 Jan 2025 15:20:44 -0800 Subject: [PATCH 27/41] LF-4691 Make Loading component fixed relative to viewport --- .../components/Form/ContextForm/Loading.tsx | 41 +++++++++++++++---- .../Form/ContextForm/styles.module.scss | 23 +++++++++++ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/packages/webapp/src/components/Form/ContextForm/Loading.tsx b/packages/webapp/src/components/Form/ContextForm/Loading.tsx index e1eb3e95af..bb90c0a115 100644 --- a/packages/webapp/src/components/Form/ContextForm/Loading.tsx +++ b/packages/webapp/src/components/Form/ContextForm/Loading.tsx @@ -13,25 +13,52 @@ * GNU General Public License for more details, see . */ +import { CSSProperties } from 'react'; +import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; import Spinner from '../../Spinner'; +import FloatingContainer from '../../FloatingContainer'; +import type { CSSLength } from '../../../types'; import styles from './styles.module.scss'; interface LoadingProps { dataName?: string; + isCompactSideMenu: boolean; + verticalMargin?: CSSLength; + horizontalMargin?: CSSLength; + mobileMargin?: CSSLength; } -const Loading = ({ dataName = '' }: LoadingProps) => { +const Loading = ({ + dataName = '', + isCompactSideMenu, + verticalMargin = '36px', + horizontalMargin = '64px', + mobileMargin = '16px', +}: LoadingProps) => { const { t } = useTranslation(['translation', 'common']); + const style = { + '--vertical-margin': verticalMargin, + '--horizontal-margin': horizontalMargin, + '--mobile-margin': mobileMargin, + } as CSSProperties; return ( -
-
- + +
+
+ +
+
{t('common:LOADING')}
+
{t('common:FETCHING_YOUR_DATA', { dataName })}
-
{t('common:LOADING')}
-
{t('common:FETCHING_YOUR_DATA', { dataName })}
-
+ ); }; diff --git a/packages/webapp/src/components/Form/ContextForm/styles.module.scss b/packages/webapp/src/components/Form/ContextForm/styles.module.scss index 0acf48f762..20aca0459b 100644 --- a/packages/webapp/src/components/Form/ContextForm/styles.module.scss +++ b/packages/webapp/src/components/Form/ContextForm/styles.module.scss @@ -29,6 +29,29 @@ flex-direction: column; justify-content: center; align-items: center; + + padding: 16px; + background-color: var(--White); + border-radius: 8px; + box-shadow: 0px 0px 1px 0px #2b303a4d; + + height: calc(100vh - var(--global-navbar-height) - var(--vertical-margin) * 2); + + &.withCompactSideMenu { + width: calc(100vw - var(--global-compact-side-menu-width) - var(--horizontal-margin) * 2); + } + &.withExpandedSideMenu { + width: calc(100vw - var(--global-side-menu-width) - var(--horizontal-margin) * 2); + } + + @include xs-breakpoint { + &.withCompactSideMenu, + &.withExpandedSideMenu { + width: calc(100vw - var(--mobile-margin) * 2); + height: calc(100vh - var(--global-navbar-height) - var(--mobile-margin) * 2); + margin: var(--mobile-margin); + } + } } .loadingText { From 33aa699cc2aa8d45914cc5ad430ae2ddf988ed71 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Fri, 31 Jan 2025 15:21:31 -0800 Subject: [PATCH 28/41] LF-4691 Send isCompactSideMenu prop to Loading in WithStepperProgressBar --- .../components/Form/ContextForm/WithStepperProgressBar.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/webapp/src/components/Form/ContextForm/WithStepperProgressBar.tsx b/packages/webapp/src/components/Form/ContextForm/WithStepperProgressBar.tsx index 4832fa03c8..fe4c98e3f6 100644 --- a/packages/webapp/src/components/Form/ContextForm/WithStepperProgressBar.tsx +++ b/packages/webapp/src/components/Form/ContextForm/WithStepperProgressBar.tsx @@ -182,7 +182,9 @@ export const WithStepperProgressBar = ({ }; if (isLoading) { - return ; + return ( + + ); } return ( From 108d7073011d56dc45fd338ad85a818d491cb0ad Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Mon, 3 Feb 2025 09:27:29 -0800 Subject: [PATCH 29/41] LF-4691 Delete unused files and code for bulk sensor upload --- .../Modals/BulkSensorUploadModal/constants.js | 6 - .../Modals/BulkSensorUploadModal/index.jsx | 49 ----- .../useValidateBulkSensorData.js | 208 ------------------ .../BulkSensorUploadModal/FileUploader.jsx | 81 ------- .../Modals/BulkSensorUploadModal/index.jsx | 97 -------- .../BulkSensorUploadModal/styles.module.scss | 116 ---------- .../BulkUploadTransitionModal/index.jsx | 23 -- .../styles.module.scss | 3 - .../webapp/src/containers/Map/constants.js | 8 - packages/webapp/src/containers/Map/saga.js | 111 ---------- .../src/containers/bulkSensorUploadSlice.js | 81 ------- packages/webapp/src/store/reducer.js | 2 - 12 files changed, 785 deletions(-) delete mode 100644 packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/constants.js delete mode 100644 packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/index.jsx delete mode 100644 packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/useValidateBulkSensorData.js delete mode 100644 packages/webapp/src/components/Modals/BulkSensorUploadModal/FileUploader.jsx delete mode 100644 packages/webapp/src/components/Modals/BulkSensorUploadModal/index.jsx delete mode 100644 packages/webapp/src/components/Modals/BulkSensorUploadModal/styles.module.scss delete mode 100644 packages/webapp/src/components/Modals/BulkUploadTransitionModal/index.jsx delete mode 100644 packages/webapp/src/components/Modals/BulkUploadTransitionModal/styles.module.scss delete mode 100644 packages/webapp/src/containers/bulkSensorUploadSlice.js diff --git a/packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/constants.js b/packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/constants.js deleted file mode 100644 index 221fb25e1d..0000000000 --- a/packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/constants.js +++ /dev/null @@ -1,6 +0,0 @@ -export const ErrorTypes = { - DEFAULT: -1, - INVALID_CSV: 0, - EMPTY_FILE: 1, - INVALID_FILE_TYPE: 2, -}; diff --git a/packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/index.jsx b/packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/index.jsx deleted file mode 100644 index 873dc66a3b..0000000000 --- a/packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/index.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import PureBulkSensorUploadModal from '../../../Modals/BulkSensorUploadModal'; -import { useValidateBulkSensorData } from './useValidateBulkSensorData'; -import PropTypes from 'prop-types'; - -export default function BulkSensorUploadModal({ dismissModal, onUpload }) { - const { t } = useTranslation(); - - const { - onUploadClicked, - handleSelectedFile, - onShowErrorClick, - disabled, - selectedFileName, - fileInputRef, - errorCount, - onTemplateDownloadClick, - uploadErrorMessage, - errorTypeCode, - } = useValidateBulkSensorData(onUpload, t); - - return ( - - ); -} - -BulkSensorUploadModal.prototype = { - dismissModal: PropTypes.func, - onUpload: PropTypes.func, -}; diff --git a/packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/useValidateBulkSensorData.js b/packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/useValidateBulkSensorData.js deleted file mode 100644 index 9f3730568e..0000000000 --- a/packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/useValidateBulkSensorData.js +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright 2019, 2020, 2021, 2022 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 . - */ - -import { useEffect, useRef, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { bulkSensorsUploadSliceSelector } from '../../../../containers/bulkSensorUploadSlice'; -import { createSensorErrorDownload } from '../../../../util/sensor'; -import { ErrorTypes } from './constants'; -import parseSensorCsv from '@shared/validation/sensorCSV.js'; -import { getLanguageFromLocalStorage } from '../../../../util/getLanguageFromLocalStorage'; -import { languageCodes } from '../../../../hooks/useLanguageOptions'; - -const getSensorTranslations = async (language) => { - try { - // return english if language not supported - if (!languageCodes.includes(language)) { - throw `LiteFarm sensors does not currently support language ${language}`; - } - return await import(`../../../../../../shared/locales/${language}/sensorCSV.json`); - } catch (error) { - console.log(error); - return await import('../../../../../../shared/locales/en/sensorCSV.json'); - } -}; - -export function useValidateBulkSensorData(onUpload, t) { - const bulkSensorsUploadResponse = useSelector(bulkSensorsUploadSliceSelector); - const [disabled, setDisabled] = useState(null); - const [selectedFile, setSelectedFile] = useState(null); - const [selectedFileName, setSelectedFileName] = useState(''); - const [sheetErrors, setSheetErrors] = useState([]); - const [errorCount, setErrorCount] = useState(0); - const fileInputRef = useRef(null); - const [translatedUploadErrors, setTranslatedUploadErrors] = useState([]); - const [uploadErrorMessage, setUploadErrorMessage] = useState(''); - const [errorTypeCode, setErrorTypeCode] = useState(ErrorTypes.DEFAULT); - const lang = getLanguageFromLocalStorage(); - - useEffect(() => { - if (!disabled) setDisabled(0); - else setDisabled(bulkSensorsUploadResponse.loading ? -1 : 1); - }, [bulkSensorsUploadResponse?.loading]); - - // bulkSensorsUploadResponse?.validationErrors from store updates the sheetErrors - // the sheetErrors will be used as single source of truth to show validation - // errors on the modal frontend. - useEffect(() => { - let validationErrorsResponseList = bulkSensorsUploadResponse?.validationErrors || []; - if (validationErrorsResponseList.length) { - const translatedErrors = translateErrors(bulkSensorsUploadResponse?.validationErrors); - setErrorCount(translatedErrors.length); - setSheetErrors(translatedErrors); - setErrorTypeCode(ErrorTypes.INVALID_CSV); - setUploadErrorMessage(t('FARM_MAP.BULK_UPLOAD_SENSORS.UPLOAD_ERROR_MESSAGE')); - } - }, [bulkSensorsUploadResponse?.validationErrors]); - - useEffect(() => { - if (bulkSensorsUploadResponse?.errorSensors.length > 0) { - setErrorCount((curr) => curr + bulkSensorsUploadResponse.errorSensors.length); - const translatedErrors = translateErrors(bulkSensorsUploadResponse?.errorSensors); - setTranslatedUploadErrors(translatedErrors); - setErrorTypeCode(ErrorTypes.INVALID_CSV); - setUploadErrorMessage(t('FARM_MAP.BULK_UPLOAD_SENSORS.UPLOAD_ERROR_MESSAGE')); - } - }, [bulkSensorsUploadResponse?.errorSensors]); - - const translateErrors = (errors) => { - return errors?.map((e) => { - return { - row: e.row, - column: e.column, - errorMessage: e.variables ? t(e.translation_key, e.variables) : t(e.translation_key), - value: e?.value ?? '', - }; - }); - }; - - useEffect(() => { - if (bulkSensorsUploadResponse?.defaultFailure) { - setErrorCount(1); - setErrorTypeCode(ErrorTypes.INVALID_CSV); - setUploadErrorMessage(t('FARM_MAP.BULK_UPLOAD_SENSORS.UPLOAD_ERROR_MESSAGE')); - } - }, [bulkSensorsUploadResponse?.defaultFailure]); - - const onUploadClicked = async (e) => { - e.preventDefault(); - if (!selectedFile) return; - onUpload(selectedFile); - }; - - const getFileExtension = (fileName) => fileName.split('.').pop(); - - const handleSelectedFile = async (e) => { - const file = e.target.files[0]; - if (!file) return; - try { - const fileExtension = getFileExtension(file?.name); - setSelectedFileName(file?.name); - setSelectedFile(file); - - const fileString = await readFile(file); - const translations = await getSensorTranslations(lang); - const { data, errors } = parseSensorCsv(fileString, lang, translations); - - const translatedErrors = translateErrors(errors); - - if (data.length >= 100) { - translatedErrors.push({ - row: 1, - column: 'N/A', - errorMessage: t('FARM_MAP.BULK_UPLOAD_SENSORS.VALIDATION.FILE_ROW_LIMIT_EXCEEDED'), - value: '', - }); - } - - if (fileExtension !== 'csv') { - setErrorTypeCode(ErrorTypes.INVALID_FILE_TYPE); - setUploadErrorMessage(t('FARM_MAP.BULK_UPLOAD_SENSORS.INVALID_FILE_TYPE')); - } else if (errors.length !== 0) { - setErrorTypeCode(ErrorTypes.INVALID_CSV); - setUploadErrorMessage(t('FARM_MAP.BULK_UPLOAD_SENSORS.UPLOAD_ERROR_MESSAGE')); - } else if (data.length === 0) { - setErrorTypeCode(ErrorTypes.EMPTY_FILE); - setUploadErrorMessage(t('FARM_MAP.BULK_UPLOAD_SENSORS.EMPTY_FILE_UPLOAD_ERROR_MESSAGE')); - } - - const newErrorCount = data.length === 0 ? 1 : translatedErrors.length; - setErrorCount(newErrorCount); - setSheetErrors(translatedErrors); - setDisabled(() => (newErrorCount === 0 ? 1 : 0)); - } catch (err) { - console.error(err); - } - }; - - const onShowErrorClick = async (errorCode) => { - if (errorCode === 2) { - await onTemplateDownloadClick(); - return; - } - if (sheetErrors.length) { - const inputFile = fileInputRef.current.files[0]; - if (inputFile) { - const downloadFileName = `${inputFile.name.replace(/.csv/, '')}_errors.txt`; - createSensorErrorDownload(downloadFileName, sheetErrors, 'validation'); - } - } else if (bulkSensorsUploadResponse?.defaultFailure) { - createSensorErrorDownload('sensor-upload-outcomes.txt', null, 'generic'); - } else { - createSensorErrorDownload( - 'sensor-upload-outcomes.txt', - translatedUploadErrors, - 'claim', - bulkSensorsUploadResponse?.success, - ); - } - }; - - const onTemplateDownloadClick = async () => { - const element = document.createElement('a'); - const { CSV_HEADER_TRANSLATIONS } = await getSensorTranslations(lang); - const file = new Blob([`${'\ufeff'}${Object.values(CSV_HEADER_TRANSLATIONS).join(',')}`], { - type: 'text/plain', - }); - element.href = URL.createObjectURL(file); - element.download = 'Add-sensors-to-LiteFarm.csv'; - document.body.appendChild(element); - element.click(); - }; - - return { - onUploadClicked, - handleSelectedFile, - onShowErrorClick, - onTemplateDownloadClick, - disabled, - selectedFileName, - fileInputRef, - errorCount, - uploadErrorMessage, - errorTypeCode, - }; -} - -function readFile(file) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => { - resolve(reader.result); - }; - reader.onerror = reject; - reader.readAsText(file); - }); -} diff --git a/packages/webapp/src/components/Modals/BulkSensorUploadModal/FileUploader.jsx b/packages/webapp/src/components/Modals/BulkSensorUploadModal/FileUploader.jsx deleted file mode 100644 index 2e89eaba5d..0000000000 --- a/packages/webapp/src/components/Modals/BulkSensorUploadModal/FileUploader.jsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import styles from './styles.module.scss'; -import { ReactComponent as UploadIcon } from '../../../assets/images/map/upload.svg'; -import { Label, Underlined } from '../../Typography'; -import clsx from 'clsx'; - -export default function FileUploader({ - handleSelectedFile, - acceptedFormat, - selectedFileName, - fileInputRef, - isValid, - onShowErrorClick, - uploadErrorLink, - uploadErrorMessage, - errorTypeCode, - invalidFileTypeErrorLink, -}) { - const handleClick = (event) => { - if (fileInputRef.current) { - fileInputRef.current.value = null; - } - fileInputRef.current.click(); - }; - - const handleChange = (event) => handleSelectedFile(event); - return ( - <> -
- - -
- - {!isValid && ( -
- -
- )} - - ); -} - -FileUploader.prototype = { - handleFile: PropTypes.func, - selectedFileName: PropTypes.string, - acceptedFormat: PropTypes.string, - isValid: PropTypes.bool, - onShowErrorClick: PropTypes.func, - uploadErrorLink: PropTypes.string, - uploadErrorMessage: PropTypes.string, - errorTypeCode: PropTypes.number, - invalidFileTypeErrorLink: PropTypes.string, -}; diff --git a/packages/webapp/src/components/Modals/BulkSensorUploadModal/index.jsx b/packages/webapp/src/components/Modals/BulkSensorUploadModal/index.jsx deleted file mode 100644 index 7dbdd492f5..0000000000 --- a/packages/webapp/src/components/Modals/BulkSensorUploadModal/index.jsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { Modal } from '..'; -import styles from './styles.module.scss'; -import { Semibold, Underlined, Label } from '../../Typography'; -import Button from '../../Form/Button'; -import { BsChevronLeft } from 'react-icons/bs'; -import PropTypes from 'prop-types'; -import FileUploader from './FileUploader'; -export default function BulkSensorUploadModal({ - title, - uploadLinkMessage, - uploadInstructionMessage, - uploadPlaceholder, - dismissModal, - onUpload, - disabled, - handleSelectedFile, - selectedFileName, - fileInputRef, - errorCount, - onShowErrorClick, - onTemplateDownloadClick, - uploadErrorMessage, - uploadErrorLink, - errorTypeCode, - invalidFileTypeErrorLink, -}) { - const { t } = useTranslation(); - - return ( - -
-
- - {title} -
- -
-
- -
- - - -
-
- ); -} - -BulkSensorUploadModal.prototype = { - title: PropTypes.string, - uploadInstructionMessage: PropTypes.string, - uploadPlaceholder: PropTypes.string, - dismissModal: PropTypes.func, - disabled: PropTypes.bool, - onUpload: PropTypes.func, - handleSelectedFile: PropTypes.func, - selectedFileName: PropTypes.string, - fileInputRef: PropTypes.func, - errorCount: PropTypes.number, - onShowErrorClick: PropTypes.func, - onTemplateDownloadClick: PropTypes.func, - uploadErrorMessage: PropTypes.string, - uploadErrorLink: PropTypes.string, - errorTypeCode: PropTypes.number, - invalidFileTypeErrorLink: PropTypes.string, -}; diff --git a/packages/webapp/src/components/Modals/BulkSensorUploadModal/styles.module.scss b/packages/webapp/src/components/Modals/BulkSensorUploadModal/styles.module.scss deleted file mode 100644 index fd704f3a4b..0000000000 --- a/packages/webapp/src/components/Modals/BulkSensorUploadModal/styles.module.scss +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2019, 2020, 2021, 2022 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 . - */ - -.container { - display: flex; - flex-direction: column; - max-width: 350px; - width: 90vw; - background: #fafafd; - border-radius: 7.05466px; - position: relative; - padding: 24px; -} - -.title { - color: var(--teal700); - line-height: 24px; - margin-bottom: 4px; - margin-left: 12px; -} - -.buttonUpload{ - margin-top: 20px; -} - -.uploadSelectInput{ - border: 1px solid var(--grey400); - box-sizing: border-box; - border-radius: 4px; - height: 48px; - padding-left: 8px; - font-size: 16px; - line-height: 24px; - color: var(--fontColor); - background-color: transparent; - background-position: calc(100% - 36px) 50%; - font-family: Open Sans,SansSerif,serif; - display: flex; - align-items: center; - cursor: pointer; -} - -.invalidateUploadSelectInput{ - border: 1px solid var(--red700); - box-sizing: border-box; - border-radius: 4px; - height: 48px; - padding-left: 8px; - font-size: 16px; - line-height: 24px; - color: var(--fontColor); - background-color: transparent; - background-position: calc(100% - 36px) 50%; - font-family: Open Sans,SansSerif,serif; - display: flex; - align-items: center; - cursor: pointer; -} - -.uploadPlaceholder{ - margin-top: 24px; - margin-bottom: 4px; -} - -.buttonContainer { - background: none; - border: 0; - color: var(--buttonPrimary); - cursor: pointer; - max-height: 32px; -} - -.modalHeaderWrapper { - display: flex; - margin-bottom: 16px; - align-items: center; -} - -.fileNameLabel{ - margin-bottom: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.uploadIconContainer{ - font-size: 20px; - color: var(--grey600); - margin-right: 8px; - min-width: 20px; -} - -.csvErrorMessageWrapper{ - font-size: 12px; - color: var(--red700); - margin-top: 8px; -} - -.errorMessage{ - text-decoration: underline; - font-size: 12px; - color: var(--red700); - cursor: pointer; -} \ No newline at end of file diff --git a/packages/webapp/src/components/Modals/BulkUploadTransitionModal/index.jsx b/packages/webapp/src/components/Modals/BulkUploadTransitionModal/index.jsx deleted file mode 100644 index 05282a1910..0000000000 --- a/packages/webapp/src/components/Modals/BulkUploadTransitionModal/index.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import ModalComponent from '../ModalComponent/v2'; -import { useTranslation } from 'react-i18next'; -import Button from '../../Form/Button'; -import styles from './styles.module.scss'; - -export default function BulkUploadTransitionModal({ dismissModal }) { - const { t } = useTranslation(); - return ( - - - - } - /> - ); -} diff --git a/packages/webapp/src/components/Modals/BulkUploadTransitionModal/styles.module.scss b/packages/webapp/src/components/Modals/BulkUploadTransitionModal/styles.module.scss deleted file mode 100644 index 80d74d6342..0000000000 --- a/packages/webapp/src/components/Modals/BulkUploadTransitionModal/styles.module.scss +++ /dev/null @@ -1,3 +0,0 @@ -.button { - width: 96px; -} \ No newline at end of file diff --git a/packages/webapp/src/containers/Map/constants.js b/packages/webapp/src/containers/Map/constants.js index 5a84362318..02e1b5b6d3 100644 --- a/packages/webapp/src/containers/Map/constants.js +++ b/packages/webapp/src/containers/Map/constants.js @@ -66,12 +66,6 @@ export const locationEnum = { residence: 'residence', }; -export const bulkSenorUploadErrorTypeEnum = { - unable_to_claim_all_sensors: 'unable_to_claim_all_sensors', - validation_failure: 'validation_failure', - timeout_and_show_transition_modal: 'timeout', -}; - export const polygonPath = (path, width, maps) => { const { leftPoints, rightPoints } = path.reduce(linePathPolygonConstructor, { leftPoints: [], @@ -83,8 +77,6 @@ export const polygonPath = (path, width, maps) => { return leftPoints.concat(rightPoints.reverse()); }; -export const SENSOR_BULK_UPLOAD_SUCCESS = 'SENSOR_BULK_UPLOAD_SUCCESS'; - const linePathPolygonConstructor = (innerState, point, i, path) => { const { bearings, leftPoints, rightPoints, width, maps } = innerState; const { diff --git a/packages/webapp/src/containers/Map/saga.js b/packages/webapp/src/containers/Map/saga.js index e65345c9f7..c773e7975e 100644 --- a/packages/webapp/src/containers/Map/saga.js +++ b/packages/webapp/src/containers/Map/saga.js @@ -19,21 +19,11 @@ import { url, sensorUrl } from '../../apiConfig'; import i18n from '../../locales/i18n'; import { axios, getHeader } from '../saga'; import { loginSelector, userFarmSelector } from '../userFarmSlice'; -import { canShowSuccessHeader, setSuccessMessage } from '../../containers/mapSlice'; import { patchSpotlightFlagsFailure, patchSpotlightFlagsSuccess, spotlightLoading, } from '../showedSpotlightSlice'; -import { - bulkSensorsUploadFailure, - bulkSensorsUploadSuccess, - bulkSensorsUploadLoading, - bulkSensorsUploadValidationFailure, - resetSensorsBulkUploadStates, - switchToAsyncSensorUpload, -} from '../bulkSensorUploadSlice'; -import { bulkSenorUploadErrorTypeEnum } from './constants'; import { enqueueErrorSnackbar } from '../Snackbar/snackbarSlice'; import { @@ -41,22 +31,14 @@ import { onLoadingSensorReadingStart, onLoadingSensorReadingFail, } from './mapSensorSlice'; -import { postManySensorsSuccess } from '../sensorSlice'; import { getSensorReadingTypesSuccess, onLoadingSensorReadingTypesFail, onLoadingSensorReadingTypesStart, } from '../sensorReadingTypesSlice'; -import { setMapCache } from './mapCacheSlice'; const sendMapToEmailUrl = (farm_id) => `${url}/export/map/farm/${farm_id}`; const showedSpotlightUrl = () => `${url}/showed_spotlight`; -const bulkUploadSensorsInfoUrl = () => { - let url = sensorUrl; - const testTimer = localStorage.getItem('sensorUploadTimer'); - if (testTimer) url += `?sensorUploadTimer=${testTimer}`; - return url; -}; export const sendMapToEmail = createAction(`sendMapToEmailSaga`); @@ -106,96 +88,6 @@ export function* setSpotlightToShownSaga({ payload: spotlights }) { } } -export const bulkUploadSensorsInfoFile = createAction(`bulkUploadSensorsInfoFileSaga`); -export const resetBulkUploadSensorsInfoFile = createAction(`resetBulkUploadSensorsInfoFileSaga`); -export const resetShowTransitionModalState = createAction(`resetShowTransitionModalStateSaga`); - -export function* resetBulkUploadSensorsInfoFileSaga() { - yield put(resetSensorsBulkUploadStates()); -} - -export function* resetShowTransitionModalStateSaga() { - yield put(switchToAsyncSensorUpload(false)); -} - -export function* bulkUploadSensorsInfoFileSaga({ payload: { file } }) { - try { - yield put(bulkSensorsUploadLoading()); - const { farm_id } = yield select(userFarmSelector); - const formData = new FormData(); - formData.append('sensors', file); - const fileUploadResponse = yield call(axios.post, bulkUploadSensorsInfoUrl(), formData, { - headers: { - 'Content-Type': 'multipart/form-data', - Authorization: 'Bearer ' + localStorage.getItem('id_token'), - farm_id: farm_id, - }, - }); - - switch (fileUploadResponse.status) { - case 200: { - yield put(setMapCache({ maxZoom: undefined, farm_id })); - yield put(bulkSensorsUploadSuccess()); - yield put(postManySensorsSuccess(fileUploadResponse?.data?.sensors)); - yield put(getAllSensorReadingTypes()); - yield put( - setSuccessMessage([ - i18n.t('FARM_MAP.MAP_FILTER.SENSOR'), - i18n.t('message:MAP.SUCCESS_UPLOAD'), - ]), - ); - yield put(canShowSuccessHeader(true)); - break; - } - case 202: { - yield put(switchToAsyncSensorUpload(true)); - break; - } - default: { - yield put(bulkSensorsUploadFailure()); - yield put(enqueueErrorSnackbar(i18n.t('message:BULK_UPLOAD.ERROR.UPLOAD'))); - } - } - } catch (error) { - switch (error?.response?.status) { - case 400: { - const errorType = error?.response?.data?.error_type || ''; - switch (errorType) { - case bulkSenorUploadErrorTypeEnum?.unable_to_claim_all_sensors: { - const { success, errorSensors } = error?.response?.data ?? { - success: [], - errorSensors: [], - }; - if (success.length > 0) { - yield put(postManySensorsSuccess(success)); - } - yield put( - bulkSensorsUploadFailure({ - success: success.map((s) => s?.sensor?.external_id || s?.name), - errorSensors, - }), - ); - break; - } - case bulkSenorUploadErrorTypeEnum?.validation_failure: - default: { - const validationErrors = error?.response?.data?.errors ?? []; - yield put(bulkSensorsUploadValidationFailure(validationErrors)); - break; - } - } - break; - } - case 500: - default: { - yield put(bulkSensorsUploadFailure({ defaultFailure: true })); - console.log(error); - break; - } - } - } -} - export const getSensorReadings = createAction('getSensorReadingsSaga'); export function* getSensorReadingsSaga() { @@ -237,9 +129,6 @@ export function* getAllSensorReadingTypesSaga() { export default function* supportSaga() { yield takeLeading(sendMapToEmail.type, sendMapToEmailSaga); yield takeLeading(setSpotlightToShown.type, setSpotlightToShownSaga); - yield takeLeading(bulkUploadSensorsInfoFile.type, bulkUploadSensorsInfoFileSaga); yield takeLeading(getSensorReadings.type, getSensorReadingsSaga); yield takeLeading(getAllSensorReadingTypes.type, getAllSensorReadingTypesSaga); - yield takeLeading(resetBulkUploadSensorsInfoFile.type, resetBulkUploadSensorsInfoFileSaga); - yield takeLeading(resetShowTransitionModalState.type, resetShowTransitionModalStateSaga); } diff --git a/packages/webapp/src/containers/bulkSensorUploadSlice.js b/packages/webapp/src/containers/bulkSensorUploadSlice.js deleted file mode 100644 index 9112829df3..0000000000 --- a/packages/webapp/src/containers/bulkSensorUploadSlice.js +++ /dev/null @@ -1,81 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit'; - -const initialState = { - loading: false, - isBulkUploadSuccessful: false, - validationErrors: [], - success: [], - errorSensors: [], - showTransitionModal: false, - defaultFailure: false, -}; - -const bulkSensorsUploadSlice = createSlice({ - name: 'bulkSensorsUploadReducer', - initialState, - reducers: { - resetSensorsBulkUploadStates: (state, action) => { - Object.assign(state, { - loading: false, - isBulkUploadSuccessful: false, - validationErrors: [], - success: [], - errorSensors: [], - showTransitionModal: false, - defaultFailure: false, - }); - }, - bulkSensorsUploadLoading: (state, action) => { - state.loading = true; - state.isBulkUploadSuccessful = false; - state.validationErrors = []; - }, - bulkSensorsUploadSuccess: (state, { payload }) => { - if (state.loading) { - state.loading = false; - state.isBulkUploadSuccessful = true; - state.validationErrors = []; - state.errorSensors = []; - state.defaultFailure = false; - Object.assign(state, payload); - } - }, - bulkSensorsUploadFailure: (state, { payload }) => { - state.loading = false; - state.isBulkUploadSuccessful = false; - state.success = payload?.success ?? []; - state.errorSensors = payload?.errorSensors ?? []; - state.validationErrors = []; - state.defaultFailure = payload?.defaultFailure ?? false; - }, - bulkSensorsUploadValidationFailure: (state, { payload }) => { - state.loading = false; - state.isBulkUploadSuccessful = false; - Object.assign(state, { validationErrors: payload }); - }, - switchToAsyncSensorUpload: (state, { payload }) => { - Object.assign(state, { - loading: false, - isBulkUploadSuccessful: false, - showTransitionModal: payload, - validationErrors: [], - }); - }, - bulkSensorsUploadReInit: (state, action) => { - state.loading = false; - state.isBulkUploadSuccessful = false; - }, - }, -}); -export const { - resetSensorsBulkUploadStates, - bulkSensorsUploadLoading, - bulkSensorsUploadSuccess, - bulkSensorsUploadFailure, - bulkSensorsUploadValidationFailure, - switchToAsyncSensorUpload, - bulkSensorsUploadReInit, -} = bulkSensorsUploadSlice.actions; -export default bulkSensorsUploadSlice.reducer; -export const bulkSensorsUploadSliceSelector = (state) => - state?.entitiesReducer[bulkSensorsUploadSlice.name]; diff --git a/packages/webapp/src/store/reducer.js b/packages/webapp/src/store/reducer.js index f64abd0149..8da0f1c1a8 100644 --- a/packages/webapp/src/store/reducer.js +++ b/packages/webapp/src/store/reducer.js @@ -67,7 +67,6 @@ import mapCacheReducer from '../containers/Map/mapCacheSlice'; import mapSensorReducer from '../containers/Map/mapSensorSlice'; import sensorReadingTypesReducer from '../containers/sensorReadingTypesSlice'; import showedSpotlightReducer from '../containers/showedSpotlightSlice'; -import bulkSensorsUploadReducer from '../containers/bulkSensorUploadSlice'; import bulkSensorsReadingsReducer from '../containers/bulkSensorReadingsSlice'; import hookFormPersistReducer from '../containers/hooks/useHookFormPersist/hookFormPersistSlice'; import offlineDetectorReducer from '../containers/hooks/useOfflineDetector/offlineDetectorSlice'; @@ -184,7 +183,6 @@ const entitiesReducer = combineReducers({ waterValveReducer, sensorReducer, showedSpotlightReducer, - bulkSensorsUploadReducer, bulkSensorsReadingsReducer, managementPlanReducer, cropManagementPlanReducer, From c53bdf2b082ed0e0d75c67dd823cac7449253c55 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Mon, 3 Feb 2025 09:56:51 -0800 Subject: [PATCH 30/41] LF-4691 Add style for mobile view --- packages/webapp/src/components/Sensor/v2/styles.module.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/webapp/src/components/Sensor/v2/styles.module.scss b/packages/webapp/src/components/Sensor/v2/styles.module.scss index c2d4a5ec81..d4de40b5c3 100644 --- a/packages/webapp/src/components/Sensor/v2/styles.module.scss +++ b/packages/webapp/src/components/Sensor/v2/styles.module.scss @@ -13,8 +13,14 @@ * GNU General Public License for more details, see . */ +@import '../../../assets/mixin.scss'; + .wrapper { padding: 34px 8px 0 8px; + + @include xs-breakpoint { + padding: 24px 0 0 0; + } } .lead { From ba938205d0ff9f643bdaf15902afcc914c92de8f Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Mon, 3 Feb 2025 10:29:42 -0800 Subject: [PATCH 31/41] LF-4691 Cleanup --- packages/webapp/src/components/Sensor/v2/styles.module.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/webapp/src/components/Sensor/v2/styles.module.scss b/packages/webapp/src/components/Sensor/v2/styles.module.scss index d4de40b5c3..252887b524 100644 --- a/packages/webapp/src/components/Sensor/v2/styles.module.scss +++ b/packages/webapp/src/components/Sensor/v2/styles.module.scss @@ -56,7 +56,6 @@ .url { color: #000; - font-family: 'Open Sans'; font-size: 16px; } } From b1d1182796f10a4f789b352a0195fbb214d5e9b0 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Tue, 4 Feb 2025 09:35:52 -0800 Subject: [PATCH 32/41] Revert "LF-4691 Delete unused files and code for bulk sensor upload" This reverts commit 108d7073011d56dc45fd338ad85a818d491cb0ad. --- .../Modals/BulkSensorUploadModal/constants.js | 6 + .../Modals/BulkSensorUploadModal/index.jsx | 49 +++++ .../useValidateBulkSensorData.js | 208 ++++++++++++++++++ .../BulkSensorUploadModal/FileUploader.jsx | 81 +++++++ .../Modals/BulkSensorUploadModal/index.jsx | 97 ++++++++ .../BulkSensorUploadModal/styles.module.scss | 116 ++++++++++ .../BulkUploadTransitionModal/index.jsx | 23 ++ .../styles.module.scss | 3 + .../webapp/src/containers/Map/constants.js | 8 + packages/webapp/src/containers/Map/saga.js | 111 ++++++++++ .../src/containers/bulkSensorUploadSlice.js | 81 +++++++ packages/webapp/src/store/reducer.js | 2 + 12 files changed, 785 insertions(+) create mode 100644 packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/constants.js create mode 100644 packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/index.jsx create mode 100644 packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/useValidateBulkSensorData.js create mode 100644 packages/webapp/src/components/Modals/BulkSensorUploadModal/FileUploader.jsx create mode 100644 packages/webapp/src/components/Modals/BulkSensorUploadModal/index.jsx create mode 100644 packages/webapp/src/components/Modals/BulkSensorUploadModal/styles.module.scss create mode 100644 packages/webapp/src/components/Modals/BulkUploadTransitionModal/index.jsx create mode 100644 packages/webapp/src/components/Modals/BulkUploadTransitionModal/styles.module.scss create mode 100644 packages/webapp/src/containers/bulkSensorUploadSlice.js diff --git a/packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/constants.js b/packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/constants.js new file mode 100644 index 0000000000..221fb25e1d --- /dev/null +++ b/packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/constants.js @@ -0,0 +1,6 @@ +export const ErrorTypes = { + DEFAULT: -1, + INVALID_CSV: 0, + EMPTY_FILE: 1, + INVALID_FILE_TYPE: 2, +}; diff --git a/packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/index.jsx b/packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/index.jsx new file mode 100644 index 0000000000..873dc66a3b --- /dev/null +++ b/packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/index.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import PureBulkSensorUploadModal from '../../../Modals/BulkSensorUploadModal'; +import { useValidateBulkSensorData } from './useValidateBulkSensorData'; +import PropTypes from 'prop-types'; + +export default function BulkSensorUploadModal({ dismissModal, onUpload }) { + const { t } = useTranslation(); + + const { + onUploadClicked, + handleSelectedFile, + onShowErrorClick, + disabled, + selectedFileName, + fileInputRef, + errorCount, + onTemplateDownloadClick, + uploadErrorMessage, + errorTypeCode, + } = useValidateBulkSensorData(onUpload, t); + + return ( + + ); +} + +BulkSensorUploadModal.prototype = { + dismissModal: PropTypes.func, + onUpload: PropTypes.func, +}; diff --git a/packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/useValidateBulkSensorData.js b/packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/useValidateBulkSensorData.js new file mode 100644 index 0000000000..9f3730568e --- /dev/null +++ b/packages/webapp/src/components/Map/Modals/BulkSensorUploadModal/useValidateBulkSensorData.js @@ -0,0 +1,208 @@ +/* + * Copyright 2019, 2020, 2021, 2022 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 . + */ + +import { useEffect, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { bulkSensorsUploadSliceSelector } from '../../../../containers/bulkSensorUploadSlice'; +import { createSensorErrorDownload } from '../../../../util/sensor'; +import { ErrorTypes } from './constants'; +import parseSensorCsv from '@shared/validation/sensorCSV.js'; +import { getLanguageFromLocalStorage } from '../../../../util/getLanguageFromLocalStorage'; +import { languageCodes } from '../../../../hooks/useLanguageOptions'; + +const getSensorTranslations = async (language) => { + try { + // return english if language not supported + if (!languageCodes.includes(language)) { + throw `LiteFarm sensors does not currently support language ${language}`; + } + return await import(`../../../../../../shared/locales/${language}/sensorCSV.json`); + } catch (error) { + console.log(error); + return await import('../../../../../../shared/locales/en/sensorCSV.json'); + } +}; + +export function useValidateBulkSensorData(onUpload, t) { + const bulkSensorsUploadResponse = useSelector(bulkSensorsUploadSliceSelector); + const [disabled, setDisabled] = useState(null); + const [selectedFile, setSelectedFile] = useState(null); + const [selectedFileName, setSelectedFileName] = useState(''); + const [sheetErrors, setSheetErrors] = useState([]); + const [errorCount, setErrorCount] = useState(0); + const fileInputRef = useRef(null); + const [translatedUploadErrors, setTranslatedUploadErrors] = useState([]); + const [uploadErrorMessage, setUploadErrorMessage] = useState(''); + const [errorTypeCode, setErrorTypeCode] = useState(ErrorTypes.DEFAULT); + const lang = getLanguageFromLocalStorage(); + + useEffect(() => { + if (!disabled) setDisabled(0); + else setDisabled(bulkSensorsUploadResponse.loading ? -1 : 1); + }, [bulkSensorsUploadResponse?.loading]); + + // bulkSensorsUploadResponse?.validationErrors from store updates the sheetErrors + // the sheetErrors will be used as single source of truth to show validation + // errors on the modal frontend. + useEffect(() => { + let validationErrorsResponseList = bulkSensorsUploadResponse?.validationErrors || []; + if (validationErrorsResponseList.length) { + const translatedErrors = translateErrors(bulkSensorsUploadResponse?.validationErrors); + setErrorCount(translatedErrors.length); + setSheetErrors(translatedErrors); + setErrorTypeCode(ErrorTypes.INVALID_CSV); + setUploadErrorMessage(t('FARM_MAP.BULK_UPLOAD_SENSORS.UPLOAD_ERROR_MESSAGE')); + } + }, [bulkSensorsUploadResponse?.validationErrors]); + + useEffect(() => { + if (bulkSensorsUploadResponse?.errorSensors.length > 0) { + setErrorCount((curr) => curr + bulkSensorsUploadResponse.errorSensors.length); + const translatedErrors = translateErrors(bulkSensorsUploadResponse?.errorSensors); + setTranslatedUploadErrors(translatedErrors); + setErrorTypeCode(ErrorTypes.INVALID_CSV); + setUploadErrorMessage(t('FARM_MAP.BULK_UPLOAD_SENSORS.UPLOAD_ERROR_MESSAGE')); + } + }, [bulkSensorsUploadResponse?.errorSensors]); + + const translateErrors = (errors) => { + return errors?.map((e) => { + return { + row: e.row, + column: e.column, + errorMessage: e.variables ? t(e.translation_key, e.variables) : t(e.translation_key), + value: e?.value ?? '', + }; + }); + }; + + useEffect(() => { + if (bulkSensorsUploadResponse?.defaultFailure) { + setErrorCount(1); + setErrorTypeCode(ErrorTypes.INVALID_CSV); + setUploadErrorMessage(t('FARM_MAP.BULK_UPLOAD_SENSORS.UPLOAD_ERROR_MESSAGE')); + } + }, [bulkSensorsUploadResponse?.defaultFailure]); + + const onUploadClicked = async (e) => { + e.preventDefault(); + if (!selectedFile) return; + onUpload(selectedFile); + }; + + const getFileExtension = (fileName) => fileName.split('.').pop(); + + const handleSelectedFile = async (e) => { + const file = e.target.files[0]; + if (!file) return; + try { + const fileExtension = getFileExtension(file?.name); + setSelectedFileName(file?.name); + setSelectedFile(file); + + const fileString = await readFile(file); + const translations = await getSensorTranslations(lang); + const { data, errors } = parseSensorCsv(fileString, lang, translations); + + const translatedErrors = translateErrors(errors); + + if (data.length >= 100) { + translatedErrors.push({ + row: 1, + column: 'N/A', + errorMessage: t('FARM_MAP.BULK_UPLOAD_SENSORS.VALIDATION.FILE_ROW_LIMIT_EXCEEDED'), + value: '', + }); + } + + if (fileExtension !== 'csv') { + setErrorTypeCode(ErrorTypes.INVALID_FILE_TYPE); + setUploadErrorMessage(t('FARM_MAP.BULK_UPLOAD_SENSORS.INVALID_FILE_TYPE')); + } else if (errors.length !== 0) { + setErrorTypeCode(ErrorTypes.INVALID_CSV); + setUploadErrorMessage(t('FARM_MAP.BULK_UPLOAD_SENSORS.UPLOAD_ERROR_MESSAGE')); + } else if (data.length === 0) { + setErrorTypeCode(ErrorTypes.EMPTY_FILE); + setUploadErrorMessage(t('FARM_MAP.BULK_UPLOAD_SENSORS.EMPTY_FILE_UPLOAD_ERROR_MESSAGE')); + } + + const newErrorCount = data.length === 0 ? 1 : translatedErrors.length; + setErrorCount(newErrorCount); + setSheetErrors(translatedErrors); + setDisabled(() => (newErrorCount === 0 ? 1 : 0)); + } catch (err) { + console.error(err); + } + }; + + const onShowErrorClick = async (errorCode) => { + if (errorCode === 2) { + await onTemplateDownloadClick(); + return; + } + if (sheetErrors.length) { + const inputFile = fileInputRef.current.files[0]; + if (inputFile) { + const downloadFileName = `${inputFile.name.replace(/.csv/, '')}_errors.txt`; + createSensorErrorDownload(downloadFileName, sheetErrors, 'validation'); + } + } else if (bulkSensorsUploadResponse?.defaultFailure) { + createSensorErrorDownload('sensor-upload-outcomes.txt', null, 'generic'); + } else { + createSensorErrorDownload( + 'sensor-upload-outcomes.txt', + translatedUploadErrors, + 'claim', + bulkSensorsUploadResponse?.success, + ); + } + }; + + const onTemplateDownloadClick = async () => { + const element = document.createElement('a'); + const { CSV_HEADER_TRANSLATIONS } = await getSensorTranslations(lang); + const file = new Blob([`${'\ufeff'}${Object.values(CSV_HEADER_TRANSLATIONS).join(',')}`], { + type: 'text/plain', + }); + element.href = URL.createObjectURL(file); + element.download = 'Add-sensors-to-LiteFarm.csv'; + document.body.appendChild(element); + element.click(); + }; + + return { + onUploadClicked, + handleSelectedFile, + onShowErrorClick, + onTemplateDownloadClick, + disabled, + selectedFileName, + fileInputRef, + errorCount, + uploadErrorMessage, + errorTypeCode, + }; +} + +function readFile(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + resolve(reader.result); + }; + reader.onerror = reject; + reader.readAsText(file); + }); +} diff --git a/packages/webapp/src/components/Modals/BulkSensorUploadModal/FileUploader.jsx b/packages/webapp/src/components/Modals/BulkSensorUploadModal/FileUploader.jsx new file mode 100644 index 0000000000..2e89eaba5d --- /dev/null +++ b/packages/webapp/src/components/Modals/BulkSensorUploadModal/FileUploader.jsx @@ -0,0 +1,81 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styles from './styles.module.scss'; +import { ReactComponent as UploadIcon } from '../../../assets/images/map/upload.svg'; +import { Label, Underlined } from '../../Typography'; +import clsx from 'clsx'; + +export default function FileUploader({ + handleSelectedFile, + acceptedFormat, + selectedFileName, + fileInputRef, + isValid, + onShowErrorClick, + uploadErrorLink, + uploadErrorMessage, + errorTypeCode, + invalidFileTypeErrorLink, +}) { + const handleClick = (event) => { + if (fileInputRef.current) { + fileInputRef.current.value = null; + } + fileInputRef.current.click(); + }; + + const handleChange = (event) => handleSelectedFile(event); + return ( + <> +
+ + +
+ + {!isValid && ( +
+ +
+ )} + + ); +} + +FileUploader.prototype = { + handleFile: PropTypes.func, + selectedFileName: PropTypes.string, + acceptedFormat: PropTypes.string, + isValid: PropTypes.bool, + onShowErrorClick: PropTypes.func, + uploadErrorLink: PropTypes.string, + uploadErrorMessage: PropTypes.string, + errorTypeCode: PropTypes.number, + invalidFileTypeErrorLink: PropTypes.string, +}; diff --git a/packages/webapp/src/components/Modals/BulkSensorUploadModal/index.jsx b/packages/webapp/src/components/Modals/BulkSensorUploadModal/index.jsx new file mode 100644 index 0000000000..7dbdd492f5 --- /dev/null +++ b/packages/webapp/src/components/Modals/BulkSensorUploadModal/index.jsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Modal } from '..'; +import styles from './styles.module.scss'; +import { Semibold, Underlined, Label } from '../../Typography'; +import Button from '../../Form/Button'; +import { BsChevronLeft } from 'react-icons/bs'; +import PropTypes from 'prop-types'; +import FileUploader from './FileUploader'; +export default function BulkSensorUploadModal({ + title, + uploadLinkMessage, + uploadInstructionMessage, + uploadPlaceholder, + dismissModal, + onUpload, + disabled, + handleSelectedFile, + selectedFileName, + fileInputRef, + errorCount, + onShowErrorClick, + onTemplateDownloadClick, + uploadErrorMessage, + uploadErrorLink, + errorTypeCode, + invalidFileTypeErrorLink, +}) { + const { t } = useTranslation(); + + return ( + +
+
+ + {title} +
+ +
+
+ +
+ + + +
+
+ ); +} + +BulkSensorUploadModal.prototype = { + title: PropTypes.string, + uploadInstructionMessage: PropTypes.string, + uploadPlaceholder: PropTypes.string, + dismissModal: PropTypes.func, + disabled: PropTypes.bool, + onUpload: PropTypes.func, + handleSelectedFile: PropTypes.func, + selectedFileName: PropTypes.string, + fileInputRef: PropTypes.func, + errorCount: PropTypes.number, + onShowErrorClick: PropTypes.func, + onTemplateDownloadClick: PropTypes.func, + uploadErrorMessage: PropTypes.string, + uploadErrorLink: PropTypes.string, + errorTypeCode: PropTypes.number, + invalidFileTypeErrorLink: PropTypes.string, +}; diff --git a/packages/webapp/src/components/Modals/BulkSensorUploadModal/styles.module.scss b/packages/webapp/src/components/Modals/BulkSensorUploadModal/styles.module.scss new file mode 100644 index 0000000000..fd704f3a4b --- /dev/null +++ b/packages/webapp/src/components/Modals/BulkSensorUploadModal/styles.module.scss @@ -0,0 +1,116 @@ +/* + * Copyright 2019, 2020, 2021, 2022 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 . + */ + +.container { + display: flex; + flex-direction: column; + max-width: 350px; + width: 90vw; + background: #fafafd; + border-radius: 7.05466px; + position: relative; + padding: 24px; +} + +.title { + color: var(--teal700); + line-height: 24px; + margin-bottom: 4px; + margin-left: 12px; +} + +.buttonUpload{ + margin-top: 20px; +} + +.uploadSelectInput{ + border: 1px solid var(--grey400); + box-sizing: border-box; + border-radius: 4px; + height: 48px; + padding-left: 8px; + font-size: 16px; + line-height: 24px; + color: var(--fontColor); + background-color: transparent; + background-position: calc(100% - 36px) 50%; + font-family: Open Sans,SansSerif,serif; + display: flex; + align-items: center; + cursor: pointer; +} + +.invalidateUploadSelectInput{ + border: 1px solid var(--red700); + box-sizing: border-box; + border-radius: 4px; + height: 48px; + padding-left: 8px; + font-size: 16px; + line-height: 24px; + color: var(--fontColor); + background-color: transparent; + background-position: calc(100% - 36px) 50%; + font-family: Open Sans,SansSerif,serif; + display: flex; + align-items: center; + cursor: pointer; +} + +.uploadPlaceholder{ + margin-top: 24px; + margin-bottom: 4px; +} + +.buttonContainer { + background: none; + border: 0; + color: var(--buttonPrimary); + cursor: pointer; + max-height: 32px; +} + +.modalHeaderWrapper { + display: flex; + margin-bottom: 16px; + align-items: center; +} + +.fileNameLabel{ + margin-bottom: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.uploadIconContainer{ + font-size: 20px; + color: var(--grey600); + margin-right: 8px; + min-width: 20px; +} + +.csvErrorMessageWrapper{ + font-size: 12px; + color: var(--red700); + margin-top: 8px; +} + +.errorMessage{ + text-decoration: underline; + font-size: 12px; + color: var(--red700); + cursor: pointer; +} \ No newline at end of file diff --git a/packages/webapp/src/components/Modals/BulkUploadTransitionModal/index.jsx b/packages/webapp/src/components/Modals/BulkUploadTransitionModal/index.jsx new file mode 100644 index 0000000000..05282a1910 --- /dev/null +++ b/packages/webapp/src/components/Modals/BulkUploadTransitionModal/index.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import ModalComponent from '../ModalComponent/v2'; +import { useTranslation } from 'react-i18next'; +import Button from '../../Form/Button'; +import styles from './styles.module.scss'; + +export default function BulkUploadTransitionModal({ dismissModal }) { + const { t } = useTranslation(); + return ( + + + + } + /> + ); +} diff --git a/packages/webapp/src/components/Modals/BulkUploadTransitionModal/styles.module.scss b/packages/webapp/src/components/Modals/BulkUploadTransitionModal/styles.module.scss new file mode 100644 index 0000000000..80d74d6342 --- /dev/null +++ b/packages/webapp/src/components/Modals/BulkUploadTransitionModal/styles.module.scss @@ -0,0 +1,3 @@ +.button { + width: 96px; +} \ No newline at end of file diff --git a/packages/webapp/src/containers/Map/constants.js b/packages/webapp/src/containers/Map/constants.js index 02e1b5b6d3..5a84362318 100644 --- a/packages/webapp/src/containers/Map/constants.js +++ b/packages/webapp/src/containers/Map/constants.js @@ -66,6 +66,12 @@ export const locationEnum = { residence: 'residence', }; +export const bulkSenorUploadErrorTypeEnum = { + unable_to_claim_all_sensors: 'unable_to_claim_all_sensors', + validation_failure: 'validation_failure', + timeout_and_show_transition_modal: 'timeout', +}; + export const polygonPath = (path, width, maps) => { const { leftPoints, rightPoints } = path.reduce(linePathPolygonConstructor, { leftPoints: [], @@ -77,6 +83,8 @@ export const polygonPath = (path, width, maps) => { return leftPoints.concat(rightPoints.reverse()); }; +export const SENSOR_BULK_UPLOAD_SUCCESS = 'SENSOR_BULK_UPLOAD_SUCCESS'; + const linePathPolygonConstructor = (innerState, point, i, path) => { const { bearings, leftPoints, rightPoints, width, maps } = innerState; const { diff --git a/packages/webapp/src/containers/Map/saga.js b/packages/webapp/src/containers/Map/saga.js index c773e7975e..e65345c9f7 100644 --- a/packages/webapp/src/containers/Map/saga.js +++ b/packages/webapp/src/containers/Map/saga.js @@ -19,11 +19,21 @@ import { url, sensorUrl } from '../../apiConfig'; import i18n from '../../locales/i18n'; import { axios, getHeader } from '../saga'; import { loginSelector, userFarmSelector } from '../userFarmSlice'; +import { canShowSuccessHeader, setSuccessMessage } from '../../containers/mapSlice'; import { patchSpotlightFlagsFailure, patchSpotlightFlagsSuccess, spotlightLoading, } from '../showedSpotlightSlice'; +import { + bulkSensorsUploadFailure, + bulkSensorsUploadSuccess, + bulkSensorsUploadLoading, + bulkSensorsUploadValidationFailure, + resetSensorsBulkUploadStates, + switchToAsyncSensorUpload, +} from '../bulkSensorUploadSlice'; +import { bulkSenorUploadErrorTypeEnum } from './constants'; import { enqueueErrorSnackbar } from '../Snackbar/snackbarSlice'; import { @@ -31,14 +41,22 @@ import { onLoadingSensorReadingStart, onLoadingSensorReadingFail, } from './mapSensorSlice'; +import { postManySensorsSuccess } from '../sensorSlice'; import { getSensorReadingTypesSuccess, onLoadingSensorReadingTypesFail, onLoadingSensorReadingTypesStart, } from '../sensorReadingTypesSlice'; +import { setMapCache } from './mapCacheSlice'; const sendMapToEmailUrl = (farm_id) => `${url}/export/map/farm/${farm_id}`; const showedSpotlightUrl = () => `${url}/showed_spotlight`; +const bulkUploadSensorsInfoUrl = () => { + let url = sensorUrl; + const testTimer = localStorage.getItem('sensorUploadTimer'); + if (testTimer) url += `?sensorUploadTimer=${testTimer}`; + return url; +}; export const sendMapToEmail = createAction(`sendMapToEmailSaga`); @@ -88,6 +106,96 @@ export function* setSpotlightToShownSaga({ payload: spotlights }) { } } +export const bulkUploadSensorsInfoFile = createAction(`bulkUploadSensorsInfoFileSaga`); +export const resetBulkUploadSensorsInfoFile = createAction(`resetBulkUploadSensorsInfoFileSaga`); +export const resetShowTransitionModalState = createAction(`resetShowTransitionModalStateSaga`); + +export function* resetBulkUploadSensorsInfoFileSaga() { + yield put(resetSensorsBulkUploadStates()); +} + +export function* resetShowTransitionModalStateSaga() { + yield put(switchToAsyncSensorUpload(false)); +} + +export function* bulkUploadSensorsInfoFileSaga({ payload: { file } }) { + try { + yield put(bulkSensorsUploadLoading()); + const { farm_id } = yield select(userFarmSelector); + const formData = new FormData(); + formData.append('sensors', file); + const fileUploadResponse = yield call(axios.post, bulkUploadSensorsInfoUrl(), formData, { + headers: { + 'Content-Type': 'multipart/form-data', + Authorization: 'Bearer ' + localStorage.getItem('id_token'), + farm_id: farm_id, + }, + }); + + switch (fileUploadResponse.status) { + case 200: { + yield put(setMapCache({ maxZoom: undefined, farm_id })); + yield put(bulkSensorsUploadSuccess()); + yield put(postManySensorsSuccess(fileUploadResponse?.data?.sensors)); + yield put(getAllSensorReadingTypes()); + yield put( + setSuccessMessage([ + i18n.t('FARM_MAP.MAP_FILTER.SENSOR'), + i18n.t('message:MAP.SUCCESS_UPLOAD'), + ]), + ); + yield put(canShowSuccessHeader(true)); + break; + } + case 202: { + yield put(switchToAsyncSensorUpload(true)); + break; + } + default: { + yield put(bulkSensorsUploadFailure()); + yield put(enqueueErrorSnackbar(i18n.t('message:BULK_UPLOAD.ERROR.UPLOAD'))); + } + } + } catch (error) { + switch (error?.response?.status) { + case 400: { + const errorType = error?.response?.data?.error_type || ''; + switch (errorType) { + case bulkSenorUploadErrorTypeEnum?.unable_to_claim_all_sensors: { + const { success, errorSensors } = error?.response?.data ?? { + success: [], + errorSensors: [], + }; + if (success.length > 0) { + yield put(postManySensorsSuccess(success)); + } + yield put( + bulkSensorsUploadFailure({ + success: success.map((s) => s?.sensor?.external_id || s?.name), + errorSensors, + }), + ); + break; + } + case bulkSenorUploadErrorTypeEnum?.validation_failure: + default: { + const validationErrors = error?.response?.data?.errors ?? []; + yield put(bulkSensorsUploadValidationFailure(validationErrors)); + break; + } + } + break; + } + case 500: + default: { + yield put(bulkSensorsUploadFailure({ defaultFailure: true })); + console.log(error); + break; + } + } + } +} + export const getSensorReadings = createAction('getSensorReadingsSaga'); export function* getSensorReadingsSaga() { @@ -129,6 +237,9 @@ export function* getAllSensorReadingTypesSaga() { export default function* supportSaga() { yield takeLeading(sendMapToEmail.type, sendMapToEmailSaga); yield takeLeading(setSpotlightToShown.type, setSpotlightToShownSaga); + yield takeLeading(bulkUploadSensorsInfoFile.type, bulkUploadSensorsInfoFileSaga); yield takeLeading(getSensorReadings.type, getSensorReadingsSaga); yield takeLeading(getAllSensorReadingTypes.type, getAllSensorReadingTypesSaga); + yield takeLeading(resetBulkUploadSensorsInfoFile.type, resetBulkUploadSensorsInfoFileSaga); + yield takeLeading(resetShowTransitionModalState.type, resetShowTransitionModalStateSaga); } diff --git a/packages/webapp/src/containers/bulkSensorUploadSlice.js b/packages/webapp/src/containers/bulkSensorUploadSlice.js new file mode 100644 index 0000000000..9112829df3 --- /dev/null +++ b/packages/webapp/src/containers/bulkSensorUploadSlice.js @@ -0,0 +1,81 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + loading: false, + isBulkUploadSuccessful: false, + validationErrors: [], + success: [], + errorSensors: [], + showTransitionModal: false, + defaultFailure: false, +}; + +const bulkSensorsUploadSlice = createSlice({ + name: 'bulkSensorsUploadReducer', + initialState, + reducers: { + resetSensorsBulkUploadStates: (state, action) => { + Object.assign(state, { + loading: false, + isBulkUploadSuccessful: false, + validationErrors: [], + success: [], + errorSensors: [], + showTransitionModal: false, + defaultFailure: false, + }); + }, + bulkSensorsUploadLoading: (state, action) => { + state.loading = true; + state.isBulkUploadSuccessful = false; + state.validationErrors = []; + }, + bulkSensorsUploadSuccess: (state, { payload }) => { + if (state.loading) { + state.loading = false; + state.isBulkUploadSuccessful = true; + state.validationErrors = []; + state.errorSensors = []; + state.defaultFailure = false; + Object.assign(state, payload); + } + }, + bulkSensorsUploadFailure: (state, { payload }) => { + state.loading = false; + state.isBulkUploadSuccessful = false; + state.success = payload?.success ?? []; + state.errorSensors = payload?.errorSensors ?? []; + state.validationErrors = []; + state.defaultFailure = payload?.defaultFailure ?? false; + }, + bulkSensorsUploadValidationFailure: (state, { payload }) => { + state.loading = false; + state.isBulkUploadSuccessful = false; + Object.assign(state, { validationErrors: payload }); + }, + switchToAsyncSensorUpload: (state, { payload }) => { + Object.assign(state, { + loading: false, + isBulkUploadSuccessful: false, + showTransitionModal: payload, + validationErrors: [], + }); + }, + bulkSensorsUploadReInit: (state, action) => { + state.loading = false; + state.isBulkUploadSuccessful = false; + }, + }, +}); +export const { + resetSensorsBulkUploadStates, + bulkSensorsUploadLoading, + bulkSensorsUploadSuccess, + bulkSensorsUploadFailure, + bulkSensorsUploadValidationFailure, + switchToAsyncSensorUpload, + bulkSensorsUploadReInit, +} = bulkSensorsUploadSlice.actions; +export default bulkSensorsUploadSlice.reducer; +export const bulkSensorsUploadSliceSelector = (state) => + state?.entitiesReducer[bulkSensorsUploadSlice.name]; diff --git a/packages/webapp/src/store/reducer.js b/packages/webapp/src/store/reducer.js index 8da0f1c1a8..f64abd0149 100644 --- a/packages/webapp/src/store/reducer.js +++ b/packages/webapp/src/store/reducer.js @@ -67,6 +67,7 @@ import mapCacheReducer from '../containers/Map/mapCacheSlice'; import mapSensorReducer from '../containers/Map/mapSensorSlice'; import sensorReadingTypesReducer from '../containers/sensorReadingTypesSlice'; import showedSpotlightReducer from '../containers/showedSpotlightSlice'; +import bulkSensorsUploadReducer from '../containers/bulkSensorUploadSlice'; import bulkSensorsReadingsReducer from '../containers/bulkSensorReadingsSlice'; import hookFormPersistReducer from '../containers/hooks/useHookFormPersist/hookFormPersistSlice'; import offlineDetectorReducer from '../containers/hooks/useOfflineDetector/offlineDetectorSlice'; @@ -183,6 +184,7 @@ const entitiesReducer = combineReducers({ waterValveReducer, sensorReducer, showedSpotlightReducer, + bulkSensorsUploadReducer, bulkSensorsReadingsReducer, managementPlanReducer, cropManagementPlanReducer, From 1e4695d2568ab922dfe09a76f92bd8ba82edcc5f Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Tue, 4 Feb 2025 09:44:52 -0800 Subject: [PATCH 33/41] LF-4691 Add constant for ESCI partner ID --- .../PointDetails/SensorDetail/v2/PostSensor.tsx | 3 ++- .../PointDetails/SensorDetail/v2/constants.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/constants.ts diff --git a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx index 7ed95517dd..b77885ac64 100644 --- a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx +++ b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx @@ -21,6 +21,7 @@ import PageTitle from '../../../../../components/PageTitle/v2'; import { enqueueErrorSnackbar } from '../../../../Snackbar/snackbarSlice'; import { useAddFarmAddonMutation, useLazyGetSensorsQuery } from '../../../../../store/api/apiSlice'; import { type AddSensorsFormFields, FarmAddonField, PARTNER } from './types'; +import { ESCI_PARTNER_ID } from './constants'; import styles from './styles.module.scss'; interface PostSensorProps { @@ -67,7 +68,7 @@ const PostSensor = ({ history, isCompactSideMenu }: PostSensorProps) => { ]; const defaultFormValues = { - [PARTNER]: { [FarmAddonField.PARTNER_ID]: 1, [FarmAddonField.ORG_UUID]: '' }, + [PARTNER]: { [FarmAddonField.PARTNER_ID]: ESCI_PARTNER_ID, [FarmAddonField.ORG_UUID]: '' }, }; return ( diff --git a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/constants.ts b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/constants.ts new file mode 100644 index 0000000000..c06292a14a --- /dev/null +++ b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/constants.ts @@ -0,0 +1,16 @@ +/* + * 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 . + */ + +export const ESCI_PARTNER_ID = 1; From b6aba3a5e44f486c02e4397f3e2ba554b4a2948d Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Wed, 5 Feb 2025 14:51:03 -0800 Subject: [PATCH 34/41] LF-4691 Update WithStepperProgressBar * accept showLoading instead of onContinueAction in steps * accept onAfterSave prop * avoid page transition blocking after form is saved * adjust stories --- .../ContextForm/WithStepperProgressBar.tsx | 37 ++++++++++--------- .../ContextForm/ContextForm.stories.tsx | 33 +++++++++++++++-- 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/packages/webapp/src/components/Form/ContextForm/WithStepperProgressBar.tsx b/packages/webapp/src/components/Form/ContextForm/WithStepperProgressBar.tsx index fe4c98e3f6..5a8d4f711d 100644 --- a/packages/webapp/src/components/Form/ContextForm/WithStepperProgressBar.tsx +++ b/packages/webapp/src/components/Form/ContextForm/WithStepperProgressBar.tsx @@ -36,7 +36,6 @@ interface WithStepperProgressBarProps { steps: { formContent: ReactNode; title: string; - onContinueAction?: (values: any) => Promise; dataName?: string; }[]; activeStepIndex: number; @@ -67,6 +66,8 @@ interface WithStepperProgressBarProps { setShowCancelFlow?: React.Dispatch>; headerComponent?: ((props: HeaderProps) => JSX.Element) | null; showPreviousButton?: boolean; + showLoading?: boolean; + onAfterSave?: () => void; } export const WithStepperProgressBar = ({ @@ -94,6 +95,8 @@ export const WithStepperProgressBar = ({ setShowCancelFlow, headerComponent = StepperProgressBar, showPreviousButton = true, + showLoading, + onAfterSave, }: WithStepperProgressBarProps) => { const [transition, setTransition] = useState<{ unblock?: () => void; retry?: () => void }>({ unblock: undefined, @@ -101,6 +104,7 @@ export const WithStepperProgressBar = ({ }); const [isSaving, setIsSaving] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [isSaved, setIsSaved] = useState(false); const isSummaryPage = hasSummaryWithinForm && activeStepIndex === steps.length - 1; const isSingleStep = steps.length === 1; @@ -108,7 +112,7 @@ export const WithStepperProgressBar = ({ // Block the page transition // https://github.com/remix-run/history/blob/dev/docs/blocking-transitions.md useEffect(() => { - if (isSummaryPage || !isDirty) { + if (isSummaryPage || !isDirty || isSaved) { return; } const unblock = history.block((tx) => { @@ -116,7 +120,13 @@ export const WithStepperProgressBar = ({ }); return () => unblock(); - }, [isSummaryPage, isDirty, history]); + }, [isSummaryPage, isDirty, history, isSaved]); + + useEffect(() => { + if (isSaved && onAfterSave) { + onAfterSave(); + } + }, [isSaved, onAfterSave]); useEffect(() => { // Reset loading state whenever the step changes @@ -141,23 +151,16 @@ export const WithStepperProgressBar = ({ }; const onContinue = async () => { - const { onContinueAction } = steps[activeStepIndex]; - - if (onContinueAction) { + if (isFinalStep) { setIsLoading(true); + setIsSaving(true); try { - // Execute the custom action for the current step before proceeding to the next one - await onContinueAction(getValues()); + await handleSubmit((data: FieldValues) => onSave(data, onSuccess, setFormResultData))(); + setIsSaved(true); } catch (error) { - console.error(error); setIsLoading(false); - return; + console.error(error); } - } - - if (isFinalStep) { - setIsSaving(true); - await handleSubmit((data: FieldValues) => onSave(data, onSuccess, setFormResultData))(); setIsSaving(false); return; } @@ -181,7 +184,7 @@ export const WithStepperProgressBar = ({ setShowCancelFlow?.(false); }; - if (isLoading) { + if (showLoading && isLoading) { return ( ); @@ -239,7 +242,7 @@ const StepperProgressBarWrapper = ({ headerComponent, ...stepperProgressBarProps }: StepperProgressBarWrapperProps) => { - if (isSingleStep) { + if (isSingleStep && !headerComponent) { return <>{children}; } diff --git a/packages/webapp/src/stories/ContextForm/ContextForm.stories.tsx b/packages/webapp/src/stories/ContextForm/ContextForm.stories.tsx index 34739f03c0..787d0aa148 100644 --- a/packages/webapp/src/stories/ContextForm/ContextForm.stories.tsx +++ b/packages/webapp/src/stories/ContextForm/ContextForm.stories.tsx @@ -114,21 +114,48 @@ const asyncFunc = async (status: 'success' | 'fail') => { }); }; -export const StepperFormWithCustomActionOnContinue: Story = { +export const StepperSuccessFormWithLoading: Story = { args: { ...stepperFormCommonProps, hasSummaryWithinForm: true, + showLoading: true, + onSave: async (data: any, onSuccess: () => void) => { + await asyncFunc('success'); + onSuccess(); + }, + getSteps: () => [ + { + title: 'Page 1', + FormContent: () =>
Page 1
, + dataName: 'sensor', + }, + { + title: 'Page 2', + FormContent: () =>
Page 2
, + }, + { + title: 'Done', + FormContent: () =>
Summary
, + }, + ], + }, +}; + +export const StepperFailedFormWithLoading: Story = { + args: { + ...stepperFormCommonProps, + hasSummaryWithinForm: true, + showLoading: true, + onSave: () => asyncFunc('fail'), getSteps: () => [ { title: 'Page 1', FormContent: () =>
Page 1
, - onContinueAction: () => asyncFunc('success'), dataName: 'sensor', }, { title: 'Page 2', FormContent: () =>
Page 2
, - onContinueAction: () => asyncFunc('fail'), }, { title: 'Done', From a559b2fbb80566d9e8f75669c0a1d9c0e63b081f Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Wed, 5 Feb 2025 15:03:12 -0800 Subject: [PATCH 35/41] LF-4691 Add sensors page URL --- packages/webapp/src/util/siteMapConstants.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/webapp/src/util/siteMapConstants.ts b/packages/webapp/src/util/siteMapConstants.ts index 20dd5d424e..eb6b1ca765 100644 --- a/packages/webapp/src/util/siteMapConstants.ts +++ b/packages/webapp/src/util/siteMapConstants.ts @@ -92,3 +92,6 @@ export const createCompleteTaskUrl = (id: string | number, hasAnimals: boolean): // Maps export const MAP_URL = '/map'; export const POST_SENSOR_URL = '/create_location/sensor'; + +// Sensors +export const SENSORS = '/sensors'; From c6be0fb323b433547da62231dea900eaafb20663 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Wed, 5 Feb 2025 15:16:42 -0800 Subject: [PATCH 36/41] LF-4691 Adjust PostSensor --- .../SensorDetail/v2/PostSensor.tsx | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx index b77885ac64..562d48db34 100644 --- a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx +++ b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx @@ -22,6 +22,7 @@ import { enqueueErrorSnackbar } from '../../../../Snackbar/snackbarSlice'; import { useAddFarmAddonMutation, useLazyGetSensorsQuery } from '../../../../../store/api/apiSlice'; import { type AddSensorsFormFields, FarmAddonField, PARTNER } from './types'; import { ESCI_PARTNER_ID } from './constants'; +import { SENSORS } from '../../../../../util/siteMapConstants'; import styles from './styles.module.scss'; interface PostSensorProps { @@ -51,21 +52,16 @@ const PostSensor = ({ history, isCompactSideMenu }: PostSensorProps) => { } }; - const onSave = async (data: AddSensorsFormFields, onSuccess: () => void) => { - // Fetch sensors - await triggerGetSensors(); + const onSave = async (data: AddSensorsFormFields) => { + await linkEsci(data); + }; - // Proceed to sensors view - onSuccess(); + const onAfterSave = () => { + triggerGetSensors(); + history.push(SENSORS); }; - const getFormSteps = () => [ - { - FormContent: () => , - onContinueAction: linkEsci, - }, - { FormContent: () =>
ESCI devices view
}, - ]; + const getFormSteps = () => [{ FormContent: Partners }]; const defaultFormValues = { [PARTNER]: { [FarmAddonField.PARTNER_ID]: ESCI_PARTNER_ID, [FarmAddonField.ORG_UUID]: '' }, @@ -79,12 +75,13 @@ const PostSensor = ({ history, isCompactSideMenu }: PostSensorProps) => { getSteps={getFormSteps} defaultFormValues={defaultFormValues} variant={Variant.STEPPER_PROGRESS_BAR} - hasSummaryWithinForm={true} onSave={onSave} headerComponent={PageTitle} showPreviousButton={false} formMode="onChange" isCompactSideMenu={isCompactSideMenu} + onAfterSave={onAfterSave} + showLoading // TODO: Make sure LF-4704 is mreged before the release. Otherwise cancelModalTitle is required />
From 9e49b222e8bf249be7cd9c36203e58a763edc160 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Wed, 5 Feb 2025 15:23:08 -0800 Subject: [PATCH 37/41] LF-4691 Update PARTNERS constant --- packages/webapp/src/components/Sensor/v2/Partners.tsx | 6 ++---- .../PointDetails/SensorDetail/v2/PostSensor.tsx | 4 ++-- .../PointDetails/SensorDetail/v2/constants.ts | 6 +++++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/webapp/src/components/Sensor/v2/Partners.tsx b/packages/webapp/src/components/Sensor/v2/Partners.tsx index 1f19b73486..c79b017445 100644 --- a/packages/webapp/src/components/Sensor/v2/Partners.tsx +++ b/packages/webapp/src/components/Sensor/v2/Partners.tsx @@ -19,7 +19,7 @@ import { validate as uuidValidate } from 'uuid'; import Input, { getInputErrors } from '../../Form/Input'; import InputBaseLabel from '../../Form/InputBase/InputBaseLabel'; import { Main } from '../../Typography'; -import EsciLogo from '../../../assets/images/partners/esci_logo.png'; +import { PARTNERS } from '../../../containers/LocationDetails/PointDetails/SensorDetail/v2/constants'; import { AddSensorsFormFields, FarmAddonField, @@ -37,8 +37,6 @@ const validateUuidFormat = (value: string, errorMessage: string) => { return uuidValidate(value) || errorMessage; }; -const PARTNERS = [{ name: 'Ensemble scientific', url: 'www.esci.io', logoPath: EsciLogo }]; - const Partner = ({ name, url, logoPath }: { name: string; url: string; logoPath: string }) => { return (
@@ -67,7 +65,7 @@ const Partners = ({ hasActiveConnection }: PartnersProps) => { return (
{t('SENSOR.ESCI.CURRENT_SUPPORT')}
- {PARTNERS.map((data) => ( + {Object.values(PARTNERS).map((data) => ( ))} {hasActiveConnection.esci ? ( diff --git a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx index 562d48db34..76fe48ca0e 100644 --- a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx +++ b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/PostSensor.tsx @@ -21,7 +21,7 @@ import PageTitle from '../../../../../components/PageTitle/v2'; import { enqueueErrorSnackbar } from '../../../../Snackbar/snackbarSlice'; import { useAddFarmAddonMutation, useLazyGetSensorsQuery } from '../../../../../store/api/apiSlice'; import { type AddSensorsFormFields, FarmAddonField, PARTNER } from './types'; -import { ESCI_PARTNER_ID } from './constants'; +import { PARTNERS } from './constants'; import { SENSORS } from '../../../../../util/siteMapConstants'; import styles from './styles.module.scss'; @@ -64,7 +64,7 @@ const PostSensor = ({ history, isCompactSideMenu }: PostSensorProps) => { const getFormSteps = () => [{ FormContent: Partners }]; const defaultFormValues = { - [PARTNER]: { [FarmAddonField.PARTNER_ID]: ESCI_PARTNER_ID, [FarmAddonField.ORG_UUID]: '' }, + [PARTNER]: { [FarmAddonField.PARTNER_ID]: PARTNERS.ESCI.id, [FarmAddonField.ORG_UUID]: '' }, }; return ( diff --git a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/constants.ts b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/constants.ts index c06292a14a..92b7352cd6 100644 --- a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/constants.ts +++ b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/constants.ts @@ -13,4 +13,8 @@ * GNU General Public License for more details, see . */ -export const ESCI_PARTNER_ID = 1; +import EsciLogo from '../../../../../assets/images/partners/esci_logo.png'; + +export const PARTNERS = { + ESCI: { id: 1, name: 'Ensemble scientific', url: 'www.esci.io', logoPath: EsciLogo }, +}; From 8bf9d807f5a55e64e5a71358767b192277564c34 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Wed, 5 Feb 2025 15:27:30 -0800 Subject: [PATCH 38/41] LF-4691 Fix capitalization in 'Ensemble Scientific' --- packages/webapp/public/locales/en/translation.json | 4 ++-- .../LocationDetails/PointDetails/SensorDetail/v2/constants.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/webapp/public/locales/en/translation.json b/packages/webapp/public/locales/en/translation.json index bdd67799de..662a7a8048 100644 --- a/packages/webapp/public/locales/en/translation.json +++ b/packages/webapp/public/locales/en/translation.json @@ -1804,9 +1804,9 @@ "ESCI": { "ACTIVE_CONNECTION": "You have an active ESCI connection", "CONNECT_NEW_SENSOR": "Connect a new sensor setup from ESCI", - "CURRENT_SUPPORT": "We currently only support sensors from \"Ensemble scientific\"", + "CURRENT_SUPPORT": "We currently only support sensors from \"Ensemble Scientific\"", "ENSEMBLE_ESID": "Ensemble ESID", - "ENTER_ID": "Enter your ensemble scientific organisation ID", + "ENTER_ID": "Enter your Ensemble Scientific organisation ID", "ORGANISATION_ID": "ESCI organisation ID", "ORGANISATION_ID_ERROR": "Invalid Organisation ID", "ORGANISATION_ID_GENERIC_ERROR": "Failed to connect to ESCI. Please try again later." diff --git a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/constants.ts b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/constants.ts index 92b7352cd6..cca30b60da 100644 --- a/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/constants.ts +++ b/packages/webapp/src/containers/LocationDetails/PointDetails/SensorDetail/v2/constants.ts @@ -16,5 +16,5 @@ import EsciLogo from '../../../../../assets/images/partners/esci_logo.png'; export const PARTNERS = { - ESCI: { id: 1, name: 'Ensemble scientific', url: 'www.esci.io', logoPath: EsciLogo }, + ESCI: { id: 1, name: 'Ensemble Scientific', url: 'www.esci.io', logoPath: EsciLogo }, }; From 76856ed974660a698206ffd2b30d9bff6387c4ab Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Wed, 5 Feb 2025 16:31:48 -0800 Subject: [PATCH 39/41] LF-4691 Add classNames prop to PageTitle --- packages/webapp/src/components/PageTitle/v2/index.jsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/webapp/src/components/PageTitle/v2/index.jsx b/packages/webapp/src/components/PageTitle/v2/index.jsx index 5f2accaeef..9c16feadb2 100644 --- a/packages/webapp/src/components/PageTitle/v2/index.jsx +++ b/packages/webapp/src/components/PageTitle/v2/index.jsx @@ -1,14 +1,15 @@ import { Title } from '../../Typography'; import React, { useState } from 'react'; +import clsx from 'clsx'; import styles from './styles.module.scss'; import { BsChevronLeft } from 'react-icons/bs'; import PropTypes from 'prop-types'; import { CancelButton } from '../CancelButton'; -function PageTitle({ title, onGoBack, onCancel, style, cancelModalTitle, label }) { +function PageTitle({ title, onGoBack, onCancel, style, cancelModalTitle, label, classNames = {} }) { const [showConfirmCancelModal, setShowConfirmCancelModal] = useState(false); return ( -
+
{onGoBack && (