diff --git a/JeMPI_Apps/JeMPI_UI/src/pages/settings/Settings.tsx b/JeMPI_Apps/JeMPI_UI/src/pages/settings/Settings.tsx index 57b3aeaa0..ee678105f 100644 --- a/JeMPI_Apps/JeMPI_UI/src/pages/settings/Settings.tsx +++ b/JeMPI_Apps/JeMPI_UI/src/pages/settings/Settings.tsx @@ -15,9 +15,11 @@ import { generateId } from 'utils/helpers' import Probabilistic from './probabilistic/Probabilistic' import { useConfig } from 'hooks/useConfig' import { useSnackbar } from 'notistack' +import { useConfiguration } from 'hooks/useUIConfiguration' const Settings = () => { const [value, setValue] = useState(0) + const {configuration} = useConfiguration() const [configurationData, setConfigurationData] = useState(() => { const storedData = localStorage.getItem('configuration') return storedData diff --git a/JeMPI_Apps/JeMPI_UI/src/pages/settings/common/Common.tsx b/JeMPI_Apps/JeMPI_UI/src/pages/settings/common/Common.tsx index 8c8aa63c5..5a4eb94eb 100644 --- a/JeMPI_Apps/JeMPI_UI/src/pages/settings/common/Common.tsx +++ b/JeMPI_Apps/JeMPI_UI/src/pages/settings/common/Common.tsx @@ -1,5 +1,5 @@ -import { useEffect, useState } from 'react' -import Box from '@mui/material/Box' +import { useEffect, useState } from 'react'; +import Box from '@mui/material/Box'; import { DataGrid, GridColDef, @@ -17,51 +17,67 @@ import CancelIcon from '@mui/icons-material/Close' import { EditToolbar } from 'components/shared/EditToolBar' import { processIndex, toSnakeCase, transformFieldName } from 'utils/helpers' import { useConfiguration } from 'hooks/useUIConfiguration' -import { Configuration, LinkMetaData } from 'types/Configuration' +import { Configuration, Field, LinkMetaData } from 'types/Configuration' +import { IconButton, Tooltip, Switch } from '@mui/material' import { RowData } from '../deterministic/SourceView' -import { Switch } from '@mui/material' +import { useSnackbar } from 'notistack' +import FieldDialog from './FieldDialog' +import AddIcon from '@mui/icons-material/Add' const CommonSettings = () => { const [rows, setRows] = useState([]) const { configuration, setConfiguration } = useConfiguration() const [rowModesModel, setRowModesModel] = useState({}) + const [openFieldModal, setOpenFieldModal] = useState(false) + const { enqueueSnackbar } = useSnackbar() useEffect(() => { - if (configuration && configuration.demographicFields) { - const rowData = configuration?.demographicFields.map( + if (configuration?.demographicFields) { + const rowData = configuration.demographicFields.map( (row: any, rowIndex: number) => ({ id: rowIndex + 1, ...row, rowIndex, disable: row.isDisabled }) - ) + ); setRows(rowData) } }, [configuration]) const handleEditClick = (id: GridRowId) => () => { setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }) - } + }; const handleSaveClick = (id: GridRowId) => () => { setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }) - } + }; const handleUpdateConfiguration = (updatedRow: any, rowIndex: number) => { - if (!configuration) return - const updatedConfiguration = getUpdatedConfiguration( - updatedRow, - rowIndex, - configuration - ) - localStorage.setItem('configuration', JSON.stringify(updatedConfiguration)) - setConfiguration(updatedConfiguration) - setRows((prevRows: any) => - prevRows.map((row: any) => - row.id === updatedRow.id ? { ...updatedRow } : row + if (!configuration) { + console.error("Cannot update configuration. Configuration is null.") + return; + } + + const prevConfiguration = { ...configuration } + try { + const updatedConfiguration = getUpdatedConfiguration( + updatedRow, + rowIndex, + configuration ) - ) + localStorage.setItem('configuration', JSON.stringify(updatedConfiguration)) + setConfiguration(updatedConfiguration) + setRows((prevRows: any) => + prevRows.map((row: any) => + row.id === updatedRow.id ? { ...updatedRow } : row + ) + ) + } catch (error) { + console.error("Error updating configuration:", error); + setConfiguration(prevConfiguration) + enqueueSnackbar("Failed to update configuration. Please try again.", { variant: 'error' }) + } } const getUpdatedConfiguration = ( @@ -73,16 +89,16 @@ const CommonSettings = () => { const fieldName = toSnakeCase(updatedRow.fieldName) if (!newConfiguration.demographicFields) { - return currentConfiguration + return currentConfiguration; } - const fieldToUpdate = { ...newConfiguration.demographicFields[rowIndex] } + const fieldToUpdate = { ...newConfiguration.demographicFields[rowIndex] }; if (!fieldToUpdate) { - return currentConfiguration + return currentConfiguration; } - fieldToUpdate.fieldName = fieldName + fieldToUpdate.fieldName = fieldName; if (updatedRow?.indexGoldenRecord) { if (!updatedRow.indexGoldenRecord.startsWith('@index(')) { @@ -99,38 +115,44 @@ const CommonSettings = () => { fieldToUpdate.linkMetaData = { ...fieldToUpdate.linkMetaData, m: Number(updatedRow.m) - } as LinkMetaData + } as LinkMetaData; } if (updatedRow?.u) { fieldToUpdate.linkMetaData = { ...fieldToUpdate.linkMetaData, u: Number(updatedRow.u) - } as LinkMetaData + } as LinkMetaData; } - newConfiguration.demographicFields[rowIndex] = fieldToUpdate + newConfiguration.demographicFields[rowIndex] = fieldToUpdate; - return newConfiguration - } + return newConfiguration; + }; const handleCancelClick = (id: GridRowId) => () => { setRowModesModel(prevRowModesModel => { - const newRowModesModel = { ...prevRowModesModel } - delete newRowModesModel[id] - return newRowModesModel - }) - } + const newRowModesModel = { ...prevRowModesModel }; + delete newRowModesModel[id]; + return newRowModesModel; + }); + }; const handleRowEditStop: GridEventListener<'rowEditStop'> = ({ reason }) => - reason === GridRowEditStopReasons.rowFocusOut + reason === GridRowEditStopReasons.rowFocusOut; const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { - setRowModesModel(newRowModesModel) - } + setRowModesModel(newRowModesModel); + }; const processRowUpdate = (newRow: GridRowModel) => { - const { id, ...updatedRow } = newRow + const isFieldNameValid = newRow?.fieldName && newRow.fieldName.toLowerCase() !== 'unknown_field'; + + if (!isFieldNameValid) { + enqueueSnackbar('Field name cannot be empty or unknown', { variant: 'error' }); + } + + const { id, ...updatedRow } = newRow; const updatedRows = rows.map((row: { id: any }) => row.id === id ? ({ ...updatedRow, id } as RowData) : row ) @@ -188,6 +210,37 @@ const CommonSettings = () => { processRowUpdate(updatedRow) } + const handleAddNewRow = (fieldName: string) => { + const newRow: Field = { + id: (rows.length + 1).toString(), + fieldName, + fieldType: 'String', + linkMetaData: { + comparison: '', + comparisonLevels: [], + m: 0, + u: 0 + }, + }; + + if (configuration) { + const newConfiguration = { + ...configuration, + demographicFields: [...configuration.demographicFields, newRow] + } + + localStorage.setItem('configuration', JSON.stringify(newConfiguration)) + setConfiguration(newConfiguration) + setRows((prevRows: any) => [...prevRows, newRow]) + setRowModesModel(prevRowModesModel => ({ + ...prevRowModesModel, + [(newRow.id) as string]: { mode: GridRowModes.Edit, fieldToFocus: 'fieldName' } + })) + } else { + console.error("Configuration is null. Cannot add new row.") + } + } + const columns: GridColDef[] = [ { field: 'fieldName', @@ -205,7 +258,7 @@ const CommonSettings = () => { width: 180, align: 'center', headerAlign: 'center', - editable: false + editable: true, }, { field: 'indexGoldenRecord', @@ -216,8 +269,8 @@ const CommonSettings = () => { headerAlign: 'center', editable: true, valueGetter: params => { - const indexGoldenRecord = params.row.indexGoldenRecord - return processIndex(indexGoldenRecord) + const indexGoldenRecord = params.row.indexGoldenRecord; + return processIndex(indexGoldenRecord); } }, { @@ -229,11 +282,11 @@ const CommonSettings = () => { align: 'center', headerAlign: 'center', valueGetter: params => { - const linkMetaData = params.row.linkMetaData + const linkMetaData = params.row.linkMetaData; if (linkMetaData && typeof linkMetaData.m === 'number') { - return linkMetaData.m.toFixed(7) + return linkMetaData.m.toFixed(7); } - return + return; } }, { @@ -245,9 +298,9 @@ const CommonSettings = () => { align: 'center', headerAlign: 'center', valueGetter: params => { - const linkMetaData = params.row.linkMetaData + const linkMetaData = params.row.linkMetaData; if (linkMetaData && typeof linkMetaData.u === 'number') { - return linkMetaData.u.toFixed(7) + return linkMetaData.u.toFixed(7); } } }, @@ -261,17 +314,19 @@ const CommonSettings = () => { width: 300, cellClassName: 'actions', getActions: ({ id }) => { - const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit + const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; + const row = rows.find((row: any) => row.id === id); + const isFieldNameValid = row?.fieldName && row.fieldName.toLowerCase() !== 'unknown_field'; + if (isInEditMode) { return [ } id="save-button" label="Save" - sx={{ - color: 'white' - }} + sx={{ color: 'white' }} onClick={handleSaveClick(id)} + disabled={!isFieldNameValid} />, } @@ -280,10 +335,11 @@ const CommonSettings = () => { className="textPrimary" onClick={handleCancelClick(id)} color="inherit" + disabled={!isFieldNameValid} /> - ] + ]; } - + return [ } @@ -293,7 +349,7 @@ const CommonSettings = () => { onClick={handleEditClick(id)} color="inherit" /> - ] + ]; } }, { @@ -311,7 +367,7 @@ const CommonSettings = () => { ) } } - ] + ]; return ( { width: '100%' }} > + + + setOpenFieldModal(true)}> + + + + + {configuration && ( { onRowModesModelChange={handleRowModesModelChange} processRowUpdate={processRowUpdate} onRowEditStop={handleRowEditStop} - getRowId={row => row.id} + onProcessRowUpdateError={(error) => { + console.error(error); + }} + getRowId={(row) => row.id} slots={{ toolbar: EditToolbar }} @@ -339,7 +410,7 @@ const CommonSettings = () => { /> )} - ) -} + ); +}; -export default CommonSettings +export default CommonSettings; diff --git a/JeMPI_Apps/JeMPI_UI/src/pages/settings/common/FieldDialog.tsx b/JeMPI_Apps/JeMPI_UI/src/pages/settings/common/FieldDialog.tsx new file mode 100644 index 000000000..20064cf9b --- /dev/null +++ b/JeMPI_Apps/JeMPI_UI/src/pages/settings/common/FieldDialog.tsx @@ -0,0 +1,61 @@ +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +import { useState } from 'react'; +import { DialogContentText } from '@mui/material'; + +interface FieldDialogProps { + openFieldModal: boolean; + setOpenFieldModal: React.Dispatch>; + onSave: (fieldName: string) => void; +} + +export default function FieldDialog({ openFieldModal, setOpenFieldModal, onSave }: FieldDialogProps) { + const [fieldName, setFieldName] = useState(''); + + const handleClose = () => { + setOpenFieldModal(false); + }; + + const handleSave = () => { + onSave(fieldName); + handleClose(); + }; + + return ( + + Add Field Name + + + To add a new field to common settings, please enter a field name. + + setFieldName(e.target.value)} + /> + + + + + + + ); +} diff --git a/JeMPI_Apps/JeMPI_UI/src/test/settings/CommonSettings.test.tsx b/JeMPI_Apps/JeMPI_UI/src/test/settings/CommonSettings.test.tsx index 5700c7a4d..da5d5f889 100644 --- a/JeMPI_Apps/JeMPI_UI/src/test/settings/CommonSettings.test.tsx +++ b/JeMPI_Apps/JeMPI_UI/src/test/settings/CommonSettings.test.tsx @@ -5,12 +5,21 @@ import CommonSettings from 'pages/settings/common/Common'; import mockData from 'services/mockData'; import '@testing-library/jest-dom'; import { useConfiguration } from 'hooks/useUIConfiguration'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ConfigProvider } from 'hooks/useConfig'; +import { BrowserRouter } from 'react-router-dom'; jest.mock('hooks/useUIConfiguration', () => ({ useConfiguration: jest.fn(), })); +const queryClient = new QueryClient({ + defaultOptions: { + queries: {} + } +}) + describe('CommonSettings Component', () => { const configData = mockData.configuration.demographicFields.map((row, index) => ({ ...row, @@ -23,12 +32,27 @@ describe('CommonSettings Component', () => { }); }); it('renders without crashing', () => { - render(); + render( + + + + + + ); }); it('handles edit mode', async () => { - render(); + render( + + + + + + + + ) + const editIcon = document.getElementById('edit-button'); const saveButton = document.getElementById('save-button'); const cancelButton = document.getElementById('cancel-button');