diff --git a/authentik/policies/password/tests/test_flows.py b/authentik/policies/password/tests/test_flows.py index bdf661b46..8f2e7998e 100644 --- a/authentik/policies/password/tests/test_flows.py +++ b/authentik/policies/password/tests/test_flows.py @@ -59,6 +59,7 @@ class TestPasswordPolicyFlow(FlowTestCase): "label": "PASSWORD_LABEL", "order": 0, "placeholder": "PASSWORD_PLACEHOLDER", + "initial_value": "", "required": True, "type": "password", "sub_text": "", diff --git a/authentik/stages/prompt/api.py b/authentik/stages/prompt/api.py index 907754496..255c97b4b 100644 --- a/authentik/stages/prompt/api.py +++ b/authentik/stages/prompt/api.py @@ -57,10 +57,12 @@ class PromptSerializer(ModelSerializer): "type", "required", "placeholder", + "initial_value", "order", "promptstage_set", "sub_text", "placeholder_expression", + "initial_value_expression", ] diff --git a/authentik/stages/prompt/migrations/0011_prompt_initial_value_prompt_initial_value_expression_and_more.py b/authentik/stages/prompt/migrations/0011_prompt_initial_value_prompt_initial_value_expression_and_more.py new file mode 100644 index 000000000..c59c7894e --- /dev/null +++ b/authentik/stages/prompt/migrations/0011_prompt_initial_value_prompt_initial_value_expression_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 4.1.7 on 2023-03-24 17:32 + +from django.apps.registry import Apps +from django.db import migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor + + +def migrate_placeholder_expressions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): + from authentik.stages.prompt.models import CHOICE_FIELDS + + db_alias = schema_editor.connection.alias + Prompt = apps.get_model("authentik_stages_prompt", "prompt") + + for prompt in Prompt.objects.using(db_alias).all(): + if not prompt.placeholder_expression or prompt.type in CHOICE_FIELDS: + continue + + prompt.initial_value = prompt.placeholder + prompt.initial_value_expression = True + prompt.placeholder = "" + prompt.placeholder_expression = False + prompt.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("authentik_stages_prompt", "0010_alter_prompt_placeholder_alter_prompt_type"), + ] + + operations = [ + migrations.AddField( + model_name="prompt", + name="initial_value", + field=models.TextField( + blank=True, + help_text="Optionally pre-fill the input with an initial value. When creating a fixed choice field, enable interpreting as expression and return a list to return multiple default choices.", + ), + ), + migrations.AddField( + model_name="prompt", + name="initial_value_expression", + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name="prompt", + name="placeholder", + field=models.TextField( + blank=True, + help_text="Optionally provide a short hint that describes the expected input value. When creating a fixed choice field, enable interpreting as expression and return a list to return multiple choices.", + ), + ), + migrations.RunPython(code=migrate_placeholder_expressions), + ] diff --git a/authentik/stages/prompt/models.py b/authentik/stages/prompt/models.py index 3487f3285..3005c157d 100644 --- a/authentik/stages/prompt/models.py +++ b/authentik/stages/prompt/models.py @@ -29,6 +29,8 @@ from authentik.flows.models import Stage from authentik.lib.models import SerializerModel from authentik.policies.models import Policy +CHOICES_CONTEXT_SUFFIX = "__choices" + LOGGER = get_logger() @@ -119,15 +121,25 @@ class Prompt(SerializerModel): placeholder = models.TextField( blank=True, help_text=_( - "When creating a Radio Button Group or Dropdown, enable interpreting as " + "Optionally provide a short hint that describes the expected input value. " + "When creating a fixed choice field, enable interpreting as " "expression and return a list to return multiple choices." ), ) + initial_value = models.TextField( + blank=True, + help_text=_( + "Optionally pre-fill the input with an initial value. " + "When creating a fixed choice field, enable interpreting as " + "expression and return a list to return multiple default choices." + ), + ) sub_text = models.TextField(blank=True, default="") order = models.IntegerField(default=0) placeholder_expression = models.BooleanField(default=False) + initial_value_expression = models.BooleanField(default=False) @property def serializer(self) -> Type[BaseSerializer]: @@ -148,8 +160,8 @@ class Prompt(SerializerModel): raw_choices = self.placeholder - if self.field_key in prompt_context: - raw_choices = prompt_context[self.field_key] + if self.field_key + CHOICES_CONTEXT_SUFFIX in prompt_context: + raw_choices = prompt_context[self.field_key + CHOICES_CONTEXT_SUFFIX] elif self.placeholder_expression: evaluator = PropertyMappingEvaluator( self, user, request, prompt_context=prompt_context, dry_run=dry_run @@ -184,16 +196,9 @@ class Prompt(SerializerModel): ) -> str: """Get fully interpolated placeholder""" if self.type in CHOICE_FIELDS: - # Make sure to return a valid choice as placeholder - choices = self.get_choices(prompt_context, user, request, dry_run=dry_run) - if not choices: - return "" - return choices[0] - - if self.field_key in prompt_context: - # We don't want to parse this as an expression since a user will - # be able to control the input - return prompt_context[self.field_key] + # Choice fields use the placeholder to define all valid choices. + # Therefore their actual placeholder is always blank + return "" if self.placeholder_expression: evaluator = PropertyMappingEvaluator( @@ -211,6 +216,47 @@ class Prompt(SerializerModel): raise wrapped from exc return self.placeholder + def get_initial_value( + self, + prompt_context: dict, + user: User, + request: HttpRequest, + dry_run: Optional[bool] = False, + ) -> str: + """Get fully interpolated initial value""" + + if self.field_key in prompt_context: + # We don't want to parse this as an expression since a user will + # be able to control the input + value = prompt_context[self.field_key] + elif self.initial_value_expression: + evaluator = PropertyMappingEvaluator( + self, user, request, prompt_context=prompt_context, dry_run=dry_run + ) + try: + value = evaluator.evaluate(self.initial_value) + except Exception as exc: # pylint:disable=broad-except + wrapped = PropertyMappingExpressionException(str(exc)) + LOGGER.warning( + "failed to evaluate prompt initial value", + exc=wrapped, + ) + if dry_run: + raise wrapped from exc + value = self.initial_value + else: + value = self.initial_value + + if self.type in CHOICE_FIELDS: + # Ensure returned value is a valid choice + choices = self.get_choices(prompt_context, user, request) + if not choices: + return "" + if value not in choices: + return choices[0] + + return value + def field(self, default: Optional[Any], choices: Optional[list[Any]] = None) -> CharField: """Get field type for Challenge and response. Choices are only valid for CHOICE_FIELDS.""" field_class = CharField diff --git a/authentik/stages/prompt/stage.py b/authentik/stages/prompt/stage.py index 343fb7612..1e3c78014 100644 --- a/authentik/stages/prompt/stage.py +++ b/authentik/stages/prompt/stage.py @@ -38,6 +38,7 @@ class StagePromptSerializer(PassiveSerializer): type = ChoiceField(choices=FieldTypes.choices) required = BooleanField() placeholder = CharField(allow_blank=True) + initial_value = CharField(allow_blank=True) order = IntegerField() sub_text = CharField(allow_blank=True) choices = ListField(child=CharField(allow_blank=True), allow_empty=True, allow_null=True) @@ -76,7 +77,7 @@ class PromptChallengeResponse(ChallengeResponse): choices = field.get_choices( plan.context.get(PLAN_CONTEXT_PROMPT, {}), user, self.request ) - current = field.get_placeholder( + current = field.get_initial_value( plan.context.get(PLAN_CONTEXT_PROMPT, {}), user, self.request ) self.fields[field.field_key] = field.field(current, choices) @@ -197,8 +198,9 @@ class PromptStageView(ChallengeStageView): serializers = [] for field in fields: data = StagePromptSerializer(field).data - # Ensure all choices and placeholders are str, as otherwise further in - # we can fail serializer validation if we return some types such as bool + # Ensure all choices, placeholders and initial values are str, as + # otherwise further in we can fail serializer validation if we return + # some types such as bool choices = field.get_choices(context, self.get_pending_user(), self.request, dry_run) if choices: data["choices"] = [str(choice) for choice in choices] @@ -207,6 +209,9 @@ class PromptStageView(ChallengeStageView): data["placeholder"] = str( field.get_placeholder(context, self.get_pending_user(), self.request, dry_run) ) + data["initial_value"] = str( + field.get_initial_value(context, self.get_pending_user(), self.request, dry_run) + ) serializers.append(data) return serializers diff --git a/authentik/stages/prompt/tests.py b/authentik/stages/prompt/tests.py index 8e30f826f..43232fc75 100644 --- a/authentik/stages/prompt/tests.py +++ b/authentik/stages/prompt/tests.py @@ -22,6 +22,7 @@ from authentik.stages.prompt.stage import ( ) +# pylint: disable=too-many-public-methods class TestPromptStage(FlowTestCase): """Prompt tests""" @@ -37,6 +38,7 @@ class TestPromptStage(FlowTestCase): type=FieldTypes.USERNAME, required=True, placeholder="USERNAME_PLACEHOLDER", + initial_value="akuser", ) text_prompt = Prompt.objects.create( name=generate_id(), @@ -45,6 +47,7 @@ class TestPromptStage(FlowTestCase): type=FieldTypes.TEXT, required=True, placeholder="TEXT_PLACEHOLDER", + initial_value="some text", ) text_area_prompt = Prompt.objects.create( name=generate_id(), @@ -53,6 +56,7 @@ class TestPromptStage(FlowTestCase): type=FieldTypes.TEXT_AREA, required=True, placeholder="TEXT_AREA_PLACEHOLDER", + initial_value="some text", ) email_prompt = Prompt.objects.create( name=generate_id(), @@ -61,6 +65,7 @@ class TestPromptStage(FlowTestCase): type=FieldTypes.EMAIL, required=True, placeholder="EMAIL_PLACEHOLDER", + initial_value="email@example.com", ) password_prompt = Prompt.objects.create( name=generate_id(), @@ -69,6 +74,7 @@ class TestPromptStage(FlowTestCase): type=FieldTypes.PASSWORD, required=True, placeholder="PASSWORD_PLACEHOLDER", + initial_value="supersecurepassword", ) password2_prompt = Prompt.objects.create( name=generate_id(), @@ -77,6 +83,7 @@ class TestPromptStage(FlowTestCase): type=FieldTypes.PASSWORD, required=True, placeholder="PASSWORD_PLACEHOLDER", + initial_value="supersecurepassword", ) number_prompt = Prompt.objects.create( name=generate_id(), @@ -85,6 +92,7 @@ class TestPromptStage(FlowTestCase): type=FieldTypes.NUMBER, required=True, placeholder="NUMBER_PLACEHOLDER", + initial_value="42", ) hidden_prompt = Prompt.objects.create( name=generate_id(), @@ -92,6 +100,7 @@ class TestPromptStage(FlowTestCase): type=FieldTypes.HIDDEN, required=True, placeholder="HIDDEN_PLACEHOLDER", + initial_value="something idk", ) static_prompt = Prompt.objects.create( name=generate_id(), @@ -99,6 +108,7 @@ class TestPromptStage(FlowTestCase): type=FieldTypes.STATIC, required=True, placeholder="static", + initial_value="something idk", ) radio_button_group = Prompt.objects.create( name=generate_id(), @@ -106,6 +116,7 @@ class TestPromptStage(FlowTestCase): type=FieldTypes.RADIO_BUTTON_GROUP, required=True, placeholder="test", + initial_value="test", ) dropdown = Prompt.objects.create( name=generate_id(), @@ -137,9 +148,9 @@ class TestPromptStage(FlowTestCase): password_prompt.field_key: "test", password2_prompt.field_key: "test", number_prompt.field_key: 3, - hidden_prompt.field_key: hidden_prompt.placeholder, - static_prompt.field_key: static_prompt.placeholder, - radio_button_group.field_key: radio_button_group.placeholder, + hidden_prompt.field_key: hidden_prompt.initial_value, + static_prompt.field_key: static_prompt.initial_value, + radio_button_group.field_key: radio_button_group.initial_value, dropdown.field_key: "", } @@ -335,106 +346,176 @@ class TestPromptStage(FlowTestCase): self.assertEqual( prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"] ) - context["text_prompt_expression"] = generate_id() - self.assertEqual( - prompt.get_placeholder(context, self.user, self.factory.get("/")), - context["text_prompt_expression"], + + def test_prompt_placeholder_does_not_take_value_from_context(self): + """Test placeholder does not automatically take value from context""" + context = { + "foo": generate_id(), + } + prompt: Prompt = Prompt( + field_key="text_prompt_expression", + label="TEXT_LABEL", + type=FieldTypes.TEXT, + placeholder="return prompt_context['foo']", + placeholder_expression=True, ) - self.assertNotEqual( + context["text_prompt_expression"] = generate_id() + + self.assertEqual( prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"] ) - def test_choice_prompts_placeholders(self): - """Test placeholders and expression of choice fields""" - context = {"foo": generate_id()} + def test_prompt_initial_value(self): + """Test initial_value and expression""" + context = { + "foo": generate_id(), + } + prompt: Prompt = Prompt( + field_key="text_prompt_expression", + label="TEXT_LABEL", + type=FieldTypes.TEXT, + initial_value="return prompt_context['foo']", + initial_value_expression=True, + ) + self.assertEqual( + prompt.get_initial_value(context, self.user, self.factory.get("/")), context["foo"] + ) + context["text_prompt_expression"] = generate_id() + self.assertEqual( + prompt.get_initial_value(context, self.user, self.factory.get("/")), + context["text_prompt_expression"], + ) + self.assertNotEqual( + prompt.get_initial_value(context, self.user, self.factory.get("/")), context["foo"] + ) + + def test_choice_prompts_placeholder_and_initial_value_no_choices(self): + """Test placeholder and initial value of choice fields with 0 choices""" + context = {} - # No choices - unusable (in the sense it creates an unsubmittable form) - # but valid behaviour prompt: Prompt = Prompt( field_key="fixed_choice_prompt_expression", label="LABEL", type=FieldTypes.RADIO_BUTTON_GROUP, placeholder="return []", placeholder_expression=True, + initial_value="Invalid choice", + initial_value_expression=False, ) self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "") + self.assertEqual(prompt.get_initial_value(context, self.user, self.factory.get("/")), "") self.assertEqual(prompt.get_choices(context, self.user, self.factory.get("/")), tuple()) - context["fixed_choice_prompt_expression"] = generate_id() - self.assertEqual( - prompt.get_placeholder(context, self.user, self.factory.get("/")), - context["fixed_choice_prompt_expression"], - ) - self.assertEqual( - prompt.get_choices(context, self.user, self.factory.get("/")), - (context["fixed_choice_prompt_expression"],), - ) - self.assertNotEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "") - self.assertNotEqual(prompt.get_choices(context, self.user, self.factory.get("/")), tuple()) - del context["fixed_choice_prompt_expression"] + def test_choice_prompts_placeholder_and_initial_value_single_choice(self): + """Test placeholder and initial value of choice fields with 1 choice""" + context = {"foo": generate_id()} - # Single choice - prompt: Prompt = Prompt( - field_key="fixed_choice_prompt_expression", - label="LABEL", - type=FieldTypes.RADIO_BUTTON_GROUP, - placeholder="return prompt_context['foo']", - placeholder_expression=True, - ) - self.assertEqual( - prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"] - ) - self.assertEqual( - prompt.get_choices(context, self.user, self.factory.get("/")), (context["foo"],) - ) - context["fixed_choice_prompt_expression"] = generate_id() - self.assertEqual( - prompt.get_placeholder(context, self.user, self.factory.get("/")), - context["fixed_choice_prompt_expression"], - ) - self.assertEqual( - prompt.get_choices(context, self.user, self.factory.get("/")), - (context["fixed_choice_prompt_expression"],), - ) - self.assertNotEqual( - prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"] - ) - self.assertNotEqual( - prompt.get_choices(context, self.user, self.factory.get("/")), (context["foo"],) - ) - - del context["fixed_choice_prompt_expression"] - - # Multi choice prompt: Prompt = Prompt( field_key="fixed_choice_prompt_expression", label="LABEL", type=FieldTypes.DROPDOWN, - placeholder="return [prompt_context['foo'], True, 'text']", + placeholder=context["foo"], + placeholder_expression=False, + initial_value=context["foo"], + initial_value_expression=False, + ) + self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "") + self.assertEqual( + prompt.get_initial_value(context, self.user, self.factory.get("/")), context["foo"] + ) + self.assertEqual( + prompt.get_choices(context, self.user, self.factory.get("/")), (context["foo"],) + ) + + prompt: Prompt = Prompt( + field_key="fixed_choice_prompt_expression", + label="LABEL", + type=FieldTypes.DROPDOWN, + placeholder="return [prompt_context['foo']]", + placeholder_expression=True, + initial_value="return prompt_context['foo']", + initial_value_expression=True, + ) + self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "") + self.assertEqual( + prompt.get_initial_value(context, self.user, self.factory.get("/")), context["foo"] + ) + self.assertEqual( + prompt.get_choices(context, self.user, self.factory.get("/")), (context["foo"],) + ) + + def test_choice_prompts_placeholder_and_initial_value_multiple_choices(self): + """Test placeholder and initial value of choice fields with multiple choices""" + context = {} + + prompt: Prompt = Prompt( + field_key="fixed_choice_prompt_expression", + label="LABEL", + type=FieldTypes.RADIO_BUTTON_GROUP, + placeholder="return ['test', True, 42]", placeholder_expression=True, ) + self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "") self.assertEqual( - prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"] + prompt.get_initial_value(context, self.user, self.factory.get("/")), "test" + ) + self.assertEqual( + prompt.get_choices(context, self.user, self.factory.get("/")), ("test", True, 42) + ) + + prompt: Prompt = Prompt( + field_key="fixed_choice_prompt_expression", + label="LABEL", + type=FieldTypes.RADIO_BUTTON_GROUP, + placeholder="return ['test', True, 42]", + placeholder_expression=True, + initial_value="return True", + initial_value_expression=True, + ) + self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "") + self.assertEqual(prompt.get_initial_value(context, self.user, self.factory.get("/")), True) + self.assertEqual( + prompt.get_choices(context, self.user, self.factory.get("/")), ("test", True, 42) + ) + + def test_choice_prompts_placeholder_and_initial_value_from_context(self): + """Test placeholder and initial value of choice fields with values from context""" + rand_value = generate_id() + context = { + "fixed_choice_prompt_expression": rand_value, + "fixed_choice_prompt_expression__choices": ["test", 42, rand_value], + } + + prompt: Prompt = Prompt( + field_key="fixed_choice_prompt_expression", + label="LABEL", + type=FieldTypes.RADIO_BUTTON_GROUP, + ) + self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "") + self.assertEqual( + prompt.get_initial_value(context, self.user, self.factory.get("/")), rand_value + ) + self.assertEqual( + prompt.get_choices(context, self.user, self.factory.get("/")), ("test", 42, rand_value) + ) + + def test_initial_value_not_valid_choice(self): + """Test initial_value not a valid choice""" + context = {} + prompt: Prompt = Prompt( + field_key="choice_prompt", + label="TEXT_LABEL", + type=FieldTypes.DROPDOWN, + placeholder="choice", + initial_value="another_choice", ) self.assertEqual( prompt.get_choices(context, self.user, self.factory.get("/")), - (context["foo"], True, "text"), - ) - context["fixed_choice_prompt_expression"] = tuple(["text", generate_id(), 2]) - self.assertEqual( - prompt.get_placeholder(context, self.user, self.factory.get("/")), - "text", + ("choice",), ) self.assertEqual( - prompt.get_choices(context, self.user, self.factory.get("/")), - context["fixed_choice_prompt_expression"], - ) - self.assertNotEqual( - prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"] - ) - self.assertNotEqual( - prompt.get_choices(context, self.user, self.factory.get("/")), - (context["foo"], True, "text"), + prompt.get_initial_value(context, self.user, self.factory.get("/")), + "choice", ) def test_choices_are_none_for_non_choice_fields(self): @@ -505,6 +586,8 @@ class TestPromptStage(FlowTestCase): "type": FieldTypes.TEXT, "placeholder": 'return "Hello world"', "placeholder_expression": True, + "initial_value": 'return "Hello Hello world"', + "initial_value_expression": True, "sub_text": "test", "order": 123, }, @@ -522,6 +605,7 @@ class TestPromptStage(FlowTestCase): "type": "text", "required": True, "placeholder": "Hello world", + "initial_value": "Hello Hello world", "order": 123, "sub_text": "test", "choices": None, diff --git a/blueprints/default/flow-default-user-settings-flow.yaml b/blueprints/default/flow-default-user-settings-flow.yaml index aba142a1d..0640a643b 100644 --- a/blueprints/default/flow-default-user-settings-flow.yaml +++ b/blueprints/default/flow-default-user-settings-flow.yaml @@ -13,12 +13,14 @@ entries: id: flow - attrs: order: 200 - placeholder: | + placeholder: Username + placeholder_expression: false + initial_value: | try: return user.username except: return '' - placeholder_expression: true + initial_value_expression: true required: true type: text field_key: username @@ -29,12 +31,14 @@ entries: model: authentik_stages_prompt.prompt - attrs: order: 201 - placeholder: | + placeholder: Name + placeholder_expression: false + initial_value: | try: return user.name except: return '' - placeholder_expression: true + initial_value_expression: true required: true type: text field_key: name @@ -45,12 +49,14 @@ entries: model: authentik_stages_prompt.prompt - attrs: order: 202 - placeholder: | + placeholder: Email + placeholder_expression: false + initial_value: | try: return user.email except: return '' - placeholder_expression: true + initial_value_expression: true required: true type: email field_key: email @@ -61,12 +67,14 @@ entries: model: authentik_stages_prompt.prompt - attrs: order: 203 - placeholder: | + placeholder: Locale + placeholder_expression: false + initial_value: | try: return user.attributes.get("settings", {}).get("locale", "") except: return '' - placeholder_expression: true + initial_value_expression: true required: true type: ak-locale field_key: attributes.settings.locale diff --git a/schema.yml b/schema.yml index 6f987de82..d64985069 100644 --- a/schema.yml +++ b/schema.yml @@ -36862,8 +36862,14 @@ components: type: boolean placeholder: type: string - description: When creating a Radio Button Group or Dropdown, enable interpreting - as expression and return a list to return multiple choices. + description: Optionally provide a short hint that describes the expected + input value. When creating a fixed choice field, enable interpreting as + expression and return a list to return multiple choices. + initial_value: + type: string + description: Optionally pre-fill the input with an initial value. When creating + a fixed choice field, enable interpreting as expression and return a list + to return multiple default choices. order: type: integer maximum: 2147483647 @@ -36876,6 +36882,8 @@ components: type: string placeholder_expression: type: boolean + initial_value_expression: + type: boolean PatchedPromptStageRequest: type: object description: PromptStage Serializer @@ -38034,8 +38042,14 @@ components: type: boolean placeholder: type: string - description: When creating a Radio Button Group or Dropdown, enable interpreting - as expression and return a list to return multiple choices. + description: Optionally provide a short hint that describes the expected + input value. When creating a fixed choice field, enable interpreting as + expression and return a list to return multiple choices. + initial_value: + type: string + description: Optionally pre-fill the input with an initial value. When creating + a fixed choice field, enable interpreting as expression and return a list + to return multiple default choices. order: type: integer maximum: 2147483647 @@ -38048,6 +38062,8 @@ components: type: string placeholder_expression: type: boolean + initial_value_expression: + type: boolean required: - field_key - label @@ -38109,8 +38125,14 @@ components: type: boolean placeholder: type: string - description: When creating a Radio Button Group or Dropdown, enable interpreting - as expression and return a list to return multiple choices. + description: Optionally provide a short hint that describes the expected + input value. When creating a fixed choice field, enable interpreting as + expression and return a list to return multiple choices. + initial_value: + type: string + description: Optionally pre-fill the input with an initial value. When creating + a fixed choice field, enable interpreting as expression and return a list + to return multiple default choices. order: type: integer maximum: 2147483647 @@ -38123,6 +38145,8 @@ components: type: string placeholder_expression: type: boolean + initial_value_expression: + type: boolean required: - field_key - label @@ -40267,6 +40291,8 @@ components: type: boolean placeholder: type: string + initial_value: + type: string order: type: integer sub_text: @@ -40279,6 +40305,7 @@ components: required: - choices - field_key + - initial_value - label - order - placeholder diff --git a/web/src/admin/stages/prompt/PromptForm.ts b/web/src/admin/stages/prompt/PromptForm.ts index 1ceb1d2b6..1f4f5161d 100644 --- a/web/src/admin/stages/prompt/PromptForm.ts +++ b/web/src/admin/stages/prompt/PromptForm.ts @@ -372,8 +372,8 @@ export class PromptForm extends ModelForm { >

- ${t`When checked, the placeholder will be evaluated in the same way environment as a property mapping. - If the evaluation failed, the placeholder itself is returned.`} + ${t`When checked, the placeholder will be evaluated in the same way a property mapping is. + If the evaluation fails, the placeholder itself is returned.`}

@@ -386,11 +386,41 @@ export class PromptForm extends ModelForm { >

- ${t`Optionally pre-fill the input value. - When creating a "Radio Button Group" or "Dropdown", enable interpreting as + ${t`Optionally provide a short hint that describes the expected input value. + When creating a fixed choice field, enable interpreting as expression and return a list to return multiple choices.`}

+ + +

+ ${t`When checked, the initial value will be evaluated in the same way a property mapping is. + If the evaluation fails, the initial value itself is returned.`} +

+
+ + + +

+ ${t`Optionally pre-fill the input with an initial value. + When creating a fixed choice field, enable interpreting as + expression and return a list to return multiple default choices.`}} +

+
`; + value="${prompt.initialValue}">`; case PromptTypeEnum.TextArea: return `