mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
Merge pull request #1653 from YunoHost/options-doc
ConfigPanel and app install form Options documentation
This commit is contained in:
commit
1acaf2af2e
4 changed files with 716 additions and 0 deletions
|
@ -13,9 +13,11 @@ generate-helpers-doc:
|
||||||
- cd doc
|
- cd doc
|
||||||
- python3 generate_helper_doc.py
|
- python3 generate_helper_doc.py
|
||||||
- python3 generate_resource_doc.py > resources.md
|
- python3 generate_resource_doc.py > resources.md
|
||||||
|
- python3 generate_configpanel_and_formoptions_doc.py > forms.md
|
||||||
- hub clone https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/doc.git doc_repo
|
- hub clone https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/doc.git doc_repo
|
||||||
- cp helpers.md doc_repo/pages/06.contribute/10.packaging_apps/80.resources/11.helpers/packaging_apps_helpers.md
|
- cp helpers.md doc_repo/pages/06.contribute/10.packaging_apps/80.resources/11.helpers/packaging_apps_helpers.md
|
||||||
- cp resources.md doc_repo/pages/06.contribute/10.packaging_apps/80.resources/15.appresources/packaging_apps_resources.md
|
- cp resources.md doc_repo/pages/06.contribute/10.packaging_apps/80.resources/15.appresources/packaging_apps_resources.md
|
||||||
|
- cp forms doc_repo/pages/06.contribute/15.dev/03.forms/forms.md
|
||||||
- cd doc_repo
|
- cd doc_repo
|
||||||
# replace ${CI_COMMIT_REF_NAME} with ${CI_COMMIT_TAG} ?
|
# replace ${CI_COMMIT_REF_NAME} with ${CI_COMMIT_TAG} ?
|
||||||
- hub checkout -b "${CI_COMMIT_REF_NAME}"
|
- hub checkout -b "${CI_COMMIT_REF_NAME}"
|
||||||
|
|
171
doc/generate_configpanel_and_formoptions_doc.py
Normal file
171
doc/generate_configpanel_and_formoptions_doc.py
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
import sys
|
||||||
|
import ast
|
||||||
|
import datetime
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
version = open("../debian/changelog").readlines()[0].split()[1].strip("()")
|
||||||
|
today = datetime.datetime.now().strftime("%d/%m/%Y")
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_commit():
|
||||||
|
p = subprocess.Popen(
|
||||||
|
"git rev-parse --verify HEAD",
|
||||||
|
shell=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
)
|
||||||
|
stdout, stderr = p.communicate()
|
||||||
|
|
||||||
|
current_commit = stdout.strip().decode("utf-8")
|
||||||
|
return current_commit
|
||||||
|
|
||||||
|
|
||||||
|
current_commit = get_current_commit()
|
||||||
|
|
||||||
|
def print_config_panel_docs():
|
||||||
|
|
||||||
|
fname = "../src/utils/configpanel.py"
|
||||||
|
content = open(fname).read()
|
||||||
|
|
||||||
|
# NB: This magic is because we want to be able to run this script outside of a YunoHost context,
|
||||||
|
# in which we cant really 'import' the file because it will trigger a bunch of moulinette/yunohost imports...
|
||||||
|
tree = ast.parse(content)
|
||||||
|
|
||||||
|
ConfigPanelClasses = reversed(
|
||||||
|
[
|
||||||
|
c
|
||||||
|
for c in tree.body
|
||||||
|
if isinstance(c, ast.ClassDef)
|
||||||
|
and c.name in {"SectionModel", "PanelModel", "ConfigPanelModel"}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("## Configuration panel structure")
|
||||||
|
|
||||||
|
for c in ConfigPanelClasses:
|
||||||
|
doc = ast.get_docstring(c)
|
||||||
|
print("")
|
||||||
|
print(f"### {c.name.replace('Model', '')}")
|
||||||
|
print("")
|
||||||
|
print(doc)
|
||||||
|
print("")
|
||||||
|
print("---")
|
||||||
|
|
||||||
|
|
||||||
|
def print_form_doc():
|
||||||
|
|
||||||
|
fname = "../src/utils/form.py"
|
||||||
|
content = open(fname).read()
|
||||||
|
|
||||||
|
# NB: This magic is because we want to be able to run this script outside of a YunoHost context,
|
||||||
|
# in which we cant really 'import' the file because it will trigger a bunch of moulinette/yunohost imports...
|
||||||
|
tree = ast.parse(content)
|
||||||
|
|
||||||
|
OptionClasses = [
|
||||||
|
c for c in tree.body if isinstance(c, ast.ClassDef) and c.name.endswith("Option")
|
||||||
|
]
|
||||||
|
|
||||||
|
OptionDocString = {}
|
||||||
|
|
||||||
|
print("## List of all option types")
|
||||||
|
|
||||||
|
for c in OptionClasses:
|
||||||
|
if not isinstance(c.body[0], ast.Expr):
|
||||||
|
continue
|
||||||
|
option_type = None
|
||||||
|
|
||||||
|
if c.name in {"BaseOption", "BaseInputOption"}:
|
||||||
|
option_type = c.name
|
||||||
|
elif c.body[1].target.id == "type":
|
||||||
|
option_type = c.body[1].value.attr
|
||||||
|
|
||||||
|
generaltype = c.bases[0].id.replace("Option", "").replace("Base", "").lower() if c.bases else None
|
||||||
|
|
||||||
|
docstring = ast.get_docstring(c)
|
||||||
|
if docstring:
|
||||||
|
if "#### Properties" not in docstring:
|
||||||
|
docstring += """
|
||||||
|
#### Properties
|
||||||
|
|
||||||
|
- [common properties](#common-properties)"""
|
||||||
|
OptionDocString[option_type] = {"doc": docstring, "generaltype": generaltype}
|
||||||
|
|
||||||
|
# Dirty hack to have "BaseOption" as first and "BaseInputOption" as 2nd in list
|
||||||
|
|
||||||
|
base = OptionDocString.pop("BaseOption")
|
||||||
|
baseinput = OptionDocString.pop("BaseInputOption")
|
||||||
|
OptionDocString2 = {
|
||||||
|
"BaseOption": base,
|
||||||
|
"BaseInputOption": baseinput,
|
||||||
|
}
|
||||||
|
OptionDocString2.update(OptionDocString)
|
||||||
|
|
||||||
|
for option_type, infos in OptionDocString2.items():
|
||||||
|
if option_type == "display_text":
|
||||||
|
# display_text is kind of legacy x_x
|
||||||
|
continue
|
||||||
|
print("")
|
||||||
|
if option_type == "BaseOption":
|
||||||
|
print("### Common properties")
|
||||||
|
elif option_type == "BaseInputOption":
|
||||||
|
print("### Common inputs properties")
|
||||||
|
else:
|
||||||
|
print(f"### `{option_type}`" + (f" ({infos['generaltype']})" if infos["generaltype"] else ""))
|
||||||
|
print("")
|
||||||
|
print(infos["doc"])
|
||||||
|
print("")
|
||||||
|
print("---")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"""---
|
||||||
|
title: Technical details for config panel structure and form option types
|
||||||
|
template: docs
|
||||||
|
taxonomy:
|
||||||
|
category: docs
|
||||||
|
routes:
|
||||||
|
default: '/dev/forms'
|
||||||
|
---
|
||||||
|
|
||||||
|
Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{current_commit}/doc/generate_options_doc.py) on {today} (YunoHost version {version})
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
You may encounter some named types which are used for simplicity.
|
||||||
|
|
||||||
|
- `Translation`: a translated property
|
||||||
|
- used for properties: `ask`, `help` and `Pattern.error`
|
||||||
|
- a `dict` with locales as keys and translations as values:
|
||||||
|
```toml
|
||||||
|
ask.en = "The text in english"
|
||||||
|
ask.fr = "Le texte en français"
|
||||||
|
```
|
||||||
|
It is not currently possible for translators to translate those string in weblate.
|
||||||
|
- a single `str` for a single english default string
|
||||||
|
```toml
|
||||||
|
help = "The text in english"
|
||||||
|
```
|
||||||
|
- `JSExpression`: a `str` JS expression to be evaluated to `true` or `false`:
|
||||||
|
- used for properties: `visible` and `enabled`
|
||||||
|
- operators availables: `==`, `!=`, `>`, `>=`, `<`, `<=`, `!`, `&&`, `||`, `+`, `-`, `*`, `/`, `%` and `match()`
|
||||||
|
- `Binding`: bind a value to a file/property/variable/getter/setter/validator
|
||||||
|
- save the value in `settings.yaml` when not defined
|
||||||
|
- nothing at all with `"null"`
|
||||||
|
- a custom getter/setter/validator with `"null"` + a function starting with `get__`, `set__`, `validate__` in `scripts/config`
|
||||||
|
- a variable/property in a file with `:__FINALPATH__/my_file.php`
|
||||||
|
- a whole file with `__FINALPATH__/my_file.php`
|
||||||
|
- `Pattern`: a `dict` with a regex to match the value against and an error message
|
||||||
|
```toml
|
||||||
|
pattern.regexp = '^[A-F]\d\d$'
|
||||||
|
pattern.error = "Provide a room number such as F12: one uppercase and 2 numbers"
|
||||||
|
# or with translated error
|
||||||
|
pattern.error.en = "Provide a room number such as F12: one uppercase and 2 numbers"
|
||||||
|
pattern.error.fr = "Entrez un numéro de salle comme F12: une lettre majuscule et deux chiffres."
|
||||||
|
```
|
||||||
|
- IMPORTANT: your `pattern.regexp` should be between simple quote, not double.
|
||||||
|
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
print_config_panel_docs()
|
||||||
|
print_form_doc()
|
|
@ -84,6 +84,38 @@ class ContainerModel(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class SectionModel(ContainerModel, OptionsModel):
|
class SectionModel(ContainerModel, OptionsModel):
|
||||||
|
"""
|
||||||
|
Sections are, basically, options grouped together. Sections are `dict`s defined inside a Panel and require a unique id (in the below example, the id is `customization` prepended by the panel's id `main`). Keep in mind that this combined id will be used in CLI to refer to the section, so choose something short and meaningfull. Also make sure to not make a typo in the panel id, which would implicitly create an other entire panel.
|
||||||
|
|
||||||
|
If at least one `button` is present it then become an action section.
|
||||||
|
Options in action sections are not considered settings and therefor are not saved, they are more like parameters that exists only during the execution of an action.
|
||||||
|
FIXME i'm not sure we have this in code.
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
```toml
|
||||||
|
[main]
|
||||||
|
|
||||||
|
[main.customization]
|
||||||
|
name.en = "Advanced configuration"
|
||||||
|
name.fr = "Configuration avancée"
|
||||||
|
help = "Every form items in this section are not saved."
|
||||||
|
services = ["__APP__", "nginx"]
|
||||||
|
|
||||||
|
[main.customization.option_id]
|
||||||
|
type = "string"
|
||||||
|
# …refer to Options doc
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Properties
|
||||||
|
- `name` (optional): `Translation` or `str`, displayed as the section's title if any
|
||||||
|
- `help`: `Translation` or `str`, text to display before the first option
|
||||||
|
- `services` (optional): `list` of services names to `reload-or-restart` when any option's value contained in the section changes
|
||||||
|
- `"__APP__` will refer to the app instance name
|
||||||
|
- `optional`: `bool` (default: `true`), set the default `optional` prop of all Options in the section
|
||||||
|
- `visible`: `bool` or `JSExpression` (default: `true`), allow to conditionally display a section depending on user's answers to previous questions.
|
||||||
|
- Be careful that the `visible` property should only refer to **previous** options's value. Hence, it should not make sense to have a `visible` property on the very first section.
|
||||||
|
"""
|
||||||
|
|
||||||
visible: Union[bool, str] = True
|
visible: Union[bool, str] = True
|
||||||
optional: bool = True
|
optional: bool = True
|
||||||
is_action_section: bool = False
|
is_action_section: bool = False
|
||||||
|
@ -138,6 +170,28 @@ class SectionModel(ContainerModel, OptionsModel):
|
||||||
|
|
||||||
|
|
||||||
class PanelModel(ContainerModel):
|
class PanelModel(ContainerModel):
|
||||||
|
"""
|
||||||
|
Panels are, basically, sections grouped together. Panels are `dict`s defined inside a ConfigPanel file and require a unique id (in the below example, the id is `main`). Keep in mind that this id will be used in CLI to refer to the panel, so choose something short and meaningfull.
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
```toml
|
||||||
|
[main]
|
||||||
|
name.en = "Main configuration"
|
||||||
|
name.fr = "Configuration principale"
|
||||||
|
help = ""
|
||||||
|
services = ["__APP__", "nginx"]
|
||||||
|
|
||||||
|
[main.customization]
|
||||||
|
# …refer to Sections doc
|
||||||
|
```
|
||||||
|
#### Properties
|
||||||
|
- `name`: `Translation` or `str`, displayed as the panel title
|
||||||
|
- `help` (optional): `Translation` or `str`, text to display before the first section
|
||||||
|
- `services` (optional): `list` of services names to `reload-or-restart` when any option's value contained in the panel changes
|
||||||
|
- `"__APP__` will refer to the app instance name
|
||||||
|
- `actions`: FIXME not sure what this does
|
||||||
|
"""
|
||||||
|
|
||||||
# FIXME what to do with `actions?
|
# FIXME what to do with `actions?
|
||||||
actions: dict[str, Translation] = {"apply": {"en": "Apply"}}
|
actions: dict[str, Translation] = {"apply": {"en": "Apply"}}
|
||||||
sections: list[SectionModel]
|
sections: list[SectionModel]
|
||||||
|
@ -177,6 +231,26 @@ class PanelModel(ContainerModel):
|
||||||
|
|
||||||
|
|
||||||
class ConfigPanelModel(BaseModel):
|
class ConfigPanelModel(BaseModel):
|
||||||
|
"""
|
||||||
|
This is the 'root' level of the config panel toml file
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
```toml
|
||||||
|
version = 1.0
|
||||||
|
|
||||||
|
[config]
|
||||||
|
# …refer to Panels doc
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Properties
|
||||||
|
|
||||||
|
- `version`: `float` (default: `1.0`), version that the config panel supports in terms of features.
|
||||||
|
- `i18n` (optional): `str`, an i18n property that let you internationalize options text.
|
||||||
|
- However this feature is only available in core configuration panel (like `yunohost domain config`), prefer the use `Translation` in `name`, `help`, etc.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
version: float = CONFIG_PANEL_VERSION_SUPPORTED
|
version: float = CONFIG_PANEL_VERSION_SUPPORTED
|
||||||
i18n: Union[str, None] = None
|
i18n: Union[str, None] = None
|
||||||
panels: list[PanelModel]
|
panels: list[PanelModel]
|
||||||
|
|
|
@ -304,6 +304,59 @@ class Pattern(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class BaseOption(BaseModel):
|
class BaseOption(BaseModel):
|
||||||
|
"""
|
||||||
|
Options are fields declaration that renders as form items, button, alert or text in the web-admin and printed or prompted in CLI.
|
||||||
|
They are used in app manifests to declare the before installation form and in config panels.
|
||||||
|
|
||||||
|
[Have a look at the app config panel doc](/packaging_apps_config_panels) for details about Panels and Sections.
|
||||||
|
|
||||||
|
! IMPORTANT: as for Panels and Sections you have to choose an id, but this one should be unique in all this document, even if the question is in an other panel.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "string"
|
||||||
|
# ask as `str`
|
||||||
|
ask = "The text in english"
|
||||||
|
# ask as `dict`
|
||||||
|
ask.en = "The text in english"
|
||||||
|
ask.fr = "Le texte en français"
|
||||||
|
# advanced props
|
||||||
|
visible = "my_other_option_id != 'success'"
|
||||||
|
readonly = true
|
||||||
|
# much advanced: config panel only?
|
||||||
|
bind = "null"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Properties
|
||||||
|
|
||||||
|
- `type`: the actual type of the option, such as 'markdown', 'password', 'number', 'email', ...
|
||||||
|
- `ask`: `Translation` (default to the option's `id` if not defined):
|
||||||
|
- text to display as the option's label for inputs or text to display for readonly options
|
||||||
|
- in config panels, questions are displayed on the left side and therefore have not much space to be rendered. Therefore, it is better to use a short question, and use the `help` property to provide additional details if necessary.
|
||||||
|
- `visible` (optional): `bool` or `JSExpression` (default: `true`)
|
||||||
|
- define if the option is diplayed/asked
|
||||||
|
- if `false` and used alongside `readonly = true`, you get a context only value that can still be used in `JSExpression`s
|
||||||
|
- `readonly` (optional): `bool` (default: `false`, forced to `true` for readonly types):
|
||||||
|
- If `true` for input types: forbid mutation of its value
|
||||||
|
- `bind` (optional): `Binding`, config panels only! A powerful feature that let you configure how and where the setting will be read, validated and written
|
||||||
|
- if not specified, the value will be read/written in the app `settings.yml`
|
||||||
|
- if `"null"`:
|
||||||
|
- the value will not be stored at all (can still be used in context evaluations)
|
||||||
|
- if in `scripts/config` there's a function named:
|
||||||
|
- `get__my_option_id`: the value will be gathered from this custom getter
|
||||||
|
- `set__my_option_id`: the value will be passed to this custom setter where you can do whatever you want with the value
|
||||||
|
- `validate__my_option_id`: the value will be passed to this custom validator before any custom setter
|
||||||
|
- if `bind` is a file path:
|
||||||
|
- if the path starts with `:`, the value be saved as its id's variable/property counterpart
|
||||||
|
- this only works for first level variables/properties and simple types (no array)
|
||||||
|
- else the value will be stored as the whole content of the file
|
||||||
|
- you can use `__FINALPATH__` or `__INSTALL_DIR__` in your path to point to dynamic install paths
|
||||||
|
- FIXME are other global variables accessible?
|
||||||
|
- [refer to `bind` doc for explaination and examples](#read-and-write-values-the)
|
||||||
|
"""
|
||||||
|
|
||||||
type: OptionType
|
type: OptionType
|
||||||
id: str
|
id: str
|
||||||
ask: Union[Translation, None]
|
ask: Union[Translation, None]
|
||||||
|
@ -375,10 +428,34 @@ class BaseReadonlyOption(BaseOption):
|
||||||
|
|
||||||
|
|
||||||
class DisplayTextOption(BaseReadonlyOption):
|
class DisplayTextOption(BaseReadonlyOption):
|
||||||
|
"""
|
||||||
|
Display simple multi-line content.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "display_text"
|
||||||
|
ask = "Simple text rendered as is."
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.display_text] = OptionType.display_text
|
type: Literal[OptionType.display_text] = OptionType.display_text
|
||||||
|
|
||||||
|
|
||||||
class MarkdownOption(BaseReadonlyOption):
|
class MarkdownOption(BaseReadonlyOption):
|
||||||
|
"""
|
||||||
|
Display markdown multi-line content.
|
||||||
|
Markdown is currently only rendered in the web-admin
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "display_text"
|
||||||
|
ask = "Text **rendered** in markdown."
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.markdown] = OptionType.markdown
|
type: Literal[OptionType.markdown] = OptionType.markdown
|
||||||
|
|
||||||
|
|
||||||
|
@ -390,6 +467,27 @@ class State(str, Enum):
|
||||||
|
|
||||||
|
|
||||||
class AlertOption(BaseReadonlyOption):
|
class AlertOption(BaseReadonlyOption):
|
||||||
|
"""
|
||||||
|
Alerts displays a important message with a level of severity.
|
||||||
|
You can use markdown in `ask` but will only be rendered in the web-admin.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "alert"
|
||||||
|
ask = "The configuration seems to be manually modified..."
|
||||||
|
style = "warning"
|
||||||
|
icon = "warning"
|
||||||
|
```
|
||||||
|
#### Properties
|
||||||
|
|
||||||
|
- [common properties](#common-properties)
|
||||||
|
- `style`: any of `"success|info|warning|danger"` (default: `"info"`)
|
||||||
|
- `icon` (optional): any icon name from [Fork Awesome](https://forkaweso.me/Fork-Awesome/icons/)
|
||||||
|
- Currently only displayed in the web-admin
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.alert] = OptionType.alert
|
type: Literal[OptionType.alert] = OptionType.alert
|
||||||
style: State = State.info
|
style: State = State.info
|
||||||
icon: Union[str, None] = None
|
icon: Union[str, None] = None
|
||||||
|
@ -406,6 +504,45 @@ class AlertOption(BaseReadonlyOption):
|
||||||
|
|
||||||
|
|
||||||
class ButtonOption(BaseReadonlyOption):
|
class ButtonOption(BaseReadonlyOption):
|
||||||
|
"""
|
||||||
|
Triggers actions.
|
||||||
|
Available only in config panels.
|
||||||
|
Renders as a `button` in the web-admin and can be called with `yunohost [app|domain|settings] action run <action_id>` in CLI.
|
||||||
|
|
||||||
|
Every options defined in an action section (a config panel section with at least one `button`) is guaranted to be shown/asked to the user and available in `scripts/config`'s scope.
|
||||||
|
[check examples in advanced use cases](#actions).
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "button"
|
||||||
|
ask = "Break the system"
|
||||||
|
style = "danger"
|
||||||
|
icon = "bug"
|
||||||
|
# enabled only if another option's value (a `boolean` for example) is positive
|
||||||
|
enabled = "aknowledged"
|
||||||
|
```
|
||||||
|
|
||||||
|
To be able to trigger an action we have to add a bash function starting with `run__` in your `scripts/config`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
run__my_action_id() {
|
||||||
|
ynh_print_info "Running 'my_action_id' action"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Properties
|
||||||
|
|
||||||
|
- [common properties](#common-properties)
|
||||||
|
- `bind`: forced to `"null"`
|
||||||
|
- `style`: any of `"success|info|warning|danger"` (default: `"success"`)
|
||||||
|
- `enabled`: `JSExpression` or `bool` (default: `true`)
|
||||||
|
- when used with `JSExpression` you can enable/disable the button depending on context
|
||||||
|
- `icon` (optional): any icon name from [Fork Awesome](https://forkaweso.me/Fork-Awesome/icons/)
|
||||||
|
- Currently only displayed in the web-admin
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.button] = OptionType.button
|
type: Literal[OptionType.button] = OptionType.button
|
||||||
bind: Literal["null"] = "null"
|
bind: Literal["null"] = "null"
|
||||||
help: Union[Translation, None] = None
|
help: Union[Translation, None] = None
|
||||||
|
@ -426,6 +563,35 @@ class ButtonOption(BaseReadonlyOption):
|
||||||
|
|
||||||
|
|
||||||
class BaseInputOption(BaseOption):
|
class BaseInputOption(BaseOption):
|
||||||
|
"""
|
||||||
|
Rest of the option types available are considered `inputs`.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "string"
|
||||||
|
# …any common props… +
|
||||||
|
optional = false
|
||||||
|
redact = false
|
||||||
|
default = "some default string"
|
||||||
|
help = "You can enter almost anything!"
|
||||||
|
example = "an example string"
|
||||||
|
placeholder = "write something…"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Properties
|
||||||
|
|
||||||
|
- [common properties](#common-properties)
|
||||||
|
- `optional`: `bool` (default: `false`, but `true` in config panels)
|
||||||
|
- `redact`: `bool` (default: `false`), to redact the value in the logs when the value contain private information
|
||||||
|
- `default`: depends on `type`, the default value to assign to the option
|
||||||
|
- in case of readonly values, you can use this `default` to assign a value (or return a dynamic `default` from a custom getter)
|
||||||
|
- `help` (optional): `Translation`, to display a short help message in cli and web-admin
|
||||||
|
- `example` (optional): `str`, to display an example value in web-admin only
|
||||||
|
- `placeholder` (optional): `str`, shown in the web-admin fields only
|
||||||
|
"""
|
||||||
|
|
||||||
help: Union[Translation, None] = None
|
help: Union[Translation, None] = None
|
||||||
example: Union[str, None] = None
|
example: Union[str, None] = None
|
||||||
placeholder: Union[str, None] = None
|
placeholder: Union[str, None] = None
|
||||||
|
@ -574,10 +740,44 @@ class BaseStringOption(BaseInputOption):
|
||||||
|
|
||||||
|
|
||||||
class StringOption(BaseStringOption):
|
class StringOption(BaseStringOption):
|
||||||
|
"""
|
||||||
|
Ask for a simple string.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "string"
|
||||||
|
default = "E10"
|
||||||
|
pattern.regexp = '^[A-F]\d\d$'
|
||||||
|
pattern.error = "Provide a room like F12 : one uppercase and 2 numbers"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Properties
|
||||||
|
- [common inputs properties](#common-inputs-properties)
|
||||||
|
- `default`: `""`
|
||||||
|
- `pattern` (optional): `Pattern`, a regex to match the value against
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.string] = OptionType.string
|
type: Literal[OptionType.string] = OptionType.string
|
||||||
|
|
||||||
|
|
||||||
class TextOption(BaseStringOption):
|
class TextOption(BaseStringOption):
|
||||||
|
"""
|
||||||
|
Ask for a multiline string.
|
||||||
|
Renders as a `textarea` in the web-admin and by opening a text editor on the CLI.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "text"
|
||||||
|
default = "multi\\nline\\ncontent"
|
||||||
|
```
|
||||||
|
#### Properties
|
||||||
|
- [common inputs properties](#common-inputs-properties)
|
||||||
|
- `default`: `""`
|
||||||
|
- `pattern` (optional): `Pattern`, a regex to match the value against
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.text] = OptionType.text
|
type: Literal[OptionType.text] = OptionType.text
|
||||||
|
|
||||||
|
|
||||||
|
@ -585,6 +785,22 @@ FORBIDDEN_PASSWORD_CHARS = r"{}"
|
||||||
|
|
||||||
|
|
||||||
class PasswordOption(BaseInputOption):
|
class PasswordOption(BaseInputOption):
|
||||||
|
"""
|
||||||
|
Ask for a password.
|
||||||
|
The password is tested as a regular user password (at least 8 chars)
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "password"
|
||||||
|
```
|
||||||
|
#### Properties
|
||||||
|
- [common inputs properties](#common-inputs-properties)
|
||||||
|
- `default`: forced to `""`
|
||||||
|
- `redact`: forced to `true`
|
||||||
|
- `example`: forbidden
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.password] = OptionType.password
|
type: Literal[OptionType.password] = OptionType.password
|
||||||
example: Literal[None] = None
|
example: Literal[None] = None
|
||||||
default: Literal[None] = None
|
default: Literal[None] = None
|
||||||
|
@ -621,6 +837,21 @@ class PasswordOption(BaseInputOption):
|
||||||
|
|
||||||
|
|
||||||
class ColorOption(BaseInputOption):
|
class ColorOption(BaseInputOption):
|
||||||
|
"""
|
||||||
|
Ask for a color represented as a hex value (with possibly an alpha channel).
|
||||||
|
Renders as color picker in the web-admin and as a prompt that accept named color like `yellow` in CLI.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "color"
|
||||||
|
default = "#ff0"
|
||||||
|
```
|
||||||
|
#### Properties
|
||||||
|
- [common inputs properties](#common-inputs-properties)
|
||||||
|
- `default`: `""`
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.color] = OptionType.color
|
type: Literal[OptionType.color] = OptionType.color
|
||||||
default: Union[str, None]
|
default: Union[str, None]
|
||||||
_annotation = Color
|
_annotation = Color
|
||||||
|
@ -653,6 +884,26 @@ class ColorOption(BaseInputOption):
|
||||||
|
|
||||||
|
|
||||||
class NumberOption(BaseInputOption):
|
class NumberOption(BaseInputOption):
|
||||||
|
"""
|
||||||
|
Ask for a number (an integer).
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "number"
|
||||||
|
default = 100
|
||||||
|
min = 50
|
||||||
|
max = 200
|
||||||
|
step = 5
|
||||||
|
```
|
||||||
|
#### Properties
|
||||||
|
- [common inputs properties](#common-inputs-properties)
|
||||||
|
- `type`: `number` or `range` (input or slider in the web-admin)
|
||||||
|
- `min` (optional): minimal int value inclusive
|
||||||
|
- `max` (optional): maximal int value inclusive
|
||||||
|
- `step` (optional): currently only used in the webadmin as the `<input/>` step jump
|
||||||
|
"""
|
||||||
|
|
||||||
# `number` and `range` are exactly the same, but `range` does render as a slider in web-admin
|
# `number` and `range` are exactly the same, but `range` does render as a slider in web-admin
|
||||||
type: Literal[OptionType.number, OptionType.range] = OptionType.number
|
type: Literal[OptionType.number, OptionType.range] = OptionType.number
|
||||||
default: Union[int, None]
|
default: Union[int, None]
|
||||||
|
@ -707,6 +958,27 @@ class NumberOption(BaseInputOption):
|
||||||
|
|
||||||
|
|
||||||
class BooleanOption(BaseInputOption):
|
class BooleanOption(BaseInputOption):
|
||||||
|
"""
|
||||||
|
Ask for a boolean.
|
||||||
|
Renders as a switch in the web-admin and a yes/no prompt in CLI.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "boolean"
|
||||||
|
default = 1
|
||||||
|
yes = "agree"
|
||||||
|
no = "disagree"
|
||||||
|
```
|
||||||
|
#### Properties
|
||||||
|
- [common inputs properties](#common-inputs-properties)
|
||||||
|
- `default`: `0`
|
||||||
|
- `yes` (optional): (default: `1`) define as what the thruthy value should output
|
||||||
|
- can be `true`, `True`, `"yes"`, etc.
|
||||||
|
- `no` (optional): (default: `0`) define as what the thruthy value should output
|
||||||
|
- can be `0`, `"false"`, `"n"`, etc.
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.boolean] = OptionType.boolean
|
type: Literal[OptionType.boolean] = OptionType.boolean
|
||||||
yes: Any = 1
|
yes: Any = 1
|
||||||
no: Any = 0
|
no: Any = 0
|
||||||
|
@ -812,6 +1084,23 @@ class BooleanOption(BaseInputOption):
|
||||||
|
|
||||||
|
|
||||||
class DateOption(BaseInputOption):
|
class DateOption(BaseInputOption):
|
||||||
|
"""
|
||||||
|
Ask for a date in the form `"2025-06-14"`.
|
||||||
|
Renders as a date-picker in the web-admin and a regular prompt in CLI.
|
||||||
|
|
||||||
|
Can also take a timestamp as value that will output as an ISO date string.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "date"
|
||||||
|
default = "2070-12-31"
|
||||||
|
```
|
||||||
|
#### Properties
|
||||||
|
- [common inputs properties](#common-inputs-properties)
|
||||||
|
- `default`: `""`
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.date] = OptionType.date
|
type: Literal[OptionType.date] = OptionType.date
|
||||||
default: Union[str, None]
|
default: Union[str, None]
|
||||||
_annotation = datetime.date
|
_annotation = datetime.date
|
||||||
|
@ -827,6 +1116,21 @@ class DateOption(BaseInputOption):
|
||||||
|
|
||||||
|
|
||||||
class TimeOption(BaseInputOption):
|
class TimeOption(BaseInputOption):
|
||||||
|
"""
|
||||||
|
Ask for an hour in the form `"22:35"`.
|
||||||
|
Renders as a date-picker in the web-admin and a regular prompt in CLI.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "time"
|
||||||
|
default = "12:26"
|
||||||
|
```
|
||||||
|
#### Properties
|
||||||
|
- [common inputs properties](#common-inputs-properties)
|
||||||
|
- `default`: `""`
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.time] = OptionType.time
|
type: Literal[OptionType.time] = OptionType.time
|
||||||
default: Union[str, int, None]
|
default: Union[str, int, None]
|
||||||
_annotation = datetime.time
|
_annotation = datetime.time
|
||||||
|
@ -846,12 +1150,41 @@ class TimeOption(BaseInputOption):
|
||||||
|
|
||||||
|
|
||||||
class EmailOption(BaseInputOption):
|
class EmailOption(BaseInputOption):
|
||||||
|
"""
|
||||||
|
Ask for an email. Validation made with [python-email-validator](https://github.com/JoshData/python-email-validator)
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "email"
|
||||||
|
default = "Abc.123@test-example.com"
|
||||||
|
```
|
||||||
|
#### Properties
|
||||||
|
- [common inputs properties](#common-inputs-properties)
|
||||||
|
- `default`: `""`
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.email] = OptionType.email
|
type: Literal[OptionType.email] = OptionType.email
|
||||||
default: Union[EmailStr, None]
|
default: Union[EmailStr, None]
|
||||||
_annotation = EmailStr
|
_annotation = EmailStr
|
||||||
|
|
||||||
|
|
||||||
class WebPathOption(BaseStringOption):
|
class WebPathOption(BaseStringOption):
|
||||||
|
"""
|
||||||
|
Ask for an web path (the part of an url after the domain). Used by default in app install to define from where the app will be accessible.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "path"
|
||||||
|
default = "/"
|
||||||
|
```
|
||||||
|
#### Properties
|
||||||
|
- [common inputs properties](#common-inputs-properties)
|
||||||
|
- `default`: `""`
|
||||||
|
- `pattern` (optional): `Pattern`, a regex to match the value against
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.path] = OptionType.path
|
type: Literal[OptionType.path] = OptionType.path
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -885,6 +1218,21 @@ class WebPathOption(BaseStringOption):
|
||||||
|
|
||||||
|
|
||||||
class URLOption(BaseStringOption):
|
class URLOption(BaseStringOption):
|
||||||
|
"""
|
||||||
|
Ask for any url.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "url"
|
||||||
|
default = "https://example.xn--zfr164b/@handle/"
|
||||||
|
```
|
||||||
|
#### Properties
|
||||||
|
- [common inputs properties](#common-inputs-properties)
|
||||||
|
- `default`: `""`
|
||||||
|
- `pattern` (optional): `Pattern`, a regex to match the value against
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.url] = OptionType.url
|
type: Literal[OptionType.url] = OptionType.url
|
||||||
_annotation = HttpUrl
|
_annotation = HttpUrl
|
||||||
|
|
||||||
|
@ -893,6 +1241,25 @@ class URLOption(BaseStringOption):
|
||||||
|
|
||||||
|
|
||||||
class FileOption(BaseInputOption):
|
class FileOption(BaseInputOption):
|
||||||
|
"""
|
||||||
|
Ask for file.
|
||||||
|
Renders a file prompt in the web-admin and ask for a path in CLI.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "file"
|
||||||
|
accept = ".json"
|
||||||
|
# bind the file to a location to save the file there
|
||||||
|
bind = "/tmp/my_file.json"
|
||||||
|
```
|
||||||
|
#### Properties
|
||||||
|
- [common inputs properties](#common-inputs-properties)
|
||||||
|
- `default`: `""`
|
||||||
|
- `accept`: a comma separated list of extension to accept like `".conf, .ini`
|
||||||
|
- /!\ currently only work on the web-admin
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.file] = OptionType.file
|
type: Literal[OptionType.file] = OptionType.file
|
||||||
# `FilePath` for CLI (path must exists and must be a file)
|
# `FilePath` for CLI (path must exists and must be a file)
|
||||||
# `bytes` for API (a base64 encoded file actually)
|
# `bytes` for API (a base64 encoded file actually)
|
||||||
|
@ -1002,6 +1369,24 @@ class BaseChoicesOption(BaseInputOption):
|
||||||
|
|
||||||
|
|
||||||
class SelectOption(BaseChoicesOption):
|
class SelectOption(BaseChoicesOption):
|
||||||
|
"""
|
||||||
|
Ask for value from a limited set of values.
|
||||||
|
Renders as a regular `<select/>` in the web-admin and as a regular prompt in CLI with autocompletion of `choices`.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "select"
|
||||||
|
choices = ["one", "two", "three"]
|
||||||
|
choices = "one,two,three"
|
||||||
|
default = "two"
|
||||||
|
```
|
||||||
|
#### Properties
|
||||||
|
- [common inputs properties](#common-inputs-properties)
|
||||||
|
- `default`: `""`, obviously the default has to be empty or an available `choices` item.
|
||||||
|
- `choices`: a (coma separated) list of values
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.select] = OptionType.select
|
type: Literal[OptionType.select] = OptionType.select
|
||||||
filter: Literal[None] = None
|
filter: Literal[None] = None
|
||||||
choices: Union[dict[str, Any], list[Any]]
|
choices: Union[dict[str, Any], list[Any]]
|
||||||
|
@ -1010,6 +1395,31 @@ class SelectOption(BaseChoicesOption):
|
||||||
|
|
||||||
|
|
||||||
class TagsOption(BaseChoicesOption):
|
class TagsOption(BaseChoicesOption):
|
||||||
|
"""
|
||||||
|
Ask for series of values. Optionally from a limited set of values.
|
||||||
|
Renders as a multi select in the web-admin and as a regular prompt in CLI without autocompletion of `choices`.
|
||||||
|
|
||||||
|
This output as a coma separated list of strings `"one,two,three"`
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "tags"
|
||||||
|
default = "word,another word"
|
||||||
|
|
||||||
|
[my_other_option_id]
|
||||||
|
type = "tags"
|
||||||
|
choices = ["one", "two", "three"]
|
||||||
|
# choices = "one,two,three"
|
||||||
|
default = "two,three"
|
||||||
|
```
|
||||||
|
#### Properties
|
||||||
|
- [common inputs properties](#common-inputs-properties)
|
||||||
|
- `default`: `""`, obviously the default has to be empty or an available `choices` item.
|
||||||
|
- `pattern` (optional): `Pattern`, a regex to match all the values against
|
||||||
|
- `choices` (optional): a (coma separated) list of values
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.tags] = OptionType.tags
|
type: Literal[OptionType.tags] = OptionType.tags
|
||||||
filter: Literal[None] = None
|
filter: Literal[None] = None
|
||||||
choices: Union[list[str], None] = None
|
choices: Union[list[str], None] = None
|
||||||
|
@ -1092,6 +1502,20 @@ class TagsOption(BaseChoicesOption):
|
||||||
|
|
||||||
|
|
||||||
class DomainOption(BaseChoicesOption):
|
class DomainOption(BaseChoicesOption):
|
||||||
|
"""
|
||||||
|
Ask for a user domain.
|
||||||
|
Renders as a select in the web-admin and as a regular prompt in CLI with autocompletion of registered domains.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "domain"
|
||||||
|
```
|
||||||
|
#### Properties
|
||||||
|
- [common inputs properties](#common-inputs-properties)
|
||||||
|
- `default`: forced to the instance main domain
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.domain] = OptionType.domain
|
type: Literal[OptionType.domain] = OptionType.domain
|
||||||
filter: Literal[None] = None
|
filter: Literal[None] = None
|
||||||
choices: Union[dict[str, str], None]
|
choices: Union[dict[str, str], None]
|
||||||
|
@ -1132,6 +1556,22 @@ class DomainOption(BaseChoicesOption):
|
||||||
|
|
||||||
|
|
||||||
class AppOption(BaseChoicesOption):
|
class AppOption(BaseChoicesOption):
|
||||||
|
"""
|
||||||
|
Ask for a user app.
|
||||||
|
Renders as a select in the web-admin and as a regular prompt in CLI with autocompletion of installed apps.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "app"
|
||||||
|
filter = "is_webapp"
|
||||||
|
```
|
||||||
|
#### Properties
|
||||||
|
- [common inputs properties](#common-inputs-properties)
|
||||||
|
- `default`: `""`
|
||||||
|
- `filter` (optional): `JSExpression` with what `yunohost app info <app_id> --full` returns as context (only first level keys)
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.app] = OptionType.app
|
type: Literal[OptionType.app] = OptionType.app
|
||||||
filter: Union[JSExpression, None] = None
|
filter: Union[JSExpression, None] = None
|
||||||
add_yunohost_portal_to_choices: bool = False
|
add_yunohost_portal_to_choices: bool = False
|
||||||
|
@ -1169,6 +1609,20 @@ class AppOption(BaseChoicesOption):
|
||||||
|
|
||||||
|
|
||||||
class UserOption(BaseChoicesOption):
|
class UserOption(BaseChoicesOption):
|
||||||
|
"""
|
||||||
|
Ask for a user.
|
||||||
|
Renders as a select in the web-admin and as a regular prompt in CLI with autocompletion of available usernames.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "user"
|
||||||
|
```
|
||||||
|
#### Properties
|
||||||
|
- [common inputs properties](#common-inputs-properties)
|
||||||
|
- `default`: the first admin user found
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.user] = OptionType.user
|
type: Literal[OptionType.user] = OptionType.user
|
||||||
filter: Literal[None] = None
|
filter: Literal[None] = None
|
||||||
choices: Union[dict[str, str], None]
|
choices: Union[dict[str, str], None]
|
||||||
|
@ -1215,6 +1669,21 @@ class UserOption(BaseChoicesOption):
|
||||||
|
|
||||||
|
|
||||||
class GroupOption(BaseChoicesOption):
|
class GroupOption(BaseChoicesOption):
|
||||||
|
"""
|
||||||
|
Ask for a group.
|
||||||
|
Renders as a select in the web-admin and as a regular prompt in CLI with autocompletion of available groups.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```toml
|
||||||
|
[section.my_option_id]
|
||||||
|
type = "group"
|
||||||
|
default = "visitors"
|
||||||
|
```
|
||||||
|
#### Properties
|
||||||
|
- [common inputs properties](#common-inputs-properties)
|
||||||
|
- `default`: `"all_users"`, `"visitors"` or `"admins"` (default: `"all_users"`)
|
||||||
|
"""
|
||||||
|
|
||||||
type: Literal[OptionType.group] = OptionType.group
|
type: Literal[OptionType.group] = OptionType.group
|
||||||
filter: Literal[None] = None
|
filter: Literal[None] = None
|
||||||
choices: Union[dict[str, str], None]
|
choices: Union[dict[str, str], None]
|
||||||
|
|
Loading…
Add table
Reference in a new issue