Skip to content

Commit 0e61322

Browse files
authored
feat: Add configurable delimiter for dropdown block options (#348)
* feat: Add configurable delimiter for dropdown block options Users can now customize the label/value delimiter for dropdown options (previously hardcoded to ":""). This enables support for data containing colons, such as URLs or timestamps. The delimiter is configurable in the UI modal and defaults to ":". - Add delimiter prop to Dropdown struct with default ":" - Implement delimiter-aware option parsing in Rust backend - Add UI field in config modal to customize delimiter - Include comprehensive tests for custom delimiters - Update documentation with examples * docs: Add custom delimiter section to dropdown documentation * fmt * test: Add parseOption tests for delimiter boundary handling * refactor: extract parseOption to own module for testability Extract the parseOption function from Dropdown.tsx to a separate parseOption.ts file so it can be tested without importing the entire Dropdown module and its browser dependencies. * remove accidental bindings * sdjfs;d
1 parent 7d935da commit 0e61322

File tree

5 files changed

+349
-26
lines changed

5 files changed

+349
-26
lines changed

crates/atuin-desktop-runtime/src/blocks/dropdown.rs

Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,25 @@ pub struct DropdownOption {
4141
}
4242

4343
impl DropdownOption {
44+
pub fn from_str_with_delimiter(value: &str, delimiter: &str) -> Result<Self, String> {
45+
if let Some(idx) = value.find(delimiter) {
46+
let label = value[..idx].to_string();
47+
let val = value[idx + delimiter.len()..].to_string();
48+
Ok(DropdownOption::builder().label(label).value(val).build())
49+
} else {
50+
Ok(DropdownOption::builder()
51+
.label(value.to_string())
52+
.value(value.to_string())
53+
.build())
54+
}
55+
}
56+
57+
#[allow(dead_code)]
4458
pub fn vec_from_str(value: &str) -> Result<Vec<Self>, String> {
59+
Self::vec_from_str_with_delimiter(value, ":")
60+
}
61+
62+
pub fn vec_from_str_with_delimiter(value: &str, delimiter: &str) -> Result<Vec<Self>, String> {
4563
if value.trim().is_empty() {
4664
return Ok(vec![]);
4765
}
@@ -50,7 +68,7 @@ impl DropdownOption {
5068
re.split(value)
5169
.map(|part| part.trim())
5270
.filter(|part| !part.is_empty())
53-
.map(|part| part.try_into())
71+
.map(|part| Self::from_str_with_delimiter(part, delimiter))
5472
.collect()
5573
}
5674
}
@@ -99,6 +117,7 @@ pub struct Dropdown {
99117
pub options: String,
100118
pub interpreter: String,
101119
pub value: String,
120+
pub delimiter: String,
102121
}
103122

104123
impl Dropdown {
@@ -120,9 +139,16 @@ impl Dropdown {
120139
}
121140
};
122141

142+
let delimiter = if self.delimiter.is_empty() {
143+
":"
144+
} else {
145+
&self.delimiter
146+
};
147+
123148
let options = match self.options_type {
124149
DropdownOptionType::Fixed => {
125-
let options = DropdownOption::vec_from_str(options_source)?;
150+
let options =
151+
DropdownOption::vec_from_str_with_delimiter(options_source, delimiter)?;
126152
Ok(options)
127153
}
128154
DropdownOptionType::Variable => {
@@ -132,7 +158,7 @@ impl Dropdown {
132158
.get_var(options_source)
133159
.map(|v| v.to_string())
134160
.unwrap_or_default();
135-
let options = DropdownOption::vec_from_str(&value)?;
161+
let options = DropdownOption::vec_from_str_with_delimiter(&value, delimiter)?;
136162
Ok(options)
137163
}
138164
DropdownOptionType::Command => {
@@ -150,7 +176,7 @@ impl Dropdown {
150176
.output()
151177
.await?;
152178
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
153-
let options = DropdownOption::vec_from_str(&stdout)?;
179+
let options = DropdownOption::vec_from_str_with_delimiter(&stdout, delimiter)?;
154180
Ok(options)
155181
}
156182
};
@@ -221,6 +247,12 @@ impl FromDocument for Dropdown {
221247
.ok_or("Missing interpreter")?
222248
.to_string();
223249

250+
let delimiter = props
251+
.get("delimiter")
252+
.and_then(|v| v.as_str())
253+
.unwrap_or(":") // Default to ":" if delimiter is missing
254+
.to_string();
255+
224256
Ok(Dropdown::builder()
225257
.id(id)
226258
.name(name)
@@ -231,6 +263,7 @@ impl FromDocument for Dropdown {
231263
.command_options(command_options)
232264
.value(value)
233265
.interpreter(interpreter)
266+
.delimiter(delimiter)
234267
.build())
235268
}
236269
}
@@ -430,6 +463,7 @@ mod tests {
430463
.command_options(command_options.to_string())
431464
.value("".to_string())
432465
.interpreter("/bin/sh".to_string())
466+
.delimiter(":".to_string())
433467
.build()
434468
}
435469

@@ -727,6 +761,7 @@ mod tests {
727761
.command_options("".to_string())
728762
.value("a".to_string())
729763
.interpreter("/bin/sh".to_string())
764+
.delimiter(":".to_string())
730765
.build();
731766

732767
let json = serde_json::to_string(&dropdown).unwrap();
@@ -735,4 +770,83 @@ mod tests {
735770
assert_eq!(dropdown, deserialized);
736771
}
737772
}
773+
774+
// Tests for custom delimiter parsing
775+
mod custom_delimiter {
776+
use super::*;
777+
778+
#[test]
779+
fn test_from_str_with_custom_delimiter() {
780+
let option = DropdownOption::from_str_with_delimiter("Label|value", "|").unwrap();
781+
assert_eq!(option.label, "Label");
782+
assert_eq!(option.value, "value");
783+
}
784+
785+
#[test]
786+
fn test_from_str_with_multi_char_delimiter() {
787+
let option = DropdownOption::from_str_with_delimiter("Label::value", "::").unwrap();
788+
assert_eq!(option.label, "Label");
789+
assert_eq!(option.value, "value");
790+
}
791+
792+
#[test]
793+
fn test_from_str_with_arrow_delimiter() {
794+
let option =
795+
DropdownOption::from_str_with_delimiter("Display Name->actual_value", "->")
796+
.unwrap();
797+
assert_eq!(option.label, "Display Name");
798+
assert_eq!(option.value, "actual_value");
799+
}
800+
801+
#[test]
802+
fn test_from_str_no_delimiter_found() {
803+
let option = DropdownOption::from_str_with_delimiter("just_a_value", "|").unwrap();
804+
assert_eq!(option.label, "just_a_value");
805+
assert_eq!(option.value, "just_a_value");
806+
}
807+
808+
#[test]
809+
fn test_value_contains_colon_with_pipe_delimiter() {
810+
// User has colons in their data, using pipe as delimiter
811+
let option =
812+
DropdownOption::from_str_with_delimiter("My Label|http://example.com:8080", "|")
813+
.unwrap();
814+
assert_eq!(option.label, "My Label");
815+
assert_eq!(option.value, "http://example.com:8080");
816+
}
817+
818+
#[test]
819+
fn test_vec_from_str_with_custom_delimiter() {
820+
let options =
821+
DropdownOption::vec_from_str_with_delimiter("A|1, B|2, C|3", "|").unwrap();
822+
assert_eq!(options.len(), 3);
823+
assert_eq!(options[0].label, "A");
824+
assert_eq!(options[0].value, "1");
825+
assert_eq!(options[1].label, "B");
826+
assert_eq!(options[1].value, "2");
827+
assert_eq!(options[2].label, "C");
828+
assert_eq!(options[2].value, "3");
829+
}
830+
831+
#[test]
832+
fn test_vec_from_str_with_multi_char_delimiter() {
833+
let options =
834+
DropdownOption::vec_from_str_with_delimiter("Label A::a\nLabel B::b", "::")
835+
.unwrap();
836+
assert_eq!(options.len(), 2);
837+
assert_eq!(options[0].label, "Label A");
838+
assert_eq!(options[0].value, "a");
839+
assert_eq!(options[1].label, "Label B");
840+
assert_eq!(options[1].value, "b");
841+
}
842+
843+
#[test]
844+
fn test_default_colon_delimiter() {
845+
// vec_from_str should use ":" as default
846+
let options = DropdownOption::vec_from_str("Label:value").unwrap();
847+
assert_eq!(options.len(), 1);
848+
assert_eq!(options[0].label, "Label");
849+
assert_eq!(options[0].value, "value");
850+
}
851+
}
738852
}

docs/docs/blocks/executable/dropdown.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,21 @@ Execute a shell command that returns a list of options. Supports multiple interp
3737
kubectl get pods --no-headers | awk '{print $1}'
3838
```
3939

40+
### Custom Delimiter
41+
42+
By default, the dropdown uses `:` to separate labels from values (e.g., `Label:value`). If your data contains colons (such as URLs or timestamps), you can customize the delimiter in the dropdown settings.
43+
44+
Open the dropdown configuration modal and set the **Label/Value Delimiter** field to any character or string:
45+
46+
| Delimiter | Example |
47+
|-----------|---------|
48+
| `:` (default) | `Production:prod` |
49+
| `\|` | `Production\|https://api.example.com:8080` |
50+
| `::` | `My Label::my-value` |
51+
| `->` | `Display Name->actual_value` |
52+
53+
This allows you to use label:value pairs even when your values contain the default delimiter character.
54+
4055
### Template Usage
4156

4257
The selected value can be accessed in other blocks using the variable name:

0 commit comments

Comments
 (0)