diff --git a/frontend/app/api/formatrunquery/route.ts b/frontend/app/api/formatrunquery/route.ts
new file mode 100644
index 00000000..e721a90d
--- /dev/null
+++ b/frontend/app/api/formatrunquery/route.ts
@@ -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' }
+ });
+}
diff --git a/frontend/app/api/runquery/route.ts b/frontend/app/api/runquery/route.ts
index 27e5f4d3..913540ba 100644
--- a/frontend/app/api/runquery/route.ts
+++ b/frontend/app/api/runquery/route.ts
@@ -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), {
diff --git a/frontend/components/datagrids/applications/msveditingmodal.tsx b/frontend/components/datagrids/applications/msveditingmodal.tsx
new file mode 100644
index 00000000..7497b14a
--- /dev/null
+++ b/frontend/components/datagrids/applications/msveditingmodal.tsx
@@ -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 = [, , , , ];
+
+ 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
+ );
+
+ 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(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 (
+
+ {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 (
+
+ {stepIcons[index]}
+
+ }
+ >
+
+ {stepKey === 'coremeasurements' ? 'CoreMeasurements' : stepKey.charAt(0).toUpperCase() + stepKey.slice(1)}
+
+
+ );
+ })}
+
+ );
+ }
+
+ return (
+
+
+ Data Editing
+
+ In order to make changes to this data set, cascading changes must be made across the schema.
+ {!beginUpload && !isConfirmStep && (
+ Press the Begin button to begin this process, or Cancel to revert your changes.
+ )}
+ {beginUpload && !isConfirmStep && (
+ <>
+
+ Beginning upload...
+
+ {IconStepper()}
+ >
+ )}
+
+
+ {!beginUpload && !isConfirmStep && (
+ <>
+
+
+ >
+ )}
+ {isConfirmStep && (
+ <>
+
+ >
+ )}
+
+
+
+ );
+}
diff --git a/frontend/components/datagrids/measurementscommons.tsx b/frontend/components/datagrids/measurementscommons.tsx
index 7e074a4b..bff77631 100644
--- a/frontend/components/datagrids/measurementscommons.tsx
+++ b/frontend/components/datagrids/measurementscommons.tsx
@@ -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 void>(fn: T, delay: number): T {
let timeoutId: ReturnType;
@@ -1578,8 +1578,14 @@ export default function MeasurementsCommons(props: Readonly
)}
- {isDialogOpen && promiseArguments && (
-
+ {isDialogOpen && promiseArguments && !promiseArguments.oldRow.isNew && (
+
)}
{isDeleteDialogOpen && (
>(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}`);
+ });
+}