Skip to content

Commit

Permalink
Allow setting an attribute as multivalued
Browse files Browse the repository at this point in the history
Closes keycloak#23539

Signed-off-by: Pedro Igor <[email protected]>

Co-authored-by: Jon Koops <[email protected]>
Co-authored-by: Erik Jan de Wit <[email protected]>
  • Loading branch information
3 people committed Feb 22, 2024
1 parent 1e12b15 commit 6509f91
Show file tree
Hide file tree
Showing 40 changed files with 1,278 additions and 450 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,22 @@ public class UserProfileAttributeMetadata {
private Map<String, Object> annotations;
private Map<String, Map<String, Object>> validators;
private String group;
private boolean multivalued;

public UserProfileAttributeMetadata() {

}

public UserProfileAttributeMetadata(String name, String displayName, boolean required, boolean readOnly, String group, Map<String, Object> annotations,
Map<String, Map<String, Object>> validators) {
Map<String, Map<String, Object>> validators, boolean multivalued) {
this.name = name;
this.displayName = displayName;
this.required = required;
this.readOnly = readOnly;
this.annotations = annotations;
this.validators = validators;
this.group = group;
this.multivalued = multivalued;
}

public String getName() {
Expand Down Expand Up @@ -85,4 +87,11 @@ public Map<String, Map<String, Object>> getValidators() {
return validators;
}

public void setMultivalued(boolean multivalued) {
this.multivalued = multivalued;
}

public boolean isMultivalued() {
return multivalued;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class UPAttribute implements Cloneable {
/** null means it is always selected */
private UPAttributeSelector selector;
private String group;
private boolean multivalued;

public UPAttribute() {
}
Expand Down Expand Up @@ -71,6 +72,11 @@ public UPAttribute(String name, UPAttributePermissions permissions) {
this(name, permissions, null);
}

public UPAttribute(String name, boolean multivalued, UPAttributePermissions permissions) {
this(name, permissions, null);
setMultivalued(multivalued);
}

public String getName() {
return name;
}
Expand Down Expand Up @@ -142,9 +148,17 @@ public void setGroup(String group) {
this.group = group != null ? group.trim() : null;
}

public void setMultivalued(boolean multivalued) {
this.multivalued = multivalued;
}

public boolean isMultivalued() {
return multivalued;
}

@Override
public String toString() {
return "UPAttribute [name=" + name + ", displayName=" + displayName + ", permissions=" + permissions + ", selector=" + selector + ", required=" + required + ", validations=" + validations + ", annotations=" + annotations + ", group=" + group + "]";
return "UPAttribute [name=" + name + ", displayName=" + displayName + ", permissions=" + permissions + ", selector=" + selector + ", required=" + required + ", validations=" + validations + ", annotations=" + annotations + ", group=" + group + ", multivalued=" + multivalued + "]";
}

@Override
Expand All @@ -169,6 +183,7 @@ protected UPAttribute clone() {
attr.setPermissions(this.permissions == null ? null : this.permissions.clone());
attr.setSelector(this.selector == null ? null : this.selector.clone());
attr.setGroup(this.group);
attr.setMultivalued(this.multivalued);
return attr;
}

Expand All @@ -193,6 +208,7 @@ public boolean equals(Object obj) {
&& Objects.equals(this.annotations, other.annotations)
&& Objects.equals(this.required, other.required)
&& Objects.equals(this.permissions, other.permissions)
&& Objects.equals(this.selector, other.selector);
&& Objects.equals(this.selector, other.selector)
&& Objects.equals(this.multivalued, other.multivalued);
}
}
62 changes: 59 additions & 3 deletions docs/documentation/server_admin/topics/users/user-profile.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ The name of the attribute, used to uniquely identify an attribute.
Display name::
A user-friendly name for the attribute, mainly used when rendering user-facing forms. It also supports link:#_using-internationalized-messages[Using Internationalized Messages]

Multivalued::
If enabled, the attribute supports multiple values and UIs are rendered accordingly to allow setting many values. When enabling this
setting, make sure to add a validator to set a hard limit to the number of values.

Attribute Group::
The attribute group to which the attribute belongs to, if any.

Expand Down Expand Up @@ -293,6 +297,15 @@ The list below provides a list of all the built-in validators:

*error-message*: the key of the error message in i18n bundle. If not set a generic message is used.

|multivalued
|Validates the size of a multivalued attribute.
|

*min*: an integer to define the minimum allowed count of attribute values.

*max*: an integer to define the maximum allowed count of attribute values.


|===

[[_defining-ui-annotations]]
Expand Down Expand Up @@ -580,9 +593,52 @@ translate into an HTML attribute in the corresponding element of an attribute, p
the same name will be loaded to the dynamic pages so that you can select elements from the DOM based on the custom `data-` attribute
and decorate them accordingly by modifying their DOM representation.

For instance, if you add a `kcMyCustomValidation` annotation to a field, the HTML attribute `data-kcMyCustomValidation` is added to
the corresponding HTML element for the attribute, and a JavaScript file is loaded from your custom theme at `<THEME TYPE>/resources/js/kcMyCustomValidation.js`. See the {developerguide_link}[{developerguide_name}] for more information about
how to deploy a custom JS script file to your theme.
For instance, if you add a `kcMyCustomValidation` annotation to an attribute, the HTML attribute `data-kcMyCustomValidation` is added to
the corresponding HTML element for the attribute, and a JavaScript module is loaded from your custom theme at `<THEME TYPE>/resources/js/kcMyCustomValidation.js`.
See the {developerguide_link}[{developerguide_name}] for more information about how to deploy a custom JavaScript module to your theme.

The JavaScript module can run any code to customize the DOM and the elements rendered for each attribute. For that,
you can use the `userProfile.js` module to register an annotation descriptor for your custom annotation as follows:

[source,javascript]
----
import { registerElementAnnotatedBy } from "./userProfile.js";
registerElementAnnotatedBy({
name: 'kcMyCustomValidation',
onAdd(element) {
var listener = function (event) {
// do something on keyup
};
element.addEventListener("keyup", listener);
// returns a cleanup function to remove the event listener
return () => element.removeEventListener("keyup", listener);
}
});
----

The `registerElementAnnotatedBy` is a method to register annotation descriptors. A descriptor is an object with a `name`,
referencing the annotation name,
and a `onAdd` function. Whenever the page is rendered or an attribute with the annotation is added to the DOM, the `onAdd`
function is invoked so that you can customize the behavior for the element.

The `onAdd` function can also return a function to perform a cleanup. For instance, if you are adding event listeners
to elements, you might want to remove them in case the element is removed from the DOM.

Alternatively, you can also use any JavaScript code you want if the `userProfile.js` is not enough for your needs:

[source,javascript]
----
document.querySelectorAll(`[data-kcMyCustomValidation]`).forEach((element) => {
var listener = function (evt) {
// do something on keyup
};
element.addEventListener("keyup", listener);
});
----

== Managing Attribute Groups

Expand Down
1 change: 1 addition & 0 deletions js/apps/account-ui/src/api/representations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export interface UserProfileAttributeMetadata {
readOnly: boolean;
annotations?: { [index: string]: any };
validators: { [index: string]: { [index: string]: any } };
multivalued: boolean;
}

export interface UserProfileMetadata {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3060,3 +3060,7 @@ bruteForceMode.PermanentLockout=Lockout permanently
bruteForceMode.TemporaryLockout=Lockout temporarily
bruteForceMode.PermanentAfterTemporaryLockout=Lockout permanently after temporary lockout
bruteForceMode=Brute Force Mode
error-invalid-multivalued-size=Attribute {{0}} must have at least {{1}} and at most {{2}} value(s).
multivalued=Multivalued
multivaluedHelp=If this attribute supports multiple values. This setting is an indicator and does not enable any validation
to the attribute. For that, make sure to use any of the built-in validators to properly validate the size and the values.
4 changes: 4 additions & 0 deletions js/apps/admin-ui/src/realm-settings/NewAttributeSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export default function NewAttributeSettings() {
permissions,
selector,
required,
multivalued,
...values
} = config.attributes!.find(
(attribute) => attribute.name === attributeName,
Expand Down Expand Up @@ -172,6 +173,7 @@ export default function NewAttributeSettings() {
})),
);
form.setValue("isRequired", required !== undefined);
form.setValue("multivalued", multivalued === true);
},
[],
);
Expand Down Expand Up @@ -217,6 +219,7 @@ export default function NewAttributeSettings() {
displayName: formFields.displayName!,
selector: formFields.selector,
permissions: formFields.permissions!,
multivalued: formFields.multivalued,
annotations,
validations,
},
Expand All @@ -234,6 +237,7 @@ export default function NewAttributeSettings() {
required: formFields.isRequired ? formFields.required : undefined,
selector: formFields.selector,
permissions: formFields.permissions!,
multivalued: formFields.multivalued,
annotations,
validations,
},
Expand Down
Loading

0 comments on commit 6509f91

Please sign in to comment.