Skip to content

Commit

Permalink
autocomplete component to cmattributes added. Basic testing is comple…
Browse files Browse the repository at this point in the history
…ted, saving changes here
  • Loading branch information
siddheshraze committed Feb 7, 2025
1 parent 57b1b10 commit 75af177
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 50 deletions.
1 change: 0 additions & 1 deletion frontend/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { Providers } from './providers';
import { LockAnimationProvider } from './contexts/lockanimationcontext';

export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
console.log('root layout encountered. loading...');
return (
<html lang="en" suppressContentEditableWarning suppressHydrationWarning className={'dark'}>
<head>
Expand Down
69 changes: 65 additions & 4 deletions frontend/components/client/datagridcolumns.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { HEADER_ALIGN } from '@/config/macros';
import { Box, Stack, Typography } from '@mui/joy';
import { GridColDef } from '@mui/x-data-grid';
import React from 'react';
import { Autocomplete, Box, Chip, IconButton, Stack, Typography } from '@mui/joy';
import { GridColDef, GridRenderEditCellParams } from '@mui/x-data-grid';
import React, { Dispatch, SetStateAction, useState } from 'react';
import { AttributeStatusOptions } from '@/config/sqlrdsdefinitions/core';
import { standardizeGridColumns } from '@/components/client/clientmacros';
import { customNumericOperators } from '@/components/datagrids/filtrationsystem';
import CloseIcon from '@mui/icons-material/Close';

export const formatHeader = (word1: string, word2: string) => (
<Stack direction={'column'} sx={{ width: '100%', alignItems: 'center', justifyContent: 'center' }}>
Expand Down Expand Up @@ -306,7 +307,67 @@ export const StemTaxonomiesViewGridColumns: GridColDef[] = standardizeGridColumn
// export const renderStemXCell = (params: any) => renderValueCell(params, 'localStemX', '');
// export const renderEditStemXCell = (params: any) => renderEditValueCell(params, 'localStemX', '', 'Stem Local X Coordinates');
// export const renderStemYCell = (params: any) => renderValueCell(params, 'localStemY', '');
// export const renderEditStemYCell = (params: any) => renderEditValueCell(params, 'localStemY', '', 'Stem Local Y Coordinates');
// export const renderEditStemYCell = (params: any) => renderEditValueCell(params, 'localStemY', '', 'Stem Local Y Coordinates');\

export function InputChip({
params,
selectableAttributes,
setReloadAttributes
}: {
params: GridRenderEditCellParams;
selectableAttributes: string[];
setReloadAttributes: Dispatch<SetStateAction<boolean>>;
}) {
const [selectedValues, setSelectedValues] = useState<string[]>(params.value?.replace(/\s+/g, '').split(';') ?? []);

const handleDelete = (valueToRemove: string) => {
const updatedValues = selectedValues.filter(value => value !== valueToRemove);
setSelectedValues(updatedValues);
params.api.setEditCellValue({ id: params.id, field: 'attributes', value: updatedValues.join(';') });
setReloadAttributes(true);
};

return (
<Box sx={{ width: '100%', minHeight: '32px' }}>
<Autocomplete
multiple
size="sm"
options={selectableAttributes}
autoHighlight
autoComplete
value={selectedValues}
filterSelectedOptions
onChange={(_, newValues) => {
setSelectedValues(newValues);
params.api.setEditCellValue({ id: params.id, field: 'attributes', value: newValues.join(';') });
setReloadAttributes(true);
}}
onKeyDown={event => {
if (event.key === 'Enter' || event.key === 'Tab') {
event.stopPropagation();
}
}}
sx={{ width: '100%' }}
renderTags={(values, getTagProps) =>
values.map((option, index) => (
<Chip
{...getTagProps({ index })}
key={index}
size="sm"
endDecorator={
<IconButton onClick={() => handleDelete(option)} size="sm" sx={{ padding: 0 }}>
<CloseIcon fontSize="small" />
</IconButton>
}
>
{option}
</Chip>
))
}
/>
</Box>
);
}

export const MeasurementsSummaryViewGridColumns: GridColDef[] = standardizeGridColumns([
{
Expand Down
100 changes: 61 additions & 39 deletions frontend/components/datagrids/applications/msveditingmodal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export default function MSVEditingModal(props: MSVEditingProps) {
quadrats: ['quadratName'],
trees: ['treeTag'],
stems: ['stemTag', 'stemLocalX', 'stemLocalY'],
species: ['speciesName', 'subspeciesName', 'speciesCode']
species: ['speciesName', 'subspeciesName', 'speciesCode'],
attributes: ['attributes']
};
type UploadStatus = 'idle' | 'in-progress' | 'completed' | 'error';
const [uploadStatus, setUploadStatus] = useState<{
Expand All @@ -37,7 +38,8 @@ export default function MSVEditingModal(props: MSVEditingProps) {
quadrats: 'idle',
trees: 'idle',
stems: 'idle',
species: 'idle'
species: 'idle',
attributes: 'idle'
});
const [loadingProgress, setLoadingProgress] = useState(0);
const stepIcons = [<PrecisionManufacturing key={v4()} />, <GridView key={v4()} />, <Forest key={v4()} />, <Grass key={v4()} />, <Diversity2 key={v4()} />];
Expand All @@ -60,7 +62,6 @@ export default function MSVEditingModal(props: MSVEditingProps) {
);

if (Object.keys(matchingFields).length > 0) {
console.log('match found: ');
if (groupName === 'stems') {
// need to correct for key matching
if (matchingFields.stemLocalX) {
Expand All @@ -72,44 +73,66 @@ export default function MSVEditingModal(props: MSVEditingProps) {
delete matchingFields.stemLocalY;
}
}
try {
const demappedData = MapperFactory.getMapper<any, any>(groupName).demapData([matchingFields])[0];
const searchExisting = `SELECT * FROM ?? WHERE ?? = ?`;
const searchResponse = (
await (
await fetch(`/api/formatrunquery`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: searchExisting, params: [`${currentSite?.schemaName}.${tableName}`, idColumn, idValue] })
})
).json()
)[0];
const query = `UPDATE ?? SET ? WHERE ?? = ?`;
const response = await fetch(`/api/formatrunquery`, {
if (groupName === 'attributes' && matchingFields.attributes.split(';').length > 0) {
// delete from cmattributes where coremeasurementID = <inserted>
const splitAttrs = matchingFields.attributes.replace(/\s+/g, '').split(';');
const deleteExisting = `DELETE FROM ?? WHERE ?? = ?`;
await fetch(`/api/formatrunquery`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: deleteExisting, params: [`${currentSite?.schemaName}.${tableName}`, idColumn, idValue] })
});
// insert into cmattributes (coremeasurementID, attributeCode) values (<inserted>, <inserted>)
const insertQuery = `INSERT IGNORE INTO ?? (CoreMeasurementID, Code) VALUES ${splitAttrs.map(() => '(?, ?)').join(', ')}`;
const insertParams = splitAttrs.flatMap((attr: any) => [idValue, attr]);
await fetch(`/api/formatrunquery`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: query,
params: [
`${currentSite?.schemaName}.${tableName}`,
demappedData,
idColumn,
searchResponse[idColumn] !== undefined || searchResponse[idColumn] !== null ? searchResponse[idColumn] : idValue
]
query: insertQuery,
params: [`${currentSite?.schemaName}.${tableName}`, ...insertParams]
})
});
if (response.ok)
} else {
try {
const demappedData = MapperFactory.getMapper<any, any>(groupName).demapData([matchingFields])[0];
const searchExisting = `SELECT * FROM ?? WHERE ?? = ?`;
const searchResponse = (
await (
await fetch(`/api/formatrunquery`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: searchExisting, params: [`${currentSite?.schemaName}.${tableName}`, idColumn, idValue] })
})
).json()
)[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,
searchResponse[idColumn] !== undefined || searchResponse[idColumn] !== null ? searchResponse[idColumn] : idValue
]
})
});
if (response.ok)
setUploadStatus(prev => ({
...prev,
[groupName]: 'completed'
}));
else throw new Error(`err`);
} catch (e) {
console.error(e);
setUploadStatus(prev => ({
...prev,
[groupName]: 'completed'
[groupName]: 'error'
}));
else throw new Error(`err`);
} catch (e) {
console.error(e);
setUploadStatus(prev => ({
...prev,
[groupName]: 'error'
}));
}
}
} else {
setUploadStatus(prev => ({
Expand All @@ -131,6 +154,10 @@ export default function MSVEditingModal(props: MSVEditingProps) {
await new Promise(resolve => setTimeout(resolve, 250));
await handleUpdate('species', 'species', 'SpeciesID', speciesID);
await new Promise(resolve => setTimeout(resolve, 250));
await handleUpdate('species', 'species', 'SpeciesID', speciesID);
await new Promise(resolve => setTimeout(resolve, 250));
await handleUpdate('attributes', 'cmattributes', 'CoreMeasurementID', coreMeasurementID);
await new Promise(resolve => setTimeout(resolve, 250));
setLoadingProgress(100);
};

Expand Down Expand Up @@ -161,12 +188,7 @@ export default function MSVEditingModal(props: MSVEditingProps) {
{loadingProgress === 100 && <Typography level={'title-md'}>Update complete!</Typography>}
</DialogContent>
<DialogActions>
<Button
variant={'soft'}
color={'primary'}
onClick={handleFinalConfirm}
disabled={Object.values(uploadStatus).some(value => value !== 'completed') || loadingProgress < 100}
>
<Button variant={'soft'} color={'primary'} onClick={handleFinalConfirm} disabled={loadingProgress < 100}>
Finish
</Button>
</DialogActions>
Expand Down
55 changes: 49 additions & 6 deletions frontend/components/datagrids/measurementscommons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
GridColDef,
GridEventListener,
GridFilterModel,
GridRenderEditCellParams,
GridRowEditStopReasons,
GridRowId,
GridRowModel,
Expand Down Expand Up @@ -81,14 +82,16 @@ import { applyFilterToColumns } from '@/components/datagrids/filtrationsystem';
import { ClearIcon } from '@mui/x-date-pickers';
import CloudDownloadIcon from '@mui/icons-material/CloudDownload';
import ValidationModal from '@/components/client/validationmodal';
import { MeasurementsSummaryViewGridColumns } from '@/components/client/datagridcolumns';
import { InputChip, MeasurementsSummaryViewGridColumns } from '@/components/client/datagridcolumns';
import { OverridableStringUnion } from '@mui/types';
import ValidationOverrideModal from '@/components/client/validationoverridemodal';
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 MSVEditingModal from '@/components/datagrids/applications/msveditingmodal';
import MapperFactory from '@/config/datamapper';
import { AttributesRDS, AttributesResult } from '@/config/sqlrdsdefinitions/core';

function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
let timeoutId: ReturnType<typeof setTimeout>;
Expand Down Expand Up @@ -533,6 +536,8 @@ export default function MeasurementsCommons(props: Readonly<MeasurementsCommonsP
const [errorCount, setErrorCount] = useState<number | null>(null);
const [validCount, setValidCount] = useState<number | null>(null);
const [pendingCount, setPendingCount] = useState<number | null>(null);
const [selectableAttributes, setSelectableAttributes] = useState<string[]>([]);
const [reloadAttrs, setReloadAttrs] = useState(true);

// context pulls and definitions
const currentSite = useSiteContext();
Expand Down Expand Up @@ -561,6 +566,19 @@ export default function MeasurementsCommons(props: Readonly<MeasurementsCommonsP
}
}, [refresh]);

useEffect(() => {
async function reloadAttributes() {
const response = await fetch(`/api/runquery`, {
method: 'POST',
body: JSON.stringify(`SELECT * FROM ${currentSite?.schemaName}.attributes;`)
});
const data = MapperFactory.getMapper<AttributesRDS, AttributesResult>('attributes').mapData(await response.json());
setSelectableAttributes(data.map(i => i.code).filter((code): code is string => code !== undefined));
setReloadAttrs(false);
}

reloadAttributes().catch(console.error);
}, []);
// helper functions for usage:
const handleSortModelChange = (newModel: GridSortModel) => {
setSortModel(newModel);
Expand Down Expand Up @@ -811,6 +829,15 @@ export default function MeasurementsCommons(props: Readonly<MeasurementsCommonsP
}
if (handleSelectQuadrat) handleSelectQuadrat(null);
setLoading(false);
if (reloadAttrs) {
const response = await fetch(`/api/runquery`, {
method: 'POST',
body: JSON.stringify(`SELECT * FROM ${currentSite?.schemaName}.attributes;`)
});
const data = MapperFactory.getMapper<AttributesRDS, AttributesResult>('attributes').mapData(await response.json());
setSelectableAttributes(data.map(i => i.code).filter((code): code is string => code !== undefined));
setReloadAttrs(false);
}
try {
setLoading(true, 'Refreshing Measurements Summary View...');
const response = await fetch(`/api/refreshviews/measurementssummary/${currentSite?.schemaName ?? ''}`, { method: 'POST' });
Expand Down Expand Up @@ -1255,9 +1282,11 @@ export default function MeasurementsCommons(props: Readonly<MeasurementsCommonsP
const cellError = cellHasError(column.field, params.id) ? getCellErrorMessages(column.field, Number(params.row.coreMeasurementID)) : '';

const isMeasurementField = column.field === 'measuredDBH' || column.field === 'measuredHOM';
const isAttributeField = column.field === 'attributes';
const attributeValues = column.field === 'attributes' && typeof params.value === 'string' ? params.value.replace(/\s+/g, '').split(';') : [];

const renderMeasurementDetails = () => (
<>
function renderMeasurementDetails() {
return (
<Typography level="body-sm">
{column.field === 'measuredDBH'
? params.row.measuredDBH
Expand All @@ -1267,8 +1296,16 @@ export default function MeasurementsCommons(props: Readonly<MeasurementsCommonsP
? Number(params.row.measuredHOM).toFixed(2)
: 'null'}
</Typography>
</>
);
);
}

function renderAttributeDetails() {
return attributeValues.map((value: string, index: number) => (
<Chip key={index} size={'sm'}>
{value}
</Chip>
));
}

return (
<Box
Expand All @@ -1283,6 +1320,8 @@ export default function MeasurementsCommons(props: Readonly<MeasurementsCommonsP
>
{isMeasurementField ? (
<Box sx={{ display: 'flex', flexDirection: 'row', gap: '0.5em', alignItems: 'center' }}>{renderMeasurementDetails()}</Box>
) : isAttributeField ? (
<Box sx={{ display: 'flex', flexDirection: 'row', gap: '0.5em', alignItems: 'center' }}>{renderAttributeDetails()}</Box>
) : (
<Typography sx={{ whiteSpace: 'normal', lineHeight: 'normal' }}>{formattedValue}</Typography>
)}
Expand All @@ -1303,7 +1342,11 @@ export default function MeasurementsCommons(props: Readonly<MeasurementsCommonsP
)}
</Box>
);
}
},
renderEditCell: (params: GridRenderEditCellParams) =>
column.field === 'attributes' && selectableAttributes.length > 0 ? (
<InputChip params={params} selectableAttributes={selectableAttributes} setReloadAttributes={setReloadAttrs} />
) : undefined
};
});
if (locked || (session?.user.userStatus !== 'global' && session?.user.userStatus !== 'db admin')) {
Expand Down
1 change: 1 addition & 0 deletions frontend/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

0 comments on commit 75af177

Please sign in to comment.