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:
parent
39e3820e9e
commit
0ca9b21765
8 changed files with 258 additions and 31 deletions
|
@ -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";
|
||||
|
|
|
@ -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…"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
62
js/apps/admin-ui/src/components/key-value-form/KeySelect.tsx
Normal file
62
js/apps/admin-ui/src/components/key-value-form/KeySelect.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -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} />
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
|
|
|
@ -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 },
|
||||
|
|
Loading…
Reference in a new issue