From 24a8b4ce74fca7bb3274aae067d0f2e41578c62f Mon Sep 17 00:00:00 2001 From: Laetitia Fesselier Date: Wed, 28 Jul 2021 13:34:15 -0400 Subject: [PATCH] Updates and bugfixes --- python/eeg2bids.py | 21 ++-- python/libs/BIDS.py | 12 +- python/libs/Modifier.py | 36 +++--- python/libs/iEEG.py | 29 ++--- requirements.txt | 10 +- src/App.js | 2 +- src/css/Authentication.module.css | 1 + src/css/Configuration.css | 7 +- src/jsx/Configuration.js | 192 ++++++++++++++---------------- src/jsx/Validator.js | 154 +++++++++++++++++------- src/jsx/elements/help.js | 2 +- src/jsx/elements/inputs.js | 4 + 12 files changed, 264 insertions(+), 206 deletions(-) diff --git a/python/eeg2bids.py b/python/eeg2bids.py index ed4bb91..13e7c38 100644 --- a/python/eeg2bids.py +++ b/python/eeg2bids.py @@ -38,8 +38,8 @@ def connect(sid, environ): return False # extra precaution. -def tarfile_bids_thread(data): - iEEG.TarFile(data) +def tarfile_bids_thread(bids_directory): + iEEG.TarFile(bids_directory) response = { 'compression_time': 'example_5mins' } @@ -47,9 +47,8 @@ def tarfile_bids_thread(data): @sio.event -def tarfile_bids(sid, data): - # data = { bids_directory: '../path/to/bids/output', output_time: 'bids output time' } - response = eventlet.tpool.execute(tarfile_bids_thread, data) +def tarfile_bids(sid, bids_directory): + response = eventlet.tpool.execute(tarfile_bids_thread, bids_directory) send = { 'compression_time': response['compression_time'] } @@ -288,16 +287,14 @@ def edf_to_bids(sid, data): @sio.event -def validate_bids(sid, data): - # data = { bids_directory: '../path/to/bids/output', output_time: 'bids output time' } - print('validate_bids: ', data) +def validate_bids(sid, bids_directory): + print('validate_bids: ', bids_directory) error_messages = [] - if 'bids_directory' not in data or not data['bids_directory']: + if not bids_directory: error_messages.append('The BIDS output directory is missing.') - if 'output_time' not in data or not data['output_time']: - error_messages.append('The BIDS format is missing.') + if not error_messages: - BIDS.Validate(data) + BIDS.Validate(bids_directory) response = { 'file_paths': BIDS.Validate.file_paths, 'result': BIDS.Validate.result diff --git a/python/libs/BIDS.py b/python/libs/BIDS.py index a47b21e..b9c5fe1 100644 --- a/python/libs/BIDS.py +++ b/python/libs/BIDS.py @@ -6,14 +6,12 @@ class Validate: file_paths = [] result = [] - def __init__(self, data): + def __init__(self, bids_directory): print('- Validate: init started.') - sep = os.path.sep - start_path = data['bids_directory'] + sep + data['output_time'] # current directory file_paths = [] result = [] validator = BIDSValidator() - for path, dirs, files in os.walk(start_path): + for path, dirs, files in os.walk(bids_directory): for filename in files: if filename == '.bidsignore': continue @@ -25,9 +23,9 @@ def __init__(self, data): continue temp = os.path.join(path, filename) - file_paths.append(temp[len(start_path):len(temp)]) - result.append(validator.is_bids(temp[len(start_path):len(temp)])) - # print(validator.is_bids(temp[len(start_path):len(temp)])) + file_paths.append(temp[len(bids_directory):len(temp)]) + result.append(validator.is_bids(temp[len(bids_directory):len(temp)])) + # print(validator.is_bids(temp[len(bids_directory):len(temp)])) self.set_file_paths(file_paths) self.set_result(result) diff --git a/python/libs/Modifier.py b/python/libs/Modifier.py index c4d06c3..e90bda0 100644 --- a/python/libs/Modifier.py +++ b/python/libs/Modifier.py @@ -29,7 +29,7 @@ def get_bids_root_path(self): def get_eeg_path(self): directory_path = 'sub-' + self.data['participantID'].replace('_', '').replace('-', '').replace(' ', '') - + return os.path.join( self.get_bids_root_path(), directory_path, @@ -40,29 +40,29 @@ def get_eeg_path(self): def clean_dataset_files(self): if len(self.data['edfData']['files']) > 0: - # for split recording, clean the duplicates _eeg.json and _channels.tsv + # for multiple run recording, clean the duplicates _eeg.json and _channels.tsv channels_files = [f for f in os.listdir(self.get_eeg_path()) if f.endswith('_channels.tsv')] for i in range(1, len(channels_files)): filename = os.path.join(self.get_eeg_path(), channels_files[i]) - os.remove(filename) - + os.remove(filename) + sidecar_files = [f for f in os.listdir(self.get_eeg_path()) if f.endswith('eeg.json')] for i in range(1, len(sidecar_files)): filename = os.path.join(self.get_eeg_path(), sidecar_files[i]) - os.remove(filename) - - # remove the split suffix in the file names + os.remove(filename) + + # remove the run suffix in the file names fileOrig = os.path.join(self.get_eeg_path(), channels_files[0]) fileDest = os.path.join( self.get_eeg_path(), - re.sub(r"_split-[0-9]+", '', channels_files[0]) + re.sub(r"_run-[0-9]+", '', channels_files[0]) ) os.rename(fileOrig, fileDest) fileOrig = os.path.join(self.get_eeg_path(), sidecar_files[0]) fileDest = os.path.join( self.get_eeg_path(), - re.sub(r"_split-[0-9]+", '', sidecar_files[0]) + re.sub(r"_run-[0-9]+", '', sidecar_files[0]) ) os.rename(fileOrig, fileDest) @@ -84,7 +84,7 @@ def modify_dataset_description_json(self): except IOError: print("Could not read or write dataset_description.json file") - + def modify_participants_tsv(self): file_path = os.path.join( self.get_bids_root_path(), @@ -180,13 +180,13 @@ def copy_annotation_files(self): for eegRun in self.data.get('eegRuns'): edf_file = eegRun['edfBIDSBasename'] filename = os.path.join(self.get_eeg_path(), edf_file + '_annotations') - + if eegRun['annotationsTSV']: shutil.copyfile( eegRun['annotationsTSV'], os.path.join(self.get_eeg_path(), filename + '.tsv') ) - + if eegRun['annotationsJSON']: shutil.copyfile( eegRun['annotationsJSON'], @@ -198,7 +198,7 @@ def copy_event_files(self): for eegRun in self.data.get('eegRuns'): edf_file = eegRun['edfBIDSBasename'] filename = os.path.join(self.get_eeg_path(), edf_file + '_annotations') - + if eegRun['eventFile']: # events.tsv data collected: output = [] @@ -228,7 +228,7 @@ def copy_event_files(self): temp = os.path.join(path, filename) if temp.endswith(eegRun['edfBIDSBasename'] + '_events.tsv'): path_event_files = temp - + # We now open BIDS events.tsv file if it exists if path_event_files: try: @@ -272,7 +272,7 @@ def modify_eeg_json(self): raise ValueError('Found more than one eeg.json file') file_path = os.path.join(self.get_eeg_path(), eeg_json[0]) - + try: with open(file_path, "r") as fp: file_data = json.load(fp) @@ -284,12 +284,12 @@ def modify_eeg_json(self): referenceField = 'EGGReference' file_data[referenceField] = self.data['reference'] - + if 'metadata' in self.data['bidsMetadata'] and 'invalid_keys' in self.data['bidsMetadata']: for key in self.data['bidsMetadata']['metadata']: - if key not in self.data['bidsMetadata']['invalid_keys']: + if key not in self.data['bidsMetadata']['invalid_keys']: file_data[key] = self.data['bidsMetadata']['metadata'][key] - + with open(file_path, "w") as fp: json.dump(file_data, fp, indent=4) diff --git a/python/libs/iEEG.py b/python/libs/iEEG.py index 78f0cd1..12c473b 100644 --- a/python/libs/iEEG.py +++ b/python/libs/iEEG.py @@ -91,23 +91,20 @@ class WriteError(PermissionError): # TarFile - tarfile the BIDS data. class TarFile: - # data = { bids_directory: '../path/to/bids/output', output_time: 'bids output time' } - def __init__(self, data): + def __init__(self, bids_directory): import tarfile - import os.path - sep = os.path.sep - source_dir = data['bids_directory'] + sep + data['output_time'] # current directory - output_filename = data['bids_directory'] + sep + data['output_time'] + '.tar.gz' + output_filename = bids_directory + '.tar.gz' with tarfile.open(output_filename, "w:gz") as tar: - tar.add(source_dir, arcname=os.path.basename(source_dir)) - import platform - import subprocess - if platform.system() == 'Windows': - os.startfile(data['bids_directory']) - elif platform.system() == 'Darwin': - subprocess.Popen(['open', data['bids_directory']]) - else: - subprocess.Popen(['xdg-open', data['bids_directory']]) + tar.add(bids_directory, arcname=os.path.basename(bids_directory)) + + #import platform + #import subprocess + #if platform.system() == 'Windows': + # os.startfile(data['bids_directory']) + #elif platform.system() == 'Darwin': + # subprocess.Popen(['open', data['bids_directory']]) + #else: + # subprocess.Popen(['xdg-open', data['bids_directory']]) # Anonymize - scrubs edf header data. @@ -257,7 +254,7 @@ def to_bids(self, bids_basename.update(session=session) try: - write_raw_bids(raw, bids_basename, anonymize=dict(daysback=33630), overwrite=False, verbose=False) + write_raw_bids(raw, bids_basename, overwrite=False, verbose=False) with open(bids_basename, 'r+b') as f: f.seek(8) # id_info field starts 8 bytes in f.write(bytes("X X X X".ljust(80), 'ascii')) diff --git a/requirements.txt b/requirements.txt index 85f997b..5708976 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,10 @@ -eventlet>=0.31.0 +eventlet~=0.31.0 mne~=0.23.0 -# MNE-BIDS 0.8 will support the .EDF extension -# Before its release we need to use the dev version -git+git://github.com/mne-tools/mne-bids@main#egg=mne-bids +mne-bids~=0.8 mne-features~=0.1 numpy~=1.19.5 python-socketio~=5.0.4 python-engineio~=4.0.0 bids-validator~=1.6.0 -pybv>=0.4 -requests>=2.25.0 \ No newline at end of file +pybv~=0.4 +requests~=2.25.0 diff --git a/src/App.js b/src/App.js index 08484d1..d86e6ef 100644 --- a/src/App.js +++ b/src/App.js @@ -84,7 +84,7 @@ const App = () => { }, }, { - title: '4) Validator', + title: '4) Validate and package', onClick: (e) => { e.preventDefault(); setActiveMenuTab(3); diff --git a/src/css/Authentication.module.css b/src/css/Authentication.module.css index 28f1ae2..d2c3aa9 100644 --- a/src/css/Authentication.module.css +++ b/src/css/Authentication.module.css @@ -6,6 +6,7 @@ cursor: default; font-size: 12px; background-color: #e9eaee; + width: 50%; } .loginMessage { font-size: 14px; diff --git a/src/css/Configuration.css b/src/css/Configuration.css index 368dca2..d0aca81 100644 --- a/src/css/Configuration.css +++ b/src/css/Configuration.css @@ -22,4 +22,9 @@ input[type="button"].primary-btn:disabled { textarea { vertical-align: top; font-family: Arial; -} \ No newline at end of file +} +.resetBtn { + text-align: right; + width: 50%; + background-color: #e9eaee; +} diff --git a/src/jsx/Configuration.js b/src/jsx/Configuration.js index fc4f7f1..6b13830 100644 --- a/src/jsx/Configuration.js +++ b/src/jsx/Configuration.js @@ -41,96 +41,56 @@ const Configuration = (props) => { const socketContext = useContext(SocketContext); // React State + const initialState = { + eegRuns: null, + edfData: [], + edfFiles: [], + modality: 'ieeg', + eventFiles: [], + annotationsTSV: [], + annotationsJSON: [], + bidsDirectory: null, + LORIScompliant: true, + siteID: 'n/a', + siteOptions: [], + siteUseAPI: false, + projectID: 'n/a', + projectOptions: [], + projectUseAPI: false, + subprojectID: 'n/a', + subprojectOptions: [], + subprojectUseAPI: false, + session: '', + sessionOptions: [], + sessionUseAPI: false, + bidsMetadataFile: [], + bidsMetadata: null, + lineFreq: 'n/a', + taskName: '', + reference: 'n/a', + recordingType: 'n/a', + participantEntryMode: 'manual', + participantCandID: '', + participantID: '', + participantDOB: null, + participantAge: 'n/a', + participantSex: 'n/a', + participantHand: 'n/a', + anonymize: false, + subjectID: '', + }; + const state = {}; - state.eegRuns = {}; - [state.eegRuns.get, state.eegRuns.set] = useState(null); - state.edfData = {}; - [state.edfData.get, state.edfData.set] = useState([]); - - state.edfFiles = {}; - [state.edfFiles.get, state.edfFiles.set] = useState([]); - state.modality = {}; - [state.modality.get, state.modality.set] = useState('ieeg'); - state.eventFiles = {}; - [state.eventFiles.get, state.eventFiles.set] = useState([]); - state.annotationsTSV = {}; - [state.annotationsTSV.get, state.annotationsTSV.set] = useState([]); - state.annotationsJSON = {}; - [state.annotationsJSON.get, state.annotationsJSON.set] = useState([]); - - state.bidsDirectory = {}; - [state.bidsDirectory.get, state.bidsDirectory.set] = useState(null); - state.LORIScompliant = {}; - [state.LORIScompliant.get, state.LORIScompliant.set] = useState(true); - state.siteID = {}; - [state.siteID.get, state.siteID.set] = useState('n/a'); - state.siteOptions = {}; - [state.siteOptions.get, state.siteOptions.set] = useState([]); - state.siteUseAPI = {}; - [state.siteUseAPI.get, state.siteUseAPI.set] = useState(false); - state.projectID = {}; - [state.projectID.get, state.projectID.set] = useState('n/a'); - state.projectOptions = {}; - [state.projectOptions.get, state.projectOptions.set] = useState([]); - state.projectUseAPI = {}; - [state.projectUseAPI.get, state.projectUseAPI.set] = useState(false); - state.subprojectID = {}; - [state.subprojectID.get, state.subprojectID.set] = useState('n/a'); - state.subprojectOptions = {}; - [state.subprojectOptions.get, state.subprojectOptions.set] = useState([]); - state.subprojectUseAPI = {}; - [state.subprojectUseAPI.get, state.subprojectUseAPI.set] = useState(false); - state.session = {}; - [state.session.get, state.session.set] = useState(''); - state.sessionOptions = {}; - state.bidsMetadataFile = {}; - [state.bidsMetadataFile.get, state.bidsMetadataFile.set] = useState([]); - state.bidsMetadata = {}; - [state.bidsMetadata.get, state.bidsMetadata.set] = useState(null); - state.lineFreq = {}; - [state.lineFreq.get, state.lineFreq.set] = useState('n/a'); - state.taskName = {}; - [state.taskName.get, state.taskName.set] = useState(''); - state.reference = {}; - [state.reference.get, state.reference.set] = useState('n/a'); - state.recordingType = {}; - [state.recordingType.get, state.recordingType.set] = useState('n/a'); - [state.sessionOptions.get, state.sessionOptions.set] = useState([]); - state.sessionUseAPI = {}; - [state.sessionUseAPI.get, state.sessionUseAPI.set] = useState(false); - state.participantEntryMode = {}; - [ - state.participantEntryMode.get, - state.participantEntryMode.set, - ] = useState('manual'); - state.participantCandID = {}; - [state.participantCandID.get, state.participantCandID.set] = useState(''); - state.participantID = {}; - [state.participantID.get, state.participantID.set] = useState(''); - state.participantDOB = {}; - [state.participantDOB.get, state.participantDOB.set] = useState(null); - state.participantAge = {}; - [state.participantAge.get, state.participantAge.set] = useState('n/a'); - state.participantSex = {}; - [state.participantSex.get, state.participantSex.set] = useState('n/a'); - state.participantHand = {}; - [state.participantHand.get, state.participantHand.set] = useState('n/a'); - state.anonymize = {}; - [state.anonymize.get, state.anonymize.set] = useState(false); - state.subjectID = {}; - [state.subjectID.get, state.subjectID.set] = useState(''); - state.authCredentialsVisible = {}; - [ - state.authCredentialsVisible.get, - state.authCredentialsVisible.set, - ] = useState(false); - state.isAuthenticated = {}; - [state.isAuthenticated.get, state.isAuthenticated.set] = useState(false); + for (const [key, value] of Object.entries(initialState)) { + state[key] = {}; + [state[key].get, state[key].set] = useState(value); + } + + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [authCredentialsVisible, setAuthCredentialsVisible] = useState(false); const [preparedBy, setPreparedBy] = useState(''); const [displayErrors, setDisplayErrors] = useState(false); - - // React State const [outputTime, setOutputTime] = useState(''); const [successMessage, setSuccessMessage] = useState(null); const [modalVisible, setModalVisible] = useState(false); @@ -156,6 +116,15 @@ const Configuration = (props) => { }, }); + /** + * reset - reset the form fields (state). + */ + const reset = () => { + for (const [key, value] of Object.entries(initialState)) { + state[key].set(value); + } + }; + /** * beginBidsCreation - create BIDS format. * Sent by socket to python: edf_to_bids. @@ -219,7 +188,7 @@ const Configuration = (props) => { }, [outputTime]); useEffect(() => { - if (state.isAuthenticated.get && state.LORIScompliant) { + if (isAuthenticated && state.LORIScompliant) { state.participantEntryMode.set('new_loris'); } }, [state.LORIScompliant.get]); @@ -492,6 +461,7 @@ const Configuration = (props) => { const validateRecordingParameters = () => { const result = []; + let invalidKeyFound = false; if (!appContext.getFromTask('bidsMetadata')) { return formatWarning('No EEG Parameter metadata file selected'); @@ -513,6 +483,7 @@ const Configuration = (props) => { Object.keys(metadata).map((key) => { if (invalidKeys.indexOf(key) > -1) { + invalidKeyFound = true; result.push(
{formatWarning(`${key}: ${metadata[key]}`)} @@ -530,12 +501,15 @@ const Configuration = (props) => { } }); - result.push( -

- - Invalid keys for the selected modality will be ignored. -

, - ); + if (invalidKeyFound) { + result.push( +

+ + Note: if invalid or extra parameters are found + in the .json file, they are ignored. +

, + ); + } return result; }; @@ -850,6 +824,7 @@ const Configuration = (props) => { }, []); useEffect(() => { + console.log(state.edfFiles.get); if (socketContext) { socketContext.emit('get_edf_data', { files: state.edfFiles.get.map((edfFile) => @@ -956,7 +931,7 @@ const Configuration = (props) => { if (data.error) { // todo display error message - login failure } else { - state.isAuthenticated.set(true); + setIsAuthenticated(true); //state.participantEntryMode.set('new_loris'); } }); @@ -1005,7 +980,7 @@ const Configuration = (props) => { appContext.setTask(name, value); break; case 'participantEntryMode': - if (state.isAuthenticated.get === false) { + if (isAuthenticated === false) { state.participantEntryMode.set('new_loris'); } else { state.participantEntryMode.set(value); @@ -1014,7 +989,7 @@ const Configuration = (props) => { case 'LORIScompliant': if (value === 'yes') { value = true; - if (state.isAuthenticated.get) { + if (isAuthenticated) { state.participantEntryMode.set('new_loris'); } else { state.participantEntryMode.set('manual'); @@ -1151,15 +1126,24 @@ const Configuration = (props) => { * @param {boolean} hidden */ const hideAuthCredentials = (hidden) => { - state.authCredentialsVisible.set(!hidden); + setAuthCredentialsVisible(!hidden); }; if (props.appMode === 'Configuration') { return ( <> - +
+ +
+ +
+
Recording data @@ -1462,14 +1446,14 @@ const Configuration = (props) => {
{state.LORIScompliant.get && - state.isAuthenticated.get && + isAuthenticated &&
{
@@ -1700,7 +1684,7 @@ const Configuration = (props) => { return ( <> - EDF to BIDS format + EDF to BIDS
diff --git a/src/jsx/Validator.js b/src/jsx/Validator.js index f448c61..4198379 100644 --- a/src/jsx/Validator.js +++ b/src/jsx/Validator.js @@ -8,6 +8,7 @@ import Modal from './elements/modal'; // Socket.io import {Event, SocketContext} from './socket.io'; +import {DirectoryInput, RadioInput} from './elements/inputs'; /** * Validator - the Validation confirmation component. @@ -22,6 +23,8 @@ const Validator = (props) => { // React State const [validator, setValidator] = useState({}); const [validPath, setValidPaths] = useState(null); + const [validationMode, setValidationMode] = useState('lastRun'); + const [bidsDirectory, setBidsDirectory] = useState(null); const [modalVisible, setModalVisible] = useState(false); const [modalText, setModalText] = useState({ mode: 'loading', @@ -54,16 +57,45 @@ const Validator = (props) => { setModalVisible(!hidden); }; + /** + * getBIDSDir - get BIDS directory. + * + * @return {string} + */ + const getBIDSDir = () => { + if (validationMode == 'lastRun') { + if (!appContext.getFromTask('bidsDirectory') || + !appContext.getFromTask('output_time') + ) { + console.error('No bidsDirectory or output_time.'); + return null; + } else { + return [ + appContext.getFromTask('bidsDirectory') ?? '', + appContext.getFromTask('output_time') ?? '', + ].filter(Boolean).join('/'); + } + } else { + if (!bidsDirectory) { + console.error('No bidsDirectory.'); + return null; + } else { + return bidsDirectory; + } + } + }; + /** * validateBIDS - get validated BIDS format. * Sent by socket to python: validate_bids. */ const validateBIDS = () => { console.info('validateBIDS();'); - socketContext.emit('validate_bids', { - bids_directory: appContext.getFromTask('bidsDirectory') ?? '', - output_time: appContext.getFromTask('output_time') ?? '', - }); + + const bidsDirectory = getBIDSDir(); + if (bidsDirectory) { + socketContext.emit('validate_bids', bidsDirectory); + } }; /** @@ -72,14 +104,16 @@ const Validator = (props) => { */ const packageBIDS = () => { console.info('packageBIDS();'); - setModalText((prevState) => { - return {...prevState, ['mode']: 'loading'}; - }); - setModalVisible(true); - socketContext.emit('tarfile_bids', { - bids_directory: appContext.getFromTask('bidsDirectory') ?? '', - output_time: appContext.getFromTask('output_time') ?? '', - }); + + const bidsDirectory = getBIDSDir(); + if (bidsDirectory) { + setModalText((prevState) => { + return {...prevState, ['mode']: 'loading'}; + }); + setModalVisible(true); + + socketContext.emit('tarfile_bids', bidsDirectory); + } }; /** @@ -87,7 +121,6 @@ const Validator = (props) => { */ useEffect(() => { const renderFields = []; - const renderPackageBIDS = []; if (validator['file_paths']) { validator['file_paths'].forEach((value, index) => { if (validator['result'][index]) { @@ -104,27 +137,30 @@ const Validator = (props) => { ); } }); - renderPackageBIDS.push( -
-
- - Package BIDS output folder into a compressed file:  - - -
-
, - ); } - setValidPaths(<> -
- {renderFields} -
- {renderPackageBIDS} - ); + setValidPaths( + <> +
+ {renderFields} +
+ , + ); }, [validator]); + useEffect(() => { + setValidator({}); + }, [validationMode, bidsDirectory]); + + useEffect(() => { + if (socketContext) { + socketContext.on('bids', (message) => { + if (message['output_time']) { + setValidator({}); + } + }); + } + }, [socketContext]); + /** * onMessage - received message from python. * @param {object} message - response @@ -140,23 +176,61 @@ const Validator = (props) => { } }; - /** - * Renders the React component. - * @return {JSX.Element} - React markup for component. - */ return props.visible ? ( <> - Validation confirmation + Validate and package
- - Run BIDS Validator:  - + setValidationMode(value)} + options={{ + folder: 'Select a folder', + lastRun: 'Last run', + }} + checked={validationMode} + /> +
+ {validationMode == 'folder' && +
+ setBidsDirectory(value)} + /> +
+ } +
+ value='Validate BIDS' + className='primary-btn' + style={{marginRight: '10px'}} + disabled={ + ( + validationMode == 'lastRun' && + !appContext.getFromTask('output_time') + ) || + (validationMode == 'folder' && !bidsDirectory) + } + /> +
{validPath} diff --git a/src/jsx/elements/help.js b/src/jsx/elements/help.js index c257106..e72c004 100644 --- a/src/jsx/elements/help.js +++ b/src/jsx/elements/help.js @@ -96,7 +96,7 @@ const Help = (props) => {

Create your BIDS output if your configuration is correct!

, Validator: <> -

4) Validator

+

4) Validate and package

Validate the BIDS output and then compress it for storage!

, }; diff --git a/src/jsx/elements/inputs.js b/src/jsx/elements/inputs.js index ca16a2a..9adc625 100644 --- a/src/jsx/elements/inputs.js +++ b/src/jsx/elements/inputs.js @@ -15,6 +15,10 @@ export const FileInput = (props) => { const handleChange = (event) => { // Send current file to parent component const files = event.target.files ? Array.from(event.target.files) : []; + + // Clear the input file + event.target.value = null; + props.onUserInput(props.id, files); }; /**