fix: add missing multiselect fields to FieldDefinition struct
Some checks failed
CI / Lint (bash) (push) Has been cancelled
CI / Lint (markdown) (push) Has been cancelled
CI / Lint (nickel) (push) Has been cancelled
CI / Lint (nushell) (push) Has been cancelled
CI / Lint (rust) (push) Has been cancelled
CI / Code Coverage (push) Has been cancelled
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (macos-latest) (push) Has been cancelled
CI / Build (ubuntu-latest) (push) Has been cancelled
CI / Build (windows-latest) (push) Has been cancelled
CI / Benchmark (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / License Compliance (push) Has been cancelled

Add display_mode, searchable, min_selected, and max_selected fields to all
   FieldDefinition struct initializers across core library and tests.
This commit is contained in:
Jesús Pérez 2025-12-25 22:59:45 +00:00
parent 29daac9014
commit 813abc057e
Signed by: jesus
GPG Key ID: 9F243E355E0BC939
21 changed files with 670 additions and 206 deletions

View File

@ -7,10 +7,10 @@ ignore = [
# atty - unmaintained but widely used, replacement (is-terminal) requires code changes
"RUSTSEC-2021-0145",
"RUSTSEC-2024-0375",
# atomic-polyfill - unmaintained, comes from surrealdb dependency
"RUSTSEC-2023-0089",
# paste - unmaintained, comes from multiple dependencies (ratatui, nickel)
"RUSTSEC-2024-0436",

2
.gitignore vendored
View File

@ -5,7 +5,7 @@ COMMIT_MESSAGE.md
.wrks
nushell
nushell-*
*.tar.gz
*.tar.gz
#*-nushell-plugins.tar.gz
github-com
.coder

View File

@ -75,15 +75,17 @@ repos:
entry: bash -c 'for f in "$@"; do nickel typecheck "$f" || exit 1; done' --
language: system
files: '\.ncl$'
exclude: '(nickel-secrets|sops-example|conditional|complex)\.ncl$'
# Nushell script validation
- repo: local
hooks:
- id: nushell-check
name: nushell check
entry: bash -c 'for f in "$@"; do nu --check "$f" || exit 1; done' --
entry: bash -c 'for f in "$@"; do nu --ide-check "$f" || exit 1; done' --
language: system
files: '\.nu$'
exclude: '(json-to-nickel)\.nu$'
# Security checks
- repo: local

View File

@ -10,4 +10,3 @@ action:
- " "
tokens:
- '\s[—–]\s'

View File

@ -1,6 +1,6 @@
# Custom Docker image for Woodpecker CI
# Pre-installs common tools to speed up CI runs
#
#
# Build: docker build -t your-registry/typedialog-ci:latest -f .woodpecker/Dockerfile .
# Push: docker push your-registry/typedialog-ci:latest
#

View File

@ -9,7 +9,7 @@ when:
steps:
# === LINTING ===
lint-rust:
image: rust:latest
commands:

View File

@ -18,12 +18,12 @@ steps:
--arg name "Release ${CI_COMMIT_TAG}" \
--arg body "Automated release from Woodpecker CI" \
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')
RELEASE_ID=$(curl -X POST "${GITEA_URL}/api/v1/repos/${CI_REPO}/releases" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "${RELEASE_DATA}" | jq -r '.id')
echo "RELEASE_ID=${RELEASE_ID}" >> /tmp/release.env
echo "✓ Created release ${CI_COMMIT_TAG} (ID: ${RELEASE_ID})"

View File

@ -90,4 +90,3 @@ This Code of Conduct is adapted from the Contributor Covenant, version 3.0, perm
Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA 4.0. To view a copy of this license, visit [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/)
For answers to common questions about Contributor Covenant, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are provided at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). Additional enforcement and community guideline resources can be found at [https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources). The enforcement ladder was inspired by the work of [Mozillas code of conduct team](https://github.com/mozilla/inclusion).

232
Cargo.lock generated
View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
version = 3
[[package]]
name = "Inflector"
@ -271,9 +271,12 @@ dependencies = [
[[package]]
name = "arc-swap"
version = "1.7.1"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e"
dependencies = [
"rustversion",
]
[[package]]
name = "argon2"
@ -855,9 +858,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.19.0"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "bytecheck"
@ -918,7 +921,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77"
dependencies = [
"serde",
"toml 0.9.8",
"toml 0.9.10+spec-1.1.0",
]
[[package]]
@ -953,9 +956,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.49"
version = "1.2.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c"
dependencies = [
"find-msvc-tools",
"jobserver",
@ -1526,7 +1529,7 @@ dependencies = [
"document-features",
"mio",
"parking_lot",
"rustix 1.1.2",
"rustix 1.1.3",
"signal-hook",
"signal-hook-mio",
"winapi",
@ -1707,18 +1710,18 @@ dependencies = [
[[package]]
name = "derive_more"
version = "2.1.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618"
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.1.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b"
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
dependencies = [
"convert_case",
"proc-macro2",
@ -1790,7 +1793,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.2",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@ -1925,7 +1928,7 @@ dependencies = [
"serde_json",
"serde_yaml",
"thiserror 2.0.17",
"toml 0.9.8",
"toml 0.9.10+spec-1.1.0",
"tracing",
]
@ -1965,7 +1968,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@ -2038,7 +2041,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
dependencies = [
"cfg-if",
"rustix 1.1.2",
"rustix 1.1.3",
"windows-sys 0.59.0",
]
@ -2219,7 +2222,7 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4"
dependencies = [
"rustix 1.1.2",
"rustix 1.1.3",
"windows-sys 0.59.0",
]
@ -2854,7 +2857,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.6.1",
"socket2",
"system-configuration",
"tokio",
"tower-service",
@ -3281,7 +3284,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi 0.5.2",
"libc",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@ -3334,9 +3337,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.15"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010"
[[package]]
name = "jobserver"
@ -3565,9 +3568,9 @@ dependencies = [
[[package]]
name = "libredox"
version = "0.1.10"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50"
dependencies = [
"bitflags 2.10.0",
"libc",
@ -3682,6 +3685,12 @@ dependencies = [
"hashbrown 0.16.1",
]
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "lscolors"
version = "0.20.0"
@ -4101,7 +4110,7 @@ dependencies = [
"strip-ansi-escapes",
"strsim",
"termimad",
"toml 0.9.8",
"toml 0.9.10+spec-1.1.0",
"toml_edit",
"topiary-core",
"topiary-queries",
@ -4224,9 +4233,9 @@ checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
[[package]]
name = "ntapi"
version = "0.4.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081"
dependencies = [
"winapi",
]
@ -4237,7 +4246,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@ -4665,7 +4674,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@ -5303,31 +5312,34 @@ dependencies = [
[[package]]
name = "quinn"
version = "0.11.6"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef"
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash 2.1.1",
"rustls",
"socket2 0.5.10",
"socket2",
"thiserror 2.0.17",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.9"
version = "0.11.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d"
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
dependencies = [
"bytes",
"getrandom 0.2.16",
"rand 0.8.5",
"getrandom 0.3.4",
"lru-slab",
"rand 0.9.2",
"ring",
"rustc-hash 2.1.1",
"rustls",
@ -5348,9 +5360,9 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.6.1",
"socket2",
"tracing",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@ -5750,33 +5762,29 @@ dependencies = [
[[package]]
name = "rmp"
version = "0.8.14"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4"
checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
dependencies = [
"byteorder",
"num-traits",
"paste",
]
[[package]]
name = "rmp-serde"
version = "1.3.0"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db"
checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
dependencies = [
"byteorder",
"rmp",
"serde",
]
[[package]]
name = "rmpv"
version = "1.3.0"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58450723cd9ee93273ce44a20b6ec4efe17f8ed2e3631474387bfdecf18bb2a9"
checksum = "7a4e1d4b9b938a26d2996af33229f0ca0956c652c1375067f0b45291c1df8417"
dependencies = [
"num-traits",
"rmp",
]
@ -5984,15 +5992,15 @@ dependencies = [
[[package]]
name = "rustix"
version = "1.1.2"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
dependencies = [
"bitflags 2.10.0",
"errno",
"libc",
"linux-raw-sys 0.11.0",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@ -6084,9 +6092,9 @@ dependencies = [
[[package]]
name = "ryu"
version = "1.0.20"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea"
[[package]]
name = "safe_arch"
@ -6148,9 +6156,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289"
checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2"
dependencies = [
"dyn-clone",
"ref-cast",
@ -6296,16 +6304,16 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.145"
version = "1.0.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
checksum = "6af14725505314343e673e9ecb7cd7e8a36aa9791eb936235a3567cc31447ae4"
dependencies = [
"indexmap 2.12.1",
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
"zmij",
]
[[package]]
@ -6321,9 +6329,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392"
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
dependencies = [
"serde_core",
]
@ -6352,7 +6360,7 @@ dependencies = [
"indexmap 1.9.3",
"indexmap 2.12.1",
"schemars 0.9.0",
"schemars 1.1.0",
"schemars 1.2.0",
"serde_core",
"serde_json",
"serde_with_macros",
@ -6438,9 +6446,9 @@ dependencies = [
[[package]]
name = "shell-words"
version = "1.1.0"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
[[package]]
name = "shlex"
@ -6471,10 +6479,11 @@ dependencies = [
[[package]]
name = "signal-hook-registry"
version = "1.4.7"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
@ -6560,16 +6569,6 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b"
[[package]]
name = "socket2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "socket2"
version = "0.6.1"
@ -6756,9 +6755,9 @@ dependencies = [
[[package]]
name = "supports-hyperlinks"
version = "3.1.0"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b"
checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91"
[[package]]
name = "supports-unicode"
@ -7195,15 +7194,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.23.0"
version = "3.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"once_cell",
"rustix 1.1.2",
"windows-sys 0.59.0",
"rustix 1.1.3",
"windows-sys 0.61.2",
]
[[package]]
@ -7256,7 +7255,7 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@ -7290,7 +7289,7 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
dependencies = [
"rustix 1.1.2",
"rustix 1.1.3",
"windows-sys 0.60.2",
]
@ -7441,7 +7440,7 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2 0.6.1",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
@ -7530,9 +7529,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.8"
version = "0.9.10+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48"
dependencies = [
"indexmap 2.12.1",
"serde_core",
@ -7545,18 +7544,18 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.7.3"
version = "0.7.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.23.9"
version = "0.23.10+spec-1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832"
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
dependencies = [
"indexmap 2.12.1",
"toml_datetime",
@ -7567,18 +7566,18 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.0.4"
version = "1.0.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.4"
version = "1.0.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
[[package]]
name = "topiary-core"
@ -7691,9 +7690,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.43"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
@ -7714,9 +7713,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.35"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
@ -7860,7 +7859,7 @@ dependencies = [
"clap",
"serde_json",
"tokio",
"toml 0.9.8",
"toml 0.9.10+spec-1.1.0",
"typedialog-core",
"unic-langid",
]
@ -7881,7 +7880,7 @@ dependencies = [
"serde_json",
"thiserror 2.0.17",
"tokio",
"toml 0.9.8",
"toml 0.9.10+spec-1.1.0",
"tower-http",
"tracing",
"tracing-subscriber",
@ -7914,7 +7913,7 @@ dependencies = [
"tera",
"thiserror 2.0.17",
"tokio",
"toml 0.9.8",
"toml 0.9.10+spec-1.1.0",
"tracing",
"uuid",
]
@ -7939,7 +7938,7 @@ dependencies = [
"surrealdb",
"thiserror 2.0.17",
"tokio",
"toml 0.9.8",
"toml 0.9.10+spec-1.1.0",
"tower",
"tower-http",
"tracing",
@ -7986,7 +7985,7 @@ dependencies = [
"tera",
"thiserror 2.0.17",
"tokio",
"toml 0.9.8",
"toml 0.9.10+spec-1.1.0",
"tower",
"tower-http",
"tracing",
@ -8016,7 +8015,7 @@ dependencies = [
"tera",
"thiserror 2.0.17",
"tokio",
"toml 0.9.8",
"toml 0.9.10+spec-1.1.0",
"tracing",
"tracing-subscriber",
"typedialog-ai",
@ -8032,7 +8031,7 @@ dependencies = [
"clap",
"serde_json",
"tokio",
"toml 0.9.8",
"toml 0.9.10+spec-1.1.0",
"typedialog-core",
"unic-langid",
]
@ -8045,7 +8044,7 @@ dependencies = [
"clap",
"serde_json",
"tokio",
"toml 0.9.8",
"toml 0.9.10+spec-1.1.0",
"typedialog-core",
"unic-langid",
]
@ -8405,12 +8404,13 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.41"
version = "0.4.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97"
checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67"
dependencies = [
"cfg-if",
"js-sys",
"once_cell",
"wasm-bindgen",
"web-sys",
]
@ -8449,9 +8449,9 @@ dependencies = [
[[package]]
name = "wasm-streams"
version = "0.4.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd"
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
dependencies = [
"futures-util",
"js-sys",
@ -8475,9 +8475,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.68"
version = "0.3.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446"
checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc"
dependencies = [
"js-sys",
"wasm-bindgen",
@ -8561,7 +8561,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@ -9219,6 +9219,12 @@ dependencies = [
"syn 2.0.111",
]
[[package]]
name = "zmij"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0095ecd462946aa3927d9297b63ef82fb9a5316d7a37d134eeb36e58228615a"
[[package]]
name = "zstd"
version = "0.13.3"

View File

@ -70,4 +70,3 @@ cache = [] # Cache layer
[lib]
name = "typedialog_ag_core"
path = "src/lib.rs"

View File

@ -50,4 +50,3 @@ watch = ["dep:notify"]
[dependencies.notify]
workspace = true
optional = true

View File

@ -524,6 +524,14 @@ async fn index_handler(State(state): State<Arc<WebFormState>>) -> impl IntoRespo
.then(r => r.text())
.then(html => {{
document.getElementById('form-fields').innerHTML = html;
// After HTML replacement, populate all multiselect hidden fields
// This ensures values are preserved when rendered HTML replaces old HTML
document.querySelectorAll('[id^="values_"]').forEach(hiddenField => {{
const fieldName = hiddenField.id.replace('values_', '');
const checkboxes = document.querySelectorAll('[data-checkbox-group="' + fieldName + '"]:checked');
const values = Array.from(checkboxes).map(cb => cb.value);
hiddenField.value = values.join(',');
}});
}})
.catch(err => console.error('Failed to update form:', err));
}}
@ -599,9 +607,22 @@ async fn get_dynamic_form_handler(
// Convert query parameters to results HashMap for condition evaluation
// JavaScript sends current field values as query parameters
// Multiselect fields use "_values" suffix and contain comma-separated values
let mut results: HashMap<String, Value> = HashMap::new();
for (key, value) in params {
results.insert(key, Value::String(value));
if key.ends_with("_values") {
// Parse comma-separated multiselect values into array
let field_name = key.strip_suffix("_values").unwrap_or(&key);
let values: Vec<Value> = value
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(|s| Value::String(s.to_string()))
.collect();
results.insert(field_name.to_string(), Value::Array(values));
} else {
results.insert(key, Value::String(value));
}
}
// Get base_dir and form for recomputation
@ -1001,6 +1022,109 @@ fn render_display_item_html(item: &DisplayItem) -> String {
}
}
/// Render multiselect field with support for different display modes
/// Modes: "list" (default), "grid", "dropdown"
fn render_multiselect_html(field: &FieldDefinition, selected_values: &[String]) -> String {
let field_name = &field.name;
let display_mode = field.display_mode.as_deref().unwrap_or("list");
let searchable = field.searchable.unwrap_or(false);
match display_mode {
"grid" => {
// Grid layout (2-3 columns)
let options_html = field
.options
.iter()
.map(|opt| {
let checked = if selected_values.contains(&opt.value) {
"checked"
} else {
""
};
format!(
r#"<label style="display: inline-block; margin: 10px 15px; cursor: pointer; min-width: 150px;">
<input type="checkbox" data-checkbox-group="{}" value="{}" {}> {}
</label>"#,
html_escape(field_name),
html_escape(&opt.value),
checked,
html_escape(opt.display_label())
)
})
.collect::<Vec<_>>()
.join("\n");
format!(
r#"<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; padding: 10px; background: #1e1e1e; border: 1px solid #3e3e42;">
{}
</div>"#,
options_html
)
}
"dropdown" => {
// Dropdown select with optional search
let options_html = field
.options
.iter()
.map(|opt| {
let selected = if selected_values.contains(&opt.value) {
"selected"
} else {
""
};
format!(
"<option value=\"{}\" {}>{}</option>",
html_escape(&opt.value),
selected,
html_escape(opt.display_label())
)
})
.collect::<Vec<_>>()
.join("\n");
let search_attr = if searchable {
r#"data-searchable="true""#
} else {
""
};
format!(
r#"<select multiple name="{}_values" id="select_{}" {} style="width: 100%; padding: 8px; background: #1e1e1e; color: #d4d4d4; border: 1px solid #3e3e42; box-sizing: border-box;">
{}
</select>"#,
html_escape(field_name),
html_escape(field_name),
search_attr,
options_html
)
}
_ => {
// Default "list" mode: vertical checkboxes
field
.options
.iter()
.map(|opt| {
let checked = if selected_values.contains(&opt.value) {
"checked"
} else {
""
};
format!(
r#"<label style="display: block; margin: 5px 0; cursor: pointer;">
<input type="checkbox" data-checkbox-group="{}" value="{}" {}> {}
</label>"#,
html_escape(field_name),
html_escape(&opt.value),
checked,
html_escape(opt.display_label())
)
})
.collect::<Vec<_>>()
.join("\n")
}
}
}
/// Render a single field for inclusion in the complete form (without individual submit button)
/// Includes state injection for selected/checked attributes based on current results
/// Returns (field_html, modals_and_scripts) where modals_and_scripts contains modals/JS for RepeatingGroup
@ -1147,8 +1271,10 @@ fn render_field_for_complete_form(
}
FieldType::MultiSelect => {
let field_name = &field.name;
let display_mode = field.display_mode.as_deref().unwrap_or("list");
// Get current selected values from results for state preservation
// If no results, use default value
let selected_values: Vec<String> = results
.get(field_name)
.and_then(|v| v.as_array())
@ -1157,32 +1283,75 @@ fn render_field_for_complete_form(
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.or_else(|| {
// Use default if available and results don't have this field
field.default.as_ref().map(|default| {
default
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
})
})
.unwrap_or_default();
let options = if field.options.is_empty() {
String::new()
let options_html = render_multiselect_html(field, &selected_values);
// Generate appropriate script based on display mode
let script = if display_mode == "dropdown" {
// For dropdown/select mode, handle change event differently
format!(
r#"<script>
document.addEventListener('change', function(e) {{
if (e.target.id === 'select_{}') {{
const select = e.target;
const values = Array.from(select.selectedOptions).map(o => o.value);
document.getElementById('values_{}').value = values.join(',');
// Stop and re-trigger for reactive form update
e.stopImmediatePropagation();
setTimeout(function() {{
e.target.dispatchEvent(new Event('change', {{ bubbles: true }}));
}}, 0);
}}
}});
document.addEventListener('submit', function(e) {{
if (e.target.id === 'complete-form') {{
const select = document.getElementById('select_{}');
if (select) {{
const values = Array.from(select.selectedOptions).map(o => o.value);
document.getElementById('values_{}').value = values.join(',');
}}
}}
}});
</script>"#,
html_escape(field_name),
html_escape(field_name),
html_escape(field_name),
html_escape(field_name)
)
} else {
field
.options
.iter()
.map(|opt| {
let checked = if selected_values.contains(&opt.value) {
"checked"
} else {
""
};
format!(
r#"<label style="display: block; margin: 5px 0; cursor: pointer;">
<input type="checkbox" data-checkbox-group="{}" value="{}" {}> {}
</label>"#,
html_escape(field_name),
html_escape(&opt.value),
checked,
html_escape(opt.display_label())
)
})
.collect::<Vec<_>>()
.join("\n")
// For checkbox modes (list, grid)
format!(
r#"<script>
document.addEventListener('change', function(e) {{
if (e.target.matches('[data-checkbox-group="{}"]')) {{
collectMultiselectValues(e, '{}');
e.stopImmediatePropagation();
setTimeout(function() {{
e.target.dispatchEvent(new Event('change', {{ bubbles: true }}));
}}, 0);
}}
}});
document.addEventListener('submit', function(e) {{
if (e.target.id === 'complete-form') {{
collectMultiselectValues(e, '{}');
}}
}});
</script>"#,
html_escape(field_name),
html_escape(field_name),
html_escape(field_name)
)
};
(
@ -1193,19 +1362,13 @@ fn render_field_for_complete_form(
<div style="border: 1px solid #3e3e42; padding: 10px; background: #1e1e1e;">
{}
</div>
<script>
document.addEventListener('submit', function(e) {{
if (e.target.id === 'complete-form') {{
collectMultiselectValues(e, '{}');
}}
}});
</script>
{}
</div>"#,
html_escape(&field.prompt),
html_escape(field_name),
html_escape(field_name),
options,
html_escape(field_name)
options_html,
script
),
String::new(),
)
@ -1382,27 +1545,58 @@ fn render_field_to_html(field: &FieldDefinition) -> String {
}
FieldType::MultiSelect => {
let field_name = &field.name;
let options = if field.options.is_empty() {
String::new()
} else {
field
.options
.iter()
.map(|opt| {
format!(
r#"<label style="display: block; margin: 5px 0;">
<input type="checkbox" data-checkbox-group="{}" value="{}"> {}
</label>"#,
html_escape(field_name),
html_escape(&opt.value),
html_escape(opt.display_label())
)
})
.collect::<Vec<_>>()
.join("\n")
};
let display_mode = field.display_mode.as_deref().unwrap_or("list");
// Parse default values for pre-checking
let default_values: Vec<String> = field
.default
.as_ref()
.map(|default| {
default
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
})
.unwrap_or_default();
let options_html = render_multiselect_html(field, &default_values);
let form_id = format!("form_{}", html_escape(field_name));
// Generate appropriate submit button and script based on display mode
let (submit_button, form_script) = if display_mode == "dropdown" {
let btn = r#"<button type="submit">Submit</button>"#.to_string();
let script = format!(
r#"<script>
document.getElementById('{}').addEventListener('submit', function(e) {{
const select = document.querySelector('select[name="{}_values"]');
if (select) {{
const values = Array.from(select.selectedOptions).map(o => o.value);
document.getElementById('values_{}').value = values.join(',');
}}
}});
</script>"#,
form_id,
html_escape(field_name),
html_escape(field_name)
);
(btn, script)
} else {
let btn = format!(
r#"<button type="submit" onclick="collectMultiselectValues(event, '{}')">Submit</button>"#,
html_escape(field_name)
);
let script = r#"<script>
function collectMultiselectValues(event, fieldName) {{
const checkboxes = document.querySelectorAll('[data-checkbox-group="' + fieldName + '"]:checked');
const values = Array.from(checkboxes).map(cb => cb.value);
document.getElementById('values_' + fieldName).value = values.join(',');
}}
</script>"#.to_string();
(btn, script)
};
format!(
r#"<div class="field">
<label>{}</label>
@ -1412,22 +1606,17 @@ fn render_field_to_html(field: &FieldDefinition) -> String {
<div style="border: 1px solid #3e3e42; padding: 10px; background: #252526;">
{}
</div>
<button type="submit" onclick="collectMultiselectValues(event, '{}')">Submit</button>
{}
</form>
</div>
<script>
function collectMultiselectValues(event, fieldName) {{
const checkboxes = document.querySelectorAll('[data-checkbox-group="' + fieldName + '"]:checked');
const values = Array.from(checkboxes).map(cb => cb.value);
document.getElementById('values_' + fieldName).value = values.join(',');
}}
</script>"#,
{}"#,
html_escape(&field.prompt),
form_id,
html_escape(field_name),
html_escape(field_name),
options,
html_escape(field_name)
options_html,
submit_button,
form_script
)
}
FieldType::Custom => {

View File

@ -193,6 +193,10 @@ mod tests {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type: None,
min_date: None,
max_date: None,

View File

@ -14,6 +14,21 @@ fn default_order() -> usize {
0
}
/// Deserialize `default` field accepting both string and boolean TOML values
fn deserialize_default<'de, D>(deserializer: D) -> std::result::Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
use serde_json::Value;
let value: Option<Value> = Option::deserialize(deserializer)?;
Ok(value.and_then(|v| match v {
Value::String(s) => Some(s),
Value::Bool(b) => Some(b.to_string()),
Value::Number(n) => Some(n.to_string()),
_ => None,
}))
}
/// Form element (can be a display item or a field)
/// Public enum for unified form structure
#[derive(Debug, Clone)]
@ -477,6 +492,8 @@ pub struct FieldDefinition {
/// Prompt message (can be literal text or i18n key)
pub prompt: String,
/// Optional default value (can contain template expressions like {{ env.USER }})
/// Accepts both string ("false", "true") and boolean (false, true) TOML values
#[serde(default, deserialize_with = "deserialize_default")]
pub default: Option<String>,
/// Optional placeholder text (can be literal text or i18n key)
pub placeholder: Option<String>,
@ -493,6 +510,18 @@ pub struct FieldDefinition {
pub page_size: Option<usize>,
/// Optional vim mode flag (for select/multiselect)
pub vim_mode: Option<bool>,
/// Optional display mode for multiselect: "list" (default), "grid", "dropdown", "tags"
#[serde(default)]
pub display_mode: Option<String>,
/// Optional searchable flag for multiselect/dropdown (enables filtering)
#[serde(default)]
pub searchable: Option<bool>,
/// Optional minimum selected items (for multiselect)
#[serde(default)]
pub min_selected: Option<usize>,
/// Optional maximum selected items (for multiselect)
#[serde(default)]
pub max_selected: Option<usize>,
/// Optional custom type name (for custom)
pub custom_type: Option<String>,
/// Optional min date (for date)
@ -2839,6 +2868,67 @@ mod tests {
assert_eq!(fields.len(), 1);
assert!(!fields.iter().any(|f| f.name == "ssl_warning"));
}
#[test]
fn test_default_field_accepts_both_string_and_bool_formats() {
let toml_string_format = r#"
name = "test_form_string"
[[fields]]
name = "agree_string"
type = "confirm"
prompt = "Do you agree?"
default = "false"
[[fields]]
name = "subscribe_string"
type = "confirm"
prompt = "Subscribe?"
default = "true"
"#;
let toml_bool_format = r#"
name = "test_form_bool"
[[fields]]
name = "agree_bool"
type = "confirm"
prompt = "Do you agree?"
default = false
[[fields]]
name = "subscribe_bool"
type = "confirm"
prompt = "Subscribe?"
default = true
"#;
let form_string = parse_toml(toml_string_format).unwrap();
let form_bool = parse_toml(toml_bool_format).unwrap();
// Check string format parsing
let agree_string = form_string.fields.iter().find(|f| f.name == "agree_string");
assert!(agree_string.is_some());
assert_eq!(agree_string.unwrap().default.as_deref(), Some("false"));
let subscribe_string = form_string
.fields
.iter()
.find(|f| f.name == "subscribe_string");
assert!(subscribe_string.is_some());
assert_eq!(subscribe_string.unwrap().default.as_deref(), Some("true"));
// Check bool format parsing
let agree_bool = form_bool.fields.iter().find(|f| f.name == "agree_bool");
assert!(agree_bool.is_some());
assert_eq!(agree_bool.unwrap().default.as_deref(), Some("false"));
let subscribe_bool = form_bool.fields.iter().find(|f| f.name == "subscribe_bool");
assert!(subscribe_bool.is_some());
assert_eq!(subscribe_bool.unwrap().default.as_deref(), Some("true"));
}
}
#[cfg(test)]

View File

@ -413,6 +413,10 @@ mod tests {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type: None,
min_date: None,
max_date: None,
@ -463,6 +467,10 @@ mod tests {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type: None,
min_date: None,
max_date: None,
@ -513,6 +521,10 @@ mod tests {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type: None,
min_date: None,
max_date: None,
@ -564,6 +576,10 @@ mod tests {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type: None,
min_date: None,
max_date: None,
@ -598,6 +614,10 @@ mod tests {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type: None,
min_date: None,
max_date: None,
@ -644,6 +664,10 @@ mod tests {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type: None,
min_date: None,
max_date: None,

View File

@ -321,6 +321,10 @@ impl TomlGenerator {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type,
min_date: None,
max_date: None,
@ -475,6 +479,10 @@ impl TomlGenerator {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type: None,
min_date: None,
max_date: None,

View File

@ -29,6 +29,10 @@ mod encryption_tests {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type: None,
min_date: None,
max_date: None,
@ -129,6 +133,10 @@ mod encryption_tests {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type: None,
min_date: None,
max_date: None,
@ -182,6 +190,10 @@ mod encryption_tests {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type: None,
min_date: None,
max_date: None,
@ -234,6 +246,10 @@ mod encryption_tests {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type: None,
min_date: None,
max_date: None,
@ -324,6 +340,10 @@ mod age_roundtrip_tests {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type: None,
min_date: None,
max_date: None,

View File

@ -1205,6 +1205,10 @@ fn test_encryption_roundtrip_with_redaction() {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type: None,
min_date: None,
max_date: None,
@ -1239,6 +1243,10 @@ fn test_encryption_roundtrip_with_redaction() {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type: None,
min_date: None,
max_date: None,
@ -1273,6 +1281,10 @@ fn test_encryption_roundtrip_with_redaction() {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type: None,
min_date: None,
max_date: None,
@ -1345,6 +1357,10 @@ fn test_encryption_auto_detection_from_field_type() {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type: None,
min_date: None,
max_date: None,
@ -1405,6 +1421,10 @@ fn test_sensitive_field_explicit_override() {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type: None,
min_date: None,
max_date: None,
@ -1471,6 +1491,10 @@ fn test_mixed_sensitive_and_non_sensitive_fields() {
prefix_text: None,
page_size: None,
vim_mode: None,
display_mode: None,
searchable: None,
min_selected: None,
max_selected: None,
custom_type: None,
min_date: None,
max_date: None,

View File

@ -134,41 +134,64 @@ label = "PostgreSQL (server)"
### MultiSelect
Multiple selections from predefined options.
Multiple selections from predefined options with flexible display modes.
```toml
# Basic example with grid display
[[elements]]
name = "features"
name = "languages"
type = "multiselect"
prompt = "Enable features"
page_size = 10
vim_mode = false
prompt = "Which languages are used?"
display_mode = "grid"
searchable = true
default = "rust"
options = [
{ value = "rust", label = "🦀 Rust" },
{ value = "python", label = "🐍 Python" },
{ value = "javascript", label = "📜 JavaScript" }
]
[[elements.options]]
value = "prometheus"
label = "Prometheus Metrics"
[[elements.options]]
value = "grafana"
label = "Grafana Dashboard"
[[elements.options]]
value = "auth"
label = "Authentication"
# Dropdown example with validation
[[elements]]
name = "user_roles"
type = "multiselect"
prompt = "Assign roles"
display_mode = "dropdown"
searchable = true
min_selected = 1
max_selected = 5
required = true
options = [
{ value = "admin", label = "Administrator" },
{ value = "editor", label = "Editor" },
{ value = "viewer", label = "Viewer" }
]
```
**Display Modes**:
- `"list"` (default): Vertical checkbox list
- `"grid"`: Responsive grid layout (2-3 columns)
- `"dropdown"`: HTML `<select multiple>` dropdown
**Attributes**:
- `prompt`: Display label
- `options`: Array of value/label pairs
- `page_size`: Number of visible items
- `vim_mode`: Enable vim navigation
- `display_mode`: Rendering style ("list", "grid", "dropdown")
- `searchable`: Enable search/filter in dropdown mode
- `default`: Pre-selected values (comma-separated)
- `min_selected`: Minimum number of selections
- `max_selected`: Maximum number of selections
- `required`: Validation flag
- `page_size`: Number of visible items (CLI/TUI only)
- `vim_mode`: Enable vim navigation (CLI/TUI only)
**Backend rendering**:
- CLI: Checkbox list with space to toggle
- TUI: Multi-selection list with checkboxes
- Web: HTML checkboxes in group
- Web: Grid/List checkboxes or HTML `<select multiple>` based on `display_mode`
---

View File

@ -43,6 +43,9 @@ required = true
name = "category"
type = "multiselect"
prompt = "Message categories"
display_mode = "grid"
searchable = true
default = "Bug Report"
options = [
{ value = "Bug Report", label = "🐛 Bug Report" },
{ value = "Feature Request", label = "✨ Feature Request" },

View File

@ -0,0 +1,75 @@
name = "MultiSelect Display Modes Demo"
description = "Demonstrates different display modes and features for multiselect fields"
# List mode (default) - vertical checkboxes
[[elements]]
name = "features_list"
type = "multiselect"
prompt = "Features (List mode - default)"
help = "Vertical checkbox list"
default = "logging"
options = [
{ value = "logging", label = "📝 Logging" },
{ value = "metrics", label = "📊 Metrics" },
{ value = "tracing", label = "🔍 Tracing" },
{ value = "profiling", label = "⚡ Profiling" },
]
# Grid mode - responsive grid layout
[[elements]]
name = "languages_grid"
type = "multiselect"
prompt = "Programming Languages (Grid mode)"
help = "Responsive grid with icons"
display_mode = "grid"
searchable = true
default = "rust,python"
options = [
{ value = "rust", label = "🦀 Rust" },
{ value = "python", label = "🐍 Python" },
{ value = "javascript", label = "📜 JavaScript" },
{ value = "go", label = "🐹 Go" },
{ value = "java", label = "☕ Java" },
{ value = "csharp", label = "🔵 C#" },
]
# Dropdown mode - native select multiple with search
[[elements]]
name = "frameworks"
type = "multiselect"
prompt = "Web Frameworks"
help = "Use dropdown mode for 10+ options"
display_mode = "dropdown"
searchable = true
min_selected = 1
max_selected = 3
required = true
options = [
{ value = "react", label = "React" },
{ value = "vue", label = "Vue" },
{ value = "angular", label = "Angular" },
{ value = "svelte", label = "Svelte" },
{ value = "nextjs", label = "Next.js" },
{ value = "nuxt", label = "Nuxt" },
{ value = "astro", label = "Astro" },
{ value = "remix", label = "Remix" },
{ value = "gatsby", label = "Gatsby" },
{ value = "qwik", label = "Qwik" },
]
# Example with min/max validation
[[elements]]
name = "permissions"
type = "multiselect"
prompt = "User Permissions"
help = "Select between 1 and 3 permissions"
display_mode = "grid"
min_selected = 1
max_selected = 3
default = "read"
options = [
{ value = "read", label = "📖 Read" },
{ value = "write", label = "✏️ Write" },
{ value = "delete", label = "🗑️ Delete" },
{ value = "admin", label = "🔑 Admin" },
]