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'], - }) + }); }, }); };