Wildcard ID assignment bug when changing order of divs

I’m trying to create a assignment interface for categories and colors. Therefore I used some code form this thread: Drag and drop cards
For getting the new order of dragable components into python , the user made, I implemented code from this thread as well:
Ordering children with drag & drop, how to trigger "dash component has changed" from JS

However, when changing the order a couple of times, the wildcard collapse/button components get confused and open wrong collapses. However the divs are in the order, the user chose with dragging and dropping.

I’m out of clues how to deal with that. Pleaase haaalp! :slight_smile:

python:

from dash import Dash, html
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output, State, ClientsideFunction, MATCH
import dash_daq as daq

NUM_CATEGORIES = 9

AVAILABLE_CATEGORIES = [f'category_{i}' for i in range(NUM_CATEGORIES)]


# app
app = Dash(__name__,
           external_scripts=["https://cdnjs.cloudflare.com/ajax/libs/dragula/3.7.3/dragula.min.js"],
           external_stylesheets=[dbc.themes.BOOTSTRAP])

# layout
app.layout = html.Div([
    dbc.Row([

        # category names
        dbc.Col([
            html.Div([
                    dbc.Card([
                        dbc.CardBody(
                            f'{category}'
                        ),
                    ])
                    for category in AVAILABLE_CATEGORIES
                ]),
        ]),

        # line color
        dbc.Col(children=[
            # cards
            html.Div(id="drag_container_line", className="container", children=[
                dbc.Card([
                    dbc.CardBody(
                        children=[
                            # name
                            f'Card-{i}-line',
                            # color display + button
                            dbc.Button(id={'type': 'btn-line-color-picker', 'index': i},
                                       children=html.Label(id={'type': 'line-color', 'index': i},
                                                           style={'background': '#FF0000'},
                                                           className='color-box'),
                                       className='btn-color-box'),
                            # collapse + color picker
                            dbc.Collapse(id={'type': 'collapse-line-color-picker', 'index': i},
                                         children=[
                                             daq.ColorPicker(
                                                 id={'type': 'line-color-picker', 'index': i},
                                                 value=dict(rgb={'r': 0.9, 'g': 0.2, 'b': 0.1})
                                             )
                                         ],
                                         is_open=False
                                         )
                        ]
                    )
                ], id=f'Card-{i}-line')
                for i in range(NUM_CATEGORIES)
            ])
        ], id='col-color-line'),

        # buttons
        dbc.Button('Apply', id='btn-see-order'),

        # order
        html.Label(id="order-line"),
    ])
])


app.clientside_callback(
    ClientsideFunction(namespace="clientside", function_name="make_card_draggable"),
    [Output("drag_container_line", "data-drag")],
    [Input("drag_container_line", "id")],
    [State("drag_container_line", "children")]
)


@app.callback(Output("order-line", "children"),
              [Input("btn-see-order", "n_clicks"),
               Input("drag_container_line", "children")], prevent_initial_call=True)
def _watch_children_line(n, children):
    # prints for testing
    for child in children:
        print(f"label:    {child['props']['children'][0]['props']['children'][1]['props']['children']['props']['id']['index']}")
        print(f"button:   {child['props']['children'][0]['props']['children'][1]['props']['id']['index']}")
        print(f"picker:   {child['props']['children'][0]['props']['children'][2]['props']['children'][0]['props']['id']['index']}")
        print(f"collapse: {child['props']['children'][0]['props']['children'][2]['props']['id']['index']}")
    print("___")
    return ", ".join([child['props']['id'] for child in children])


@app.callback(Output({'type': 'collapse-line-color-picker', 'index': MATCH}, 'is_open'),
              Input({'type': 'btn-line-color-picker', 'index': MATCH}, 'n_clicks'),
              State({'type': 'collapse-line-color-picker', 'index': MATCH}, 'is_open'), prevent_initial_call=True)
def _open_fill_color_picker(n, is_open):
    return not is_open


if __name__ == '__main__':
    app.run_server(debug=True, port=8050)

assets/scripts.js

        if (!window.dash_clientside) {
    window.dash_clientside = {};
}
window.dash_clientside.clientside = {
    make_card_draggable: function (id, children) {
        setTimeout(function () {
            const drake = dragula({});
            const el = document.getElementById(id);
            drake.containers.push(el);
            drake.on("drop", function (_el, target, source, sibling) {
                // a component has been dragged & dropped
                // get the order of the ids from the DOM
                var order_ids = Array.from(target.children).map(function (child) {
                    return child.id;
                });

                // in place sorting of the children to match the new order
                children.sort(function (child1, child2) {
                    return order_ids.indexOf(child1.props.id) - order_ids.indexOf(child2.props.id)
                });
            })
        }, 1)
        return  window.dash_clientside.PreventUpdate;
    }
}

assets/dragula.css

.gu-mirror {
    position: fixed !important;
    margin: 0 !important;
    z-index: 9999 !important;
    opacity: 0.8;
    -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
    filter: alpha(opacity=80);
  }

.gu-hide {
display: none !important;
}

.gu-unselectable {
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
user-select: none !important;
}

.gu-transit {
opacity: 0.2;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";
filter: alpha(opacity=20);
}

.color-box {
    width: 2rem;
    height: 2rem;
    border-radius: 4px;
}

.btn-color-box, .btn-color-box:active, .btn-color-box:focus, .btn-color-box:hover  {
    background: transparent;
    padding: 0;
    border: none;
}

.btn-color-box:hover {
    filter: brightness(70%);
}