chore: add new conditionals to docs
This commit is contained in:
parent
f7f7fec13b
commit
39e5c35a28
@ -453,12 +453,135 @@ when = "database_driver == mysql"
|
||||
required = true
|
||||
```
|
||||
|
||||
**Supported operators**:
|
||||
### Comparison Operators
|
||||
|
||||
- `==`: Equality
|
||||
- `!=`: Inequality
|
||||
- Parentheses for grouping (future)
|
||||
- Logical AND/OR (future)
|
||||
**Equality and inequality**:
|
||||
|
||||
- `==`: Equal to
|
||||
- `!=`: Not equal to
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "postgres_config"
|
||||
type = "text"
|
||||
when = "database_driver == postgresql"
|
||||
|
||||
[[elements]]
|
||||
name = "legacy_warning"
|
||||
type = "section"
|
||||
content = "⚠️ SQLite is for development only"
|
||||
when = "database_driver != postgresql"
|
||||
```
|
||||
|
||||
**Numeric comparisons**:
|
||||
|
||||
- `>`: Greater than
|
||||
- `<`: Less than
|
||||
- `>=`: Greater than or equal
|
||||
- `<=`: Less than or equal
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "performance_warning"
|
||||
type = "section"
|
||||
content = "⚠️ High port number may require elevated privileges"
|
||||
when = "port >= 1024"
|
||||
|
||||
[[elements]]
|
||||
name = "pool_size_warning"
|
||||
type = "section"
|
||||
when = "connection_pool > 100"
|
||||
```
|
||||
|
||||
### String Operators
|
||||
|
||||
**String matching**:
|
||||
|
||||
- `contains`: Check if field contains substring
|
||||
- `startswith`: Check if field starts with prefix
|
||||
- `endswith`: Check if field ends with suffix
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "rust_specific"
|
||||
type = "text"
|
||||
prompt = "Rust toolchain version"
|
||||
when = "language contains rust"
|
||||
|
||||
[[elements]]
|
||||
name = "protocol_warning"
|
||||
type = "section"
|
||||
when = "url startswith https"
|
||||
|
||||
[[elements]]
|
||||
name = "yaml_parser"
|
||||
type = "select"
|
||||
when = "config_file endswith .yaml"
|
||||
```
|
||||
|
||||
### Array Membership
|
||||
|
||||
**`in` operator**: Check if value exists in array field
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "rust_features"
|
||||
type = "multiselect"
|
||||
prompt = "Rust-specific features"
|
||||
when = "rust in detected_languages"
|
||||
options = [
|
||||
{ value = "clippy", label = "Clippy linting" },
|
||||
{ value = "cargo_audit", label = "Security auditing" }
|
||||
]
|
||||
|
||||
[[elements]]
|
||||
name = "python_virtualenv"
|
||||
type = "confirm"
|
||||
when = "python in languages"
|
||||
```
|
||||
|
||||
**Note**: The `in` operator works with:
|
||||
|
||||
- Array fields (JSON array values)
|
||||
- MultiSelect fields (comma-separated strings)
|
||||
|
||||
### File System Conditions
|
||||
|
||||
**`file_exists(path)`**: Check if file or directory exists
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "use_existing_config"
|
||||
type = "confirm"
|
||||
prompt = "Existing config.toml found. Use it?"
|
||||
when = "file_exists(config.toml)"
|
||||
|
||||
[[elements]]
|
||||
name = "create_new_config"
|
||||
type = "text"
|
||||
prompt = "Configuration name"
|
||||
when = "!file_exists(.env)"
|
||||
```
|
||||
|
||||
**Negation with `!`**:
|
||||
|
||||
- `!file_exists(path)`: File does NOT exist
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "docker_setup"
|
||||
type = "group"
|
||||
includes = ["fragments/docker-init.toml"]
|
||||
when = "!file_exists(Dockerfile)"
|
||||
```
|
||||
|
||||
### Future Support
|
||||
|
||||
**Planned features**:
|
||||
|
||||
- Parentheses for grouping: `(a == b) && (c == d)`
|
||||
- Logical AND: `&&`
|
||||
- Logical OR: `||`
|
||||
|
||||
---
|
||||
|
||||
|
||||
256
examples/13-conditional-logic/README.md
Normal file
256
examples/13-conditional-logic/README.md
Normal file
@ -0,0 +1,256 @@
|
||||
# Conditional Logic Examples
|
||||
|
||||
This directory demonstrates all supported conditional operators in TypeDialog forms.
|
||||
|
||||
## Overview
|
||||
|
||||
TypeDialog supports rich conditional logic for dynamic form behavior. Fields can be shown/hidden based on previous answers using the `when` attribute.
|
||||
|
||||
## Running the Examples
|
||||
|
||||
### CLI Backend
|
||||
|
||||
```bash
|
||||
cargo run --bin typedialog -- examples/13-conditional-logic/conditional-demo.toml
|
||||
```
|
||||
|
||||
### TUI Backend
|
||||
|
||||
```bash
|
||||
cargo run --bin typedialog-tui -- examples/13-conditional-logic/conditional-demo.toml
|
||||
```
|
||||
|
||||
### Web Backend
|
||||
|
||||
```bash
|
||||
cargo run --bin typedialog-web -- examples/13-conditional-logic/conditional-demo.toml
|
||||
```
|
||||
|
||||
## Supported Operators
|
||||
|
||||
### Comparison Operators
|
||||
|
||||
#### Equality and Inequality
|
||||
|
||||
- `==`: Equal to
|
||||
- `!=`: Not equal to
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "mysql_config"
|
||||
when = "database_driver == mysql"
|
||||
|
||||
[[elements]]
|
||||
name = "server_warning"
|
||||
when = "database_driver != sqlite"
|
||||
```
|
||||
|
||||
#### Numeric Comparisons
|
||||
|
||||
- `>`: Greater than
|
||||
- `<`: Less than
|
||||
- `>=`: Greater than or equal
|
||||
- `<=`: Less than or equal
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "privileged_port_warning"
|
||||
when = "server_port < 1024"
|
||||
|
||||
[[elements]]
|
||||
name = "high_port_warning"
|
||||
when = "server_port > 10000"
|
||||
```
|
||||
|
||||
### String Operators
|
||||
|
||||
- `contains`: Check if field contains substring
|
||||
- `startswith`: Check if field starts with prefix
|
||||
- `endswith`: Check if field ends with suffix
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "https_notice"
|
||||
when = "project_url startswith https"
|
||||
|
||||
[[elements]]
|
||||
name = "github_specific"
|
||||
when = "project_url endswith github.com"
|
||||
|
||||
[[elements]]
|
||||
name = "gitlab_ci"
|
||||
when = "project_url contains gitlab"
|
||||
```
|
||||
|
||||
### Array Membership
|
||||
|
||||
- `in`: Check if value exists in array field
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "rust_toolchain"
|
||||
when = "rust in detected_languages"
|
||||
|
||||
[[elements]]
|
||||
name = "python_venv"
|
||||
when = "python in detected_languages"
|
||||
```
|
||||
|
||||
**Works with**:
|
||||
|
||||
- MultiSelect fields (JSON array or comma-separated string)
|
||||
- Array values from JSON output
|
||||
|
||||
### File System Conditions
|
||||
|
||||
- `file_exists(path)`: Check if file or directory exists
|
||||
- `!file_exists(path)`: Check if file does NOT exist (negation)
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "dockerfile_exists_notice"
|
||||
when = "file_exists(Dockerfile)"
|
||||
|
||||
[[elements]]
|
||||
name = "create_dockerfile"
|
||||
when = "!file_exists(Dockerfile)"
|
||||
|
||||
[[elements]]
|
||||
name = "env_setup"
|
||||
type = "group"
|
||||
includes = ["fragments/environment-setup.toml"]
|
||||
when = "!file_exists(.env)"
|
||||
```
|
||||
|
||||
**Path resolution**:
|
||||
|
||||
- Absolute paths: `/etc/config.toml`
|
||||
- Relative paths: `Dockerfile`, `.env`, `config/settings.toml`
|
||||
- Works with both files and directories
|
||||
|
||||
## Form Flow Example
|
||||
|
||||
1. **Select database** → Shows driver-specific fields
|
||||
2. **Enter port number** → Shows warnings based on port range
|
||||
3. **Enter project URL** → Shows platform-specific options
|
||||
4. **Select languages** → Shows language-specific tooling
|
||||
5. **Check for files** → Conditionally load setup fragments
|
||||
|
||||
## Key Features
|
||||
|
||||
### Dynamic Field Visibility
|
||||
|
||||
Fields appear/disappear based on user input:
|
||||
|
||||
```toml
|
||||
# Only shows if user selects MySQL
|
||||
[[elements]]
|
||||
name = "mysql_password"
|
||||
type = "password"
|
||||
when = "database_driver == mysql"
|
||||
```
|
||||
|
||||
### Conditional Fragment Loading
|
||||
|
||||
Load entire form sections conditionally:
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "docker_setup"
|
||||
type = "group"
|
||||
includes = ["fragments/docker-init.toml"]
|
||||
when = "!file_exists(Dockerfile)"
|
||||
```
|
||||
|
||||
### Multi-Condition Fields
|
||||
|
||||
Same field can have complex logic (future: `&&` and `||`):
|
||||
|
||||
```toml
|
||||
# Current: Single condition per field
|
||||
when = "rust in detected_languages"
|
||||
|
||||
# Future: Compound conditions
|
||||
when = "(rust in detected_languages) && (server_port >= 1024)"
|
||||
```
|
||||
|
||||
## Testing Scenarios
|
||||
|
||||
### Scenario 1: Database Configuration
|
||||
|
||||
1. Select `mysql` → See MySQL-specific fields
|
||||
2. Select `postgresql` → See PostgreSQL-specific fields
|
||||
3. Select `sqlite` → No extra server fields
|
||||
|
||||
### Scenario 2: Port Validation
|
||||
|
||||
1. Enter `80` → See privileged port warning (`< 1024`)
|
||||
2. Enter `8080` → See standard port notice (`>= 1024`)
|
||||
3. Enter `15000` → See high port warning (`> 10000`)
|
||||
|
||||
### Scenario 3: Language Detection
|
||||
|
||||
1. Select `rust` + `python` → See both Rust and Python config fields
|
||||
2. Select only `javascript` → See only Node.js version selector
|
||||
3. Select none → No language-specific fields appear
|
||||
|
||||
### Scenario 4: File System Checks
|
||||
|
||||
1. **With Dockerfile present**:
|
||||
- Shows "Dockerfile found" notice
|
||||
- Skips Docker setup wizard
|
||||
|
||||
2. **Without Dockerfile**:
|
||||
- Asks to create Dockerfile
|
||||
- Shows Docker setup group
|
||||
|
||||
3. **With .env file**:
|
||||
- Asks to use existing config
|
||||
- Skips environment setup
|
||||
|
||||
4. **Without .env file**:
|
||||
- Loads environment setup fragment
|
||||
- Prompts for all env variables
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Condition Evaluation
|
||||
|
||||
Conditions are evaluated at runtime during form execution:
|
||||
|
||||
1. User answers a field
|
||||
2. System checks all pending fields for `when` conditions
|
||||
3. Fields with satisfied conditions become visible
|
||||
4. Fields with unsatisfied conditions remain hidden
|
||||
|
||||
### Fragment Loading
|
||||
|
||||
Conditional groups with `includes` load fragments only when condition is true:
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "advanced_config"
|
||||
type = "group"
|
||||
includes = ["fragments/advanced-settings.toml"]
|
||||
when = "enable_advanced == true"
|
||||
```
|
||||
|
||||
This prevents loading unnecessary form definitions until needed.
|
||||
|
||||
### Type Conversion
|
||||
|
||||
TypeDialog automatically handles type conversions in conditions:
|
||||
|
||||
- String `"8080"` compared to number `1024` → converted to number
|
||||
- Boolean `true` compared to string `"true"` → compared as boolean
|
||||
- Number compared to string number → compared numerically
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Field Types Reference](../../docs/field_types.md) - All field types and attributes
|
||||
- [Fragment System](../../docs/fragment-search-paths.md) - Dynamic form composition
|
||||
- [Nickel Integration](../../docs/nickel.md) - Schema-driven forms
|
||||
|
||||
## Source Code
|
||||
|
||||
Condition evaluation logic: `crates/typedialog-core/src/form_parser/conditions.rs`
|
||||
198
examples/13-conditional-logic/conditional-demo.toml
Normal file
198
examples/13-conditional-logic/conditional-demo.toml
Normal file
@ -0,0 +1,198 @@
|
||||
# Conditional Logic Demo
|
||||
# Demonstrates all supported conditional operators in TypeDialog
|
||||
|
||||
name = "conditional_demo"
|
||||
description = "Complete demonstration of conditional field visibility"
|
||||
|
||||
# ====================
|
||||
# COMPARISON OPERATORS
|
||||
# ====================
|
||||
|
||||
[[elements]]
|
||||
name = "database_driver"
|
||||
type = "select"
|
||||
prompt = "Select database driver"
|
||||
required = true
|
||||
options = [
|
||||
{ value = "sqlite", label = "SQLite (embedded)" },
|
||||
{ value = "mysql", label = "MySQL" },
|
||||
{ value = "postgresql", label = "PostgreSQL" }
|
||||
]
|
||||
|
||||
# Equality (==)
|
||||
[[elements]]
|
||||
name = "mysql_config"
|
||||
type = "text"
|
||||
prompt = "MySQL connection string"
|
||||
when = "database_driver == mysql"
|
||||
placeholder = "mysql://localhost:3306/db"
|
||||
|
||||
# Inequality (!=)
|
||||
[[elements]]
|
||||
name = "server_warning"
|
||||
type = "section"
|
||||
content = "⚠️ You selected a server-based database. Ensure the server is running."
|
||||
when = "database_driver != sqlite"
|
||||
|
||||
# ====================
|
||||
# NUMERIC COMPARISONS
|
||||
# ====================
|
||||
|
||||
[[elements]]
|
||||
name = "server_port"
|
||||
type = "text"
|
||||
prompt = "Server port"
|
||||
default = "8080"
|
||||
required = true
|
||||
|
||||
# Greater than (>)
|
||||
[[elements]]
|
||||
name = "high_port_warning"
|
||||
type = "section"
|
||||
content = "⚠️ Port > 10000 is uncommon. Double-check your configuration."
|
||||
when = "server_port > 10000"
|
||||
|
||||
# Less than (<)
|
||||
[[elements]]
|
||||
name = "privileged_port_warning"
|
||||
type = "section"
|
||||
content = "⚠️ Port < 1024 requires root/admin privileges."
|
||||
when = "server_port < 1024"
|
||||
|
||||
# Greater than or equal (>=)
|
||||
[[elements]]
|
||||
name = "standard_port_notice"
|
||||
type = "section"
|
||||
content = "✓ Using standard user port range (>= 1024)"
|
||||
when = "server_port >= 1024"
|
||||
|
||||
# Less than or equal (<=)
|
||||
[[elements]]
|
||||
name = "low_port_range"
|
||||
type = "section"
|
||||
content = "Using low port range (<= 5000)"
|
||||
when = "server_port <= 5000"
|
||||
|
||||
# ====================
|
||||
# STRING OPERATORS
|
||||
# ====================
|
||||
|
||||
[[elements]]
|
||||
name = "project_url"
|
||||
type = "text"
|
||||
prompt = "Project repository URL"
|
||||
placeholder = "https://github.com/user/repo"
|
||||
|
||||
# startswith
|
||||
[[elements]]
|
||||
name = "https_notice"
|
||||
type = "section"
|
||||
content = "✓ Secure HTTPS URL detected"
|
||||
when = "project_url startswith https"
|
||||
|
||||
# endswith
|
||||
[[elements]]
|
||||
name = "github_specific"
|
||||
type = "text"
|
||||
prompt = "GitHub Actions enabled?"
|
||||
when = "project_url endswith github.com"
|
||||
|
||||
# contains
|
||||
[[elements]]
|
||||
name = "gitlab_ci"
|
||||
type = "confirm"
|
||||
prompt = "Enable GitLab CI integration?"
|
||||
when = "project_url contains gitlab"
|
||||
|
||||
# ====================
|
||||
# ARRAY MEMBERSHIP (in)
|
||||
# ====================
|
||||
|
||||
[[elements]]
|
||||
name = "detected_languages"
|
||||
type = "multiselect"
|
||||
prompt = "Which languages are used in your project?"
|
||||
display_mode = "grid"
|
||||
options = [
|
||||
{ value = "rust", label = "🦀 Rust" },
|
||||
{ value = "python", label = "🐍 Python" },
|
||||
{ value = "javascript", label = "📜 JavaScript" },
|
||||
{ value = "go", label = "🐹 Go" }
|
||||
]
|
||||
|
||||
# Array membership check
|
||||
[[elements]]
|
||||
name = "rust_toolchain"
|
||||
type = "select"
|
||||
prompt = "Rust toolchain version"
|
||||
when = "rust in detected_languages"
|
||||
options = [
|
||||
{ value = "stable", label = "Stable" },
|
||||
{ value = "nightly", label = "Nightly" },
|
||||
{ value = "beta", label = "Beta" }
|
||||
]
|
||||
|
||||
[[elements]]
|
||||
name = "python_venv"
|
||||
type = "confirm"
|
||||
prompt = "Use Python virtual environment?"
|
||||
when = "python in detected_languages"
|
||||
default = true
|
||||
|
||||
[[elements]]
|
||||
name = "nodejs_version"
|
||||
type = "select"
|
||||
prompt = "Node.js version"
|
||||
when = "javascript in detected_languages"
|
||||
options = [
|
||||
{ value = "18", label = "Node.js 18 LTS" },
|
||||
{ value = "20", label = "Node.js 20 LTS" },
|
||||
{ value = "latest", label = "Latest" }
|
||||
]
|
||||
|
||||
# ====================
|
||||
# FILE SYSTEM CONDITIONS
|
||||
# ====================
|
||||
|
||||
# file_exists(path)
|
||||
[[elements]]
|
||||
name = "dockerfile_exists_notice"
|
||||
type = "section"
|
||||
content = "✓ Dockerfile found in current directory"
|
||||
when = "file_exists(Dockerfile)"
|
||||
|
||||
# !file_exists(path) - negation
|
||||
[[elements]]
|
||||
name = "create_dockerfile"
|
||||
type = "confirm"
|
||||
prompt = "No Dockerfile found. Create one?"
|
||||
when = "!file_exists(Dockerfile)"
|
||||
default = true
|
||||
|
||||
[[elements]]
|
||||
name = "use_existing_config"
|
||||
type = "confirm"
|
||||
prompt = "Existing .env file found. Use existing configuration?"
|
||||
when = "file_exists(.env)"
|
||||
|
||||
[[elements]]
|
||||
name = "env_setup"
|
||||
type = "group"
|
||||
includes = ["fragments/environment-setup.toml"]
|
||||
when = "!file_exists(.env)"
|
||||
|
||||
# ====================
|
||||
# COMBINED CONDITIONS
|
||||
# ====================
|
||||
|
||||
[[elements]]
|
||||
name = "rust_docker_setup"
|
||||
type = "section"
|
||||
content = "🦀 Rust + Docker detected. Consider using rust:alpine base image."
|
||||
when = "rust in detected_languages"
|
||||
|
||||
[[elements]]
|
||||
name = "production_ready_check"
|
||||
type = "section"
|
||||
content = "✅ Production-ready configuration detected (HTTPS + standard port)"
|
||||
when = "server_port >= 1024"
|
||||
@ -0,0 +1,72 @@
|
||||
# Environment Setup Fragment
|
||||
# Loaded conditionally when .env file doesn't exist
|
||||
|
||||
name = "environment_setup"
|
||||
description = "Environment variables configuration"
|
||||
display_mode = "complete"
|
||||
|
||||
[[elements]]
|
||||
name = "env_header"
|
||||
type = "section_header"
|
||||
title = "🔧 Environment Configuration"
|
||||
border_top = true
|
||||
border_bottom = true
|
||||
|
||||
[[elements]]
|
||||
name = "env_mode"
|
||||
type = "select"
|
||||
prompt = "Environment mode"
|
||||
required = true
|
||||
options = [
|
||||
{ value = "development", label = "Development" },
|
||||
{ value = "staging", label = "Staging" },
|
||||
{ value = "production", label = "Production" }
|
||||
]
|
||||
|
||||
[[elements]]
|
||||
name = "log_level"
|
||||
type = "select"
|
||||
prompt = "Log level"
|
||||
default = "info"
|
||||
options = [
|
||||
{ value = "trace", label = "Trace (verbose)" },
|
||||
{ value = "debug", label = "Debug" },
|
||||
{ value = "info", label = "Info" },
|
||||
{ value = "warn", label = "Warning" },
|
||||
{ value = "error", label = "Error" }
|
||||
]
|
||||
|
||||
[[elements]]
|
||||
name = "debug_mode"
|
||||
type = "confirm"
|
||||
prompt = "Enable debug mode?"
|
||||
when = "env_mode == development"
|
||||
default = true
|
||||
|
||||
[[elements]]
|
||||
name = "api_key"
|
||||
type = "password"
|
||||
prompt = "API Key"
|
||||
required = true
|
||||
help = "Your application API key"
|
||||
|
||||
[[elements]]
|
||||
name = "database_url"
|
||||
type = "text"
|
||||
prompt = "Database URL"
|
||||
placeholder = "postgresql://localhost:5432/mydb"
|
||||
required = true
|
||||
|
||||
[[elements]]
|
||||
name = "redis_url"
|
||||
type = "text"
|
||||
prompt = "Redis URL"
|
||||
placeholder = "redis://localhost:6379"
|
||||
when = "env_mode == production"
|
||||
|
||||
[[elements]]
|
||||
name = "sentry_dsn"
|
||||
type = "text"
|
||||
prompt = "Sentry DSN (error tracking)"
|
||||
when = "env_mode == production"
|
||||
help = "Leave empty to disable error tracking"
|
||||
@ -189,6 +189,41 @@ cat examples/12-agent-execution/README.md
|
||||
- [Tests](../tests/agent/) - Agent validation tests
|
||||
- [Core Examples](../crates/typedialog-agent/typedialog-ag-core/examples/) - Rust API usage
|
||||
|
||||
### 13. **Conditional Logic** → [`13-conditional-logic/`](13-conditional-logic/)
|
||||
|
||||
Complete guide to all conditional operators and expressions.
|
||||
|
||||
**Operators demonstrated:**
|
||||
|
||||
| Category | Operators | Example |
|
||||
|----------|-----------|---------|
|
||||
| **Comparison** | `==`, `!=`, `>`, `<`, `>=`, `<=` | `port >= 1024` |
|
||||
| **String** | `contains`, `startswith`, `endswith` | `url startswith https` |
|
||||
| **Array** | `in` | `rust in detected_languages` |
|
||||
| **File System** | `file_exists()`, `!file_exists()` | `!file_exists(Dockerfile)` |
|
||||
|
||||
**Features:**
|
||||
|
||||
- Dynamic field visibility based on user input
|
||||
- Conditional fragment loading
|
||||
- Multi-condition scenarios
|
||||
- Type-safe comparisons
|
||||
|
||||
**Running the example:**
|
||||
|
||||
```bash
|
||||
# CLI
|
||||
cargo run --bin typedialog -- examples/13-conditional-logic/conditional-demo.toml
|
||||
|
||||
# TUI
|
||||
cargo run --bin typedialog-tui -- examples/13-conditional-logic/conditional-demo.toml
|
||||
|
||||
# Web
|
||||
cargo run --bin typedialog-web -- examples/13-conditional-logic/conditional-demo.toml
|
||||
```
|
||||
|
||||
**Best for:** Dynamic forms, adaptive UX, configuration wizards
|
||||
|
||||
## Learning Path
|
||||
|
||||
```
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user