dash-core-components
dash-core-components copied to clipboard
[BUG] dcc.Upload does not support re-upload the same file.
It appears that if one uses the dcc.Upload.contents property for triggering callback, it fails when the user tries to upload the same file again, due to the content is the same.
I would imagine that a property similar to n_clicks
should be provided to trigger callback on each upload event.
Versions:
dash 1.12.0
dash-core-components 1.10.0
dash-html-components 1.0.3
dash-renderer 1.4.1
Hm that's odd and not what I would expect. I don't think that we check if content
changed or not, and in Dash we don't have any internal checks to "not fire the same callback if the value remains the same". So, perhaps this is something being done by the internal upload component?
Confirmed by trying out the second example on the docs: https://dash.plotly.com/dash-core-components/upload. Thanks for reporting @Jerry-Ma !
If anyone in the community wants to dig into this:
- Here is the
Upload.react.js
code: https://github.com/plotly/dash-core-components/blob/dev/src/fragments/Upload.react.js#L1-L94 - Here is the underlying React component we're using: https://github.com/react-dropzone/react-dropzone
This answer in a stack overflow thread about uploading the same file twice (albeit not in dropstuff) and this answer in a github issue describing a similar problem in read-file-reader-input
both point to a similar problem, that the browser only fires a callback (change, in these cases, but it feels similar) when something actually changes.
From the browser's point of view, when one drags in the same file nothing has changed. They both advocate setting .value = ''
.
I wonder if setting this.value = ''
after line 44 in Upload.react.js
would solve the problem. I'm don't have a JS environment set up (I'm not a JS programmer) so I can't easily try it.
Perhaps someone can give it a test (or something similar)?
The react-dropzone folks refer questions to Stackoverflow rather than their Issues.
First time posting on an issue. A big thank you for this entire framework!
Seems like https://github.com/plotly/dash-core-components/pull/859 would fix this bug. Could this or any equivalent solution be considered and merged?
Is there anything wrong with the solution provided in #859 ? Would be nice to see it merged (or reviewed).
@noxthot I'm not really sure why the contributor of #859 closed that PR, it does seem like that solution would work though requiring users to add an extra input upload_timestamp
to their callbacks just to cover this edge case is a little awkward, I'd rather we try and find a solution that doesn't require that. Regardless, at this point the PR would need to be recreated in the main dash repo as the dash-core-components repo is no longer used.
In the meantime, a workaround is to simply use a callback to replace the upload component after reading its contents.
@JonThom You mean to pass a new dcc.Upload (with the same ID) component to a container div around it every time it is used? That seems very unelegenat :D No updates on this?
@luggie yes that's been my workaround. Haven't really been following this since Feb 2022 so don't know if there are better solutions now.
@JonThom @luggie A simpler workaround that's worked for me is to set the contents of the dcc.Upload component to None
after processing the contents.
Here's as minimal an example I could come up with which demonstrates the workaround. You can upload a file, then click the 'delete all' button, then re-upload the same file again without a problem. You can also upload the same file twice in a row to get two copies of it in the dropdown list.
from dash import Dash, html, dcc, Input, Output, State, callback
app = Dash(__name__)
app.layout = html.Div([
dcc.Store(id='session', storage_type='memory', data={'filenames': []}),
dcc.Upload([html.Div('Drag and drop file or click to Upload...')], id='file-picker', multiple=True),
dcc.Dropdown(id='file-dropdown', className='file-dropdown'),
html.Button('delete all', id='delete-all')
])
@callback(
[Output('file-dropdown', 'options'),
Output('file-dropdown', 'value'),
Output('session', 'data'),
Output('file-picker', 'contents')], # workaround for issue 816
Input('file-picker', 'contents'),
State('file-picker', 'filename'),
State('session', 'data'),
prevent_initial_call=True
)
def upload_file(file_contents_list, filename_list, session_data):
for file_contents, filename in zip(file_contents_list, filename_list):
session_data = process_file(file_contents, filename, session_data)
filenames = session_data['filenames']
currently_selected_file = filenames[0] if filenames[0] else None
return filenames, currently_selected_file, session_data, None # Always return None for the file-picker contents
@callback(
[Output('file-dropdown', 'options', allow_duplicate=True),
Output('file-dropdown', 'value', allow_duplicate=True),
Output('session', 'data', allow_duplicate=True)],
Input('delete-all', 'n_clicks'),
prevent_initial_call=True
)
def delete_all_uploads(_):
return [], None, {'filenames': []}
def process_file(file_contents, filename, session_data):
# Do whatever you want with file_contents and filename in here. In your application, you'd probably want to do
# something more sophisticated. Here, I'm just adding the filename to a list in a dcc.Store component for
# demonstration purposes.
session_data['filenames'].append(filename)
return session_data
app.run(debug=True)
Great @hydrusbeta, that's worked fine, thanks!!