From 58e9c9cdb8dc5dd6f3176a7fb64d116001c770f1 Mon Sep 17 00:00:00 2001
From: Kevin Jackson <30411845+KevinJJackson@users.noreply.github.com>
Date: Tue, 10 Dec 2024 18:37:47 -0500
Subject: [PATCH] enhancement/uploader-edit-config (#250)
---
package.json | 2 +-
.../uploader/modals/_configurationOption.jsx | 11 +-
.../project/uploader/modals/_formGen.jsx | 172 ++++++-----
.../uploader/modals/_generalColumnFields.jsx | 271 +++++++++++++-----
.../uploader/modals/newConfiguration.jsx | 137 +++++----
src/app-services/collections/uploader.ts | 79 ++++-
6 files changed, 462 insertions(+), 210 deletions(-)
diff --git a/package.json b/package.json
index 3f0df46..bb7f4dc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "hhd-ui",
- "version": "0.18.5",
+ "version": "0.18.6",
"private": true,
"dependencies": {
"@ag-grid-community/client-side-row-model": "^30.0.3",
diff --git a/src/app-pages/project/uploader/modals/_configurationOption.jsx b/src/app-pages/project/uploader/modals/_configurationOption.jsx
index 5eb94e1..591d9cf 100644
--- a/src/app-pages/project/uploader/modals/_configurationOption.jsx
+++ b/src/app-pages/project/uploader/modals/_configurationOption.jsx
@@ -10,6 +10,7 @@ const ConfigurationOption = connect(
instrumentTimeseriesItems: instrumentTimeseries,
fieldName,
onChange,
+ value,
}) => {
const timeseries = useMemo(() => formatTimeseriesOptions(instrumentTimeseries), [instrumentTimeseries]);
@@ -26,13 +27,17 @@ const ConfigurationOption = connect(
sx={{ marginTop: 2 }}
groupBy={opt => opt.instrumentName}
getOptionLabel={option => option?.label}
+ isOptionEqualToValue={(opt, val) => opt?.value === val?.value}
options={timeseries.filter(e => e.type === 'standard').sort((a, b) => -b.label.localeCompare(a.label))}
- onChange={(_e, val) => onChange(val)}
- renderInput={(params) => (
+ onChange={(_e, val) => {
+ onChange(val);
+ }}
+ value={value}
+ renderInput={params => (
)}
/>
diff --git a/src/app-pages/project/uploader/modals/_formGen.jsx b/src/app-pages/project/uploader/modals/_formGen.jsx
index 50acd6a..dc98527 100644
--- a/src/app-pages/project/uploader/modals/_formGen.jsx
+++ b/src/app-pages/project/uploader/modals/_formGen.jsx
@@ -3,7 +3,9 @@ import { useDeepCompareEffect } from 'react-use';
import { Box } from '@mui/material';
import ConfigurationOption from './_configurationOption';
+import { formatTimeseriesId } from '../../batch-plotting/helper';
import { extractInstrumentNames } from '../helper';
+import { connect } from 'redux-bundler-react';
const removeMappedColumns = (columnConfig, fields) => {
const {
@@ -18,92 +20,126 @@ const removeMappedColumns = (columnConfig, fields) => {
let final = [...fields];
if (time_field) {
- final = final.filter(el => el.fieldName !== time_field?.value)
+ final = final.filter(el => ![time_field?.value, time_field].includes(el.fieldName))
}
if (instrument_field_enabled) {
- final = final.filter(el => el.fieldName !== instrument_field?.value);
+ final = final.filter(el => ![instrument_field?.value, instrument_field].includes(el.fieldName));
}
if (masked_field_enabled) {
- final = final.filter(el => el.fieldName !== masked_field?.value);
+ final = final.filter(el => ![masked_field?.value, masked_field].includes(el.fieldName));
}
if (validated_field_enabled) {
- final = final.filter(el => el.fieldName !== validated_field?.value);
+ final = final.filter(el => ![validated_field?.value, validated_field].includes(el.fieldName));
}
if (comment_field_enabled) {
- final = final.filter(el => el.fieldName !== comment_field?.value);
+ final = final.filter(el => ![comment_field?.value, comment_field].includes(el.fieldName));
}
if (depth_based_instrument_id) {
- final = final.filter(el => el.fieldName === depth_based_instrument_id?.value);
+ final = final.filter(el => ![depth_based_instrument_id?.value, depth_based_instrument_id].includes(el.fieldName));
}
return final;
};
-const FormGen = ({
- fields = [],
- parsedData,
- fileType,
- columnConfig = {},
- onChange,
-}) => {
- const { instrument_field_enabled, instrument_field } = columnConfig || {};
+const generateDefaultResult = (currentMappings = [], timeseries = []) => {
+ const defaultResult = currentMappings.reduce((accum, curr) => {
+ const { field_name, instrument_field_name, timeseries_id } = curr;
+ const data = {};
- const [result, setResult] = useState({});
- const cleanedFields = removeMappedColumns(columnConfig, fields);
- const instrumentEnabled = ['csv', 'xlsx'].includes(fileType) && instrument_field_enabled;
- const instrumentNames = instrumentEnabled ? extractInstrumentNames(parsedData, fileType, instrument_field?.value) : [];
+ if (instrument_field_name) {
+ data[instrument_field_name] = {
+ fields: {
+ ...(accum[instrument_field_name]?.fields),
+ [field_name]: formatTimeseriesId(timeseries_id, timeseries),
+ },
+ isInstrument: true,
+ };
+ } else {
+ data[field_name] = formatTimeseriesId(timeseries_id, timeseries);
+ }
- useDeepCompareEffect(() => {
- onChange(result);
- }, [result]);
+ return {
+ ...accum,
+ ...data,
+ };
+ }, {});
- return (
- <>
- {instrumentEnabled ? (
- <>
- {instrumentNames?.map(name => (
-
- {name}
- {cleanedFields?.map(field => (
- setResult(prev => ({
- ...prev,
- [name]: {
- ...(prev[name] || {}),
- fields: {
- ...(prev[name]?.fields || {}),
- [field.fieldName]: val,
- },
- isInstrument: true,
- }
- })))}
- />
- ))}
-
- ))}
- >
- ) : (
- <>
- {cleanedFields?.map(field => (
- setResult(prev => ({ ...prev, [field.fieldName]: val })))}
- />
- ))}
- >
- )}
- >
- );
+ return defaultResult;
};
+const FormGen = connect(
+ 'selectInstrumentTimeseriesItems',
+ ({
+ instrumentTimeseriesItems: instrumentTimeseries,
+ fields = [],
+ parsedData,
+ fileType,
+ columnConfig = {},
+ currentMappings = [],
+ onChange,
+ }) => {
+ const { instrument_field_enabled, instrument_field } = columnConfig || {};
+
+ const cleanedFields = removeMappedColumns(columnConfig, fields);
+ const instrumentEnabled = ['csv', 'xlsx'].includes(fileType) && instrument_field_enabled;
+ const instrumentNames = instrumentEnabled ? extractInstrumentNames(parsedData, fileType, typeof instrument_field === 'string' ? instrument_field : instrument_field?.value) : [];
+
+ const [result, setResult] = useState(generateDefaultResult(currentMappings, instrumentTimeseries));
+
+ useDeepCompareEffect(() => {
+ onChange(result);
+ }, [result]);
+
+ return (
+ <>
+ {instrumentEnabled ? (
+ <>
+ {instrumentNames?.map(name => (
+
+ {name}
+ {cleanedFields?.map(field => (
+ setResult(prev => ({
+ ...prev,
+ [name]: {
+ ...(prev[name] || {}),
+ fields: {
+ ...(prev[name]?.fields || {}),
+ [field.fieldName]: val,
+ },
+ isInstrument: true,
+ }
+ })))}
+ />
+ ))}
+
+ ))}
+ >
+ ) : (
+ <>
+ {cleanedFields?.map(field => (
+ setResult(prev => ({ ...prev, [field.fieldName]: val })))}
+ />
+ ))}
+ >
+ )}
+ >
+ );
+ },
+);
+
export default FormGen;
diff --git a/src/app-pages/project/uploader/modals/_generalColumnFields.jsx b/src/app-pages/project/uploader/modals/_generalColumnFields.jsx
index ba4c0b4..95dd3e8 100644
--- a/src/app-pages/project/uploader/modals/_generalColumnFields.jsx
+++ b/src/app-pages/project/uploader/modals/_generalColumnFields.jsx
@@ -1,6 +1,6 @@
-import React from 'react';
+import React, { useState } from 'react';
import { connect } from 'redux-bundler-react';
-import { Autocomplete, Checkbox, FormControlLabel, Grid, TextField, Typography } from '@mui/material';
+import { Autocomplete, Box, Checkbox, FormControlLabel, Grid, TextField, Typography } from '@mui/material';
import HelperTooltip from '../../../../app-components/helper-tooltip';
@@ -11,6 +11,24 @@ const generateInstrumentOptions = (instruments = []) => instruments?.length ? (
}))
) : [];
+const getInstrumentValue = (instruments, instrumentId) => {
+ const i = instruments.find(el => el.id === instrumentId);
+
+ return i ? {
+ label: i.name,
+ value: i.id,
+ } : null;
+};
+
+const getFileColumn = (fileColumns, columnName) => {
+ const c = fileColumns.find(el => el.fieldName === columnName);
+
+ return c ? {
+ label: c.fieldName,
+ value: c.fieldName,
+ } : null;
+};
+
const GeneralColumnFields = connect(
'selectInstrumentsItems',
({
@@ -18,6 +36,8 @@ const GeneralColumnFields = connect(
fileType,
fileColumns = [],
onChange = (_fieldName, _val) => {},
+ currentConfig = {},
+ defaultValues = {},
}) => {
const options = fileColumns.map(col => ({
value: col?.fieldName,
@@ -27,31 +47,59 @@ const GeneralColumnFields = connect(
const instrumentEnabled = ['csv', 'xlsx'].includes(fileType);
const depthBasedEnabled = ['dux'].includes(fileType);
+ const [timeField, setTimeField] = useState(null);
+ const [commentField, setCommentField] = useState(null);
+ const [maskedField, setMaskedField] = useState(null);
+ const [validatedField, setValidatedField] = useState(null);
+ const [instrumentField, setInstrumentField] = useState(null);
+ const [depthBasedInstrument, setDepthBasedInstrument] = useState(null);
+
return (
- <>
- {instrumentEnabled && (
+
+ {instrumentEnabled && fileColumns?.length && (
<>
- }
- isOptionEqualToValue={(opt, val) => opt.id === val.id}
- onChange={(_e, val) => onChange('time_field', val)}
- />
+ {options && (
+ (
+
+ )}
+ isOptionEqualToValue={(opt, val) => opt.id === val.id}
+ onChange={(_e, val) => {
+ setTimeField(val);
+ onChange('time_field', val);
+ }}
+ />
+ )}
-
+
onChange('prefer_day_first', e.target.checked)} />}
+ control={(
+ onChange('prefer_day_first', e.target.checked)}
+ defaultChecked={defaultValues.prefer_day_first}
+ />
+ )}
label='Prefer DD / MM / YYYY'
/>
@@ -64,80 +112,160 @@ const GeneralColumnFields = connect(
- }
- isOptionEqualToValue={(opt, val) => opt.id === val.id}
- onChange={(_e, val) => onChange('validated_field', val)}
- />
+ {options && (
+ (
+
+ )}
+ isOptionEqualToValue={(opt, val) => opt.id === val.id}
+ onChange={(_e, val) => {
+ setValidatedField(val);
+ onChange('validated_field', val);
+ }}
+ />
+ )}
onChange('validated_field_enabled', e.target.checked)} />}
+ control={(
+ onChange('validated_field_enabled', e.target.checked)}
+ defaultChecked={defaultValues.validated_field_enabled}
+ />
+ )}
label='Enable'
/>
- }
- isOptionEqualToValue={(opt, val) => opt.id === val.id}
- onChange={(_e, val) => onChange('masked_field', val)}
- />
+ {options && (
+ (
+
+ )}
+ isOptionEqualToValue={(opt, val) => opt.id === val.id}
+ onChange={(_e, val) => {
+ setMaskedField(val);
+ onChange('masked_field', val);
+ }}
+ />
+ )}
onChange('masked_field_enabled', e.target.checked)} />}
+ control={(
+ onChange('masked_field_enabled', e.target.checked)}
+ defaultChecked={defaultValues.masked_field_enabled}
+ />
+ )}
label='Enable'
/>
- }
- isOptionEqualToValue={(opt, val) => opt.id === val.id}
- onChange={(_e, val) => onChange('comment_field', val)}
- />
+ {options && (
+ (
+
+ )}
+ isOptionEqualToValue={(opt, val) => opt.id === val.id}
+ onChange={(_e, val) => {
+ setCommentField(val);
+ onChange('comment_field', val);
+ }}
+ />
+ )}
onChange('comment_field_enabled', e.target.checked)} />}
+ control={(
+ onChange('comment_field_enabled', e.target.checked)}
+ defaultChecked={defaultValues.comment_field_enabled}
+ />
+ )}
label='Enable'
/>
- }
- isOptionEqualToValue={(opt, val) => opt.id === val.id}
- onChange={(_e, val) => onChange('instrument_field', val)}
- />
+ {options && (
+ (
+
+ )}
+ isOptionEqualToValue={(opt, val) => opt.id === val.id}
+ onChange={(_e, val) => {
+ setInstrumentField(val);
+ onChange('instrument_field', val);
+ }}
+ />
+ )}
-
+
onChange('instrument_field_enabled', e.target.checked)} />}
+ control={(
+ onChange('instrument_field_enabled', e.target.checked)}
+ />
+ )}
label='Enable'
/>
}
+ value={depthBasedInstrument || getFileColumn(fileColumns, defaultValues.depth_based_instrument_id)}
+ renderInput={params => (
+
+ )}
isOptionEqualToValue={(opt, val) => opt.id === val.id}
- onChange={(_e, val) => onChange('depth_based_instrument_id', val)}
+ onChange={(_e, val) => {
+ setDepthBasedInstrument(val);
+ onChange('depth_based_instrument_id', val);
+ }}
/>
)}
- >
+
);
},
);
diff --git a/src/app-pages/project/uploader/modals/newConfiguration.jsx b/src/app-pages/project/uploader/modals/newConfiguration.jsx
index 2721bd9..f874eb5 100644
--- a/src/app-pages/project/uploader/modals/newConfiguration.jsx
+++ b/src/app-pages/project/uploader/modals/newConfiguration.jsx
@@ -25,6 +25,8 @@ import { useGetTimezoneDomain } from '../../../../app-services/collections/domai
import {
useCreateUploadConfigurationMappingMutation,
useCreateUploadConfigurationMutation,
+ useUpdateUploadConfigurationMutation,
+ useUpdateUploadConfigurationMappingMutation,
} from '../../../../app-services/collections/uploader.ts';
const formControlStyles = { backgroundColor: '#fff' };
@@ -36,39 +38,23 @@ const cleanConfig = columnConfig => {
keys.forEach(key => {
if (typeof columnConfig[key] === 'boolean') {
ret[key] = columnConfig[key];
- } else {
+ } else if (typeof columnConfig[key] === 'object') {
ret[key] = columnConfig[key]?.value;
+ } else {
+ ret[key] = columnConfig[key];
}
});
return ret;
};
-const generateDefaultColumnConfig = (isEdit, columnConfig) => {
- if (!isEdit) return {};
-
- console.log('test config:', columnConfig);
-
- // @TODO
- return {};
-};
-
-const generateDefaultMappingConfig = (isEdit, mappings) => {
- if (!isEdit) return {};
-
- console.log('test config:', mappings);
-
- // @TODO
- return {};
-};
-
const NewConfigurationModal = ({
projectId,
isEdit,
configuration,
mappings,
}) => {
- const { name, description, tz_name, ...rest } = configuration || {};
+ const { id, name, description, tz_name, ...rest } = configuration || {};
const [file, setFile] = useState(null);
const [parsed, setParsed] = useState(undefined);
@@ -76,8 +62,8 @@ const NewConfigurationModal = ({
const [configName, setConfigName] = useState(name);
const [desc, setDesc] = useState(description);
const [timezone, setTimezone] = useState(getTimezoneOption(tz_name));
- const [columnConfig, setColumnConfig] = useState(generateDefaultColumnConfig(isEdit, rest));
- const [mappingConfig, setMappingConfig] = useState(generateDefaultMappingConfig(isEdit, mappings));
+ const [columnConfig, setColumnConfig] = useState({});
+ const [mappingConfig, setMappingConfig] = useState({});
const [worksheet, setWorksheet] = useState(null);
const [worksheetOptions, setWorksheetOptions] = useState([]);
@@ -87,10 +73,10 @@ const NewConfigurationModal = ({
const client = useQueryClient();
const { data: timezones, isLoading } = useGetTimezoneDomain();
- const createMappingMutator =
- useCreateUploadConfigurationMappingMutation(client);
+ const createMappingMutator = useCreateUploadConfigurationMappingMutation(client);
+ const updateMappingMutator = useUpdateUploadConfigurationMappingMutation(client);
- const saveMappings = body => {
+ const saveMappings = (body, isEdit = false) => {
const { id } = body;
const keys = Object.keys(mappingConfig) || [];
const formData = keys
@@ -103,42 +89,58 @@ const NewConfigurationModal = ({
field_name: field,
timeseries_id: mappingConfig[key].fields[field].value,
instrument_field_name: key,
+ uploader_config_id: id,
}));
} else {
return {
field_name: key,
timeseries_id: mappingConfig[key].value,
+ uploader_config_id: id,
};
}
})
.flat();
- createMappingMutator.mutate({
- projectId,
- uploadConfigId: id,
- body: formData,
- });
+ if (isEdit) {
+ updateMappingMutator.mutate({
+ projectId,
+ uploadConfigId: id,
+ body: formData,
+ });
+ } else {
+ createMappingMutator.mutate({
+ projectId,
+ uploadConfigId: id,
+ body: formData,
+ });
+ }
};
- const createConfigMutator = useCreateUploadConfigurationMutation(
- client,
- saveMappings,
- );
+ const createConfigMutator = useCreateUploadConfigurationMutation(client, projectId, saveMappings);
+ const updateConfigMutator = useUpdateUploadConfigurationMutation(client, projectId, saveMappings);
const saveNewConfiguration = () => {
const formData = {
+ ...cleanConfig(columnConfig),
name: configName,
description: desc,
tz_name: timezone.value,
type: fileType,
xlsx_sheet_name: worksheet?.value,
- ...cleanConfig(columnConfig),
};
- createConfigMutator.mutate({
- projectId,
- body: formData,
- });
+ if (isEdit) {
+ updateConfigMutator.mutate({
+ projectId,
+ uploadConfigId: id,
+ body: formData,
+ })
+ } else {
+ createConfigMutator.mutate({
+ projectId,
+ body: formData,
+ });
+ }
};
useEffect(() => {
@@ -152,9 +154,11 @@ const NewConfigurationModal = ({
} else {
throw new Error(`Invalid fileType: ${fileType}.`);
}
- setColumnConfig({});
+ if (!isEdit) {
+ setColumnConfig({});
+ }
}
- }, [file, fileType, worksheet]);
+ }, [file, fileType, worksheet, isEdit]);
useEffect(() => {
if (file && fileType === 'xlsx') {
@@ -162,6 +166,13 @@ const NewConfigurationModal = ({
}
}, [file, fileType]);
+ useEffect(() => {
+ if (isEdit && parsed) {
+ setColumnConfig(rest);
+ setMappingConfig({}); // TODO
+ }
+ }, [isEdit, parsed]);
+
return (
@@ -246,36 +257,37 @@ const NewConfigurationModal = ({
/>
{!isLoading && (
- (
-
- )}
- isOptionEqualToValue={(opt, val) => opt.id === val.id}
- value={timezone}
- onChange={(_e, val) => setTimezone(val)}
- />
+ (
+
+ )}
+ isOptionEqualToValue={(opt, val) => opt.id === val.id}
+ value={timezone}
+ onChange={(_e, val) => setTimezone(val)}
+ />
)}
- setColumnConfig(prev => ({ ...prev, [fieldName]: val }))
- }
+ currentConfig={columnConfig}
+ defaultValues={isEdit ? rest : {}}
+ onChange={(fieldName, val) => setColumnConfig(prev => ({ ...prev, [fieldName]: val }))}
/>
setMappingConfig(c)}
@@ -286,6 +298,7 @@ const NewConfigurationModal = ({
{
+export const useCreateUploadConfigurationMutation = (client: QueryClient, projectId: string, createMapping: Function) => {
return useMutation({
- mutationFn: ({ projectId, body }: UploadConfigurationParams) => {
+ mutationFn: ({ body }: UploadConfigurationParams) => {
const uri = `/projects/${projectId}/uploader_configs`;
return apiPost(uri, body);
},
- onSuccess: (body, _, _ctx) => {
- createMapping(body);
+ onMutate: () => {
+ const toastId = toast.loading('Creating new configuration...');
+ return { toastId };
+ },
+ onSuccess: (body, _, ctx) => {
+ createMapping(body, false);
client.invalidateQueries({
- queryKey: ['uploadConfigurations'],
- })
+ queryKey: ['uploadConfigurations', projectId],
+ });
+ tUpdateSuccess(ctx?.toastId, 'Successfully created new configuration!');
+ },
+ onError: (error, _vars, ctx) => {
+ tUpdateError(ctx?.toastId, `Failed to create configuration. \n\n${error.message}`);
},
});
};
@@ -68,7 +78,46 @@ export const useCreateUploadConfigurationMappingMutation = (client: QueryClient)
onSuccess: (_body, _, _ctx) => {
client.invalidateQueries({
queryKey: ['uploadConfigurationsMappings'],
- })
+ });
+ },
+ });
+};
+
+export const useUpdateUploadConfigurationMutation = (client: QueryClient, projectId: string, updateMapping: Function) => {
+ return useMutation({
+ mutationFn: ({ uploadConfigId, body }: UploadMappingConfigurationParams) => {
+ const uri = `/projects/${projectId}/uploader_configs/${uploadConfigId}`;
+
+ return apiPut(uri, body);
+ },
+ onMutate: () => {
+ const toastId = toast.loading('Updating configuration...');
+ return { toastId };
+ },
+ onSuccess: (body, _, ctx) => {
+ updateMapping(body, true);
+ client.invalidateQueries({
+ queryKey: ['uploadConfigurations', projectId],
+ });
+ tUpdateSuccess(ctx?.toastId, 'Successfully updated configuration!');
+ },
+ onError: (error, _vars, ctx) => {
+ tUpdateError(ctx?.toastId, `Failed to update configuration. \n\n${error.message}`);
+ },
+ });
+};
+
+export const useUpdateUploadConfigurationMappingMutation = (client: QueryClient) => {
+ return useMutation({
+ mutationFn: ({ projectId, uploadConfigId, body }: UploadMappingConfigurationParams) => {
+ const uri = `/projects/${projectId}/uploader_configs/${uploadConfigId}/mappings`;
+
+ return apiPut(uri, body);
+ },
+ onSuccess: (_body, _, _ctx) => {
+ client.invalidateQueries({
+ queryKey: ['uploadConfigurationsMappings'],
+ });
},
});
};
@@ -80,10 +129,18 @@ export const useDeleteUploadConfigurationMutation = (client: QueryClient, projec
return apiDelete(uri);
},
- onSuccess: (_body, _, _ctx) => {
+ onMutate: () => {
+ const toastId = toast.loading('Deleting configuration...');
+ return { toastId };
+ },
+ onSuccess: (_body, _, ctx) => {
client.invalidateQueries({
queryKey: ['uploadConfigurations', projectId],
- })
+ });
+ tUpdateSuccess(ctx?.toastId, 'Successfully deleted configuration!');
+ },
+ onError: (error, _vars, ctx) => {
+ tUpdateError(ctx?.toastId, `Failed to delete configuration. \n\n${error.message}`);
},
});
};
@@ -98,7 +155,7 @@ export const useUploadFile = (client: QueryClient) => {
onSuccess: (_body, _, _ctx) => {
client.invalidateQueries({
queryKey: ['uploadConfigurations', 'uploadConfigurationsMappings'],
- })
+ });
},
});
};