streamlit-sortable-multiselect 0.7.3__tar.gz → 0.7.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {streamlit_sortable_multiselect-0.7.3/streamlit_sortable_multiselect.egg-info → streamlit_sortable_multiselect-0.7.5}/PKG-INFO +88 -1
- {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/README.md +87 -0
- {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/pyproject.toml +1 -1
- {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/streamlit_sortable_multiselect/__init__.py +145 -7
- streamlit_sortable_multiselect-0.7.5/streamlit_sortable_multiselect/frontend/build/assets/index-DGpJ50qI.css +1 -0
- streamlit_sortable_multiselect-0.7.5/streamlit_sortable_multiselect/frontend/build/assets/index-RH2LvVA2.js +67 -0
- {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/streamlit_sortable_multiselect/frontend/build/index.html +2 -2
- {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5/streamlit_sortable_multiselect.egg-info}/PKG-INFO +88 -1
- {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/streamlit_sortable_multiselect.egg-info/SOURCES.txt +2 -2
- {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/tests/test_wrapper.py +64 -0
- streamlit_sortable_multiselect-0.7.3/streamlit_sortable_multiselect/frontend/build/assets/index-DdljhZDS.js +0 -67
- streamlit_sortable_multiselect-0.7.3/streamlit_sortable_multiselect/frontend/build/assets/index-DobBL0Fg.css +0 -1
- {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/LICENSE +0 -0
- {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/MANIFEST.in +0 -0
- {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/setup.cfg +0 -0
- {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/streamlit_sortable_multiselect.egg-info/dependency_links.txt +0 -0
- {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/streamlit_sortable_multiselect.egg-info/requires.txt +0 -0
- {streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/streamlit_sortable_multiselect.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: streamlit-sortable-multiselect
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.5
|
|
4
4
|
Summary: A Streamlit custom component for selecting and reordering multiple string values.
|
|
5
5
|
Author: Kentaro Aoki
|
|
6
6
|
License-Expression: MIT
|
|
@@ -36,6 +36,12 @@ A Streamlit custom component for searching, selecting, and reordering multiple s
|
|
|
36
36
|
python -m pip install streamlit-sortable-multiselect
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
Upgrade an existing installation:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python -m pip install --upgrade streamlit-sortable-multiselect
|
|
43
|
+
```
|
|
44
|
+
|
|
39
45
|
For local development:
|
|
40
46
|
|
|
41
47
|
```bash
|
|
@@ -104,6 +110,17 @@ st.write(selected)
|
|
|
104
110
|
| `selected_position` | `str` | `"bottom"` | Position of the selected item list relative to the search/add input. Use `"bottom"` or `"top"`. |
|
|
105
111
|
| `icon_size` | `int` | `20` | Icon display size in pixels for `icon_url` images. Images are displayed inside a square area while preserving their aspect ratio. |
|
|
106
112
|
| `options_max_height` | `int` | `190` | Maximum height in pixels for the available options dropdown. |
|
|
113
|
+
| `suggestions_api_url` | `str \| None` | `None` | Absolute HTTP(S) endpoint used to fetch suggestions in the browser. `None` disables API suggestions. |
|
|
114
|
+
| `suggestions_query_param` | `str` | `"q"` | Query parameter name used to send the current search text. |
|
|
115
|
+
| `suggestions_response_path` | `str` | `""` | Dot-separated path to the suggestions array in the JSON response. Empty means the response root. |
|
|
116
|
+
| `suggestions_label_path` | `str` | `"label"` | Dot-separated path to each suggestion's display label. |
|
|
117
|
+
| `suggestions_value_path` | `str` | `"value"` | Dot-separated path to each suggestion's returned value. |
|
|
118
|
+
| `suggestions_icon_url_path` | `str \| None` | `"icon_url"` | Optional dot-separated path to each suggestion's icon URL. `None` disables API icons. |
|
|
119
|
+
| `suggestions_headers` | `Mapping[str, str] \| None` | `None` | HTTP headers sent with suggestions requests. Header values are visible to browser users and must not contain secrets. |
|
|
120
|
+
| `suggestions_min_chars` | `int` | `1` | Minimum trimmed query length before requesting suggestions. Use `0` to allow an empty query. |
|
|
121
|
+
| `suggestions_debounce_ms` | `int` | `300` | Delay in milliseconds between the latest input and the API request. |
|
|
122
|
+
| `suggestions_loading_message` | `str` | `"Loading suggestions..."` | Message shown while an API request is in progress. |
|
|
123
|
+
| `suggestions_error_message` | `str` | `"Failed to load suggestions"` | Message shown when the request or response cannot be processed. |
|
|
107
124
|
| `key` | `str \| None` | `None` | Optional Streamlit component key. Use this when rendering multiple sortable multiselects. |
|
|
108
125
|
|
|
109
126
|
Option dictionaries use this shape:
|
|
@@ -114,6 +131,54 @@ Option dictionaries use this shape:
|
|
|
114
131
|
|
|
115
132
|
`icon_url` may be omitted. The returned value is always the `value`, not the display `label`.
|
|
116
133
|
|
|
134
|
+
## API Suggestions
|
|
135
|
+
|
|
136
|
+
Set `suggestions_api_url` to request suggestions as the user types. Static `options` that match the query are shown first, followed by API results. Duplicate `value` entries prefer the static option.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
selected = sortable_multiselect(
|
|
140
|
+
"Repositories",
|
|
141
|
+
options=[{"label": "Streamlit", "value": "streamlit/streamlit"}],
|
|
142
|
+
suggestions_api_url="https://api.example.com/repositories",
|
|
143
|
+
suggestions_query_param="query",
|
|
144
|
+
suggestions_response_path="data.items",
|
|
145
|
+
suggestions_label_path="name",
|
|
146
|
+
suggestions_value_path="full_name",
|
|
147
|
+
suggestions_icon_url_path="owner.avatar_url",
|
|
148
|
+
suggestions_headers={"X-Public-Client": "streamlit-app"},
|
|
149
|
+
suggestions_min_chars=2,
|
|
150
|
+
suggestions_debounce_ms=300,
|
|
151
|
+
suggestions_loading_message="Searching repositories...",
|
|
152
|
+
suggestions_error_message="Repository search is unavailable",
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The example above accepts a response such as:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"data": {
|
|
161
|
+
"items": [
|
|
162
|
+
{
|
|
163
|
+
"name": "streamlit-sortable-multiselect",
|
|
164
|
+
"full_name": "example/streamlit-sortable-multiselect",
|
|
165
|
+
"owner": {
|
|
166
|
+
"avatar_url": "https://example.com/avatar.png"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Requests are made directly from the component iframe, so the endpoint must allow browser requests with CORS. Values in `suggestions_headers` are exposed to browser users. Do not pass API secrets, private bearer tokens, or other credentials. Use a server-side proxy when authentication must remain private.
|
|
175
|
+
|
|
176
|
+
The component sends a `GET` request after the trimmed input reaches
|
|
177
|
+
`suggestions_min_chars` and remains unchanged for `suggestions_debounce_ms`.
|
|
178
|
+
Changing the input cancels the previous in-flight request. Set `options=[]` to
|
|
179
|
+
use only API results. Selected API values remain selected when a later query
|
|
180
|
+
returns different suggestions.
|
|
181
|
+
|
|
117
182
|
Build the frontend before packaging or using release mode:
|
|
118
183
|
|
|
119
184
|
```bash
|
|
@@ -125,8 +190,30 @@ Run the example app:
|
|
|
125
190
|
|
|
126
191
|
```bash
|
|
127
192
|
streamlit run examples/basic.py
|
|
193
|
+
streamlit run examples/api_suggestions.py
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`api_suggestions.py` starts a CORS-enabled sample API on a local random port, so
|
|
197
|
+
it can be tried without a separate API process. This embedded server is intended
|
|
198
|
+
for local development; use a separately deployed HTTPS API in remote deployments.
|
|
199
|
+
The Streamlit page displays the address and port selected for the API. Test the
|
|
200
|
+
endpoint from the same machine with:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
curl "http://127.0.0.1:<displayed-port>/suggest?q=py"
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Pass a fixed API port after `--` when needed:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
streamlit run examples/api_suggestions.py -- --api-port 8765
|
|
128
210
|
```
|
|
129
211
|
|
|
212
|
+
The embedded API binds to `127.0.0.1`. Because suggestions are fetched by the
|
|
213
|
+
browser, it works only when the browser and Streamlit run on the same machine.
|
|
214
|
+
For Docker, another computer, or a hosted Streamlit app, configure
|
|
215
|
+
`suggestions_api_url` with a browser-accessible HTTPS endpoint instead.
|
|
216
|
+
|
|
130
217
|
## Release
|
|
131
218
|
|
|
132
219
|
Build and check the distribution files:
|
|
@@ -8,6 +8,12 @@ A Streamlit custom component for searching, selecting, and reordering multiple s
|
|
|
8
8
|
python -m pip install streamlit-sortable-multiselect
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Upgrade an existing installation:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
python -m pip install --upgrade streamlit-sortable-multiselect
|
|
15
|
+
```
|
|
16
|
+
|
|
11
17
|
For local development:
|
|
12
18
|
|
|
13
19
|
```bash
|
|
@@ -76,6 +82,17 @@ st.write(selected)
|
|
|
76
82
|
| `selected_position` | `str` | `"bottom"` | Position of the selected item list relative to the search/add input. Use `"bottom"` or `"top"`. |
|
|
77
83
|
| `icon_size` | `int` | `20` | Icon display size in pixels for `icon_url` images. Images are displayed inside a square area while preserving their aspect ratio. |
|
|
78
84
|
| `options_max_height` | `int` | `190` | Maximum height in pixels for the available options dropdown. |
|
|
85
|
+
| `suggestions_api_url` | `str \| None` | `None` | Absolute HTTP(S) endpoint used to fetch suggestions in the browser. `None` disables API suggestions. |
|
|
86
|
+
| `suggestions_query_param` | `str` | `"q"` | Query parameter name used to send the current search text. |
|
|
87
|
+
| `suggestions_response_path` | `str` | `""` | Dot-separated path to the suggestions array in the JSON response. Empty means the response root. |
|
|
88
|
+
| `suggestions_label_path` | `str` | `"label"` | Dot-separated path to each suggestion's display label. |
|
|
89
|
+
| `suggestions_value_path` | `str` | `"value"` | Dot-separated path to each suggestion's returned value. |
|
|
90
|
+
| `suggestions_icon_url_path` | `str \| None` | `"icon_url"` | Optional dot-separated path to each suggestion's icon URL. `None` disables API icons. |
|
|
91
|
+
| `suggestions_headers` | `Mapping[str, str] \| None` | `None` | HTTP headers sent with suggestions requests. Header values are visible to browser users and must not contain secrets. |
|
|
92
|
+
| `suggestions_min_chars` | `int` | `1` | Minimum trimmed query length before requesting suggestions. Use `0` to allow an empty query. |
|
|
93
|
+
| `suggestions_debounce_ms` | `int` | `300` | Delay in milliseconds between the latest input and the API request. |
|
|
94
|
+
| `suggestions_loading_message` | `str` | `"Loading suggestions..."` | Message shown while an API request is in progress. |
|
|
95
|
+
| `suggestions_error_message` | `str` | `"Failed to load suggestions"` | Message shown when the request or response cannot be processed. |
|
|
79
96
|
| `key` | `str \| None` | `None` | Optional Streamlit component key. Use this when rendering multiple sortable multiselects. |
|
|
80
97
|
|
|
81
98
|
Option dictionaries use this shape:
|
|
@@ -86,6 +103,54 @@ Option dictionaries use this shape:
|
|
|
86
103
|
|
|
87
104
|
`icon_url` may be omitted. The returned value is always the `value`, not the display `label`.
|
|
88
105
|
|
|
106
|
+
## API Suggestions
|
|
107
|
+
|
|
108
|
+
Set `suggestions_api_url` to request suggestions as the user types. Static `options` that match the query are shown first, followed by API results. Duplicate `value` entries prefer the static option.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
selected = sortable_multiselect(
|
|
112
|
+
"Repositories",
|
|
113
|
+
options=[{"label": "Streamlit", "value": "streamlit/streamlit"}],
|
|
114
|
+
suggestions_api_url="https://api.example.com/repositories",
|
|
115
|
+
suggestions_query_param="query",
|
|
116
|
+
suggestions_response_path="data.items",
|
|
117
|
+
suggestions_label_path="name",
|
|
118
|
+
suggestions_value_path="full_name",
|
|
119
|
+
suggestions_icon_url_path="owner.avatar_url",
|
|
120
|
+
suggestions_headers={"X-Public-Client": "streamlit-app"},
|
|
121
|
+
suggestions_min_chars=2,
|
|
122
|
+
suggestions_debounce_ms=300,
|
|
123
|
+
suggestions_loading_message="Searching repositories...",
|
|
124
|
+
suggestions_error_message="Repository search is unavailable",
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The example above accepts a response such as:
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"data": {
|
|
133
|
+
"items": [
|
|
134
|
+
{
|
|
135
|
+
"name": "streamlit-sortable-multiselect",
|
|
136
|
+
"full_name": "example/streamlit-sortable-multiselect",
|
|
137
|
+
"owner": {
|
|
138
|
+
"avatar_url": "https://example.com/avatar.png"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Requests are made directly from the component iframe, so the endpoint must allow browser requests with CORS. Values in `suggestions_headers` are exposed to browser users. Do not pass API secrets, private bearer tokens, or other credentials. Use a server-side proxy when authentication must remain private.
|
|
147
|
+
|
|
148
|
+
The component sends a `GET` request after the trimmed input reaches
|
|
149
|
+
`suggestions_min_chars` and remains unchanged for `suggestions_debounce_ms`.
|
|
150
|
+
Changing the input cancels the previous in-flight request. Set `options=[]` to
|
|
151
|
+
use only API results. Selected API values remain selected when a later query
|
|
152
|
+
returns different suggestions.
|
|
153
|
+
|
|
89
154
|
Build the frontend before packaging or using release mode:
|
|
90
155
|
|
|
91
156
|
```bash
|
|
@@ -97,8 +162,30 @@ Run the example app:
|
|
|
97
162
|
|
|
98
163
|
```bash
|
|
99
164
|
streamlit run examples/basic.py
|
|
165
|
+
streamlit run examples/api_suggestions.py
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`api_suggestions.py` starts a CORS-enabled sample API on a local random port, so
|
|
169
|
+
it can be tried without a separate API process. This embedded server is intended
|
|
170
|
+
for local development; use a separately deployed HTTPS API in remote deployments.
|
|
171
|
+
The Streamlit page displays the address and port selected for the API. Test the
|
|
172
|
+
endpoint from the same machine with:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
curl "http://127.0.0.1:<displayed-port>/suggest?q=py"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Pass a fixed API port after `--` when needed:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
streamlit run examples/api_suggestions.py -- --api-port 8765
|
|
100
182
|
```
|
|
101
183
|
|
|
184
|
+
The embedded API binds to `127.0.0.1`. Because suggestions are fetched by the
|
|
185
|
+
browser, it works only when the browser and Streamlit run on the same machine.
|
|
186
|
+
For Docker, another computer, or a hosted Streamlit app, configure
|
|
187
|
+
`suggestions_api_url` with a browser-accessible HTTPS endpoint instead.
|
|
188
|
+
|
|
102
189
|
## Release
|
|
103
190
|
|
|
104
191
|
Build and check the distribution files:
|
{streamlit_sortable_multiselect-0.7.3 → streamlit_sortable_multiselect-0.7.5}/pyproject.toml
RENAMED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "streamlit-sortable-multiselect"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.5"
|
|
8
8
|
description = "A Streamlit custom component for selecting and reordering multiple string values."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -5,10 +5,11 @@ from __future__ import annotations
|
|
|
5
5
|
import os
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any, Iterable, Mapping, Sequence, cast
|
|
8
|
+
from urllib.parse import urlparse
|
|
8
9
|
|
|
9
10
|
import streamlit.components.v1 as components
|
|
10
11
|
|
|
11
|
-
__version__ = "0.7.
|
|
12
|
+
__version__ = "0.7.5"
|
|
12
13
|
__all__ = ["sortable_multiselect"]
|
|
13
14
|
|
|
14
15
|
_COMPONENT_NAME = "streamlit_sortable_multiselect"
|
|
@@ -111,6 +112,59 @@ def _validate_options_max_height(options_max_height: int) -> int:
|
|
|
111
112
|
return options_max_height
|
|
112
113
|
|
|
113
114
|
|
|
115
|
+
def _validate_non_negative_int(name: str, value: int) -> int:
|
|
116
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
117
|
+
raise TypeError(f"{name} must be an integer.")
|
|
118
|
+
if value < 0:
|
|
119
|
+
raise ValueError(f"{name} must be 0 or greater.")
|
|
120
|
+
return value
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _validate_optional_non_empty_string(name: str, value: str | None) -> str | None:
|
|
124
|
+
if value is None:
|
|
125
|
+
return None
|
|
126
|
+
if not isinstance(value, str):
|
|
127
|
+
raise TypeError(f"{name} must be a string or None.")
|
|
128
|
+
if not value:
|
|
129
|
+
raise ValueError(f"{name} must not be empty.")
|
|
130
|
+
return value
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _validate_non_empty_string(name: str, value: str) -> str:
|
|
134
|
+
if not isinstance(value, str):
|
|
135
|
+
raise TypeError(f"{name} must be a string.")
|
|
136
|
+
if not value:
|
|
137
|
+
raise ValueError(f"{name} must not be empty.")
|
|
138
|
+
return value
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _validate_suggestions_api_url(value: str | None) -> str | None:
|
|
142
|
+
validated = _validate_optional_non_empty_string("suggestions_api_url", value)
|
|
143
|
+
if validated is None:
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
parsed = urlparse(validated)
|
|
147
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
148
|
+
raise ValueError("suggestions_api_url must be an absolute HTTP(S) URL.")
|
|
149
|
+
return validated
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _normalize_suggestions_headers(
|
|
153
|
+
headers: Mapping[str, str] | None,
|
|
154
|
+
) -> dict[str, str]:
|
|
155
|
+
if headers is None:
|
|
156
|
+
return {}
|
|
157
|
+
if not isinstance(headers, Mapping):
|
|
158
|
+
raise TypeError("suggestions_headers must be a mapping of strings to strings.")
|
|
159
|
+
|
|
160
|
+
result: dict[str, str] = {}
|
|
161
|
+
for name, value in headers.items():
|
|
162
|
+
if not isinstance(name, str) or not isinstance(value, str):
|
|
163
|
+
raise TypeError("suggestions_headers must contain only string keys and values.")
|
|
164
|
+
result[name] = value
|
|
165
|
+
return result
|
|
166
|
+
|
|
167
|
+
|
|
114
168
|
def sortable_multiselect(
|
|
115
169
|
label: str,
|
|
116
170
|
options: Sequence[str | Mapping[str, Any]],
|
|
@@ -128,6 +182,17 @@ def sortable_multiselect(
|
|
|
128
182
|
selected_position: str = "bottom",
|
|
129
183
|
icon_size: int = 20,
|
|
130
184
|
options_max_height: int = 190,
|
|
185
|
+
suggestions_api_url: str | None = None,
|
|
186
|
+
suggestions_query_param: str = "q",
|
|
187
|
+
suggestions_response_path: str = "",
|
|
188
|
+
suggestions_label_path: str = "label",
|
|
189
|
+
suggestions_value_path: str = "value",
|
|
190
|
+
suggestions_icon_url_path: str | None = "icon_url",
|
|
191
|
+
suggestions_headers: Mapping[str, str] | None = None,
|
|
192
|
+
suggestions_min_chars: int = 1,
|
|
193
|
+
suggestions_debounce_ms: int = 300,
|
|
194
|
+
suggestions_loading_message: str = "Loading suggestions...",
|
|
195
|
+
suggestions_error_message: str = "Failed to load suggestions",
|
|
131
196
|
key: str | None = None,
|
|
132
197
|
) -> list[str]:
|
|
133
198
|
"""Select multiple string values and return them in user-defined order.
|
|
@@ -166,6 +231,28 @@ def sortable_multiselect(
|
|
|
166
231
|
Icon display size in pixels. Images keep their aspect ratio within this square size.
|
|
167
232
|
options_max_height:
|
|
168
233
|
Maximum height in pixels for the available options dropdown.
|
|
234
|
+
suggestions_api_url:
|
|
235
|
+
Absolute HTTP(S) endpoint used to fetch suggestions. None disables API suggestions.
|
|
236
|
+
suggestions_query_param:
|
|
237
|
+
Query parameter name used to send the current search text.
|
|
238
|
+
suggestions_response_path:
|
|
239
|
+
Dot-separated path to the options array in the JSON response. Empty means the root.
|
|
240
|
+
suggestions_label_path:
|
|
241
|
+
Dot-separated path to each suggestion's display label.
|
|
242
|
+
suggestions_value_path:
|
|
243
|
+
Dot-separated path to each suggestion's returned value.
|
|
244
|
+
suggestions_icon_url_path:
|
|
245
|
+
Optional dot-separated path to each suggestion's icon URL.
|
|
246
|
+
suggestions_headers:
|
|
247
|
+
HTTP headers sent by the browser. Do not include secret credentials.
|
|
248
|
+
suggestions_min_chars:
|
|
249
|
+
Minimum query length before requesting suggestions.
|
|
250
|
+
suggestions_debounce_ms:
|
|
251
|
+
Delay in milliseconds between input and the suggestions request.
|
|
252
|
+
suggestions_loading_message:
|
|
253
|
+
Text shown while suggestions are loading.
|
|
254
|
+
suggestions_error_message:
|
|
255
|
+
Text shown when suggestions cannot be loaded.
|
|
169
256
|
key:
|
|
170
257
|
Optional Streamlit component key.
|
|
171
258
|
"""
|
|
@@ -199,18 +286,58 @@ def sortable_multiselect(
|
|
|
199
286
|
max_selection_count = _validate_max_selections(max_selections)
|
|
200
287
|
icon_size_value = _validate_icon_size(icon_size)
|
|
201
288
|
options_max_height_value = _validate_options_max_height(options_max_height)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
289
|
+
suggestions_api_url_value = _validate_suggestions_api_url(suggestions_api_url)
|
|
290
|
+
suggestions_query_param_value = _validate_non_empty_string(
|
|
291
|
+
"suggestions_query_param", suggestions_query_param
|
|
292
|
+
)
|
|
293
|
+
if not isinstance(suggestions_response_path, str):
|
|
294
|
+
raise TypeError("suggestions_response_path must be a string.")
|
|
295
|
+
suggestions_label_path_value = _validate_non_empty_string(
|
|
296
|
+
"suggestions_label_path", suggestions_label_path
|
|
297
|
+
)
|
|
298
|
+
suggestions_value_path_value = _validate_non_empty_string(
|
|
299
|
+
"suggestions_value_path", suggestions_value_path
|
|
300
|
+
)
|
|
301
|
+
suggestions_icon_url_path_value = _validate_optional_non_empty_string(
|
|
302
|
+
"suggestions_icon_url_path", suggestions_icon_url_path
|
|
303
|
+
)
|
|
304
|
+
suggestions_headers_value = _normalize_suggestions_headers(suggestions_headers)
|
|
305
|
+
suggestions_min_chars_value = _validate_non_negative_int(
|
|
306
|
+
"suggestions_min_chars", suggestions_min_chars
|
|
307
|
+
)
|
|
308
|
+
suggestions_debounce_ms_value = _validate_non_negative_int(
|
|
309
|
+
"suggestions_debounce_ms", suggestions_debounce_ms
|
|
310
|
+
)
|
|
311
|
+
if not isinstance(suggestions_loading_message, str):
|
|
312
|
+
raise TypeError("suggestions_loading_message must be a string.")
|
|
313
|
+
if not isinstance(suggestions_error_message, str):
|
|
314
|
+
raise TypeError("suggestions_error_message must be a string.")
|
|
315
|
+
|
|
316
|
+
# Set membership keeps these checks O(n); list.count / `in list` here are
|
|
317
|
+
# O(n**2) and hang the rerun once options reaches tens of thousands.
|
|
318
|
+
seen_option_values: set[str] = set()
|
|
319
|
+
has_duplicate_options = False
|
|
320
|
+
for value in option_values:
|
|
321
|
+
if value in seen_option_values:
|
|
322
|
+
has_duplicate_options = True
|
|
323
|
+
break
|
|
324
|
+
seen_option_values.add(value)
|
|
325
|
+
if has_duplicate_options:
|
|
205
326
|
raise ValueError("options must not contain duplicate values.")
|
|
206
327
|
|
|
207
|
-
missing_defaults = [value for value in default_values if value not in
|
|
328
|
+
missing_defaults = [value for value in default_values if value not in seen_option_values]
|
|
208
329
|
if missing_defaults:
|
|
209
330
|
missing = ", ".join(repr(value) for value in missing_defaults)
|
|
210
331
|
raise ValueError(f"default contains values not present in options: {missing}.")
|
|
211
332
|
|
|
212
|
-
|
|
213
|
-
|
|
333
|
+
seen_default_values: set[str] = set()
|
|
334
|
+
has_duplicate_defaults = False
|
|
335
|
+
for value in default_values:
|
|
336
|
+
if value in seen_default_values:
|
|
337
|
+
has_duplicate_defaults = True
|
|
338
|
+
break
|
|
339
|
+
seen_default_values.add(value)
|
|
340
|
+
if has_duplicate_defaults:
|
|
214
341
|
raise ValueError("default must not contain duplicate values.")
|
|
215
342
|
|
|
216
343
|
if max_selection_count is not None and len(default_values) > max_selection_count:
|
|
@@ -233,6 +360,17 @@ def sortable_multiselect(
|
|
|
233
360
|
selected_position=selected_position,
|
|
234
361
|
icon_size=icon_size_value,
|
|
235
362
|
options_max_height=options_max_height_value,
|
|
363
|
+
suggestions_api_url=suggestions_api_url_value,
|
|
364
|
+
suggestions_query_param=suggestions_query_param_value,
|
|
365
|
+
suggestions_response_path=suggestions_response_path,
|
|
366
|
+
suggestions_label_path=suggestions_label_path_value,
|
|
367
|
+
suggestions_value_path=suggestions_value_path_value,
|
|
368
|
+
suggestions_icon_url_path=suggestions_icon_url_path_value,
|
|
369
|
+
suggestions_headers=suggestions_headers_value,
|
|
370
|
+
suggestions_min_chars=suggestions_min_chars_value,
|
|
371
|
+
suggestions_debounce_ms=suggestions_debounce_ms_value,
|
|
372
|
+
suggestions_loading_message=suggestions_loading_message,
|
|
373
|
+
suggestions_error_message=suggestions_error_message,
|
|
236
374
|
key=key,
|
|
237
375
|
default=default_values,
|
|
238
376
|
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;color:#111827}body{margin:0}button,input{font:inherit}.sortable-multiselect{box-sizing:border-box;width:100%;padding:2px 0 6px}.component-label{display:block;margin:0 0 6px;color:#31333f;font-size:14px;font-weight:400}.search-combobox{position:relative}.search-input{box-sizing:border-box;width:100%;min-height:38px;border:1px solid #d1d5db;border-radius:6px;background:#fff;color:#111827;padding:6px 10px}.search-input:focus{border-color:#ff4b4b;box-shadow:0 0 0 1px #ff4b4b;outline:none}.search-input:disabled{background:#f9fafb;color:#6b7280}.options-popover{background:#fff;border:1px solid #d1d5db;border-radius:6px;box-shadow:0 6px 18px #31333f24;box-sizing:border-box;left:0;margin:4px 0 0;padding:4px;position:absolute;right:0;top:100%;z-index:10}.options-scroll{box-sizing:border-box;list-style:none;margin:0;max-height:var(--options-max-height, 190px);overflow-y:auto;padding:0}.options-inner{list-style:none;margin:0;padding:0}.option-item,.option-empty{align-items:center;border-radius:4px;box-sizing:border-box;color:#111827;display:flex;gap:8px;min-height:32px;padding:7px 9px}.option-item{cursor:pointer;overflow:hidden}.option-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.option-item.highlighted,.option-item:hover{background:#f3f4f6}.option-empty{color:#6b7280}.option-status{border-bottom:1px solid #e5e7eb;color:#6b7280;font-size:13px;padding:7px 9px}.option-error{color:#b91c1c}.selected-list{display:flex;flex-direction:column;gap:6px;list-style:none;margin:8px 0 0;padding:0}.sortable-item{align-items:center;background:var(--item-bg, #ffffff);border:1px solid #d1d5db;border-radius:6px;box-sizing:border-box;color:var(--item-fg, #111827);display:grid;gap:8px;grid-template-columns:34px minmax(0,1fr) auto;min-height:40px;padding:4px}.sortable-item.with-number{grid-template-columns:34px 30px minmax(0,1fr) auto}.sortable-item.dragging{border-color:#ff4b4b;box-shadow:0 4px 12px #31333f24}.drag-handle,.item-actions button{align-items:center;background:transparent;border:1px solid transparent;border-radius:4px;color:var(--item-muted-fg, #374151);cursor:pointer;display:inline-flex;height:30px;justify-content:center;min-width:30px;padding:0 7px}.drag-handle:hover,.item-actions button:hover{background:#f3f4f6}.drag-handle:focus,.item-actions button:focus{border-color:#ff4b4b;outline:none}.drag-handle:disabled,.item-actions button:disabled{color:color-mix(in srgb,var(--item-muted-fg, #374151) 45%,transparent);cursor:not-allowed}.item-content{align-items:center;display:inline-flex;gap:8px;min-width:0}.item-label{min-width:0;overflow-wrap:anywhere}.item-icon,.option-icon{border-radius:4px;flex:0 0 auto;height:var(--icon-size, 20px);object-fit:contain;width:var(--icon-size, 20px)}.item-number{align-items:center;background:color-mix(in srgb,var(--item-bg, #f3f4f6) 72%,#ffffff);border-radius:999px;color:var(--item-fg, #4b5563);display:inline-flex;font-size:12px;font-weight:600;height:24px;justify-content:center;line-height:1;min-width:24px;padding:0 6px}.item-actions{display:inline-flex;gap:2px}.empty-state{border:1px dashed #d1d5db;border-radius:6px;color:#6b7280;margin-top:8px;padding:10px;text-align:center}.selected-position-top .selected-list,.selected-position-top .empty-state{margin:0 0 8px}
|