KEYCLOAK-18497 - Support different input types in built-in dynamic forms
This commit is contained in:
parent
375e47877e
commit
28e220fa6d
22 changed files with 611 additions and 46 deletions
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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());
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
org.keycloak.testsuite.validate.DummyOptionsValidator
|
|
@ -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();
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue