diff --git a/src/client/cypress/e2e/detailPage/boreholeform.cy.js b/src/client/cypress/e2e/detailPage/boreholeform.cy.js index 0c9ef7cba..3bb360f74 100644 --- a/src/client/cypress/e2e/detailPage/boreholeform.cy.js +++ b/src/client/cypress/e2e/detailPage/boreholeform.cy.js @@ -1,4 +1,4 @@ -import { exportCSVItem, exportJsonItem, saveWithSaveBar } from "../helpers/buttonHelpers"; +import { exportCSVItem, exportItem, exportJsonItem, saveWithSaveBar } from "../helpers/buttonHelpers"; import { clickOnRowWithText, showTableAndWaitForData, sortBy } from "../helpers/dataGridHelpers"; import { evaluateInput, evaluateSelect, isDisabled, setInput, setSelect } from "../helpers/formHelpers"; import { @@ -248,7 +248,9 @@ describe("Test for the borehole form.", () => { cy.get("@borehole_id").then(id => { goToRouteAndAcceptTerms(`/${id}`); ensureEditingDisabled(); + exportItem(); exportJsonItem(); + exportItem(); exportCSVItem(); }); diff --git a/src/client/cypress/e2e/helpers/buttonHelpers.js b/src/client/cypress/e2e/helpers/buttonHelpers.js index 28e4b1247..61130f3b2 100644 --- a/src/client/cypress/e2e/helpers/buttonHelpers.js +++ b/src/client/cypress/e2e/helpers/buttonHelpers.js @@ -59,11 +59,20 @@ export const deleteItem = parent => { cy.get(selector).click({ force: true }); }; +/** + * Clicks on the Export button. + */ +export const exportItem = () => { + const selector = '[data-cy="export-button"]'; + cy.get(selector).should("not.be.disabled"); + cy.get(selector).click({ force: true }); +}; + /** * Clicks on the JSON-export button. */ export const exportJsonItem = () => { - const selector = '[data-cy="exportjson-button"]'; + const selector = '[data-cy="json-button"]'; cy.get(selector).should("not.be.disabled"); cy.get(selector).click({ force: true }); }; @@ -72,7 +81,7 @@ export const exportJsonItem = () => { * Clicks on the CSV-export button. */ export const exportCSVItem = () => { - const selector = '[data-cy="exportcsv-button"]'; + const selector = '[data-cy="csv-button"]'; cy.get(selector).should("not.be.disabled"); cy.get(selector).click({ force: true }); }; diff --git a/src/client/cypress/e2e/mainPage/export.cy.js b/src/client/cypress/e2e/mainPage/export.cy.js index 693da6e56..a57849b77 100644 --- a/src/client/cypress/e2e/mainPage/export.cy.js +++ b/src/client/cypress/e2e/mainPage/export.cy.js @@ -1,4 +1,4 @@ -import { addItem, deleteItem, exportCSVItem, exportJsonItem, saveWithSaveBar } from "../helpers/buttonHelpers"; +import { deleteItem, exportCSVItem, exportItem, exportJsonItem } from "../helpers/buttonHelpers"; import { checkAllVisibleRows, checkRowWithText, showTableAndWaitForData } from "../helpers/dataGridHelpers.js"; import { evaluateInput, setInput, setSelect } from "../helpers/formHelpers"; import { @@ -58,7 +58,11 @@ describe("Test for exporting boreholes.", () => { checkRowWithText("AAA_NINTIC"); checkRowWithText("AAA_LOMONE"); + deleteDownloadedFile(jsonFileName); + deleteDownloadedFile(csvFileName); + exportItem(); exportJsonItem(); + exportItem(); exportCSVItem(); readDownloadedFile(jsonFileName); readDownloadedFile(csvFileName); @@ -187,14 +191,15 @@ describe("Test for exporting boreholes.", () => { deleteDownloadedFile(jsonFileName); showTableAndWaitForData(); checkAllVisibleRows(); - exportCSVItem(); + deleteDownloadedFile(csvFileName); + exportItem(); const moreThan100SelectedPrompt = "You have selected more than 100 boreholes and a maximum of 100 boreholes can be exported. Do you want to continue?"; handlePrompt(moreThan100SelectedPrompt, "Cancel"); - cy.get("@borehole_export_csv").should("not.exist"); - exportCSVItem(); + exportItem(); handlePrompt(moreThan100SelectedPrompt, "Export 100 boreholes"); + exportCSVItem(); cy.wait("@borehole_export_csv").its("response.statusCode").should("eq", 200); readDownloadedFile(csvFileName); @@ -203,7 +208,7 @@ describe("Test for exporting boreholes.", () => { const lines = fileContent.split("\n"); expect(lines.length).to.equal(102); }); - exportJsonItem(); - handlePrompt(moreThan100SelectedPrompt, "Cancel"); + + deleteDownloadedFile(jsonFileName); }); }); diff --git a/src/client/public/locale/de/common.json b/src/client/public/locale/de/common.json index 5fdf5edf9..cfa7a1987 100644 --- a/src/client/public/locale/de/common.json +++ b/src/client/public/locale/de/common.json @@ -307,6 +307,7 @@ "maxValue": "Maximalwert", "member": "Member", "messageDiscardUnsavedChanges": "Es gibt ungespeicherte Änderungen. Möchten Sie alle Änderungen verwerfen?", + "messageUnsavedChangesAtExport": "Es gibt ungespeicherte Änderungen. Möchten Sie das Bohrloch ohne Speichern exportieren?", "meter": "Meter", "minValue": "Minimalwert", "minute": "Minute", diff --git a/src/client/public/locale/en/common.json b/src/client/public/locale/en/common.json index 3185533fa..0b4161c45 100644 --- a/src/client/public/locale/en/common.json +++ b/src/client/public/locale/en/common.json @@ -307,6 +307,7 @@ "maxValue": "Maximum", "member": "Member", "messageDiscardUnsavedChanges": "There are unsaved changes. Do you want to discard all changes?", + "messageUnsavedChangesAtExport": "There are unsaved changes. Do you want to export the borehole without saving?", "meter": "Meter", "minValue": "Minimum", "minute": "minute", diff --git a/src/client/public/locale/fr/common.json b/src/client/public/locale/fr/common.json index e00675dcf..e81e7c5a5 100644 --- a/src/client/public/locale/fr/common.json +++ b/src/client/public/locale/fr/common.json @@ -307,6 +307,7 @@ "maxValue": "Valeur maximale", "member": "Membre", "messageDiscardUnsavedChanges": "Il y a des modifications non enregistrées. Voulez-vous annuler toutes les modifications?", + "messageUnsavedChangesAtExport": "Il y a des modifications non enregistrées. Voulez-vous exporter le forage sans enregistrer?", "meter": "Mètres", "minValue": "Valeur minimale", "minute": "minute", diff --git a/src/client/public/locale/it/common.json b/src/client/public/locale/it/common.json index 72e596fed..b3491680e 100644 --- a/src/client/public/locale/it/common.json +++ b/src/client/public/locale/it/common.json @@ -307,6 +307,7 @@ "maxValue": "Valore massimo", "member": "Membro", "messageDiscardUnsavedChanges": "Sono presenti modifiche non salvate. Si desidera annullare tutte le modifiche?", + "messageUnsavedChangesAtExport": "Ci sono modifiche non salvate. Si desidera esportare la perforazione senza salvare?", "meter": "Metri", "minValue": "Valore minimo", "minute": "minuto", diff --git a/src/client/src/components/legacyComponents/bulkedit/BulkEditFormProps.ts b/src/client/src/components/bulkedit/BulkEditFormProps.ts similarity index 84% rename from src/client/src/components/legacyComponents/bulkedit/BulkEditFormProps.ts rename to src/client/src/components/bulkedit/BulkEditFormProps.ts index e7299dae1..d8bf948ab 100644 --- a/src/client/src/components/legacyComponents/bulkedit/BulkEditFormProps.ts +++ b/src/client/src/components/bulkedit/BulkEditFormProps.ts @@ -1,9 +1,10 @@ import { GridRowSelectionModel } from "@mui/x-data-grid"; -import { FormValueType } from "../../form/form.ts"; +import { FormValueType } from "../form/form.ts"; export interface BulkEditFormProps { selected: GridRowSelectionModel; loadBoreholes: () => void; + isOpen: boolean; } export type BulkEditFormValue = string | number | boolean | undefined | null; diff --git a/src/client/src/components/legacyComponents/bulkedit/bulkEditForm.tsx b/src/client/src/components/bulkedit/bulkEditDialog.tsx similarity index 63% rename from src/client/src/components/legacyComponents/bulkedit/bulkEditForm.tsx rename to src/client/src/components/bulkedit/bulkEditDialog.tsx index a1109f6fe..6dac51530 100644 --- a/src/client/src/components/legacyComponents/bulkedit/bulkEditForm.tsx +++ b/src/client/src/components/bulkedit/bulkEditDialog.tsx @@ -7,6 +7,7 @@ import { AccordionDetails, AccordionSummary, Box, + Dialog, DialogActions, DialogContent, DialogTitle, @@ -15,20 +16,20 @@ import { Typography, } from "@mui/material"; import { ChevronDownIcon, RotateCcw } from "lucide-react"; -import { patchBoreholes } from "../../../api-lib"; -import { ReduxRootState, User } from "../../../api-lib/ReduxStateInterfaces.ts"; -import { theme } from "../../../AppTheme.ts"; -import WorkgroupSelect from "../../../pages/overview/sidePanelContent/commons/workgroupSelect.tsx"; -import { AlertContext } from "../../alert/alertContext.tsx"; -import { CancelButton, SaveButton } from "../../buttons/buttons"; -import { FormValueType } from "../../form/form.ts"; -import { FormBooleanSelect } from "../../form/formBooleanSelect.tsx"; -import { FormDomainSelect } from "../../form/formDomainSelect.tsx"; -import { FormInput } from "../../form/formInput.tsx"; -import { StackFullWidth } from "../../styledComponents.ts"; +import { patchBoreholes } from "../../api-lib"; +import { ReduxRootState, User } from "../../api-lib/ReduxStateInterfaces.ts"; +import { theme } from "../../AppTheme.ts"; +import WorkgroupSelect from "../../pages/overview/sidePanelContent/commons/workgroupSelect.tsx"; +import { AlertContext } from "../alert/alertContext.tsx"; +import { CancelButton, SaveButton } from "../buttons/buttons.tsx"; +import { FormValueType } from "../form/form.ts"; +import { FormBooleanSelect } from "../form/formBooleanSelect.tsx"; +import { FormDomainSelect } from "../form/formDomainSelect.tsx"; +import { FormInput } from "../form/formInput.tsx"; +import { StackFullWidth } from "../styledComponents.ts"; import { BulkEditFormField, BulkEditFormProps, BulkEditFormValue } from "./BulkEditFormProps.ts"; -export const BulkEditForm = ({ selected, loadBoreholes }: BulkEditFormProps) => { +export const BulkEditDialog = ({ isOpen, selected, loadBoreholes }: BulkEditFormProps) => { const [fieldsToUpdate, setFieldsToUpdate] = useState>([]); const [workgroupId, setWorkgroupId] = useState(""); const { showAlert } = useContext(AlertContext); @@ -200,68 +201,82 @@ export const BulkEditForm = ({ selected, loadBoreholes }: BulkEditFormProps) => ); return ( - - - {t("bulkEditing")} - - - - - {bulkEditFormFields.map(field => { - if (field.type != FormValueType.Workgroup || enabledWorkgroups.length > 1) { - return ( - - } - sx={{ - pl: 1, - "& .MuiAccordionSummary-content": { - m: 0, - }, - }}> - - f[0]).includes(field.api ?? field.fieldName) - ? "visible" - : "hidden", - mr: 1, - }} - onClick={e => { - e.stopPropagation(); - undoChange(field); - }}> - - - - {t(field.fieldName)} - - - - - - <>{renderInput(field)} - - - - ); - } - })} - - - - - - - - - - + + + + {t("bulkEditing")} + + + + + {bulkEditFormFields.map(field => { + if (field.type != FormValueType.Workgroup || enabledWorkgroups.length > 1) { + return ( + + } + sx={{ + pl: 1, + "& .MuiAccordionSummary-content": { + m: 0, + }, + }}> + + f[0]).includes(field.api ?? field.fieldName) + ? "visible" + : "hidden", + mr: 1, + }} + onClick={e => { + e.stopPropagation(); + undoChange(field); + }}> + + + + {t(field.fieldName)} + + + + + + <>{renderInput(field)} + + + + ); + } + })} + + + + + + + + + + + ); }; diff --git a/src/client/src/components/export/exportDialog.tsx b/src/client/src/components/export/exportDialog.tsx new file mode 100644 index 000000000..d430ae885 --- /dev/null +++ b/src/client/src/components/export/exportDialog.tsx @@ -0,0 +1,50 @@ +import { useTranslation } from "react-i18next"; +import { Dialog, DialogActions, DialogContent, DialogTitle, Stack, Typography } from "@mui/material"; +import { GridRowSelectionModel } from "@mui/x-data-grid"; +import { exportCSVBorehole, getAllBoreholes } from "../../api/borehole.ts"; +import { downloadData } from "../../utils.ts"; +import { CancelButton, ExportButton } from "../buttons/buttons.tsx"; + +interface ExportDialogProps { + isExporting: boolean; + setIsExporting: React.Dispatch>; + selectionModel: GridRowSelectionModel; + fileName: string; +} +export const ExportDialog = ({ isExporting, setIsExporting, selectionModel, fileName }: ExportDialogProps) => { + const { t } = useTranslation(); + + const exportJson = async () => { + const paginatedResponse = await getAllBoreholes(selectionModel, 1, selectionModel.length); + const jsonString = JSON.stringify(paginatedResponse.boreholes, null, 2); + downloadData(jsonString, `${fileName}.json`, "application/json"); + setIsExporting(false); + }; + + const exportCsv = async () => { + const csvData = await exportCSVBorehole(selectionModel.slice(0, 100)); + downloadData(csvData, `${fileName}.csv`, "text/csv"); + setIsExporting(false); + }; + + return ( + + + + {t("export")} + + + + + + + + + + setIsExporting(false)} /> + + + + + ); +}; diff --git a/src/client/src/pages/detail/detailHeader.tsx b/src/client/src/pages/detail/detailHeader.tsx index 0e24b5ddd..4dfefaf24 100644 --- a/src/client/src/pages/detail/detailHeader.tsx +++ b/src/client/src/pages/detail/detailHeader.tsx @@ -1,11 +1,11 @@ -import { useContext } from "react"; +import { useContext, useState } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; import { useHistory } from "react-router-dom"; import { Chip, Stack, Typography } from "@mui/material"; -import { Check, Trash2, X } from "lucide-react"; +import { ArrowDownToLine, Check, Trash2, X } from "lucide-react"; import { deleteBorehole, lockBorehole, unlockBorehole } from "../../api-lib"; -import { BoreholeV2, exportCSVBorehole } from "../../api/borehole.ts"; +import { BoreholeV2 } from "../../api/borehole.ts"; import { useAuth } from "../../auth/useBdmsAuth.tsx"; import { DeleteButton, @@ -14,10 +14,10 @@ import { ExportButton, ReturnButton, } from "../../components/buttons/buttons.tsx"; +import { ExportDialog } from "../../components/export/exportDialog.tsx"; import DateText from "../../components/legacyComponents/dateText"; import { PromptContext } from "../../components/prompt/promptContext.tsx"; import { DetailHeaderStack } from "../../components/styledComponents.ts"; -import { downloadData } from "../../utils.ts"; import { useFormDirty } from "./useFormDirty.tsx"; interface DetailHeaderProps { @@ -35,6 +35,7 @@ const DetailHeader = ({ triggerReset, borehole, }: DetailHeaderProps) => { + const [isExporting, setIsExporting] = useState(false); const history = useHistory(); const dispatch = useDispatch(); const { t } = useTranslation(); @@ -80,24 +81,27 @@ const DetailHeader = ({ ]); }; + const startExportWithUnsavedChanges = () => { + showPrompt(t("messageUnsavedChangesAtExport"), [ + { + label: t("cancel"), + icon: , + variant: "outlined", + }, + { + label: t("export"), + icon: , + variant: "contained", + action: () => setIsExporting(true), + }, + ]); + }; + const handleDelete = async () => { await deleteBorehole(borehole.id); history.push("/"); }; - const getFileName = (name: string) => { - return name.replace(/\s/g, "_"); - }; - const handleJsonExport = () => { - const jsonString = JSON.stringify([borehole], null, 2); - downloadData(jsonString, getFileName(borehole.name), "application/json"); - }; - - const handleCSVExport = async () => { - const csvData = await exportCSVBorehole([borehole.id]); - downloadData(csvData, getFileName(borehole.name), "text/csv"); - }; - return ( @@ -125,37 +129,46 @@ const DetailHeader = ({ /> - {editableByCurrentUser && - (editingEnabled ? ( - <> - - showPrompt(t("deleteBoreholesMessage", { count: 1 }), [ - { - label: t("cancel"), - }, - { - label: t("delete"), - icon: , - variant: "contained", - action: () => { - handleDelete(); + {editableByCurrentUser && ( + <> + setIsExporting(true)} + /> + {editingEnabled ? ( + <> + + showPrompt(t("deleteBoreholesMessage", { count: 1 }), [ + { + label: t("cancel"), + }, + { + label: t("delete"), + icon: , + variant: "contained", + action: () => { + handleDelete(); + }, }, - }, - ]) - } - /> - - - ) : ( - <> - - + ]) + } + /> + + + ) : ( - - ))} + )} + + )} + ); }; diff --git a/src/client/src/pages/overview/boreholeTable/bottomBar.tsx b/src/client/src/pages/overview/boreholeTable/bottomBar.tsx index ab0e3bab9..7eaa896ff 100644 --- a/src/client/src/pages/overview/boreholeTable/bottomBar.tsx +++ b/src/client/src/pages/overview/boreholeTable/bottomBar.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { Box, Button, Stack, Typography } from "@mui/material"; import { GridRowSelectionModel } from "@mui/x-data-grid"; -import { ChevronDown, ChevronUp, Trash2 } from "lucide-react"; +import { ArrowDownToLine, ChevronDown, ChevronUp, Trash2 } from "lucide-react"; import CopyIcon from "../../../assets/icons/copy.svg?react"; import { Boreholes, ReduxRootState, User } from "../../../api-lib/ReduxStateInterfaces.ts"; import { theme } from "../../../AppTheme.ts"; @@ -20,10 +20,9 @@ interface BottomBarProps { search: { filter: string }; onDeleteMultiple: () => void; onCopyBorehole: () => void; - onExportMultipleJson: () => void; - onExportMultipleCsv: () => void; workgroup: string; setWorkgroup: React.Dispatch>; + setIsExporting: React.Dispatch>; } const BottomBar = ({ @@ -32,11 +31,10 @@ const BottomBar = ({ onDeleteMultiple, search, onCopyBorehole, - onExportMultipleJson, - onExportMultipleCsv, boreholes, workgroup, setWorkgroup, + setIsExporting, }: BottomBarProps) => { const { t } = useTranslation(); const { showPrompt, promptIsOpen } = useContext(PromptContext); @@ -83,6 +81,30 @@ const BottomBar = ({ multipleSelected(selectionModel, search.filter); } + const showPromptExportMoreThan100 = (callback: () => void) => { + showPrompt(t("bulkExportMoreThan100"), [ + { + label: t("cancel"), + }, + { + label: t("export100Boreholes"), + icon: , + variant: "contained", + action: callback, + }, + ]); + }; + + const onExportMultiple = async () => { + if (selectionModel.length > 100) { + showPromptExportMoreThan100(() => { + setIsExporting(true); + }); + } else { + setIsExporting(true); + } + }; + return ( showCopyPromptForSelectedWorkgroup()} /> )} - - + onExportMultiple()} /> {t("selectedCount", { count: selectionModel.length })} ) : ( diff --git a/src/client/src/pages/overview/boreholeTable/bottomBarContainer.tsx b/src/client/src/pages/overview/boreholeTable/bottomBarContainer.tsx index b6c5989bd..1dbb9c00c 100644 --- a/src/client/src/pages/overview/boreholeTable/bottomBarContainer.tsx +++ b/src/client/src/pages/overview/boreholeTable/bottomBarContainer.tsx @@ -1,14 +1,10 @@ import React, { useCallback, useContext, useLayoutEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { useHistory } from "react-router-dom"; import { GridRowSelectionModel, GridSortDirection, GridSortModel } from "@mui/x-data-grid"; -import { ArrowDownToLine } from "lucide-react"; import { deleteBoreholes } from "../../../api-lib"; import { Boreholes, ReduxRootState, User } from "../../../api-lib/ReduxStateInterfaces.ts"; -import { copyBorehole, exportCSVBorehole, getAllBoreholes } from "../../../api/borehole.ts"; -import { PromptContext } from "../../../components/prompt/promptContext.tsx"; -import { downloadData } from "../../../utils.ts"; +import { copyBorehole } from "../../../api/borehole.ts"; import { OverViewContext } from "../overViewContext.tsx"; import { FilterContext } from "../sidePanelContent/filter/filterContext.tsx"; import { BoreholeTable } from "./boreholeTable.tsx"; @@ -31,6 +27,7 @@ interface BottomBarContainerProps { rowToHighlight: number | null; selectionModel: GridRowSelectionModel; setSelectionModel: React.Dispatch>; + setIsExporting: React.Dispatch>; } const BottomBarContainer = ({ @@ -42,13 +39,12 @@ const BottomBarContainer = ({ rowToHighlight, selectionModel, setSelectionModel, + setIsExporting, }: BottomBarContainerProps) => { const user: User = useSelector((state: ReduxRootState) => state.core_user); const history = useHistory(); - const { t } = useTranslation(); const { featureIds } = useContext(FilterContext); const { bottomDrawerOpen } = useContext(OverViewContext); - const { showPrompt } = useContext(PromptContext); const [workgroupId, setWorkgroupId] = useState(user.data.workgroups[0]?.id); const [isBusy, setIsBusy] = useState(false); const [paginationModel, setPaginationModel] = useState({ @@ -94,43 +90,6 @@ const BottomBarContainer = ({ setIsBusy(false); }; - const getBulkExportFilename = (suffix: string) => { - return `bulkexport_${new Date().toISOString().split("T")[0]}.${suffix}`; - }; - - const handleExportMultipleJson = async () => { - const paginatedResponse = await getAllBoreholes(selectionModel, 1, selectionModel.length); - const jsonString = JSON.stringify(paginatedResponse.boreholes, null, 2); - downloadData(jsonString, getBulkExportFilename("json"), "application/json"); - }; - - const handleExportMultipleCsv = async () => { - const csvData = await exportCSVBorehole(selectionModel.slice(0, 100)); - downloadData(csvData, getBulkExportFilename("csv"), "text/csv"); - }; - - const showPromptExportMoreThan100 = (callback: () => void) => { - showPrompt(t("bulkExportMoreThan100"), [ - { - label: t("cancel"), - }, - { - label: t("export100Boreholes"), - icon: , - variant: "contained", - action: callback, - }, - ]); - }; - - const onExportMultiple = async (callback: () => Promise) => { - if (selectionModel.length > 100) { - showPromptExportMoreThan100(callback); - } else { - await callback(); - } - }; - return ( <> onExportMultiple(handleExportMultipleJson)} - onExportMultipleCsv={() => onExportMultiple(handleExportMultipleCsv)} search={search} boreholes={boreholes} workgroup={workgroupId} setWorkgroup={setWorkgroupId} + setIsExporting={setIsExporting} /> { const [hover, setHover] = useState(null); const [rowToHighlight, setRowToHighlight] = useState(null); const [selectionModel, setSelectionModel] = useState([]); + const [isExporting, setIsExporting] = useState(false); const history = useHistory(); const { filterPolygon, @@ -73,30 +75,26 @@ export const MapView = ({ displayErrorMessage }: MapViewProps) => { sx={{ flex: "1 1.5 100%", }}> - - { - loadBoreholes( - boreholes.page, - boreholes.limit, - search.filter, - boreholes.orderby, - boreholes.direction, - featureIds, - ); - }} - selected={selectionModel} - /> - + { + loadBoreholes( + boreholes.page, + boreholes.limit, + search.filter, + boreholes.orderby, + boreholes.direction, + featureIds, + ); + }} + selected={selectionModel} + /> + { setFeatureIds={setFeatureIds} displayErrorMessage={displayErrorMessage} /> - { setSelectionModel={setSelectionModel} rowToHighlight={rowToHighlight} setHover={setHover} + setIsExporting={setIsExporting} /> );