ohtaman / streamlit-sortables

An Streamlit component to provide sortable items.
Other
87 stars 9 forks source link

Streamlit-sortables bugs out when adding new items to it during runtime. #4

Open hamishcraze opened 2 years ago

hamishcraze commented 2 years ago

Summary I am attempting to use streamlit-sortables to create text processing pipelines (a set of interoperable functions). What we do is allow the user to create an operation. This operation is added to the pipeline. However, perhaps a user wishes to change the order that operations run, streamlit-sortables works well for this.

However we are running into when we try to add new items to these lists (via some button press).

Reproducible Code Example

import streamlit as st
from streamlit_sortables import sort_items

if not "pipeline_left" in st.session_state:
    st.session_state.pipeline_left = ['item1', 'item2', 'item3']

if not "pipeline_right" in st.session_state:
    st.session_state.pipeline_right = ['item4', 'item5', 'item6']

button = st.button("ADD", key="BUTTON")

if button:
    st.session_state.pipeline_left.append("ADDED")

items = [
    {'header': 'Pipeline', 'items': st.session_state.pipeline_left},
    {'header': 'Disabled', 'items': st.session_state.pipeline_right},
]

sorted_items = sort_items(items, multi_containers=True, direction='vertical')

Steps To Reproduce I.E Streamlit starts up. You initialize the streamlit sortable via the following:

items = [ {'header': 'Pipeline', 'items': ["item1","item2,","item3"]}, {'header': 'Disabled', 'items': ["item4","item5,","item6"]}, ]

All is well. Items can be moved between lists and reordered as per usual. But we wish for the end-user to be able to assemble a pipeline themselves. Meaning they need to be able to add new elements to the lists.

We thought we'd implement that using session state:

if not "pipeline_left" in st.session_state:
    st.session_state.pipeline_left = ['item1', 'item2', 'item3']

if not "pipeline_right" in st.session_state:
    st.session_state.pipeline_right = ['item4', 'item5', 'item6']

button = st.button("ADD", key="BUTTON")

if button:
    st.session_state.pipeline_left.append("ADDED")

items = [
    {'header': 'Pipeline', 'items': st.session_state.pipeline_left},
    {'header': 'Disabled', 'items': st.session_state.pipeline_right},
]

sorted_items = sort_items(items, multi_containers=True, direction='vertical')

As you can see, when a button element is clicked, the item "ADDED" is added to the pipeline_left list which is stored in session_state. This means that on reload, the sorted_items element should be populated with the new items. This proceeds as expected.

The issue occurs when you then try to reorder the sortable list via the frontend. Something is going wrong that is preventing the sortable list for updating correctly and it becomes unresponsive (i) forgets the added element entirely (ii) if you drag ADD from pipeline_left to pipeline_right, when a new element is added, all previously added items appear in pipeline_left. Ignoring all previous state changes.

Expected Behavior We expect to be able to add new elements to the sortable_items list during runtime without completely breaking it's functionality

Current Behavior becomes unresponsive (i) forgets the added element entirely (ii) if you drag "ADDED" from pipeline_left to pipeline_right, when a new element is added (via a second button press), all previously added items appear in pipeline_left. Ignoring all previous state changes.

Something in streamlit_sortables is not talking to the streamlit backend correctly.

ohtaman commented 1 year ago

If key is not specified, st_sortables will look at the contents of items to determine the uniqueness of the component, as same as official widgets. In this case, each time a new item is added, a new st_sortables object is created and it behaves as if it were forgotten.

We should sync session_state with st_sortables' internal state something like code below. Might be a little tricky.

import streamlit as st
from streamlit_sortables import sort_items

if not 'pipeline' in st.session_state:
    st.session_state.pipeline = {
        'left': ['item1', 'item2', 'item3'],
        'right': ['item4', 'item5', 'item6']
    }

pipeline = st.session_state.pipeline
items = [
    {'header': 'Pipeline', 'items': pipeline['left']},
    {'header': 'Disabled', 'items': pipeline['right']},
]

sorted_items = sort_items(items, multi_containers=True, direction='vertical')

if st.button('Add', key='BUTTON'):
    n_items = len(pipeline['left']) + len(pipeline['right'])
    # the item value must be unique.
    new_item = f'item{n_items + 1}'
    # session_state must be sorted
    st.session_state.pipeline = {
        'left': sorted_items[0]['items'] + [new_item],
        'right': sorted_items[1]['items']
    }
    # should rerun to reflect the change of session_state
    st.experimental_rerun()