KEYCLOAK-18497 - Support different input types in built-in dynamic forms

This commit is contained in:
Vlastimil Elias 2021-08-11 16:21:00 +02:00 committed by Pedro Igor
parent 375e47877e
commit 28e220fa6d
22 changed files with 611 additions and 46 deletions

View file

@ -30,6 +30,7 @@ import org.keycloak.validate.validators.IntegerValidator;
import org.keycloak.validate.validators.LengthValidator; import org.keycloak.validate.validators.LengthValidator;
import org.keycloak.validate.validators.NotBlankValidator; import org.keycloak.validate.validators.NotBlankValidator;
import org.keycloak.validate.validators.NotEmptyValidator; import org.keycloak.validate.validators.NotEmptyValidator;
import org.keycloak.validate.validators.OptionsValidator;
import org.keycloak.validate.validators.DoubleValidator; import org.keycloak.validate.validators.DoubleValidator;
import org.keycloak.validate.validators.PatternValidator; import org.keycloak.validate.validators.PatternValidator;
import org.keycloak.validate.validators.UriValidator; import org.keycloak.validate.validators.UriValidator;
@ -55,7 +56,8 @@ public class Validators {
PatternValidator.INSTANCE, PatternValidator.INSTANCE,
DoubleValidator.INSTANCE, DoubleValidator.INSTANCE,
IntegerValidator.INSTANCE, IntegerValidator.INSTANCE,
ValidatorConfigValidator.INSTANCE ValidatorConfigValidator.INSTANCE,
OptionsValidator.INSTANCE
); );
INTERNAL_VALIDATORS = list.stream().collect(Collectors.toMap(SimpleValidator::getId, v -> v)); INTERNAL_VALIDATORS = list.stream().collect(Collectors.toMap(SimpleValidator::getId, v -> v));
@ -159,10 +161,14 @@ public class Validators {
return LocalDateValidator.INSTANCE; return LocalDateValidator.INSTANCE;
} }
public static OptionsValidator optionsValidator() {
return OptionsValidator.INSTANCE;
}
public static ValidatorConfigValidator validatorConfigValidator() { public static ValidatorConfigValidator validatorConfigValidator() {
return ValidatorConfigValidator.INSTANCE; return ValidatorConfigValidator.INSTANCE;
} }
/** /**
* Look-up up for a built-in or registered {@link Validator} with the given validatorId. * Look-up up for a built-in or registered {@link Validator} with the given validatorId.
* *

View file

@ -0,0 +1,94 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.validate.validators;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ConfiguredProvider;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.validate.AbstractStringValidator;
import org.keycloak.validate.ValidationContext;
import org.keycloak.validate.ValidationError;
import org.keycloak.validate.ValidationResult;
import org.keycloak.validate.ValidatorConfig;
/**
* Validation against list of allowed values - accepts plain string and collection of strings (every value is validated against allowed values), for basic behavior like null/blank
* values handling and collections support see {@link AbstractStringValidator}.
* <p>
* Configuration have to be always provided using {@link #KEY_OPTIONS} option, which have to contain <code>List</code> of <code>String</code> values.
*/
public class OptionsValidator extends AbstractStringValidator implements ConfiguredProvider {
public static final OptionsValidator INSTANCE = new OptionsValidator();
public static final String ID = "options";
public static final String KEY_OPTIONS = "options";
private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
static {
ProviderConfigProperty property;
property = new ProviderConfigProperty();
property.setName(KEY_OPTIONS);
property.setLabel("Options");
property.setHelpText("List of allowed options");
property.setType(ProviderConfigProperty.MULTIVALUED_STRING_TYPE);
configProperties.add(property);
}
@Override
public String getId() {
return ID;
}
@Override
protected void doValidate(String value, String inputHint, ValidationContext context, ValidatorConfig config) {
List<String> allowedValues = config.getStringListOrDefault(KEY_OPTIONS);
if (allowedValues == null || !allowedValues.contains(value)) {
context.addError(new ValidationError(ID, inputHint, ValidationError.MESSAGE_INVALID_VALUE));
}
}
@Override
public ValidationResult validateConfig(KeycloakSession session, ValidatorConfig config) {
Set<ValidationError> errors = new LinkedHashSet<>();
if (config == null || !config.containsKey(KEY_OPTIONS)) {
errors.add(new ValidationError(ID, KEY_OPTIONS, ValidatorConfigValidator.MESSAGE_CONFIG_MISSING_VALUE));
} else if (!(config.get(KEY_OPTIONS) instanceof List)) {
errors.add(new ValidationError(ID, KEY_OPTIONS, ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_VALUE, "must be list of values"));
}
return new ValidationResult(errors);
}
@Override
public String getHelpText() {
return "Options validator";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
}

View file

@ -6,4 +6,5 @@ org.keycloak.validate.validators.NotBlankValidator
org.keycloak.validate.validators.PatternValidator org.keycloak.validate.validators.PatternValidator
org.keycloak.validate.validators.DoubleValidator org.keycloak.validate.validators.DoubleValidator
org.keycloak.validate.validators.IntegerValidator org.keycloak.validate.validators.IntegerValidator
org.keycloak.validate.validators.LocalDateValidator org.keycloak.validate.validators.LocalDateValidator
org.keycloak.validate.validators.OptionsValidator

View file

@ -14,6 +14,7 @@ import org.junit.Test;
import org.keycloak.validate.validators.DoubleValidator; import org.keycloak.validate.validators.DoubleValidator;
import org.keycloak.validate.validators.IntegerValidator; import org.keycloak.validate.validators.IntegerValidator;
import org.keycloak.validate.validators.LengthValidator; import org.keycloak.validate.validators.LengthValidator;
import org.keycloak.validate.validators.OptionsValidator;
import org.keycloak.validate.validators.PatternValidator; import org.keycloak.validate.validators.PatternValidator;
import org.keycloak.validate.validators.UriValidator; import org.keycloak.validate.validators.UriValidator;
@ -24,7 +25,7 @@ public class BuiltinValidatorsTest {
private static final ValidatorConfig valConfigIgnoreEmptyValues = ValidatorConfig.builder().config(AbstractSimpleValidator.IGNORE_EMPTY_VALUE, true).build(); private static final ValidatorConfig valConfigIgnoreEmptyValues = ValidatorConfig.builder().config(AbstractSimpleValidator.IGNORE_EMPTY_VALUE, true).build();
@Test @Test
public void validateLength() { public void testLengthValidator() {
Validator validator = Validators.lengthValidator(); Validator validator = Validators.lengthValidator();
@ -82,7 +83,7 @@ public class BuiltinValidatorsTest {
} }
@Test @Test
public void validateLength_ConfigValidation() { public void testLengthValidator_ConfigValidation() {
// invalid min and max config values // invalid min and max config values
ValidatorConfig config = new ValidatorConfig(ImmutableMap.of(LengthValidator.KEY_MIN, new Object(), LengthValidator.KEY_MAX, "invalid")); ValidatorConfig config = new ValidatorConfig(ImmutableMap.of(LengthValidator.KEY_MIN, new Object(), LengthValidator.KEY_MAX, "invalid"));
@ -118,7 +119,7 @@ public class BuiltinValidatorsTest {
} }
@Test @Test
public void validateEmail() { public void testEmailValidator() {
// this also validates StringFormatValidatorBase for simple values // this also validates StringFormatValidatorBase for simple values
Validator validator = Validators.emailValidator(); Validator validator = Validators.emailValidator();
@ -140,7 +141,7 @@ public class BuiltinValidatorsTest {
} }
@Test @Test
public void validateStringFormatValidatorBaseForCollections() { public void testAbstractSimpleValidatorSupportForCollections() {
Validator validator = Validators.emailValidator(); Validator validator = Validators.emailValidator();
@ -164,7 +165,7 @@ public class BuiltinValidatorsTest {
} }
@Test @Test
public void validateNotBlank() { public void testNotBlankValidator() {
Validator validator = Validators.notBlankValidator(); Validator validator = Validators.notBlankValidator();
@ -187,7 +188,7 @@ public class BuiltinValidatorsTest {
} }
@Test @Test
public void validateNotEmpty() { public void testNotEmptyValidator() {
Validator validator = Validators.notEmptyValidator(); Validator validator = Validators.notEmptyValidator();
@ -205,7 +206,7 @@ public class BuiltinValidatorsTest {
} }
@Test @Test
public void validateDoubleNumber() { public void testDoubleValidator() {
Validator validator = Validators.doubleValidator(); Validator validator = Validators.doubleValidator();
@ -271,7 +272,7 @@ public class BuiltinValidatorsTest {
} }
@Test @Test
public void validateDoubleNumber_ConfigValidation() { public void testDoubleValidator_ConfigValidation() {
// invalid min and max config values // invalid min and max config values
ValidatorConfig config = new ValidatorConfig(ImmutableMap.of(DoubleValidator.KEY_MIN, new Object(), DoubleValidator.KEY_MAX, "invalid")); ValidatorConfig config = new ValidatorConfig(ImmutableMap.of(DoubleValidator.KEY_MIN, new Object(), DoubleValidator.KEY_MAX, "invalid"));
@ -307,7 +308,7 @@ public class BuiltinValidatorsTest {
} }
@Test @Test
public void validateIntegerNumber() { public void testIntegerValidator() {
Validator validator = Validators.integerValidator(); Validator validator = Validators.integerValidator();
// null value and empty String // null value and empty String
@ -373,7 +374,7 @@ public class BuiltinValidatorsTest {
} }
@Test @Test
public void validateIntegerNumber_ConfigValidation() { public void testIntegerValidator_ConfigValidation() {
// invalid min and max config values // invalid min and max config values
ValidatorConfig config = new ValidatorConfig(ImmutableMap.of(IntegerValidator.KEY_MIN, new Object(), IntegerValidator.KEY_MAX, "invalid")); ValidatorConfig config = new ValidatorConfig(ImmutableMap.of(IntegerValidator.KEY_MIN, new Object(), IntegerValidator.KEY_MAX, "invalid"));
@ -409,7 +410,7 @@ public class BuiltinValidatorsTest {
} }
@Test @Test
public void validatePattern() { public void testPatternValidator() {
Validator validator = Validators.patternValidator(); Validator validator = Validators.patternValidator();
@ -440,7 +441,7 @@ public class BuiltinValidatorsTest {
} }
@Test @Test
public void validateUri() throws Exception { public void testUriValidator() throws Exception {
Validator validator = Validators.uriValidator(); Validator validator = Validators.uriValidator();
@ -462,5 +463,58 @@ public class BuiltinValidatorsTest {
Assert.assertFalse(Validators.uriValidator().validateUri(new URI("http://customurl"), Collections.singleton("https"), true, true)); Assert.assertFalse(Validators.uriValidator().validateUri(new URI("http://customurl"), Collections.singleton("https"), true, true));
} }
@Test
public void testOptionsValidator(){
Validator validator = Validators.optionsValidator();
// options not configured - always invalid
Assert.assertFalse(validator.validate(null, "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, null).build()).isValid());
Assert.assertFalse(validator.validate("", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, null).build()).isValid());
Assert.assertFalse(validator.validate(" ", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, null).build()).isValid());
Assert.assertFalse(validator.validate("s", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, null).build()).isValid());
// options not configured but empty and blanks ignored, others invalid
Assert.assertTrue(validator.validate(null, "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, null).config(valConfigIgnoreEmptyValues).build()).isValid());
Assert.assertTrue(validator.validate("", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, null).config(valConfigIgnoreEmptyValues).build()).isValid());
Assert.assertFalse(validator.validate(" ", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, null).config(valConfigIgnoreEmptyValues).build()).isValid());
Assert.assertFalse(validator.validate("s", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, null).config(valConfigIgnoreEmptyValues).build()).isValid());
List<String> options = Arrays.asList("opt1", "opt2");
// options configured
Assert.assertFalse(validator.validate(null, "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, options).build()).isValid());
Assert.assertFalse(validator.validate("", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, options).build()).isValid());
Assert.assertFalse(validator.validate(" ", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, options).build()).isValid());
Assert.assertFalse("must be case sensitive", validator.validate("Opt1", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, options).build()).isValid());
Assert.assertTrue(validator.validate("opt1", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, options).build()).isValid());
Assert.assertTrue(validator.validate("opt2", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, options).build()).isValid());
Assert.assertFalse("trim not expected", validator.validate("opt2 ", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, options).build()).isValid());
Assert.assertFalse("trim not expected", validator.validate(" opt2", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, options).build()).isValid());
// options configured - empty and blanks ignored
Assert.assertTrue(validator.validate(null, "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, options).config(valConfigIgnoreEmptyValues).build()).isValid());
Assert.assertTrue(validator.validate("", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, options).config(valConfigIgnoreEmptyValues).build()).isValid());
Assert.assertFalse(validator.validate(" ", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, options).config(valConfigIgnoreEmptyValues).build()).isValid());
Assert.assertFalse("must be case sensitive", validator.validate("Opt1", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, options).config(valConfigIgnoreEmptyValues).build()).isValid());
Assert.assertTrue(validator.validate("opt1", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, options).config(valConfigIgnoreEmptyValues).build()).isValid());
Assert.assertTrue(validator.validate("opt2", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, options).config(valConfigIgnoreEmptyValues).build()).isValid());
Assert.assertFalse("trim not expected", validator.validate(" opt2", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, options).config(valConfigIgnoreEmptyValues).build()).isValid());
Assert.assertFalse("trim not expected", validator.validate("opt2 ", "test", ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, options).config(valConfigIgnoreEmptyValues).build()).isValid());
}
@Test
public void testOptionsValidator_Config_Validation() {
ValidationResult result = Validators.validatorConfigValidator().validate(ValidatorConfig.builder().build(), OptionsValidator.ID).toResult();
Assert.assertFalse(result.isValid());
// invalid type of the config value
result = Validators.validatorConfigValidator().validate(ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, "a").build(), OptionsValidator.ID).toResult();
Assert.assertFalse(result.isValid());
result = Validators.validatorConfigValidator().validate(ValidatorConfig.builder().config(OptionsValidator.KEY_OPTIONS, Arrays.asList("opt1")).build(), OptionsValidator.ID).toResult();
Assert.assertTrue(result.isValid());
}
} }

View file

@ -4,6 +4,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
@ -52,12 +53,12 @@ public abstract class AbstractUserProfileBean {
protected abstract UserProfile createUserProfile(UserProfileProvider provider); protected abstract UserProfile createUserProfile(UserProfileProvider provider);
/** /**
* Get attribute default value to be pre-filled into the form on first show. * Get attribute default values to be pre-filled into the form on first show.
* *
* @param name of the attribute * @param name of the attribute
* @return attribute default value (can be null) * @return attribute default value (can be null)
*/ */
protected abstract String getAttributeDefaultValue(String name); protected abstract Stream<String> getAttributeDefaultValues(String name);
/** /**
* Get context the template is used for, so view can be customized for distinct contexts. * Get context the template is used for, so view can be customized for distinct contexts.
@ -110,13 +111,26 @@ public abstract class AbstractUserProfileBean {
} }
public String getValue() { public String getValue() {
List<String> v = formData != null ? formData.get(getName()) : null; List<String> v = getValues();
if (v == null || v.isEmpty()) { if (v == null || v.isEmpty()) {
return getAttributeDefaultValue(getName()); return null;
} else { } else {
return v.get(0); return v.get(0);
} }
} }
public List<String> getValues() {
List<String> v = formData != null ? formData.get(getName()) : null;
if (v == null || v.isEmpty()) {
Stream<String> vs = getAttributeDefaultValues(getName());
if(vs == null)
return Collections.emptyList();
else
return vs.collect(Collectors.toList());
} else {
return v;
}
}
public boolean isRequired() { public boolean isRequired() {
return profile.getAttributes().isRequired(getName()); return profile.getAttributes().isRequired(getName());

View file

@ -16,6 +16,8 @@
*/ */
package org.keycloak.forms.login.freemarker.model; package org.keycloak.forms.login.freemarker.model;
import java.util.stream.Stream;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext; import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
@ -43,8 +45,8 @@ public class IdpReviewProfileBean extends AbstractUserProfileBean {
} }
@Override @Override
protected String getAttributeDefaultValue(String name) { protected Stream<String> getAttributeDefaultValues(String name) {
return idpCtx.getFirstAttribute(name); return idpCtx.getAttributeStream(name);
} }
@Override @Override

View file

@ -18,6 +18,7 @@ package org.keycloak.forms.login.freemarker.model;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.stream.Stream;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
@ -52,7 +53,7 @@ public class RegisterBean extends AbstractUserProfileBean {
} }
@Override @Override
protected String getAttributeDefaultValue(String name) { protected Stream<String> getAttributeDefaultValues(String name) {
return null; return null;
} }

View file

@ -1,5 +1,7 @@
package org.keycloak.forms.login.freemarker.model; package org.keycloak.forms.login.freemarker.model;
import java.util.stream.Stream;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -27,8 +29,8 @@ public class VerifyProfileBean extends AbstractUserProfileBean {
} }
@Override @Override
protected String getAttributeDefaultValue(String name) { protected Stream<String> getAttributeDefaultValues(String name){
return user.getFirstAttribute(name); return user.getAttributeStream(name);
} }
@Override @Override

View file

@ -166,6 +166,19 @@ public class UPConfigUtils {
errors.add("Attribute '" + attributeName + "' references unknown group '" + attributeConfig.getGroup() + "'"); errors.add("Attribute '" + attributeName + "' references unknown group '" + attributeConfig.getGroup() + "'");
} }
} }
if (attributeConfig.getAnnotations()!=null) {
validateAnnotations(attributeConfig.getAnnotations(), errors, attributeName);
}
}
private static void validateAnnotations(Map<String, Object> annotations, List<String> errors, String attributeName) {
if (annotations.containsKey("inputOptions") && !(annotations.get("inputOptions") instanceof List)) {
errors.add(new StringBuilder("Annotation 'inputOptions' configured for attribute '").append(attributeName).append("' must be an array of values!'").toString());
}
if (annotations.containsKey("inputOptionLabels") && !(annotations.get("inputOptionLabels") instanceof Map)) {
errors.add(new StringBuilder("Annotation 'inputOptionLabels' configured for attribute '").append(attributeName).append("' must be an object!'").toString());
}
} }
private static void validateScopes(Set<String> scopes, String propertyName, String attributeName, List<String> errors, KeycloakSession session) { private static void validateScopes(Set<String> scopes, String propertyName, String attributeName, List<String> errors, KeycloakSession session) {

View file

@ -0,0 +1,55 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.validate;
import java.util.Collections;
import java.util.List;
import org.keycloak.provider.ConfiguredProvider;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.validate.SimpleValidator;
import org.keycloak.validate.ValidationContext;
import org.keycloak.validate.ValidatorConfig;
/**
* Dummy Options validator for User Profile configuration tests.
*/
public class DummyOptionsValidator implements SimpleValidator, ConfiguredProvider {
public static final String ID = "dummyOptions";
@Override
public String getId() {
return ID;
}
@Override
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
return context;
}
@Override
public String getHelpText() {
return "Dummy Options Validator";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return Collections.emptyList();
}
}

View file

@ -43,6 +43,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.forms.RegisterWithUserProfileTest;
import org.keycloak.testsuite.forms.VerifyProfileTest; import org.keycloak.testsuite.forms.VerifyProfileTest;
import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.util.ClientScopeBuilder; import org.keycloak.testsuite.util.ClientScopeBuilder;
@ -227,6 +228,21 @@ public class RequiredActionUpdateProfileWithUserProfileTest extends RequiredActi
); );
} }
@Test
public void testAttributeInputTypes() {
setUserProfileConfiguration("{\"attributes\": ["
+ RegisterWithUserProfileTest.UP_CONFIG_PART_INPUT_TYPES
+ "]}");
loginPage.open();
loginPage.login(USERNAME1, PASSWORD);
updateProfilePage.assertCurrent();
RegisterWithUserProfileTest.assertFieldTypes(driver);
}
@Test @Test
public void testUsernameOnlyIfEditAllowed() { public void testUsernameOnlyIfEditAllowed() {
RealmRepresentation realm = testRealm().toRepresentation(); RealmRepresentation realm = testRealm().toRepresentation();

View file

@ -31,6 +31,7 @@ import org.keycloak.common.Profile;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.forms.RegisterWithUserProfileTest;
import org.keycloak.testsuite.forms.VerifyProfileTest; import org.keycloak.testsuite.forms.VerifyProfileTest;
import org.keycloak.testsuite.util.ClientScopeBuilder; import org.keycloak.testsuite.util.ClientScopeBuilder;
import org.openqa.selenium.By; import org.openqa.selenium.By;
@ -193,6 +194,24 @@ public class KcOidcFirstBrokerLoginWithUserProfileTest extends KcOidcFirstBroker
); );
} }
@Test
public void testAttributeInputTypes() {
updateExecutions(AbstractBrokerTest::enableUpdateProfileOnFirstLogin);
setUserProfileConfiguration("{\"attributes\": ["
+ RegisterWithUserProfileTest.UP_CONFIG_PART_INPUT_TYPES
+ "]}");
driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName()));
logInWithBroker(bc);
waitForPage(driver, "update account information", false);
updateAccountInformationPage.assertCurrent();
RegisterWithUserProfileTest.assertFieldTypes(driver);
}
@Test @Test
public void testDynamicUserProfileReviewWhenMissing_requiredReadOnlyAttributeDoesnotForceUpdate() { public void testDynamicUserProfileReviewWhenMissing_requiredReadOnlyAttributeDoesnotForceUpdate() {

View file

@ -42,6 +42,7 @@ import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.util.ClientScopeBuilder; import org.keycloak.testsuite.util.ClientScopeBuilder;
import org.keycloak.testsuite.util.KeycloakModelUtils; import org.keycloak.testsuite.util.KeycloakModelUtils;
import org.openqa.selenium.By; import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
/** /**
* @author Vlastimil Elias <velias@redhat.com> * @author Vlastimil Elias <velias@redhat.com>
@ -298,6 +299,103 @@ public class RegisterWithUserProfileTest extends RegisterTest {
).isDisplayed() ).isDisplayed()
); );
} }
public static final String UP_CONFIG_PART_INPUT_TYPES = "{\"name\": \"defaultType\"," + VerifyProfileTest.PERMISSIONS_ALL + "},"
+ "{\"name\": \"placeholderAttribute\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"annotations\":{\"inputType\":\"text\",\"inputTypePlaceholder\":\"Example.\"}},"
+ "{\"name\": \"helperTexts\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"annotations\":{\"inputType\":\"text\",\"inputHelperTextBefore\":\"Example <b>bold text</b> before.\",\"inputHelperTextAfter\":\"Example <i>i text</i> after.\"}},"
+ "{\"name\": \"textWithBasicAttributes\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"annotations\":{\"inputType\":\"text\",\"inputTypeSize\":\"35\",\"inputTypeMinlength\":\"1\",\"inputTypeMaxlength\":\"10\",\"inputTypePattern\":\".*\"}},"
+ "{\"name\": \"html5NumberWithAttributes\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"annotations\":{\"inputType\":\"html5-number\",\"inputTypeMin\":\"10\",\"inputTypeMax\":\"20\",\"inputTypeStep\":1}},"
+ "{\"name\": \"textareaWithAttributes\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"annotations\":{\"inputType\":\"textarea\",\"inputTypeCols\":\"35\",\"inputTypeRows\":\"7\",\"inputTypeMaxlength\":\"10\"}},"
+ "{\"name\": \"selectWithoutOptions\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"annotations\":{\"inputType\":\"select\",\"inputTypeSize\":\"5\"}},"
+ "{\"name\": \"selectWithOptionsWithoutLabels\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"validations\":{\"options\":{\"options\":[ \"opt1\",\"opt2\"]}}, \"annotations\":{\"inputType\":\"select\"}},"
+ "{\"name\": \"multiselectWithOptionsAndSimpleI18nLabels\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"validations\":{\"options\":{ \"options\":[\"totp\",\"opt2\"]}}, \"annotations\":{\"inputType\":\"multiselect\",\"inputOptionLabelsI18nPrefix\": \"loginTotp\"}},"
+ "{\"name\": \"multiselectWithOptionsAndLabels\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"validations\":{\"options\":{ \"options\":[\"opt1\",\"opt2\",\"opt3\"]}}, \"annotations\":{\"inputType\":\"multiselect\",\"inputOptionLabels\":{\"opt1\": \"Option 1\",\"opt2\":\"${username}\"}}},"
+ "{\"name\": \"selectWithOptionsFromCustomValidatorAndLabels\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"validations\":{\"dummyOptions\":{\"options\" : [\"vopt1\",\"vopt2\",\"vopt3\"]}} ,\"annotations\":{\"inputType\":\"select\",\"inputOptionsFromValidation\":\"dummyOptions\",\"inputOptionLabels\":{\"vopt1\": \"Option 1\",\"vopt2\":\"${username}\"}}},"
+ "{\"name\": \"selectRadiobuttons\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"validations\" : {\"options\" : {\"options\":[\"opt1\",\"opt2\",\"opt3\"]}}, \"annotations\":{\"inputType\":\"select-radiobuttons\",\"inputOptionLabels\":{\"opt1\": \"Option 1\",\"opt2\":\"${username}\"}}},"
+ "{\"name\": \"selectRadiobuttonsWithOptionsFromCustomValidatorAndLabels\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"validations\" : {\"dummyOptions\" : {\"options\" : [\"vopt1\",\"vopt2\",\"vopt3\"]}} ,\"annotations\":{\"inputType\":\"select-radiobuttons\",\"inputOptionsFromValidation\":\"dummyOptions\",\"inputOptionLabels\":{\"vopt1\": \"Option 1\",\"vopt2\":\"${username}\"}}},"
+ "{\"name\": \"multiselectCheckboxes\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"validations\": {\"options\":{\"options\":[\"opt1\",\"opt2\",\"opt3\"]}}, \"annotations\":{\"inputType\":\"multiselect-checkboxes\",\"inputOptionLabels\":{\"opt1\": \"Option 1\",\"opt2\":\"${username}\"}}}";
@Test
public void testAttributeInputTypes() {
setUserProfileConfiguration("{\"attributes\": [" + UP_CONFIG_PART_INPUT_TYPES + "]}");
loginPage.open();
loginPage.clickRegister();
registerPage.assertCurrent();
assertFieldTypes(driver);
}
public static void assertFieldTypes(WebDriver driver) {
Assert.assertEquals("text", driver.findElement(By.cssSelector("input#defaultType")).getAttribute("type"));
Assert.assertEquals("text", driver.findElement(By.cssSelector("input#placeholderAttribute")).getAttribute("type"));
Assert.assertEquals("Example.", driver.findElement(By.cssSelector("input#placeholderAttribute")).getAttribute("placeholder"));
Assert.assertEquals("Example bold text before.", driver.findElement(By.cssSelector("div#form-help-text-before-helperTexts")).getText());
Assert.assertEquals("bold text", driver.findElement(By.cssSelector("div#form-help-text-before-helperTexts b")).getText());
Assert.assertEquals("Example i text after.", driver.findElement(By.cssSelector("div#form-help-text-after-helperTexts")).getText());
Assert.assertEquals("i text", driver.findElement(By.cssSelector("div#form-help-text-after-helperTexts i")).getText());
Assert.assertEquals("text", driver.findElement(By.cssSelector("input#textWithBasicAttributes")).getAttribute("type"));
Assert.assertEquals("35", driver.findElement(By.cssSelector("input#textWithBasicAttributes")).getAttribute("size"));
Assert.assertEquals("1", driver.findElement(By.cssSelector("input#textWithBasicAttributes")).getAttribute("minlength"));
Assert.assertEquals("10", driver.findElement(By.cssSelector("input#textWithBasicAttributes")).getAttribute("maxlength"));
Assert.assertEquals(".*", driver.findElement(By.cssSelector("input#textWithBasicAttributes")).getAttribute("pattern"));
Assert.assertEquals("number", driver.findElement(By.cssSelector("input#html5NumberWithAttributes")).getAttribute("type"));
Assert.assertEquals("10", driver.findElement(By.cssSelector("input#html5NumberWithAttributes")).getAttribute("min"));
Assert.assertEquals("20", driver.findElement(By.cssSelector("input#html5NumberWithAttributes")).getAttribute("max"));
Assert.assertEquals("1", driver.findElement(By.cssSelector("input#html5NumberWithAttributes")).getAttribute("step"));
Assert.assertEquals("35", driver.findElement(By.cssSelector("textarea#textareaWithAttributes")).getAttribute("cols"));
Assert.assertEquals("7", driver.findElement(By.cssSelector("textarea#textareaWithAttributes")).getAttribute("rows"));
Assert.assertEquals("10", driver.findElement(By.cssSelector("textarea#textareaWithAttributes")).getAttribute("maxlength"));
Assert.assertEquals("5", driver.findElement(By.cssSelector("select#selectWithoutOptions")).getAttribute("size"));
Assert.assertEquals(null, driver.findElement(By.cssSelector("select#selectWithOptionsWithoutLabels")).getAttribute("multiple"));
Assert.assertEquals("opt1", driver.findElement(By.cssSelector("select#selectWithOptionsWithoutLabels option[value=opt1]")).getText());
Assert.assertEquals("opt2", driver.findElement(By.cssSelector("select#selectWithOptionsWithoutLabels option[value=opt2]")).getText());
Assert.assertEquals("default empty option is missing in select","", driver.findElement(By.cssSelector("select#selectWithOptionsWithoutLabels option[value='']")).getText());
Assert.assertEquals("true", driver.findElement(By.cssSelector("select#multiselectWithOptionsAndSimpleI18nLabels")).getAttribute("multiple"));
Assert.assertEquals("Time-based", driver.findElement(By.cssSelector("select#multiselectWithOptionsAndSimpleI18nLabels option[value=totp]")).getText());
Assert.assertEquals("loginTotp.opt2", driver.findElement(By.cssSelector("select#multiselectWithOptionsAndSimpleI18nLabels option[value=opt2]")).getText());
Assert.assertEquals("true", driver.findElement(By.cssSelector("select#multiselectWithOptionsAndLabels")).getAttribute("multiple"));
Assert.assertEquals("Option 1", driver.findElement(By.cssSelector("select#multiselectWithOptionsAndLabels option[value=opt1]")).getText());
Assert.assertEquals("Username", driver.findElement(By.cssSelector("select#multiselectWithOptionsAndLabels option[value=opt2]")).getText());
Assert.assertEquals("opt3", driver.findElement(By.cssSelector("select#multiselectWithOptionsAndLabels option[value=opt3]")).getText());
Assert.assertEquals(null, driver.findElement(By.cssSelector("select#selectWithOptionsFromCustomValidatorAndLabels")).getAttribute("multiple"));
Assert.assertEquals("Option 1", driver.findElement(By.cssSelector("select#selectWithOptionsFromCustomValidatorAndLabels option[value=vopt1]")).getText());
Assert.assertEquals("Username", driver.findElement(By.cssSelector("select#selectWithOptionsFromCustomValidatorAndLabels option[value=vopt2]")).getText());
Assert.assertEquals("vopt3", driver.findElement(By.cssSelector("select#selectWithOptionsFromCustomValidatorAndLabels option[value=vopt3]")).getText());
Assert.assertEquals("radio", driver.findElement(By.cssSelector("input#selectRadiobuttons-opt1")).getAttribute("type"));
Assert.assertEquals("Option 1", driver.findElement(By.cssSelector("label[for=selectRadiobuttons-opt1]")).getText());
Assert.assertEquals("radio", driver.findElement(By.cssSelector("input#selectRadiobuttons-opt2")).getAttribute("type"));
Assert.assertEquals("Username", driver.findElement(By.cssSelector("label[for=selectRadiobuttons-opt2]")).getText());
Assert.assertEquals("radio", driver.findElement(By.cssSelector("input#selectRadiobuttons-opt3")).getAttribute("type"));
Assert.assertEquals("opt3", driver.findElement(By.cssSelector("label[for=selectRadiobuttons-opt3]")).getText());
Assert.assertEquals("radio", driver.findElement(By.cssSelector("input#selectRadiobuttonsWithOptionsFromCustomValidatorAndLabels-vopt1")).getAttribute("type"));
Assert.assertEquals("Option 1", driver.findElement(By.cssSelector("label[for=selectRadiobuttonsWithOptionsFromCustomValidatorAndLabels-vopt1]")).getText());
Assert.assertEquals("radio", driver.findElement(By.cssSelector("input#selectRadiobuttonsWithOptionsFromCustomValidatorAndLabels-vopt2")).getAttribute("type"));
Assert.assertEquals("Username", driver.findElement(By.cssSelector("label[for=selectRadiobuttonsWithOptionsFromCustomValidatorAndLabels-vopt2]")).getText());
Assert.assertEquals("radio", driver.findElement(By.cssSelector("input#selectRadiobuttonsWithOptionsFromCustomValidatorAndLabels-vopt3")).getAttribute("type"));
Assert.assertEquals("vopt3", driver.findElement(By.cssSelector("label[for=selectRadiobuttonsWithOptionsFromCustomValidatorAndLabels-vopt3]")).getText());
Assert.assertEquals("checkbox", driver.findElement(By.cssSelector("input#multiselectCheckboxes-opt1")).getAttribute("type"));
Assert.assertEquals("Option 1", driver.findElement(By.cssSelector("label[for=multiselectCheckboxes-opt1]")).getText());
Assert.assertEquals("checkbox", driver.findElement(By.cssSelector("input#multiselectCheckboxes-opt2")).getAttribute("type"));
Assert.assertEquals("Username", driver.findElement(By.cssSelector("label[for=multiselectCheckboxes-opt2]")).getText());
Assert.assertEquals("checkbox", driver.findElement(By.cssSelector("input#multiselectCheckboxes-opt3")).getAttribute("type"));
Assert.assertEquals("opt3", driver.findElement(By.cssSelector("label[for=multiselectCheckboxes-opt3]")).getText());
}
@Test @Test
public void testAttributeGrouping() { public void testAttributeGrouping() {

View file

@ -300,6 +300,25 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
); );
} }
@Test
public void testAttributeInputTypes() {
setUserProfileConfiguration(CONFIGURATION_FOR_USER_EDIT);
updateUser(user5Id, "ExistingFirst", "ExistingLast", null);
setUserProfileConfiguration("{\"attributes\": ["
+ "{\"name\": \"department\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{}},"
+ RegisterWithUserProfileTest.UP_CONFIG_PART_INPUT_TYPES
+ "]}");
loginPage.open();
loginPage.login("login-test5", "password");
verifyProfilePage.assertCurrent();
RegisterWithUserProfileTest.assertFieldTypes(driver);
}
@Test @Test
public void testEvents() { public void testEvents() {

View file

@ -355,5 +355,22 @@ public class UPConfigParserTest extends AbstractTestRealmKeycloakTest {
Assert.assertEquals(1, errors.size()); Assert.assertEquals(1, errors.size());
Assert.assertEquals("Attribute 'username' references unknown group 'non-existing-group'", errors.get(0)); Assert.assertEquals("Attribute 'username' references unknown group 'non-existing-group'", errors.get(0));
} }
@Test
public void validateConfiguration_attributeAnnotationsErrors() {
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UPConfigParserTest::validateConfiguration_attributeAnnotationsErrors);
}
private static void validateConfiguration_attributeAnnotationsErrors(KeycloakSession session) throws IOException {
UPConfig config = loadValidConfig();
// attribute references group that is not configured
UPAttribute att = config.getAttributes().get(1);
att.getAnnotations().put("inputOptions", "");
att.getAnnotations().put("inputOptionLabels", "");
List<String> errors = validate(session, config);
Assert.assertEquals(2, errors.size());
}
} }

View file

@ -1759,8 +1759,12 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, clientS
for (let key in validator.config) { for (let key in validator.config) {
let values = validator.config[key]; let values = validator.config[key];
for (let k in values) { if (Array.isArray(values)) {
config[key] = values[k]; config[key] = values;
} else {
for (let k in values) {
config[key] = values[k];
}
} }
} }

View file

@ -273,7 +273,7 @@
<td>{{key}}</td> <td>{{key}}</td>
<td><input ng-model="currentAttribute.annotations[key]" class="form-control" type="text" name="{{key}}" <td><input ng-model="currentAttribute.annotations[key]" class="form-control" type="text" name="{{key}}"
id="attribute-{{key}}"/></td> id="attribute-{{key}}"/></td>
<td class="kc-action-cell" data-ng-click="removeAnnotation(key)">{{:: 'delete' | translate}}</td> <td class="kc-action-cell" data-ng-click="removeAttributeAnnotation(key)">{{:: 'delete' | translate}}</td>
</tr> </tr>
<tr> <tr>
<td><input ng-model="newAnnotation.key" class="form-control" type="text" id="newAnnotationKey"/></td> <td><input ng-model="newAnnotation.key" class="form-control" type="text" id="newAnnotationKey"/></td>

View file

@ -17,7 +17,7 @@
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}"> <div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<#if isAppInitiatedAction??> <#if isAppInitiatedAction??>
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" /> <input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
<button class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" type="submit" name="cancel-aia" value="true" />${msg("doCancel")}</button> <button class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" type="submit" name="cancel-aia" value="true" formnovalidate/>${msg("doCancel")}</button>
<#else> <#else>
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" /> <input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
</#if> </#if>

View file

@ -32,25 +32,156 @@
<#nested "beforeField" attribute> <#nested "beforeField" attribute>
<div class="${properties.kcFormGroupClass!}"> <div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}"> <div class="${properties.kcLabelWrapperClass!}">
<label for="${attribute.name}" class="${properties.kcLabelClass!}">${advancedMsg(attribute.displayName!'')}</label> <label for="${attribute.name}" class="${properties.kcLabelClass!}">${advancedMsg(attribute.displayName!'')}</label>
<#if attribute.required>*</#if> <#if attribute.required>*</#if>
</div> </div>
<div class="${properties.kcInputWrapperClass!}"> <div class="${properties.kcInputWrapperClass!}">
<input type="text" id="${attribute.name}" name="${attribute.name}" value="${(attribute.value!'')}" <#if attribute.annotations.inputHelperTextBefore??>
class="${properties.kcInputClass!}" <div class="${properties.kcInputHelperTextBeforeClass!}" id="form-help-text-before-${attribute.name}" aria-live="polite">${kcSanitize(advancedMsg(attribute.annotations.inputHelperTextBefore))?no_esc}</div>
aria-invalid="<#if messagesPerField.existsError('${attribute.name}')>true</#if>" </#if>
<#if attribute.readOnly>disabled</#if> <@inputFieldByType attribute=attribute/>
<#if attribute.autocomplete??>autocomplete="${attribute.autocomplete}"</#if> <#if messagesPerField.existsError('${attribute.name}')>
/> <span id="input-error-${attribute.name}" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('${attribute.name}'))?no_esc}
<#if messagesPerField.existsError('${attribute.name}')> </span>
<span id="input-error-${attribute.name}" class="${properties.kcInputErrorMessageClass!}" aria-live="polite"> </#if>
${kcSanitize(messagesPerField.get('${attribute.name}'))?no_esc} <#if attribute.annotations.inputHelperTextAfter??>
</span> <div class="${properties.kcInputHelperTextAfterClass!}" id="form-help-text-after-${attribute.name}" aria-live="polite">${kcSanitize(advancedMsg(attribute.annotations.inputHelperTextAfter))?no_esc}</div>
</#if> </#if>
</div> </div>
</div> </div>
<#nested "afterField" attribute> <#nested "afterField" attribute>
</#list> </#list>
</#macro>
<#macro inputFieldByType attribute>
<#switch attribute.annotations.inputType!''>
<#case 'textarea'>
<@textareaTag attribute=attribute/>
<#break>
<#case 'select'>
<#case 'multiselect'>
<@selectTag attribute=attribute/>
<#break>
<#case 'select-radiobuttons'>
<#case 'multiselect-checkboxes'>
<@inputTagSelects attribute=attribute/>
<#break>
<#default>
<@inputTag attribute=attribute/>
</#switch>
</#macro>
<#macro inputTag attribute>
<input type="<@inputTagType attribute=attribute/>" id="${attribute.name}" name="${attribute.name}" value="${(attribute.value!'')}" class="${properties.kcInputClass!}"
aria-invalid="<#if messagesPerField.existsError('${attribute.name}')>true</#if>"
<#if attribute.readOnly>disabled</#if>
<#if attribute.autocomplete??>autocomplete="${attribute.autocomplete}"</#if>
<#if attribute.annotations.inputTypePlaceholder??>placeholder="${attribute.annotations.inputTypePlaceholder}"</#if>
<#if attribute.annotations.inputTypePattern??>pattern="${attribute.annotations.inputTypePattern}"</#if>
<#if attribute.annotations.inputTypeSize??>size="${attribute.annotations.inputTypeSize}"</#if>
<#if attribute.annotations.inputTypeMaxlength??>maxlength="${attribute.annotations.inputTypeMaxlength}"</#if>
<#if attribute.annotations.inputTypeMinlength??>minlength="${attribute.annotations.inputTypeMinlength}"</#if>
<#if attribute.annotations.inputTypeMax??>max="${attribute.annotations.inputTypeMax}"</#if>
<#if attribute.annotations.inputTypeMin??>min="${attribute.annotations.inputTypeMin}"</#if>
<#if attribute.annotations.inputTypeStep??>step="${attribute.annotations.inputTypeStep}"</#if>
/>
</#macro>
<#macro inputTagType attribute>
<#compress>
<#if attribute.annotations.inputType??>
<#if attribute.annotations.inputType?starts_with("html5-")>
${attribute.annotations.inputType[6..]}
<#else>
${attribute.annotations.inputType}
</#if>
<#else>
text
</#if>
</#compress>
</#macro>
<#macro textareaTag attribute>
<textarea id="${attribute.name}" name="${attribute.name}" class="${properties.kcInputClass!}"
aria-invalid="<#if messagesPerField.existsError('${attribute.name}')>true</#if>"
<#if attribute.readOnly>disabled</#if>
<#if attribute.annotations.inputTypeCols??>cols="${attribute.annotations.inputTypeCols}"</#if>
<#if attribute.annotations.inputTypeRows??>rows="${attribute.annotations.inputTypeRows}"</#if>
<#if attribute.annotations.inputTypeMaxlength??>maxlength="${attribute.annotations.inputTypeMaxlength}"</#if>
>${(attribute.value!'')}</textarea>
</#macro>
<#macro selectTag attribute>
<select id="${attribute.name}" name="${attribute.name}" class="${properties.kcInputClass!}"
aria-invalid="<#if messagesPerField.existsError('${attribute.name}')>true</#if>"
<#if attribute.readOnly>disabled</#if>
<#if attribute.annotations.inputType=='multiselect'>multiple</#if>
<#if attribute.annotations.inputTypeSize??>size="${attribute.annotations.inputTypeSize}"</#if>
>
<#if attribute.annotations.inputType=='select'>
<option value=""></option>
</#if>
<#if attribute.annotations.inputOptionsFromValidation?? && attribute.validators[attribute.annotations.inputOptionsFromValidation]?? && attribute.validators[attribute.annotations.inputOptionsFromValidation].options??>
<#assign options=attribute.validators[attribute.annotations.inputOptionsFromValidation].options>
<#elseif attribute.validators.options?? && attribute.validators.options.options??>
<#assign options=attribute.validators.options.options>
</#if>
<#if options??>
<#list options as option>
<option value="${option}" <#if attribute.values?seq_contains(option)>selected</#if>><@selectOptionLabelText attribute=attribute option=option/></option>
</#list>
</#if>
</select>
</#macro>
<#macro inputTagSelects attribute>
<#if attribute.annotations.inputType=='select-radiobuttons'>
<#assign inputType='radio'>
<#assign classDiv=properties.kcInputClassRadio!>
<#assign classInput=properties.kcInputClassRadioInput!>
<#assign classLabel=properties.kcInputClassRadioLabel!>
<#else>
<#assign inputType='checkbox'>
<#assign classDiv=properties.kcInputClassCheckbox!>
<#assign classInput=properties.kcInputClassCheckboxInput!>
<#assign classLabel=properties.kcInputClassCheckboxLabel!>
</#if>
<#if attribute.annotations.inputOptionsFromValidation?? && attribute.validators[attribute.annotations.inputOptionsFromValidation]?? && attribute.validators[attribute.annotations.inputOptionsFromValidation].options??>
<#assign options=attribute.validators[attribute.annotations.inputOptionsFromValidation].options>
<#elseif attribute.validators.options?? && attribute.validators.options.options??>
<#assign options=attribute.validators.options.options>
</#if>
<#if options??>
<#list options as option>
<div class="${classDiv}">
<input type="${inputType}" id="${attribute.name}-${option}" name="${attribute.name}" value="${option}" class="${classInput}"
aria-invalid="<#if messagesPerField.existsError('${attribute.name}')>true</#if>"
<#if attribute.readOnly>disabled</#if>
<#if attribute.values?seq_contains(option)>checked</#if>
/>
<label for="${attribute.name}-${option}" class="${classLabel}<#if attribute.readOnly> ${properties.kcInputClassRadioCheckboxLabelDisabled!}</#if>"><@selectOptionLabelText attribute=attribute option=option/></label>
</div>
</#list>
</#if>
</select>
</#macro>
<#macro selectOptionLabelText attribute option>
<#compress>
<#if attribute.annotations.inputOptionLabels??>
${advancedMsg(attribute.annotations.inputOptionLabels[option]!option)}
<#else>
<#if attribute.annotations.inputOptionLabelsI18nPrefix??>
${msg(attribute.annotations.inputOptionLabelsI18nPrefix + '.' + option)}
<#else>
${option}
</#if>
</#if>
</#compress>
</#macro> </#macro>

View file

@ -20,6 +20,10 @@
padding: var(--pf-c-form-control--PaddingTop) var(--pf-c-form-control--PaddingRight) var(--pf-c-form-control--PaddingBottom) var(--pf-c-form-control--PaddingLeft); padding: var(--pf-c-form-control--PaddingTop) var(--pf-c-form-control--PaddingRight) var(--pf-c-form-control--PaddingBottom) var(--pf-c-form-control--PaddingLeft);
} }
textarea.pf-c-form-control {
height: auto;
}
.pf-c-form-control:hover, .pf-c-form-control:focus { .pf-c-form-control:hover, .pf-c-form-control:focus {
border-bottom-color: #0066CC; border-bottom-color: #0066CC;
border-bottom-color: var(--pf-global--primary-color--100); border-bottom-color: var(--pf-global--primary-color--100);
@ -34,6 +38,11 @@
border-bottom-width: var(--pf-global--BorderWidth--md); border-bottom-width: var(--pf-global--BorderWidth--md);
} }
.pf-c-check__label, .pf-c-radio__label {
font-size: 14px;
font-size: var(--pf-global--FontSize--sm);
}
.pf-c-alert.pf-m-inline { .pf-c-alert.pf-m-inline {
margin-bottom: 0.5rem; /* default - IE compatibility */ margin-bottom: 0.5rem; /* default - IE compatibility */
margin-bottom: var(--pf-global--spacer--sm); margin-bottom: var(--pf-global--spacer--sm);

View file

@ -57,6 +57,15 @@ kcFormGroupErrorClass=has-error
kcLabelClass=pf-c-form__label pf-c-form__label-text kcLabelClass=pf-c-form__label pf-c-form__label-text
kcLabelWrapperClass=col-xs-12 col-sm-12 col-md-12 col-lg-12 kcLabelWrapperClass=col-xs-12 col-sm-12 col-md-12 col-lg-12
kcInputClass=pf-c-form-control kcInputClass=pf-c-form-control
kcInputHelperTextBeforeClass=pf-c-form__helper-text pf-c-form__helper-text-before
kcInputHelperTextAfterClass=pf-c-form__helper-text pf-c-form__helper-text-after
kcInputClassRadio=pf-c-radio
kcInputClassRadioInput=pf-c-radio__input
kcInputClassRadioLabel=pf-c-radio__label
kcInputClassCheckbox=pf-c-check
kcInputClassCheckboxInput=pf-c-check__input
kcInputClassCheckboxLabel=pf-c-check__label
kcInputClassRadioCheckboxLabelDisabled=pf-m-disabled
kcInputErrorMessageClass=pf-c-form__helper-text pf-m-error required kc-feedback-text kcInputErrorMessageClass=pf-c-form__helper-text pf-m-error required kc-feedback-text
kcInputWrapperClass=col-xs-12 col-sm-12 col-md-12 col-lg-12 kcInputWrapperClass=col-xs-12 col-sm-12 col-md-12 col-lg-12
kcFormOptionsClass=col-xs-12 col-sm-12 col-md-12 col-lg-12 kcFormOptionsClass=col-xs-12 col-sm-12 col-md-12 col-lg-12