diff --git a/CHANGELOG.md b/CHANGELOG.md index e47c67ed7..3b8a29900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - Some filter chips were missing translations or where not displayed correctly. - Filtering striae for `not specified` returned wrong results. - Filtering by `borehole status` did not work. +- When saving with ctrl+s in the borehole sections, the form content was reset. ## v2.1.870 - 2024-09-27 diff --git a/src/client/cypress/e2e/detailPage/sections.cy.js b/src/client/cypress/e2e/detailPage/sections.cy.js index f91a3457d..cec161aa6 100644 --- a/src/client/cypress/e2e/detailPage/sections.cy.js +++ b/src/client/cypress/e2e/detailPage/sections.cy.js @@ -1,4 +1,4 @@ -import { addItem, deleteItem, saveForm, startEditing } from "../helpers/buttonHelpers"; +import { addItem, deleteItem, saveForm, saveWithSaveBar, startEditing } from "../helpers/buttonHelpers"; import { evaluateDisplayValue, evaluateInput, setInput, setSelect } from "../helpers/formHelpers"; import { createBorehole, handlePrompt, loginAsAdmin, startBoreholeEditing } from "../helpers/testHelpers"; @@ -102,6 +102,52 @@ describe("Section crud tests", () => { cy.get('[data-cy="section-card.0"]').should("not.exist"); }); + it("saves section with ctrl s without resetting content", () => { + cy.wait(30); + // add section and save with ctrl s + addItem("addSection"); + cy.wait("@codelist_GET"); + setInput("name", "A"); + setInput("sectionElements.0.fromDepth", "0"); + setInput("sectionElements.0.toDepth", "1"); + setSelect("sectionElements.0.drillingMudTypeId", 5); + cy.get("body").type("{ctrl}s"); + evaluateDisplayValue("0.drilling_mud_type", "water-based dispersed"); + + // switch tab to borehole general tab and edit depth + cy.get('[data-cy="general-tab"]').click(); + setInput("totalDepth", 5); + evaluateInput("totalDepth", "5"); + + // click on sections without saving + cy.get('[data-cy="sections-tab"]').click(); + const messageUnsavedChanges = "There are unsaved changes. Do you want to discard all changes?"; + handlePrompt(messageUnsavedChanges, "cancel"); + evaluateInput("totalDepth", "5"); + cy.get('[data-cy="sections-tab"]').click(); + handlePrompt(messageUnsavedChanges, "discard changes"); + + // sections tab should be unchanged when retuning from borehole tab + evaluateDisplayValue("0.drilling_mud_type", "water-based dispersed"); + + // switch tab to borehole general tab and edit depth with saving + cy.get('[data-cy="general-tab"]').click(); + evaluateInput("totalDepth", ""); + setInput("totalDepth", 7); + evaluateInput("totalDepth", "7"); + saveWithSaveBar(); + + // edit sections tab and save again + cy.get('[data-cy="sections-tab"]').click(); + startEditing(); + setSelect("sectionElements.0.drillingMudTypeId", 4); + cy.get("body").type("{ctrl}s"); + + // borehole tab should still display saved depth value + cy.get('[data-cy="general-tab"]').click(); + evaluateInput("totalDepth", "7"); + }); + it("changes drillingMudSubtype select options based on drillingMudType", () => { cy.wait(30); addItem("addSection"); diff --git a/src/client/src/components/dataCard/dataInputCard.jsx b/src/client/src/components/dataCard/dataInputCard.jsx index f623b8c7c..139b00810 100644 --- a/src/client/src/components/dataCard/dataInputCard.jsx +++ b/src/client/src/components/dataCard/dataInputCard.jsx @@ -2,6 +2,7 @@ import { useContext, useEffect } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { DevTool } from "../../../hookformDevtools"; +import { useSaveOnCtrlS } from "../../pages/detail/useSaveOnCtrlS"; import { CancelButton, SaveButton } from "../buttons/buttons.tsx"; import { FormContainer } from "../form/form"; import { PromptContext } from "../prompt/promptContext.tsx"; @@ -73,6 +74,9 @@ export const DataInputCard = props => { } }; + // Save with ctrl+s + useSaveOnCtrlS(formMethods.handleSubmit(submitForm)); + return ( <> diff --git a/src/client/src/pages/detail/detailPageContent.tsx b/src/client/src/pages/detail/detailPageContent.tsx index 4bf0ec86c..821c5e576 100644 --- a/src/client/src/pages/detail/detailPageContent.tsx +++ b/src/client/src/pages/detail/detailPageContent.tsx @@ -109,10 +109,10 @@ export const DetailPageContent = ({ render={() => ( )} /> diff --git a/src/client/src/pages/detail/form/borehole/boreholeForm.tsx b/src/client/src/pages/detail/form/borehole/boreholeForm.tsx index 2d105378f..919c4e862 100644 --- a/src/client/src/pages/detail/form/borehole/boreholeForm.tsx +++ b/src/client/src/pages/detail/form/borehole/boreholeForm.tsx @@ -1,4 +1,6 @@ -import { useCallback, useEffect, useState } from "react"; +import { forwardRef, useCallback, useEffect, useState } from "react"; +import { FormProvider, useForm } from "react-hook-form"; +import { DevTool } from "../../../../../hookformDevtools.ts"; import { getBoreholeGeometryDepthTVD } from "../../../../api/fetchApiV2.js"; import { FormBooleanSelect, @@ -9,9 +11,10 @@ import { } from "../../../../components/form/form.ts"; import { parseFloatWithThousandsSeparator } from "../../../../components/legacyComponents/formUtils.ts"; import { FormSegmentBox } from "../../../../components/styledComponents.ts"; -import { BoreholeDetailProps } from "./boreholePanelInterfaces.ts"; +import { UseFormWithSaveBar } from "../useFormWithSaveBar.ts"; +import { BoreholeDetailProps, BoreholeFormInputs } from "./boreholePanelInterfaces.ts"; -export const BoreholeForm = ({ formMethods, borehole, editingEnabled }: BoreholeDetailProps) => { +export const BoreholeForm = forwardRef(({ borehole, editingEnabled, onSubmit }: BoreholeDetailProps, ref) => { const [totalDepthTVD, setTotalDepthTVD] = useState(null); const [topBedrockFreshTVD, setTopBedrockFreshTVD] = useState(null); const [topBedrockWeatheredTVD, setTopBedrockWeatheredTVD] = useState(null); @@ -19,10 +22,34 @@ export const BoreholeForm = ({ formMethods, borehole, editingEnabled }: Borehole return value ? Math.round(value * 100) / 100 : null; }; + const formMethods = useForm({ + mode: "onChange", + defaultValues: { + typeId: borehole.typeId, + purposeId: borehole.purposeId, + statusId: borehole.statusId, + totalDepth: borehole.totalDepth, + qtDepthId: borehole.qtDepthId, + topBedrockFreshMd: borehole.topBedrockFreshMd, + topBedrockWeatheredMd: borehole.topBedrockWeatheredMd, + lithologyTopBedrockId: borehole.lithologyTopBedrockId, + lithostratigraphyId: borehole.lithostratigraphyId, + chronostratigraphyId: borehole.chronostratigraphyId, + hasGroundwater: borehole.hasGroundwater === true ? 1 : borehole.hasGroundwater === false ? 0 : 2, + remarks: borehole.remarks, + }, + }); + const totalDepth = formMethods.watch("totalDepth"); const topBedrockFreshMd = formMethods.watch("topBedrockFreshMd"); const topBedrockWeatheredMd = formMethods.watch("topBedrockWeatheredMd"); + UseFormWithSaveBar({ + formMethods, + onSubmit, + ref, + }); + const fetchDepthTVD = useCallback( async (fieldValue: number | null) => { if (!fieldValue) return null; @@ -63,118 +90,125 @@ export const BoreholeForm = ({ formMethods, borehole, editingEnabled }: Borehole }, [fetchDepthTVD, topBedrockWeatheredMd]); return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + <> + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ ); -}; +}); diff --git a/src/client/src/pages/detail/form/borehole/boreholePanel.tsx b/src/client/src/pages/detail/form/borehole/boreholePanel.tsx index 6bafa8147..bd5ff91cd 100644 --- a/src/client/src/pages/detail/form/borehole/boreholePanel.tsx +++ b/src/client/src/pages/detail/form/borehole/boreholePanel.tsx @@ -1,12 +1,9 @@ -import { forwardRef, SyntheticEvent, useEffect, useState } from "react"; -import { FormProvider, useForm } from "react-hook-form"; +import { forwardRef, SyntheticEvent, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useHistory, useLocation } from "react-router-dom"; -import { DevTool } from "../../../../../hookformDevtools.ts"; import { BdmsTab, BdmsTabContentBox, BdmsTabs } from "../../../../components/styledTabComponents.jsx"; -import { UseFormWithSaveBar } from "../useFormWithSaveBar.ts"; import { BoreholeForm } from "./boreholeForm.tsx"; -import { BoreholeFormInputs, BoreholePanelProps } from "./boreholePanelInterfaces"; +import { BoreholePanelProps } from "./boreholePanelInterfaces"; import Geometry from "./geometry.jsx"; import Sections from "./sections.jsx"; @@ -16,61 +13,37 @@ export const BoreholePanel = forwardRef( const history = useHistory(); const location = useLocation(); const [activeIndex, setActiveIndex] = useState(0); - const formMethods = useForm({ - mode: "onChange", - defaultValues: { - typeId: borehole.typeId, - purposeId: borehole.purposeId, - statusId: borehole.statusId, - totalDepth: borehole.totalDepth, - qtDepthId: borehole.qtDepthId, - topBedrockFreshMd: borehole.topBedrockFreshMd, - topBedrockWeatheredMd: borehole.topBedrockWeatheredMd, - lithologyTopBedrockId: borehole.lithologyTopBedrockId, - lithostratigraphyId: borehole.lithostratigraphyId, - chronostratigraphyId: borehole.chronostratigraphyId, - hasGroundwater: borehole.hasGroundwater === true ? 1 : borehole.hasGroundwater === false ? 0 : 2, - remarks: borehole.remarks, - }, - }); - UseFormWithSaveBar({ - formMethods, - onSubmit, - ref, - }); - - const tabs = [ - { - label: t("general"), - hash: "general", - }, - { label: t("sections"), hash: "sections" }, - { label: t("boreholeGeometry"), hash: "geometry" }, - ]; + const tabs = useMemo( + () => [ + { + label: t("general"), + hash: "general", + }, + { label: t("sections"), hash: "sections" }, + { label: t("boreholeGeometry"), hash: "geometry" }, + ], + [t], + ); const handleIndexChange = (event: SyntheticEvent | null, index: number) => { - setActiveIndex(index); const newLocation = location.pathname + "#" + tabs[index].hash; if (location.pathname + location.hash !== newLocation) { history.push(newLocation); } }; - useEffect(() => { - const newTabIndex = tabs.findIndex(t => t.hash === location.hash.replace("#", "")); - if (newTabIndex > -1 && activeIndex !== newTabIndex) { - handleIndexChange(null, newTabIndex); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [location.hash]); - + // Update active tab index based on hash useEffect(() => { if (!location.hash) { history.replace(location.pathname + "#" + tabs[activeIndex].hash); + } else { + const newTabIndex = tabs.findIndex(t => t.hash === location.hash.replace("#", "")); + if (newTabIndex > -1) { + setActiveIndex(newTabIndex); + } } - // eslint-disable-next-line - }, []); + }, [activeIndex, history, location.hash, location.pathname, tabs]); return ( <> @@ -81,14 +54,7 @@ export const BoreholePanel = forwardRef( {activeIndex === 0 && ( - <> - - -
- - -
- + )} {activeIndex === 1 && } {activeIndex === 2 && ( diff --git a/src/client/src/pages/detail/form/borehole/boreholePanelInterfaces.ts b/src/client/src/pages/detail/form/borehole/boreholePanelInterfaces.ts index 9dbcfe07a..5b0d971c4 100644 --- a/src/client/src/pages/detail/form/borehole/boreholePanelInterfaces.ts +++ b/src/client/src/pages/detail/form/borehole/boreholePanelInterfaces.ts @@ -1,4 +1,3 @@ -import { UseFormReturn } from "react-hook-form"; import { BoreholeV2 } from "../../../../api/borehole.ts"; export interface BoreholeGeneralProps { @@ -7,7 +6,7 @@ export interface BoreholeGeneralProps { } export interface BoreholeDetailProps extends BoreholeGeneralProps { - formMethods: UseFormReturn; + onSubmit: (data: BoreholeFormInputs) => void; } export interface BoreholePanelProps extends BoreholeGeneralProps { diff --git a/src/client/src/pages/detail/form/borehole/sectionInput.jsx b/src/client/src/pages/detail/form/borehole/sectionInput.jsx index 1d2da05b3..16cdcfb39 100644 --- a/src/client/src/pages/detail/form/borehole/sectionInput.jsx +++ b/src/client/src/pages/detail/form/borehole/sectionInput.jsx @@ -10,11 +10,14 @@ import { DataCardButtonContainer } from "../../../../components/dataCard/dataCar import { DataCardContext } from "../../../../components/dataCard/dataCardContext.jsx"; import { FormCheckbox, FormContainer, FormInput, FormSelect, FormValueType } from "../../../../components/form/form"; import { FormDomainSelect } from "../../../../components/form/formDomainSelect"; +import { useFormDirty } from "../../useFormDirty"; +import { useSaveOnCtrlS } from "../../useSaveOnCtrlS"; const SectionInput = ({ item, parentId }) => { const { triggerReload, selectCard } = useContext(DataCardContext); const { data: domains } = useDomains(); const { i18n } = useTranslation(); + const { setIsFormDirty } = useFormDirty(); const sectionElementDefaults = { fromDepth: null, @@ -92,6 +95,20 @@ const SectionInput = ({ item, parentId }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [formMethods.trigger]); + // Track form dirty state + useEffect(() => { + setIsFormDirty(Object.keys(formMethods.formState.dirtyFields).length > 0); + return () => setIsFormDirty(false); + }, [ + formMethods.formState.dirtyFields, + formMethods.formState.isDirty, + formMethods, + formMethods.formState, + setIsFormDirty, + ]); + + useSaveOnCtrlS(formMethods.handleSubmit(submitForm)); + useEffect(() => { formMethods.trigger("sectionElements"); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/client/src/pages/detail/form/useFormWithSaveBar.ts b/src/client/src/pages/detail/form/useFormWithSaveBar.ts index 3652f7110..bb79eb318 100644 --- a/src/client/src/pages/detail/form/useFormWithSaveBar.ts +++ b/src/client/src/pages/detail/form/useFormWithSaveBar.ts @@ -3,6 +3,7 @@ import { FieldValues, UseFormReturn } from "react-hook-form"; import { useHistory } from "react-router-dom"; import { useBlockNavigation } from "../useBlockNavigation.tsx"; import { useFormDirty } from "../useFormDirty.tsx"; +import { useSaveOnCtrlS } from "../useSaveOnCtrlS.ts"; interface UseFormWithSaveBarProps { formMethods: UseFormReturn; @@ -23,7 +24,7 @@ export function UseFormWithSaveBar({ // Block navigation if form is dirty history.block(nextLocation => { - if (!handleBlockedNavigation(nextLocation.pathname)) { + if (!handleBlockedNavigation(nextLocation.pathname + nextLocation.hash)) { return false; } }); @@ -48,19 +49,7 @@ export function UseFormWithSaveBar({ }, [formMethods, onSubmit]); // Save with ctrl+s - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if (event.ctrlKey && event.key === "s") { - event.preventDefault(); - resetAndSubmitForm(); - } - }; - window.addEventListener("keydown", handleKeyDown); - - return () => { - window.removeEventListener("keydown", handleKeyDown); - }; - }, [resetAndSubmitForm]); + useSaveOnCtrlS(resetAndSubmitForm); // Expose form methods to parent component (save bar) useImperativeHandle(ref, () => ({ diff --git a/src/client/src/pages/detail/useSaveOnCtrlS.ts b/src/client/src/pages/detail/useSaveOnCtrlS.ts new file mode 100644 index 000000000..9409667d4 --- /dev/null +++ b/src/client/src/pages/detail/useSaveOnCtrlS.ts @@ -0,0 +1,17 @@ +import { useEffect } from "react"; + +export function useSaveOnCtrlS(callback: () => void) { + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.ctrlKey && event.key === "s") { + event.preventDefault(); + callback(); + } + }; + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [callback]); +}