Skip to content

Commit

Permalink
CRUD system reimplemented for msv. editing modal that moves through t…
Browse files Browse the repository at this point in the history
…he system added.
  • Loading branch information
siddheshraze committed Jan 24, 2025
1 parent 3c7b488 commit 3f0917e
Show file tree
Hide file tree
Showing 5 changed files with 326 additions and 4 deletions.
16 changes: 16 additions & 0 deletions frontend/app/api/formatrunquery/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { NextRequest, NextResponse } from 'next/server';
import ConnectionManager from '@/config/connectionmanager';
import { format } from 'mysql2/promise';

// this is intended as a dedicated server-side execution pipeline for a given query. Results will be returned as-is to caller.
export async function POST(request: NextRequest) {
const body = await request.json(); // receiving query already formatted and prepped for execution
const { query, params } = body;
const connectionManager = ConnectionManager.getInstance();
const formattedQuery = format(query, params);
const results = await connectionManager.executeQuery(formattedQuery);
return new NextResponse(JSON.stringify(results), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
1 change: 0 additions & 1 deletion frontend/app/api/runquery/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import ConnectionManager from '@/config/connectionmanager';
// this is intended as a dedicated server-side execution pipeline for a given query. Results will be returned as-is to caller.
export async function POST(request: NextRequest) {
const query = await request.json(); // receiving query already formatted and prepped for execution

const connectionManager = ConnectionManager.getInstance();
const results = await connectionManager.executeQuery(query);
return new NextResponse(JSON.stringify(results), {
Expand Down
262 changes: 262 additions & 0 deletions frontend/components/datagrids/applications/msveditingmodal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
'use client';

import { GridRowModel } from '@mui/x-data-grid';
import React, { useEffect, useState } from 'react';
import {
Button,
DialogActions,
DialogContent,
DialogTitle,
Modal,
ModalDialog,
Step,
stepClasses,
StepIndicator,
stepIndicatorClasses,
Stepper,
Typography
} from '@mui/joy';
import { getUpdatedValues } from '@/config/utils';
import MapperFactory from '@/config/datamapper';
import { useSiteContext } from '@/app/contexts/userselectionprovider';
import { Diversity2, Forest, Grass, GridView, PrecisionManufacturing } from '@mui/icons-material';
import { v4 } from 'uuid';

interface MSVEditingProps {
gridType: string;
oldRow: GridRowModel;
newRow: GridRowModel;
handleClose: () => void;
handleSave: (confirmedRow: GridRowModel) => void;
}

export default function MSVEditingModal(props: MSVEditingProps) {
const currentSite = useSiteContext();
const { gridType, handleClose, oldRow, newRow, handleSave } = props;
const updatedFields = getUpdatedValues(oldRow, newRow);
const { coreMeasurementID, quadratID, treeID, stemID, speciesID } = newRow;
const fieldGroups = {
coremeasurements: ['measuredDBH', 'measuredHOM', 'measurementDate'],
quadrats: ['quadratName'],
trees: ['treeTag'],
stems: ['stemTag', 'stemLocalX', 'stemLocalY'],
species: ['speciesName', 'subspeciesName', 'speciesCode']
};
type UploadStatus = 'idle' | 'in-progress' | 'completed' | 'error';
const [beginUpload, setBeginUpload] = useState(false);
const [isConfirmStep, setIsConfirmStep] = useState(false);
const [uploadStatus, setUploadStatus] = useState<{
[Key in keyof typeof fieldGroups]: UploadStatus;
}>({
coremeasurements: 'idle',
quadrats: 'idle',
trees: 'idle',
stems: 'idle',
species: 'idle'
});
const stepIcons = [<PrecisionManufacturing key={v4()} />, <GridView key={v4()} />, <Forest key={v4()} />, <Grass key={v4()} />, <Diversity2 key={v4()} />];

const handleUpdate = async (groupName: keyof typeof fieldGroups, tableName: string, idColumn: string, idValue: any) => {
console.log('handle update entered for group name: ', groupName);
setUploadStatus(prev => ({
...prev,
[groupName]: 'in-progress'
}));
const matchingFields = Object.keys(updatedFields).reduce(
(acc, key) => {
if (fieldGroups[groupName].includes(key)) {
acc[key] = updatedFields[key];
}
return acc;
},
{} as Partial<typeof updatedFields>
);

console.log(`matching fields for group name ${groupName}: `, matchingFields);

if (Object.keys(matchingFields).length > 0) {
console.log('match found: ');
if (groupName === 'stems') {
// need to correct for key matching
if (matchingFields.stemLocalX) {
matchingFields.localX = matchingFields.stemLocalX;
delete matchingFields.stemLocalX;
}
if (matchingFields.stemLocalY) {
matchingFields.localY = matchingFields.stemLocalY;
delete matchingFields.stemLocalY;
}
}
try {
const demappedData = MapperFactory.getMapper<any, any>(groupName).demapData([matchingFields])[0];
const query = `UPDATE ?? SET ? WHERE ?? = ?`;
const response = await fetch(`/api/formatrunquery`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: query, params: [`${currentSite?.schemaName}.${tableName}`, demappedData, idColumn, idValue] })
});
if (response.ok)
setUploadStatus(prev => ({
...prev,
[groupName]: 'completed'
}));
else throw new Error(`err`);
} catch (e) {
console.error(e);
setUploadStatus(prev => ({
...prev,
[groupName]: 'error'
}));
}
} else {
setUploadStatus(prev => ({
...prev,
[groupName]: 'completed'
}));
}
};

const handleBeginUpload = async () => {
await handleUpdate('coremeasurements', 'coremeasurements', 'CoreMeasurementID', coreMeasurementID);
await new Promise(resolve => setTimeout(resolve, 1000));
await handleUpdate('quadrats', 'quadrats', 'QuadratID', quadratID);
await new Promise(resolve => setTimeout(resolve, 1000));
await handleUpdate('trees', 'trees', 'TreeID', treeID);
await new Promise(resolve => setTimeout(resolve, 1000));
await handleUpdate('stems', 'stems', 'StemID', stemID);
await new Promise(resolve => setTimeout(resolve, 1000));
await handleUpdate('species', 'species', 'SpeciesID', speciesID);
};

const handleFinalConfirm = () => {
handleSave(newRow);
};

useEffect(() => {
if (beginUpload) handleBeginUpload();
}, [beginUpload]);

useEffect(() => {
console.log('use effect upload status: ', uploadStatus);
if (Object.values(uploadStatus).every(value => value === 'completed')) setIsConfirmStep(true);
}, [uploadStatus]);

// pulled from JoyUI doc example
function IconStepper() {
const steps = Object.keys(uploadStatus);
return (
<Stepper
size="lg"
sx={{
width: '100%',
'--StepIndicator-size': '3rem',
'--Step-connectorInset': '0px',
[`& .${stepIndicatorClasses.root}`]: {
borderWidth: 4
},
[`& .${stepClasses.root}::after`]: {
height: 4
},
[`& .${stepClasses.completed}`]: {
[`& .${stepIndicatorClasses.root}`]: {
borderColor: 'primary.300',
color: 'primary.300'
},
'&::after': {
bgcolor: 'primary.300'
}
},
[`& .${stepClasses.active}`]: {
[`& .${stepIndicatorClasses.root}`]: {
borderColor: 'currentColor'
}
},
[`& .${stepClasses.disabled} *`]: {
color: 'neutral.outlinedDisabledColor'
}
}}
>
{steps.map((stepKey, index) => {
const status = uploadStatus[stepKey as keyof typeof uploadStatus];

// Determine the step's state
const isCompleted = status === 'completed';
const isActive = status === 'in-progress';
const isDisabled = status === 'idle';

return (
<Step
key={stepKey}
completed={isCompleted}
active={isActive}
disabled={isDisabled}
orientation="vertical"
indicator={
<StepIndicator variant={isActive ? 'solid' : 'outlined'} color={isCompleted ? 'primary' : isActive ? 'primary' : 'neutral'}>
{stepIcons[index]}
</StepIndicator>
}
>
<Typography
sx={{
textTransform: 'uppercase',
fontWeight: 'lg',
fontSize: '0.75rem',
letterSpacing: '0.5px'
}}
>
{stepKey === 'coremeasurements' ? 'CoreMeasurements' : stepKey.charAt(0).toUpperCase() + stepKey.slice(1)}
</Typography>
</Step>
);
})}
</Stepper>
);
}

return (
<Modal open onClose={handleClose}>
<ModalDialog variant="outlined" sx={{ maxWidth: '90vw', overflow: 'auto' }}>
<DialogTitle>Data Editing</DialogTitle>
<DialogContent>
<Typography level={'title-lg'}>In order to make changes to this data set, cascading changes must be made across the schema. </Typography>
{!beginUpload && !isConfirmStep && (
<Typography level={'title-md'}>Press the Begin button to begin this process, or Cancel to revert your changes. </Typography>
)}
{beginUpload && !isConfirmStep && (
<>
<Typography level={'title-md'} sx={{ marginBottom: '2em' }}>
Beginning upload...
</Typography>
{IconStepper()}
</>
)}
</DialogContent>
<DialogActions>
{!beginUpload && !isConfirmStep && (
<>
<Button variant={'soft'} color={'danger'} onClick={handleClose}>
Cancel
</Button>
<Button variant={'soft'} color={'success'} onClick={() => setBeginUpload(true)}>
Begin Upload
</Button>
</>
)}
{isConfirmStep && (
<>
<Button
variant={'soft'}
color={'primary'}
onClick={handleFinalConfirm}
disabled={Object.values(uploadStatus).some(value => value !== 'completed')}
>
Confirm Changes
</Button>
</>
)}
</DialogActions>
</ModalDialog>
</Modal>
);
}
12 changes: 9 additions & 3 deletions frontend/components/datagrids/measurementscommons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ import { MeasurementsSummaryResult } from '@/config/sqlrdsdefinitions/views';
import Divider from '@mui/joy/Divider';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import GridOnIcon from '@mui/icons-material/GridOn';
import SkipReEnterDataModal from '@/components/datagrids/skipreentrydatamodal';
import MSVEditingModal from '@/components/datagrids/applications/msveditingmodal';

function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
let timeoutId: ReturnType<typeof setTimeout>;
Expand Down Expand Up @@ -1578,8 +1578,14 @@ export default function MeasurementsCommons(props: Readonly<MeasurementsCommonsP
<Alert {...snackbar} onClose={handleCloseSnackbar} />
</Snackbar>
)}
{isDialogOpen && promiseArguments && (
<SkipReEnterDataModal gridType={gridType} row={promiseArguments.newRow} handleClose={handleCancelAction} handleSave={handleConfirmAction} />
{isDialogOpen && promiseArguments && !promiseArguments.oldRow.isNew && (
<MSVEditingModal
gridType={gridType}
oldRow={promiseArguments.oldRow}
newRow={promiseArguments.newRow}
handleClose={handleCancelAction}
handleSave={handleConfirmAction}
/>
)}
{isDeleteDialogOpen && (
<ConfirmationDialog
Expand Down
39 changes: 39 additions & 0 deletions frontend/config/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,3 +299,42 @@ export function getUpdatedValues<T extends Record<string, any>>(original: T, upd

return changes;
}

export function mysqlEscape(value: any): string {
if (value === null || value === undefined) return 'NULL';
if (typeof value === 'number') return value.toString();
if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE';
if (typeof value === 'string') return `'${value.replace(/'/g, "''")}'`;
throw new Error('Unsupported value type');
}

export function formatQuery(query: string, values: any[]): string {
let valueIndex = 0; // Pointer for values
return query
.replace(/\?\?/g, () => {
const identifier = values[valueIndex++];
if (Array.isArray(identifier) && identifier.length === 2) {
// If identifier is [schema, table], format it as `schema`.`table`
const [schema, table] = identifier;
return `\`${schema.replace(/`/g, '``')}\`.\`${table.replace(/`/g, '``')}\``;
} else if (typeof identifier === 'string') {
// Single string identifier
return `\`${identifier.replace(/`/g, '``')}\``;
} else {
throw new Error(`Invalid identifier for ?? placeholder: ${JSON.stringify(identifier)}`);
}
})
.replace(/\?/g, () => {
const value = values[valueIndex++];
if (value === null || value === undefined) return 'NULL';
if (typeof value === 'number') return value.toString();
if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE';
if (typeof value === 'object' && !Array.isArray(value)) {
return Object.entries(value)
.map(([key, val]) => `\`${key}\` = ${mysqlEscape(val)}`)
.join(', ');
}
if (typeof value === 'string') return `'${value.replace(/'/g, "''")}'`;
throw new Error(`Unsupported value type: ${typeof value}`);
});
}

0 comments on commit 3f0917e

Please sign in to comment.