added default values for user profile annotations (#20949)

* initial version of the "known" annotation picker

* fixed empty value

* add input type select when validator is options

* fixed test

* Update js/apps/admin-ui/public/locales/en/common.json

Co-authored-by: Jon Koops <jonkoops@gmail.com>

---------

Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Erik Jan de Wit 2023-06-19 10:06:40 +02:00 committed by GitHub
parent 39e3820e9e
commit 0ca9b21765
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 258 additions and 31 deletions

View file

@ -18,8 +18,8 @@ export default class UserProfile {
private newAttributeRequiredWhen = 'input[name="requiredWhen"]';
private newAttributeEmptyValidators = ".kc-emptyValidators";
private newAttributeAnnotationBtn = "annotations-add-row";
private newAttributeAnnotationKey = "annotations-key";
private newAttributeAnnotationValue = "annotations-value";
private newAttributeAnnotationKey = "annotations.0.key";
private newAttributeAnnotationValue = "annotations.0.value";
private validatorRolesList = "#validator";
private validatorsList = 'tbody [data-label="name"]';
private saveNewAttributeBtn = "attribute-create";

View file

@ -214,5 +214,6 @@
"forbidden_one": "Forbidden, permission needed:",
"forbidden_other": "Forbidden, permissions needed:",
"noRealmRolesToAssign": "There are no realm roles to assign",
"loadingRealms": "Loading realms…"
"loadingRealms": "Loading realms…",
"customAttribute": "Custom Attribute…"
}

View file

@ -18,6 +18,15 @@
"displayDescriptionField": "Display description",
"displayDescriptionHint": "A text that should be used as a tooltip when rendering user-facing forms.",
"annotationsText": "Annotations",
"inputType": "Input type",
"inputHelperTextBefore": "Helper text (above) the input field",
"inputHelperTextAfter": "Helper text (under) the input field",
"inputOptionLabelsI18nPrefix": "Internationalization key prefix",
"inputTypePlaceholder": "Input placeholder",
"inputTypeSize": "Input size",
"inputTypeCols": "Input cols",
"inputTypeRows": "Input rows",
"inputTypeStep": "Input step size",
"removeAnnotationText": "Remove annotation",
"keyPlaceholder": "Type a key",
"keyLabel": "Key",

View file

@ -0,0 +1,62 @@
import { Grid, GridItem, Select, SelectOption } from "@patternfly/react-core";
import { useState } from "react";
import { UseControllerProps, useController } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { KeycloakTextInput } from "ui-shared";
import useToggle from "../../utils/useToggle";
import { DefaultValue } from "./KeyValueInput";
type KeySelectProp = UseControllerProps & {
selectItems: DefaultValue[];
};
export const KeySelect = ({ selectItems, ...rest }: KeySelectProp) => {
const { t } = useTranslation("common");
const [open, toggle] = useToggle();
const { field } = useController(rest);
const [custom, setCustom] = useState(
!selectItems.map(({ key }) => key).includes(field.value)
);
return (
<Grid>
<GridItem lg={custom ? 2 : 12}>
<Select
onToggle={() => toggle()}
isOpen={open}
onSelect={(_, value) => {
if (value) {
setCustom(false);
}
field.onChange(value);
toggle();
}}
selections={!custom ? [field.value] : ""}
>
{[
<SelectOption key="custom" onClick={() => setCustom(true)}>
{t("customAttribute")}
</SelectOption>,
...selectItems.map((item) => (
<SelectOption key={item.key} value={item.key}>
{item.label}
</SelectOption>
)),
]}
</Select>
</GridItem>
{custom && (
<GridItem lg={10}>
<KeycloakTextInput
id="customValue"
data-testid={rest.name}
placeholder={t("keyPlaceholder")}
value={field.value}
onChange={field.onChange}
autoFocus
/>
</GridItem>
)}
</Grid>
);
};

View file

@ -8,20 +8,36 @@ import {
GridItem,
HelperText,
HelperTextItem,
InputGroup,
} from "@patternfly/react-core";
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
import { Fragment } from "react";
import { useFieldArray, useFormContext } from "react-hook-form";
import {
FieldValues,
useFieldArray,
useFormContext,
useWatch,
} from "react-hook-form";
import { useTranslation } from "react-i18next";
import { KeycloakTextInput } from "../keycloak-text-input/KeycloakTextInput";
import { KeySelect } from "./KeySelect";
import { ValueSelect } from "./ValueSelect";
export type DefaultValue = {
key: string;
values?: string[];
label: string;
};
type KeyValueInputProps = {
name: string;
defaultKeyValue?: DefaultValue[];
};
export const KeyValueInput = ({ name }: KeyValueInputProps) => {
export const KeyValueInput = ({
name,
defaultKeyValue,
}: KeyValueInputProps) => {
const { t } = useTranslation("common");
const {
control,
@ -30,19 +46,26 @@ export const KeyValueInput = ({ name }: KeyValueInputProps) => {
} = useFormContext();
const { fields, append, remove } = useFieldArray({
shouldUnregister: true,
control,
name,
});
const appendNew = () => append({ key: "", value: "" });
const values = useWatch<FieldValues>({
name,
control,
defaultValue: [],
});
return fields.length > 0 ? (
<>
<Grid hasGutter>
<GridItem className="pf-c-form__label" span={6}>
<GridItem className="pf-c-form__label" span={5}>
<span className="pf-c-form__label-text">{t("key")}</span>
</GridItem>
<GridItem className="pf-c-form__label" span={6}>
<GridItem className="pf-c-form__label" span={7}>
<span className="pf-c-form__label-text">{t("value")}</span>
</GridItem>
{fields.map((attribute, index) => {
@ -51,15 +74,23 @@ export const KeyValueInput = ({ name }: KeyValueInputProps) => {
return (
<Fragment key={attribute.id}>
<GridItem span={6}>
<KeycloakTextInput
placeholder={t("keyPlaceholder")}
aria-label={t("key")}
data-testid={`${name}-key`}
{...register(`${name}.${index}.key`, { required: true })}
validated={keyError ? "error" : "default"}
isRequired
/>
<GridItem span={5}>
{defaultKeyValue ? (
<KeySelect
name={`${name}.${index}.key`}
selectItems={defaultKeyValue}
rules={{ required: true }}
/>
) : (
<KeycloakTextInput
placeholder={t("keyPlaceholder")}
aria-label={t("key")}
data-testid={`${name}-key`}
{...register(`${name}.${index}.key`, { required: true })}
validated={keyError ? "error" : "default"}
isRequired
/>
)}
{keyError && (
<HelperText>
<HelperTextItem variant="error">
@ -68,8 +99,15 @@ export const KeyValueInput = ({ name }: KeyValueInputProps) => {
</HelperText>
)}
</GridItem>
<GridItem span={6}>
<InputGroup>
<GridItem span={5}>
{defaultKeyValue ? (
<ValueSelect
name={`${name}.${index}.value`}
keyValue={values[index]?.key}
selectItems={defaultKeyValue}
rules={{ required: true }}
/>
) : (
<KeycloakTextInput
placeholder={t("valuePlaceholder")}
aria-label={t("value")}
@ -78,15 +116,7 @@ export const KeyValueInput = ({ name }: KeyValueInputProps) => {
validated={valueError ? "error" : "default"}
isRequired
/>
<Button
variant="link"
title={t("removeAttribute")}
onClick={() => remove(index)}
data-testid={`${name}-remove`}
>
<MinusCircleIcon />
</Button>
</InputGroup>
)}
{valueError && (
<HelperText>
<HelperTextItem variant="error">
@ -95,6 +125,16 @@ export const KeyValueInput = ({ name }: KeyValueInputProps) => {
</HelperText>
)}
</GridItem>
<GridItem span={2}>
<Button
variant="link"
title={t("removeAttribute")}
onClick={() => remove(index)}
data-testid={`${name}-remove`}
>
<MinusCircleIcon />
</Button>
</GridItem>
</Fragment>
);
})}

View file

@ -0,0 +1,46 @@
import { Select, SelectOption } from "@patternfly/react-core";
import { useMemo, useState } from "react";
import { UseControllerProps, useController } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { KeycloakTextInput } from "ui-shared";
import { DefaultValue } from "./KeyValueInput";
type ValueSelectProps = UseControllerProps & {
selectItems: DefaultValue[];
keyValue: string;
};
export const ValueSelect = ({
selectItems,
keyValue,
...rest
}: ValueSelectProps) => {
const { t } = useTranslation();
const { field } = useController(rest);
const [open, setOpen] = useState(false);
const defaultItem = useMemo(
() => selectItems.find((v) => v.key === keyValue),
[selectItems, keyValue]
);
return defaultItem?.values ? (
<Select
onToggle={(isOpen) => setOpen(isOpen)}
isOpen={open}
onSelect={(_, value) => {
field.onChange(value);
setOpen(false);
}}
selections={field.value ? [field.value] : t("choose")}
placeholder={t("valuePlaceholder")}
>
{defaultItem.values.map((item) => (
<SelectOption key={item} value={item} />
))}
</Select>
) : (
<KeycloakTextInput id="customValue" data-testid={rest.name} {...field} />
);
};

View file

@ -19,7 +19,65 @@ export const AttributeAnnotations = () => {
>
<Grid className="kc-annotations">
<GridItem>
<KeyValueInput name="annotations" />
<KeyValueInput
name="annotations"
defaultKeyValue={[
{
key: "inputType",
label: t("inputType"),
values: [
"text",
"textarea",
"select",
"select-radiobuttons",
"multiselect",
"multiselect-checkboxes",
"html5-email",
"html5-tel",
"html5-url",
"html5-number",
"html5-range",
"html5-datetime-local",
"html5-date",
"html5-month",
"html5-week",
"html5-time",
],
},
{
key: "inputHelperTextBefore",
label: t("inputHelperTextBefore"),
},
{
key: "inputHelperTextAfter",
label: t("inputHelperTextAfter"),
},
{
key: "inputOptionLabelsI18nPrefix",
label: t("inputOptionLabelsI18nPrefix"),
},
{
key: "inputTypePlaceholder",
label: t("inputTypePlaceholder"),
},
{
key: "inputTypeSize",
label: t("inputTypeSize"),
},
{
key: "inputTypeCols",
label: t("inputTypeCols"),
},
{
key: "inputTypeRows",
label: t("inputTypeRows"),
},
{
key: "inputTypeStep",
label: t("inputTypeStep"),
},
]}
/>
</GridItem>
</Grid>
</FormGroup>

View file

@ -19,6 +19,7 @@ import { useFormContext, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useConfirmDialog } from "../../../components/confirm-dialog/ConfirmDialog";
import { DefaultValue } from "../../../components/key-value-form/KeyValueInput";
import useToggle from "../../../utils/useToggle";
import type { IndexedValidations } from "../../NewAttributeSettings";
import { AddValidatorDialog } from "../attribute/AddValidatorDialog";
@ -29,7 +30,7 @@ export const AttributeValidations = () => {
const { t } = useTranslation("realm-settings");
const [addValidatorModalOpen, toggleModal] = useToggle();
const [validatorToDelete, setValidatorToDelete] = useState<string>();
const { setValue, control, register } = useFormContext();
const { setValue, control, register, getValues } = useFormContext();
const validators: IndexedValidations[] = useWatch({
name: "validations",
@ -39,7 +40,7 @@ export const AttributeValidations = () => {
useEffect(() => {
register("validations");
}, []);
}, [register]);
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: t("deleteValidatorConfirmTitle"),
@ -63,6 +64,16 @@ export const AttributeValidations = () => {
<AddValidatorDialog
selectedValidators={validators}
onConfirm={(newValidator) => {
const annotations: DefaultValue[] = getValues("annotations");
if (
newValidator.id === "options" &&
!annotations.find((a) => a.key === "inputType")
) {
setValue("annotations", [
...annotations,
{ key: "inputType", value: "select" },
]);
}
setValue("validations", [
...validators,
{ key: newValidator.id, value: newValidator.config },