diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d224a75e..1b724fa41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ - Hide the `help` button in the navigation bar in read-only mode. - Hide the `original_name` field in the borehole detail view in read-only mode. - Updated the style of the location tab. -- Changes on the location tab are now saved by clicking the `Save` button, instead of immediately. +- Changes on the location tab and the borehole tab are now saved by clicking the `Save` button, instead of immediately. ### Fixed diff --git a/src/client/cypress/e2e/detailPage/boreholeform.cy.js b/src/client/cypress/e2e/detailPage/boreholeform.cy.js index d4b70dcc4..0fb4e0ffa 100644 --- a/src/client/cypress/e2e/detailPage/boreholeform.cy.js +++ b/src/client/cypress/e2e/detailPage/boreholeform.cy.js @@ -1,9 +1,10 @@ -import { saveLocationForm } from "../helpers/buttonHelpers"; +import { saveWithSaveBar } from "../helpers/buttonHelpers"; import { clickOnRowWithText, showTableAndWaitForData, sortBy } from "../helpers/dataGridHelpers"; -import { evaluateInput, evaluateSelect, isDisabled, setSelect } from "../helpers/formHelpers"; +import { evaluateInput, evaluateSelect, isDisabled, setInput, setSelect } from "../helpers/formHelpers"; import { createBorehole, goToRouteAndAcceptTerms, + handlePrompt, newEditableBorehole, returnToOverview, startBoreholeEditing, @@ -44,7 +45,7 @@ describe("Test for the borehole form.", () => { evaluateSelect("qtReferenceElevationId", "20114002"); evaluateSelect("referenceElevationTypeId", "20117004"); - saveLocationForm(); + saveWithSaveBar(); // navigate away and back to check if values are saved cy.get('[data-cy="borehole-menu-item"]').click(); cy.get('[data-cy="location-menu-item"]').click(); @@ -58,19 +59,92 @@ describe("Test for the borehole form.", () => { // fill all dropdowns on borehole tab cy.get('[data-cy="borehole-menu-item"]').click(); - cy.get('[data-cy="domain-dropdown"]') - .should("have.length", 4) - .each(el => cy.wrap(el).click().find('[role="option"]').eq(1).click()); - - const boreholeDropdownValues = []; - cy.get('[data-cy="domain-dropdown"]') - .each(el => { - const value = el[0].children[1].firstChild.data; - boreholeDropdownValues.push(value); - }) - .then(() => { - expect(boreholeDropdownValues).to.deep.eq(["borehole", "geotechnics", "open, no completion", "2"]); - }); + setSelect("purposeId", 1); + setSelect("typeId", 1); + setSelect("qtDepthId", 1); + setSelect("statusId", 1); + + evaluateSelect("purposeId", "22103001"); + evaluateSelect("typeId", "20101001"); + evaluateSelect("qtDepthId", "22108001"); + evaluateSelect("statusId", "22104001"); + + saveWithSaveBar(); + + // navigate away and back to check if values are saved + cy.get('[data-cy="location-menu-item"]').click(); + cy.get('[data-cy="borehole-menu-item"]').click(); + + evaluateSelect("purposeId", "22103001"); + evaluateSelect("typeId", "20101001"); + evaluateSelect("qtDepthId", "22108001"); + evaluateSelect("statusId", "22104001"); + }); + + it("Fills all inputs on borehole tab and saves", () => { + createBorehole({ "extended.original_name": "AAA_Ferret", "custom.alternate_name": "AAA_Ferret" }).as("borehole_id"); + cy.get("@borehole_id").then(id => { + goToRouteAndAcceptTerms(`/${id}/borehole`); + startBoreholeEditing(); + + setSelect("purposeId", 1); + setSelect("typeId", 1); + setSelect("qtDepthId", 1); + setSelect("statusId", 1); + setSelect("lithologyTopBedrockId", 1); + setSelect("lithostratigraphyId", 1); + setSelect("chronostratigraphyId", 1); + setSelect("hasGroundwater", 1); + + setInput("totalDepth", 700); + setInput("topBedrockFreshMd", 0.60224); + setInput("topBedrockWeatheredMd", 78945100); + setInput("remarks", "This is a test remark"); + + // navigate away is blocked before saving + cy.get('[data-cy="location-menu-item"]').click(); + + const messageUnsavedChanges = "There are unsaved changes. Do you want to discard all changes?"; + handlePrompt(messageUnsavedChanges, "cancel"); + + saveWithSaveBar(); + cy.get('[data-cy="location-menu-item"]').click(); + cy.contains("Boreholes.swissgeol.ch ID"); + }); + }); + + it("Updates TVD Values when depth values change in boreholeform", () => { + createBorehole({ "extended.original_name": "AAA_Ferret", "custom.alternate_name": "AAA_Ferret" }).as("borehole_id"); + cy.get("@borehole_id").then(id => { + goToRouteAndAcceptTerms(`/${id}/borehole`); + startBoreholeEditing(); + setInput("totalDepth", 700); + setInput("topBedrockFreshMd", 0.60224); + setInput("topBedrockWeatheredMd", 78945100); + + evaluateInput("totalDepth", "700"); + evaluateInput("topBedrockFreshMd", "0.60224"); + evaluateInput("topBedrockWeatheredMd", "78'945'100"); + + // in display only inputs the label is used for the data-cy instead of the field name + evaluateInput("total_depth_tvd", "700"); + evaluateInput("top_bedrock_fresh_tvd", "0.6"); + evaluateInput("top_bedrock_weathered_tvd", "78'945'100"); + + saveWithSaveBar(); + + returnToOverview(); + showTableAndWaitForData(); + clickOnRowWithText("AAA_Ferret"); + cy.get('[data-cy="borehole-menu-item"]').click(); + evaluateInput("totalDepth", "700"); + evaluateInput("topBedrockFreshMd", "0.60224"); + evaluateInput("topBedrockWeatheredMd", "78'945'100"); + + evaluateInput("total_depth_tvd", "700"); + evaluateInput("top_bedrock_fresh_tvd", "0.6"); + evaluateInput("top_bedrock_weathered_tvd", "78'945'100"); + }); }); it("Checks if form values are updated when borehole changes", () => { diff --git a/src/client/cypress/e2e/detailPage/coordinates.cy.js b/src/client/cypress/e2e/detailPage/coordinates.cy.js index e01f9c20d..aa59482aa 100644 --- a/src/client/cypress/e2e/detailPage/coordinates.cy.js +++ b/src/client/cypress/e2e/detailPage/coordinates.cy.js @@ -1,4 +1,4 @@ -import { saveLocationForm } from "../helpers/buttonHelpers"; +import { saveWithSaveBar } from "../helpers/buttonHelpers"; import { evaluateSelect, setSelect } from "../helpers/formHelpers"; import { delayedType, @@ -143,7 +143,7 @@ describe("Tests for editing coordinates of a borehole.", () => { checkDecimalPlaces("@LV03X-input", 2); checkDecimalPlaces("@LV03Y-input", 2); - saveLocationForm(); + saveWithSaveBar(); returnToOverview(); newUneditableBorehole(); // verify input are cleared for new borehole @@ -169,7 +169,7 @@ describe("Tests for editing coordinates of a borehole.", () => { checkDecimalPlaces("@LV03X-input", 4); checkDecimalPlaces("@LV03Y-input", 4); - saveLocationForm(); + saveWithSaveBar(); // Navigate somewhere else and return cy.get('[data-cy="borehole-menu-item"]').click(); cy.get('[data-cy="location-menu-item"]').click(); diff --git a/src/client/cypress/e2e/detailPage/location.cy.js b/src/client/cypress/e2e/detailPage/location.cy.js index 6ce906983..f3c8532dc 100644 --- a/src/client/cypress/e2e/detailPage/location.cy.js +++ b/src/client/cypress/e2e/detailPage/location.cy.js @@ -1,4 +1,4 @@ -import { addItem, saveLocationForm, stopEditing } from "../helpers/buttonHelpers"; +import { addItem, saveWithSaveBar, stopEditing } from "../helpers/buttonHelpers"; import { checkRowWithText, clickOnRowWithText, showTableAndWaitForData } from "../helpers/dataGridHelpers"; import { evaluateInput, evaluateSelect, setInput, setSelect } from "../helpers/formHelpers"; import { @@ -41,7 +41,7 @@ describe("Tests for 'Location' edit page.", () => { originalNameInput.type("AAA_SCATORPS"); // save borehole - saveLocationForm(); + saveWithSaveBar(); // stop editing stopBoreholeEditing(); @@ -86,7 +86,7 @@ describe("Tests for 'Location' edit page.", () => { cy.get("@alternateNameInput").should("have.value", "PHOTOMOUSE"); cy.get("@alternateNameInput").clear(); - saveLocationForm(); + saveWithSaveBar(); // should be reset to original name if alternate name is empty cy.get("@originalNameInput").should("have.value", "PHOTOCAT"); cy.get("@alternateNameInput").should("have.value", "PHOTOCAT"); @@ -174,7 +174,7 @@ describe("Tests for 'Location' edit page.", () => { originalNameInput.type("AAA_FELIX_THE_PANDA"); function saveFormAndReturnToOverview() { - saveLocationForm(); + saveWithSaveBar(); returnToOverview(); } diff --git a/src/client/cypress/e2e/filters/identifierFilter.cy.js b/src/client/cypress/e2e/filters/identifierFilter.cy.js index cb80e294c..c92ebce06 100644 --- a/src/client/cypress/e2e/filters/identifierFilter.cy.js +++ b/src/client/cypress/e2e/filters/identifierFilter.cy.js @@ -1,4 +1,4 @@ -import { addItem, saveLocationForm } from "../helpers/buttonHelpers"; +import { addItem, saveWithSaveBar } from "../helpers/buttonHelpers"; import { checkAllVisibleRows, verifyPaginationText } from "../helpers/dataGridHelpers"; import { setInput, setSelect } from "../helpers/formHelpers"; import { newEditableBorehole, returnToOverview, stopBoreholeEditing } from "../helpers/testHelpers.js"; @@ -10,7 +10,7 @@ describe("Tests for filtering data by identifier.", () => { addItem("addIdentifier"); setSelect("boreholeCodelists.0.codelistId", 1); setInput("boreholeCodelists.0.value", 819544732); - saveLocationForm(); + saveWithSaveBar(); stopBoreholeEditing(); returnToOverview(); @@ -40,7 +40,7 @@ describe("Tests for filtering data by identifier.", () => { addItem("addIdentifier"); setSelect("boreholeCodelists.0.codelistId", 1); setInput("boreholeCodelists.0.value", 64531274); - saveLocationForm(); + saveWithSaveBar(); stopBoreholeEditing(); returnToOverview(); @@ -49,7 +49,7 @@ describe("Tests for filtering data by identifier.", () => { addItem("addIdentifier"); setSelect("boreholeCodelists.0.codelistId", 1); setInput("boreholeCodelists.0.value", 436584127); - saveLocationForm(); + saveWithSaveBar(); stopBoreholeEditing(); returnToOverview(); diff --git a/src/client/cypress/e2e/filters/srsFilter.cy.js b/src/client/cypress/e2e/filters/srsFilter.cy.js index aea72bf49..1034e0686 100644 --- a/src/client/cypress/e2e/filters/srsFilter.cy.js +++ b/src/client/cypress/e2e/filters/srsFilter.cy.js @@ -1,4 +1,4 @@ -import { saveLocationForm } from "../helpers/buttonHelpers"; +import { saveWithSaveBar } from "../helpers/buttonHelpers"; import { verifyPaginationText } from "../helpers/dataGridHelpers"; import { setSelect } from "../helpers/formHelpers"; import { loginAsAdmin, newEditableBorehole, returnToOverview, stopBoreholeEditing } from "../helpers/testHelpers.js"; @@ -43,7 +43,7 @@ describe("Tests for filtering data by reference system.", () => { cy.get("@LV03X-input").type("645778", { delay: 10 }); cy.get("@LV03Y-input").type("245794", { delay: 10 }); - saveLocationForm(); + saveWithSaveBar(); stopBoreholeEditing(); returnToOverview(); diff --git a/src/client/cypress/e2e/helpers/buttonHelpers.js b/src/client/cypress/e2e/helpers/buttonHelpers.js index e474a5360..b18e071e4 100644 --- a/src/client/cypress/e2e/helpers/buttonHelpers.js +++ b/src/client/cypress/e2e/helpers/buttonHelpers.js @@ -4,7 +4,7 @@ import { createBaseSelector } from "./testHelpers"; * Clicks on the save button and waits for borehole update. * @param {string} parent (optional) The parent of the button. */ -export const saveLocationForm = parent => { +export const saveWithSaveBar = parent => { saveForm(parent); cy.wait(["@borehole_by_id", "@update-borehole"]); }; diff --git a/src/client/src/api-lib/actions/borehole.js b/src/client/src/api-lib/actions/borehole.js index a0e05c715..da824c483 100644 --- a/src/client/src/api-lib/actions/borehole.js +++ b/src/client/src/api-lib/actions/borehole.js @@ -57,15 +57,6 @@ export function unlockBorehole(id) { }); } -export function patchBorehole(id, field, value) { - return fetch("/borehole/edit", { - action: "PATCH", - id: id, - field: field, - value: value, - }); -} - export function patchBoreholes(ids, fields) { return fetch("/borehole/edit", { action: "MULTIPATCH", diff --git a/src/client/src/api-lib/index.js b/src/client/src/api-lib/index.js index d07cc2eb9..cfb83302a 100644 --- a/src/client/src/api-lib/index.js +++ b/src/client/src/api-lib/index.js @@ -8,7 +8,6 @@ import { loadBoreholes, loadEditingBoreholes, lockBorehole, - patchBorehole, patchBoreholes, unlockBorehole, updateBorehole, @@ -51,7 +50,6 @@ export { unlockBorehole, deleteBorehole, deleteBoreholes, - patchBorehole, patchBoreholes, getGeojson, loadWorkflows, diff --git a/src/client/src/api/borehole.ts b/src/client/src/api/borehole.ts index e57f66104..7b8cc6f66 100644 --- a/src/client/src/api/borehole.ts +++ b/src/client/src/api/borehole.ts @@ -16,6 +16,19 @@ export interface Identifier extends BasicIdentifier { } export interface BoreholeV2 { + lithologyTopBedrockId: number; + lithostratigraphyId: number; + chronostratigraphyId: number; + hasGroundwater: boolean | null; + topBedrockWeatheredMd: number; + topBedrockFreshMd: number; + qtDepthId: number; + totalDepth: number; + purposeId: number; + typeId: number; + remarks: string; + statusId: number; + workflow: Workflow; boreholeCodelists: BasicIdentifier[]; workflows: Workflow[]; originalReferenceSystem: number; diff --git a/src/client/src/components/form/form.ts b/src/client/src/components/form/form.ts index 34ca54864..d383bf502 100644 --- a/src/client/src/components/form/form.ts +++ b/src/client/src/components/form/form.ts @@ -42,3 +42,4 @@ export { FormDisplay } from "./formDisplay"; export { FormCoordinate } from "./formCoordinate"; export { FormContainer } from "./formContainer"; export { FormBooleanSelect } from "./formBooleanSelect"; +export { FormInputDisplayOnly } from "./formInputDisplayOnly"; diff --git a/src/client/src/components/form/formInputDisplayOnly.tsx b/src/client/src/components/form/formInputDisplayOnly.tsx new file mode 100644 index 000000000..9aac74ef2 --- /dev/null +++ b/src/client/src/components/form/formInputDisplayOnly.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { TextField, TextFieldProps } from "@mui/material"; +import { NumericFormatWithThousandSeparator } from "./numericFormatWithThousandSeparator.tsx"; + +interface FormInputDisplayOnlyProps extends Omit { + label: string; + value: number | null; + withThousandSeparator?: boolean; + disabled?: boolean; + readOnly?: boolean; +} + +export const FormInputDisplayOnly: React.FC = ({ + label, + value, + withThousandSeparator, + readOnly = true, + disabled = true, + ...props +}) => { + const { t } = useTranslation(); + return ( + + ); +}; diff --git a/src/client/src/components/form/simpleDomainSelect.tsx b/src/client/src/components/form/simpleDomainSelect.tsx deleted file mode 100644 index 570c1633b..000000000 --- a/src/client/src/components/form/simpleDomainSelect.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { FC } from "react"; -import { useTranslation } from "react-i18next"; -import { MenuItem, SxProps } from "@mui/material"; -import { TextField } from "@mui/material/"; -import { useDomains } from "../../api/fetchApiV2"; -import { Codelist } from "../legacyComponents/domain/domainInterface.ts"; -import { FormSelectMenuItem } from "./formSelect.tsx"; - -// This component is needed as an intermediate step to refactor borehole input. -// The standard form components are not usable with autosave components as they are now. -// Once the saving mechanism is refactored, this component can be replaced. - -interface SimpleDomainSelectProps { - fieldName: string; - schemaName: string; - label: string; - required?: boolean; - disabled?: boolean; - readonly?: boolean; - selected?: number | boolean | null; - sx?: SxProps; - className?: string; - onUpdate?: (value: number | null) => void; -} - -export const SimpleDomainSelect: FC = ({ - fieldName, - label, - required, - disabled, - readonly, - selected, - schemaName, - sx, - className, - onUpdate, -}) => { - const { t, i18n } = useTranslation(); - const { data: domains } = useDomains(); - - const menuItems: FormSelectMenuItem[] = []; - if (!required) { - menuItems.push({ key: 0, value: undefined, label: t("reset"), italic: true }); - } - domains - ?.filter((d: Codelist) => d.schema === schemaName) - .sort((a: Codelist, b: Codelist) => a.order - b.order) - .forEach((d: Codelist) => - menuItems.push({ - key: d.id, - value: d.id, - label: d[i18n.language] as string, - }), - ); - - return ( - { - if (onUpdate) { - onUpdate(parseInt(e.target.value)); - } - }} - value={selected} - disabled={disabled ?? false} - data-cy={fieldName + "-formSelect"} - InputProps={{ readOnly: readonly, disabled: disabled }}> - {menuItems.map(item => ( - - {item.italic ? {item.label} : item.label} - - ))} - - ); -}; diff --git a/src/client/src/components/form/simpleFormInput.tsx b/src/client/src/components/form/simpleFormInput.tsx deleted file mode 100644 index 815338a81..000000000 --- a/src/client/src/components/form/simpleFormInput.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { FC } from "react"; -import { useTranslation } from "react-i18next"; -import { InputProps, SxProps, TextField } from "@mui/material"; -import { FormValueType } from "./form.ts"; -import { NumericFormatWithThousandSeparator } from "./numericFormatWithThousandSeparator.tsx"; - -interface SimpleInputProps { - label: string; - required?: boolean; - disabled?: boolean; - readonly?: boolean; - type?: FormValueType; - multiline?: boolean; - rows?: number; - value?: string | number | Date | null; - sx?: SxProps; - className?: string; - inputProps?: InputProps; - onUpdate?: (value: string) => void; - withThousandSeparator?: boolean; -} - -// This component is needed as an intermediate step to refactor borehole input. -// The standard form components are not usable with autosave components as they are now. -// Once the saving mechanism is refactored, this component can be replaced. - -export const SimpleFormInput: FC = ({ - value, - className, - required, - readonly, - rows, - onUpdate, - label, - type, - multiline, - withThousandSeparator, - inputProps, - disabled, - sx, -}) => { - const { t } = useTranslation(); - - return ( - { - if (onUpdate) { - onUpdate(e.target.value); - } - }} - InputProps={{ - ...inputProps /* eslint-disable @typescript-eslint/no-explicit-any */, - ...(withThousandSeparator && { inputComponent: NumericFormatWithThousandSeparator as any }), - readOnly: readonly, - disabled: disabled, - }} - /> - ); -}; diff --git a/src/client/src/components/legacyComponents/formUtils.ts b/src/client/src/components/legacyComponents/formUtils.ts index fa533d7fc..5cca47e54 100644 --- a/src/client/src/components/legacyComponents/formUtils.ts +++ b/src/client/src/components/legacyComponents/formUtils.ts @@ -1,3 +1,10 @@ +import { Identifier } from "../../api/borehole.ts"; +import { BoreholeFormInputs } from "../../pages/detail/form/borehole/boreholePanelInterfaces.ts"; +import { + LocationFormInputs, + LocationFormSubmission, +} from "../../pages/detail/form/location/locationPanelInterfaces.tsx"; + /** * Parse the input value if it's a string. If it's a number, return it as is. * @param {string | number} value The value to parse. @@ -33,3 +40,64 @@ export const getMaxPrecision = (numericString1: string, numericString2: string) * @returns The precision of the string. */ export const getPrecisionFromString = (numericString: string) => numericString.split(".")[1]?.length || 0; + +/** + * Transforms the location form data into a format that can be submitted to the API. + * @param {LocationFormInputs} formInputs The data from the location form. + * @returns The location data in a format that can be submitted to the API. + */ +export const prepareLocationDataForSubmit = (formInputs: LocationFormInputs) => { + const data = { ...formInputs } as LocationFormSubmission; + + const ensureDatetime = (date: string) => (date.endsWith("Z") ? date : `${date}T00:00:00.000Z`); + const parseValueIfNotNull = (value: string | number | null) => + value ? parseFloatWithThousandsSeparator(String(value)) : null; + + const getCompleteCodelists = (codelists: Identifier[]) => { + return codelists + .map(c => { + delete c.borehole; + delete c.codelist; + return c; + }) + .filter(c => c.codelistId && c.value && c.boreholeId); + }; + + data.restrictionUntil = data?.restrictionUntil ? ensureDatetime(data.restrictionUntil.toString()) : null; + data.elevationZ = parseValueIfNotNull(data?.elevationZ); + data.referenceElevation = parseValueIfNotNull(data?.referenceElevation); + data.nationalInterest = data?.nationalInterest === 1 ? true : data?.nationalInterest === 0 ? false : null; + data.restrictionId = data.restrictionId ?? null; + data.referenceElevationTypeId = data.referenceElevationTypeId ?? null; + data.elevationPrecisionId = data.elevationPrecisionId ?? null; + data.locationPrecisionId = data.locationPrecisionId ?? null; + data.qtReferenceElevationId = data.qtReferenceElevationId ?? null; + data.alternateName = data?.alternateName ?? data.originalName; + data.precisionLocationX = data?.locationX ? getPrecisionFromString(formInputs.locationX) : null; + data.precisionLocationY = data?.locationY ? getPrecisionFromString(formInputs.locationY) : null; + data.precisionLocationXLV03 = data?.locationXLV03 ? getPrecisionFromString(formInputs.locationXLV03) : null; + data.precisionLocationYLV03 = data?.locationYLV03 ? getPrecisionFromString(formInputs.locationYLV03) : null; + data.locationX = parseValueIfNotNull(data?.locationX); + data.locationY = parseValueIfNotNull(data?.locationY); + data.locationXLV03 = parseValueIfNotNull(data?.locationXLV03); + data.locationYLV03 = parseValueIfNotNull(data?.locationYLV03); + data.boreholeCodelists = getCompleteCodelists(data.boreholeCodelists); + return data; +}; + +/** + * Transforms the borehole form data into a format that can be submitted to the API. + * @param {BoreholeFormInputs} formInputs The data from the borehole form. + * @returns The borehole data in a format that can be submitted to the API. + */ +export const prepareBoreholeDataForSubmit = (formInputs: BoreholeFormInputs) => { + const data = { ...formInputs }; + const parseValueIfNotNull = (value: string | number | null) => + value ? parseFloatWithThousandsSeparator(String(value)) : null; + data.totalDepth = parseValueIfNotNull(data?.totalDepth); + data.topBedrockFreshMd = parseValueIfNotNull(data?.topBedrockFreshMd); + data.topBedrockWeatheredMd = parseValueIfNotNull(data?.topBedrockWeatheredMd); + data.hasGroundwater = data?.hasGroundwater === 1 ? true : data?.hasGroundwater === 0 ? false : null; + + return data; +}; diff --git a/src/client/src/pages/detail/detailPage.tsx b/src/client/src/pages/detail/detailPage.tsx index f326d4004..bd733a8d1 100644 --- a/src/client/src/pages/detail/detailPage.tsx +++ b/src/client/src/pages/detail/detailPage.tsx @@ -8,12 +8,16 @@ import { User } from "../../api/apiInterfaces.ts"; import { BoreholeV2, getBoreholeById, updateBorehole } from "../../api/borehole.ts"; import { fetchUser } from "../../api/user.ts"; import { LabelingToggleButton } from "../../components/buttons/labelingButton.tsx"; +import { + prepareBoreholeDataForSubmit, + prepareLocationDataForSubmit, +} from "../../components/legacyComponents/formUtils.ts"; import { LayoutBox, MainContentBox, SidebarBox } from "../../components/styledComponents.ts"; import DetailHeader from "./detailHeader.tsx"; import { DetailPageContent } from "./detailPageContent.tsx"; import { DetailSideNav } from "./detailSideNav.tsx"; -import { prepareLocationDataForSubmit } from "./form/location/locationFormUtils.ts"; -import { LocationFormInputs } from "./form/location/locationPanelInterfaces.tsx"; +import { BoreholeFormInputs } from "./form/borehole/boreholePanelInterfaces.ts"; +import { LocationFormInputs, LocationFormSubmission } from "./form/location/locationPanelInterfaces.tsx"; import { useLabelingContext } from "./labeling/labelingInterfaces.tsx"; import LabelingPanel from "./labeling/labelingPanel.tsx"; import { SaveBar } from "./saveBar"; @@ -65,14 +69,22 @@ export const DetailPage: FC = () => { ); const locationPanelRef = useRef<{ submit: () => void; reset: () => void }>(null); + const boreholePanelRef = useRef<{ submit: () => void; reset: () => void }>(null); - const onFormSubmit = (formInputs: LocationFormInputs) => { - const boreholeSubmission = prepareLocationDataForSubmit(formInputs); + function getAndUpdateBorehole(boreholeSubmission: BoreholeFormInputs | LocationFormSubmission) { getBoreholeById(parseInt(id)).then(b => { updateBorehole({ ...b, ...boreholeSubmission }).then(r => { setBorehole(r); }); }); + } + + const onBoreholeFormSubmit = (formInputs: BoreholeFormInputs) => { + getAndUpdateBorehole(prepareBoreholeDataForSubmit(formInputs)); + }; + + const onLocationFormSubmit = (formInputs: LocationFormInputs) => { + getAndUpdateBorehole(prepareLocationDataForSubmit(formInputs)); }; const handleDirtyChange = (isDirty: boolean) => { @@ -80,15 +92,13 @@ export const DetailPage: FC = () => { }; const triggerSubmit = () => { - if (locationPanelRef.current) { - locationPanelRef.current.submit(); - } + boreholePanelRef.current?.submit(); + locationPanelRef.current?.submit(); }; const triggerReset = () => { - if (locationPanelRef.current) { - locationPanelRef.current.reset(); - } + boreholePanelRef.current?.reset(); + locationPanelRef.current?.reset(); }; useEffect(() => { @@ -128,6 +138,10 @@ export const DetailPage: FC = () => { ); + const shouldShowSaveBar = + location.pathname.endsWith("/location") || + (location.pathname.endsWith("/borehole") && location.hash === "#general"); + return ( <> { {editingEnabled && panelOpen && } - {editingEnabled && location.pathname.endsWith("/location") && ( + {editingEnabled && shouldShowSaveBar && ( )} diff --git a/src/client/src/pages/detail/detailPageContent.tsx b/src/client/src/pages/detail/detailPageContent.tsx index 272dcab97..fa6674557 100644 --- a/src/client/src/pages/detail/detailPageContent.tsx +++ b/src/client/src/pages/detail/detailPageContent.tsx @@ -1,16 +1,15 @@ import { RefObject, useContext } from "react"; import { useTranslation } from "react-i18next"; -import { useDispatch, useSelector } from "react-redux"; +import { useSelector } from "react-redux"; import { Redirect, Route, Switch, useParams } from "react-router-dom"; import { Box } from "@mui/material"; -import _ from "lodash"; -import { patchBorehole, updateBorehole } from "../../api-lib"; -import { BoreholeAttributes, ReduxRootState } from "../../api-lib/ReduxStateInterfaces.ts"; -import { BoreholeV2, getBoreholeById } from "../../api/borehole.ts"; +import { ReduxRootState } from "../../api-lib/ReduxStateInterfaces.ts"; +import { BoreholeV2 } from "../../api/borehole.ts"; import { theme } from "../../AppTheme"; import { AlertContext } from "../../components/alert/alertContext"; import EditorBoreholeFilesTable from "./attachments/table/editorBoreholeFilesTable.tsx"; -import BoreholePanel from "./form/borehole/boreholePanel.tsx"; +import { BoreholePanel } from "./form/borehole/boreholePanel.tsx"; +import { BoreholeFormInputs } from "./form/borehole/boreholePanelInterfaces.ts"; import Completion from "./form/completion/completion.jsx"; import FieldMeasurement from "./form/hydrogeology/fieldMeasurement.jsx"; import GroundwaterLevelMeasurement from "./form/hydrogeology/groundwaterLevelMeasurement.jsx"; @@ -27,10 +26,11 @@ interface DetailPageContentProps { editingEnabled: boolean; editableByCurrentUser: boolean; locationPanelRef: RefObject<{ submit: () => void; reset: () => void }>; - onFormSubmit: (data: LocationFormInputs) => void; + boreholePanelRef: RefObject<{ submit: () => void; reset: () => void }>; + onLocationFormSubmit: (data: LocationFormInputs) => void; + onBoreholeFormSubmit: (data: BoreholeFormInputs) => void; handleDirtyChange: (isDirty: boolean) => void; borehole: BoreholeV2; - setBorehole: (borehole: BoreholeV2) => void; } type DetailPageParams = { id: string; @@ -40,19 +40,16 @@ export const DetailPageContent = ({ editingEnabled, editableByCurrentUser, locationPanelRef, - onFormSubmit, + boreholePanelRef, + onLocationFormSubmit, + onBoreholeFormSubmit, handleDirtyChange, borehole, - setBorehole, }: DetailPageContentProps) => { const { t } = useTranslation(); const { showAlert } = useContext(AlertContext); const { id } = useParams(); const legacyBorehole = useSelector((state: ReduxRootState) => state.core_borehole); - const dispatch = useDispatch(); - - /* eslint-disable @typescript-eslint/no-explicit-any */ - const updateAttributeDelay: { [index: string]: any } = {}; function checkLock() { if (legacyBorehole.data.role !== "EDIT") { @@ -67,95 +64,6 @@ export const DetailPageContent = ({ return true; } - function isNumber(value: string | number): boolean { - return typeof value === "number" || !isNaN(Number(value)); - } - - function updateNumber(attribute: string, value: number | null, to = true) { - if (!checkLock()) return; - const updatedBorehole = { - ...legacyBorehole, - }; - _.set(updatedBorehole.data, attribute, value); - - if (value === null) { - patch(updatedBorehole.data, attribute, value, to); - } else if (isNumber(value)) { - patch(updatedBorehole.data, attribute, _.toNumber(value), to); - } - } - - function updateChange( - attribute: string, - value: string | number | boolean | null | (number | string | null)[], - to = true, - ) { - if (!checkLock()) { - return; - } - const updatedBorehole = { - ...legacyBorehole, - }; - if (attribute === "location") { - const arrayValue = value as (number | string | null)[]; - _.set(updatedBorehole.data, "location_x", arrayValue[0]); - _.set(updatedBorehole.data, "location_y", arrayValue[1]); - if (arrayValue[2] !== null && isNumber(arrayValue[2])) { - _.set(updatedBorehole.data, "elevation_z", arrayValue[2]); - } - _.set(updatedBorehole.data, "custom.country", arrayValue[3]); - _.set(updatedBorehole.data, "custom.canton", arrayValue[4]); - _.set(updatedBorehole.data, "custom.municipality", arrayValue[5]); - } else { - _.set(updatedBorehole.data, attribute, value); - } - - patch(updatedBorehole.data, attribute, value, to); - } - - function patch( - borehole: BoreholeAttributes, - attribute: string, - value: string | number | boolean | null | (number | string | null)[], - to = true, - ) { - if (Object.prototype.hasOwnProperty.call(updateAttributeDelay, attribute) && updateAttributeDelay[attribute]) { - clearTimeout(updateAttributeDelay[attribute]); - updateAttributeDelay[attribute] = false; - } - updateAttributeDelay[attribute] = setTimeout( - () => { - patchBorehole(borehole.id, attribute, value) - //@ts-expect-error legacy fetch function returns not typed - .then(response => { - if (response.data.success) { - borehole.lock = response.data.lock; - borehole.updater = response.data.updater; - if (response.data.location) { - borehole.custom.country = response.data.location.country; - borehole.custom.canton = response.data.location.canton; - borehole.custom.municipality = response.data.location.municipality; - } - dispatch(updateBorehole(borehole)); - getBoreholeById(borehole.id).then(res => { - setBorehole(res); - }); - } else if (response.status === 200) { - showAlert(response.data.message, "error"); - if (response.data.error === "errorLocked") { - patch(response.data, attribute, value, to); - borehole.lock = null; - dispatch(updateBorehole(borehole)); - } else { - window.location.reload(); - } - } - }); - }, - to ? 500 : 0, - ); - } - if (legacyBorehole.error !== null) { showAlert(legacyBorehole.error, "error"); } @@ -184,7 +92,7 @@ export const DetailPageContent = ({ @@ -195,11 +103,12 @@ export const DetailPageContent = ({ path={"/:id/borehole"} render={() => ( )} /> diff --git a/src/client/src/pages/detail/form/borehole/boreholeDetailSegment.tsx b/src/client/src/pages/detail/form/borehole/boreholeDetailSegment.tsx deleted file mode 100644 index 51e1e47a4..000000000 --- a/src/client/src/pages/detail/form/borehole/boreholeDetailSegment.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import { ChangeEvent, useCallback, useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { NumericFormat } from "react-number-format"; -import { FormControl, FormControlLabel, RadioGroup } from "@mui/material"; -import { Form, Segment } from "semantic-ui-react"; -import _ from "lodash"; -import { getBoreholeGeometryDepthTVD } from "../../../../api/fetchApiV2.js"; -import DomainDropdown from "../../../../components/legacyComponents/domain/dropdown/domainDropdown.jsx"; -import DomainTree from "../../../../components/legacyComponents/domain/tree/domainTree.jsx"; -import { parseIfString } from "../../../../components/legacyComponents/formUtils.ts"; -import TranslationText from "../../../../components/legacyComponents/translationText.jsx"; -import { DisabledRadio } from "../styledComponents.jsx"; -import { BoreholeDetailProps, DepthTVD } from "./boreholePanelInterfaces.ts"; - -const BoreholeDetailSegment = ({ legacyBorehole, updateChange, updateNumber, isEditable }: BoreholeDetailProps) => { - const { t } = useTranslation(); - const [depthTVD, setDepthTVD] = useState(); - - const updateTVD = useCallback( - (field: string, depthMD: number) => { - if (depthMD == null) { - setDepthTVD(value => ({ ...value, [field]: null })); - } else { - getBoreholeGeometryDepthTVD(legacyBorehole.data.id, depthMD).then(response => { - if (response != null) { - setDepthTVD(value => { - return { ...value, [field]: response }; - }); - } else { - setDepthTVD(value => ({ ...value, [field]: null })); - } - }); - } - }, - [legacyBorehole.data.id], - ); - - useEffect(() => { - updateTVD("total_depth", legacyBorehole.data.total_depth); - }, [legacyBorehole.data.total_depth, updateTVD]); - - useEffect(() => { - updateTVD("extended.top_bedrock_fresh_md", legacyBorehole.data.extended.top_bedrock_fresh_md); - }, [legacyBorehole.data.extended.top_bedrock_fresh_md, updateTVD]); - - useEffect(() => { - updateTVD("custom.top_bedrock_weathered_md", legacyBorehole.data.custom.top_bedrock_weathered_md); - }, [legacyBorehole.data.custom.top_bedrock_weathered_md, updateTVD]); - - const updateNumericField = (fieldNameMD: string, event: ChangeEvent) => { - const value = event.target.value === "" ? null : parseIfString(event.target.value); - updateNumber(fieldNameMD, value); - }; - - const roundTvdValue = (value: number | undefined) => { - return value ? Math.round(value * 100) / 100 : ""; - }; - - return ( - -
- - - - updateNumericField("total_depth", e)} - spellCheck="false" - value={_.isNil(legacyBorehole.data.total_depth) ? "" : legacyBorehole.data.total_depth} - thousandSeparator="'" - readOnly={!isEditable} - /> - - - - - { - updateChange("custom.qt_depth", selected.id, false); - }} - schema="depth_precision" - selected={legacyBorehole.data.custom.qt_depth} - readOnly={!isEditable} - /> - - - - - - - - - - updateNumericField("extended.top_bedrock_fresh_md", e)} - spellCheck="false" - value={ - _.isNil(legacyBorehole.data.extended.top_bedrock_fresh_md) - ? "" - : legacyBorehole.data.extended.top_bedrock_fresh_md - } - thousandSeparator="'" - readOnly={!isEditable} - /> - - - - - - - - - - updateNumericField("custom.top_bedrock_weathered_md", e)} - spellCheck="false" - value={legacyBorehole.data.custom.top_bedrock_weathered_md} - thousandSeparator="'" - readOnly={!isEditable} - /> - - - - - - - - - { - updateChange("custom.lithology_top_bedrock", selected.id, false); - }} - schema="custom.lithology_top_bedrock" - selected={legacyBorehole.data.custom.lithology_top_bedrock} - title={t("lithology_top_bedrock")} - isEditable={isEditable} - /> - - - - { - updateChange("custom.lithostratigraphy_top_bedrock", selected.id, false); - }} - schema="custom.lithostratigraphy_top_bedrock" - selected={legacyBorehole.data.custom.lithostratigraphy_top_bedrock} - title={t("lithostratigraphy_top_bedrock")} - isEditable={isEditable} - /> - - - - { - updateChange("custom.chronostratigraphy_top_bedrock", selected.id, false); - }} - schema="custom.chronostratigraphy_top_bedrock" - selected={legacyBorehole.data.custom.chronostratigraphy_top_bedrock} - title={t("chronostratigraphy_top_bedrock")} - isEditable={isEditable} - /> - - - - - { - const value = e.target.value === "TRUE" ? true : e.target.value === "FALSE" ? false : null; - updateChange("extended.groundwater", value, false); - }}> - } - label={} - /> - } - label={} - /> - } - label={} - /> - - - -
-
- ); -}; - -export default BoreholeDetailSegment; diff --git a/src/client/src/pages/detail/form/borehole/boreholeForm.tsx b/src/client/src/pages/detail/form/borehole/boreholeForm.tsx new file mode 100644 index 000000000..2379033c6 --- /dev/null +++ b/src/client/src/pages/detail/form/borehole/boreholeForm.tsx @@ -0,0 +1,176 @@ +import { useCallback, useEffect, useState } from "react"; +import { getBoreholeGeometryDepthTVD } from "../../../../api/fetchApiV2.js"; +import { + FormBooleanSelect, + FormContainer, + FormDomainSelect, + FormInput, + FormInputDisplayOnly, +} from "../../../../components/form/form.ts"; +import { parseFloatWithThousandsSeparator } from "../../../../components/legacyComponents/formUtils.ts"; +import { FormSegmentBox } from "../../../../components/styledComponents.ts"; +import { BoreholeDetailProps } from "./boreholePanelInterfaces.ts"; + +export const BoreholeForm = ({ formMethods, borehole, editingEnabled }: BoreholeDetailProps) => { + const [totalDepthTVD, setTotalDepthTVD] = useState(null); + const [topBedrockFreshTVD, setTopBedrockFreshTVD] = useState(null); + const [topBedrockWeatheredTVD, setTopBedrockWeatheredTVD] = useState(null); + const roundTvdValue = (value: number | null) => { + return value ? Math.round(value * 100) / 100 : null; + }; + + const totalDepth = formMethods.watch("totalDepth"); + const topBedrockFreshMd = formMethods.watch("topBedrockFreshMd"); + const topBedrockWeatheredMd = formMethods.watch("topBedrockWeatheredMd"); + + const fetchDepthTVD = useCallback( + async (fieldValue: number | null) => { + if (!fieldValue) return null; + const getDepthTVD = async (depthMD: number | null) => { + if (depthMD == null) { + return null; + } else { + const response = await getBoreholeGeometryDepthTVD(borehole.id, depthMD); + return response ?? null; + } + }; + + const depth = await getDepthTVD(parseFloatWithThousandsSeparator(fieldValue.toString())); + return roundTvdValue(depth); + }, + [borehole.id], + ); + + useEffect(() => { + const fetchAndSetTotalDepthTVD = async () => { + setTotalDepthTVD(await fetchDepthTVD(totalDepth)); + }; + fetchAndSetTotalDepthTVD(); + }, [fetchDepthTVD, totalDepth]); + + useEffect(() => { + const fetchAndSetTotalDepthTVD = async () => { + setTopBedrockFreshTVD(await fetchDepthTVD(topBedrockFreshMd)); + }; + fetchAndSetTotalDepthTVD(); + }, [fetchDepthTVD, topBedrockFreshMd]); + + useEffect(() => { + const fetchAndSetTotalDepthTVD = async () => { + setTopBedrockWeatheredTVD(await fetchDepthTVD(topBedrockWeatheredMd)); + }; + fetchAndSetTotalDepthTVD(); + }, [fetchDepthTVD, topBedrockWeatheredMd]); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/client/src/pages/detail/form/borehole/boreholeGeneralSegment.tsx b/src/client/src/pages/detail/form/borehole/boreholeGeneralSegment.tsx deleted file mode 100644 index 165c3b4e2..000000000 --- a/src/client/src/pages/detail/form/borehole/boreholeGeneralSegment.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Form, Segment, TextArea } from "semantic-ui-react"; -import DomainDropdown from "../../../../components/legacyComponents/domain/dropdown/domainDropdown.jsx"; -import { capitalizeFirstLetter } from "../../../../utils"; -import { BoreholeGeneralProps } from "./boreholePanelInterfaces.ts"; - -const BoreholeGeneralSegment = ({ legacyBorehole, updateChange, isEditable }: BoreholeGeneralProps) => { - const { t } = useTranslation(); - - return ( - -
-
-
- - {/* drilling type in Borehole */} - - - { - updateChange("borehole_type", selected.id, false); - }} - schema="borehole_type" - selected={legacyBorehole.data.borehole_type} - readOnly={!isEditable} - /> - - - - - - { - updateChange("extended.purpose", selected.id, false); - }} - schema="extended.purpose" - selected={legacyBorehole.data.extended.purpose} - readOnly={!isEditable} - /> - - - - - - { - updateChange("extended.status", selected.id, false); - }} - schema="extended.status" - selected={legacyBorehole.data.extended.status} - readOnly={!isEditable} - /> - - -
-
-
-
- - {t("remarks")} -