Merge pull request #331 from jenny-s51/erikBlankFields
Realm roles(role attributes): adds initial empty field on load, also adds empty field on save
This commit is contained in:
commit
a73b2d3a4b
4 changed files with 105 additions and 90 deletions
|
@ -8,14 +8,14 @@ import {
|
|||
ValidatedOptions,
|
||||
} from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SubmitHandler, UseFormMethods } from "react-hook-form";
|
||||
import { UseFormMethods } from "react-hook-form";
|
||||
|
||||
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
|
||||
import { FormAccess } from "../components/form-access/FormAccess";
|
||||
import { RoleFormType } from "./RealmRoleTabs";
|
||||
|
||||
export type RealmRoleFormProps = {
|
||||
form: UseFormMethods;
|
||||
save: SubmitHandler<RoleRepresentation>;
|
||||
form: UseFormMethods<RoleFormType>;
|
||||
save: (role: RoleFormType) => void;
|
||||
editMode: boolean;
|
||||
reset: () => void;
|
||||
};
|
||||
|
@ -27,7 +27,6 @@ export const RealmRoleForm = ({
|
|||
reset,
|
||||
}: RealmRoleFormProps) => {
|
||||
const { t } = useTranslation("roles");
|
||||
|
||||
return (
|
||||
<FormAccess
|
||||
isHorizontal
|
||||
|
@ -81,7 +80,7 @@ export const RealmRoleForm = ({
|
|||
<Button variant="primary" type="submit">
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button onClick={reset} variant="link">
|
||||
<Button onClick={() => reset()} variant="link">
|
||||
{editMode ? t("common:reload") : t("common:cancel")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
TabTitleText,
|
||||
} from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
|
@ -30,81 +30,78 @@ const arrayToAttributes = (attributeArray: KeyValueType[]) => {
|
|||
}, initValue);
|
||||
};
|
||||
|
||||
const attributesToArray = (attributes: { [key: string]: string }): any => {
|
||||
const attributesToArray = (attributes?: {
|
||||
[key: string]: string[];
|
||||
}): KeyValueType[] => {
|
||||
if (!attributes || Object.keys(attributes).length == 0) {
|
||||
return [
|
||||
{
|
||||
key: "",
|
||||
value: "",
|
||||
},
|
||||
];
|
||||
return [];
|
||||
}
|
||||
return Object.keys(attributes).map((key) => ({
|
||||
key: key,
|
||||
value: attributes[key],
|
||||
value: attributes[key][0],
|
||||
}));
|
||||
};
|
||||
|
||||
export type RoleFormType = Omit<RoleRepresentation, "attributes"> & {
|
||||
attributes: KeyValueType[];
|
||||
};
|
||||
|
||||
export const RealmRoleTabs = () => {
|
||||
const { t } = useTranslation("roles");
|
||||
const form = useForm<RoleRepresentation>({ mode: "onChange" });
|
||||
const form = useForm<RoleFormType>({ mode: "onChange" });
|
||||
const history = useHistory();
|
||||
const [name, setName] = useState("");
|
||||
const adminClient = useAdminClient();
|
||||
const { realm } = useRealm();
|
||||
const [role, setRole] = useState<RoleRepresentation>();
|
||||
const [role, setRole] = useState<RoleFormType>();
|
||||
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
const { addAlert } = useAlerts();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const convert = (role: RoleRepresentation) => {
|
||||
const { attributes, ...rest } = role;
|
||||
return {
|
||||
attributes: attributesToArray(attributes),
|
||||
...rest,
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (id) {
|
||||
const fetchedRole = await adminClient.roles.findOneById({ id });
|
||||
setName(fetchedRole.name!);
|
||||
setupForm(fetchedRole);
|
||||
setRole(fetchedRole);
|
||||
} else {
|
||||
setName(t("createRole"));
|
||||
const convertedRole = convert(fetchedRole);
|
||||
Object.entries(convertedRole).map((entry) => {
|
||||
form.setValue(entry[0], entry[1]);
|
||||
});
|
||||
setRole(convertedRole);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const setupForm = (role: RoleRepresentation) => {
|
||||
Object.entries(role).map((entry) => {
|
||||
if (entry[0] === "attributes") {
|
||||
form.setValue(entry[0], attributesToArray(entry[1]));
|
||||
} else {
|
||||
form.setValue(entry[0], entry[1]);
|
||||
}
|
||||
});
|
||||
};
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "attributes",
|
||||
});
|
||||
|
||||
// reset form to default values
|
||||
const reset = () => {
|
||||
setupForm(role!);
|
||||
};
|
||||
useEffect(() => append({ key: "", value: "" }), [append, role]);
|
||||
|
||||
const save = async (role: RoleRepresentation) => {
|
||||
const save = async (role: RoleFormType) => {
|
||||
try {
|
||||
const { attributes, ...rest } = role;
|
||||
const roleRepresentation: RoleRepresentation = rest;
|
||||
if (id) {
|
||||
if (role.attributes) {
|
||||
// react-hook-form will use `KeyValueType[]` here we convert it back into an indexed property of string[]
|
||||
role.attributes = arrayToAttributes(
|
||||
(role.attributes as unknown) as KeyValueType[]
|
||||
);
|
||||
if (attributes) {
|
||||
roleRepresentation.attributes = arrayToAttributes(attributes);
|
||||
}
|
||||
setRole(role!);
|
||||
setupForm(role!);
|
||||
await adminClient.roles.updateById({ id }, role);
|
||||
await adminClient.roles.updateById({ id }, roleRepresentation);
|
||||
setRole(role);
|
||||
} else {
|
||||
await adminClient.roles.create(role);
|
||||
await adminClient.roles.create(roleRepresentation);
|
||||
const createdRole = await adminClient.roles.findOneByName({
|
||||
name: role.name!,
|
||||
});
|
||||
setRole(convert(createdRole));
|
||||
history.push(`/${realm}/roles/${createdRole.id}`);
|
||||
}
|
||||
addAlert(t(id ? "roleSaveSuccess" : "roleCreated"), AlertVariant.success);
|
||||
|
@ -120,7 +117,9 @@ export const RealmRoleTabs = () => {
|
|||
|
||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
titleKey: "roles:roleDeleteConfirm",
|
||||
messageKey: t("roles:roleDeleteConfirmDialog", { name }),
|
||||
messageKey: t("roles:roleDeleteConfirmDialog", {
|
||||
name: role?.name || t("createRole"),
|
||||
}),
|
||||
continueButtonLabel: "common:delete",
|
||||
continueButtonVariant: ButtonVariant.danger,
|
||||
onConfirm: async () => {
|
||||
|
@ -141,7 +140,7 @@ export const RealmRoleTabs = () => {
|
|||
<DeleteConfirm />
|
||||
<AssociatedRolesModal open={open} toggleDialog={() => setOpen(!open)} />
|
||||
<ViewHeader
|
||||
titleKey={name}
|
||||
titleKey={role?.name || t("createRole")}
|
||||
subKey={id ? "" : "roles:roleCreateExplain"}
|
||||
dropdownItems={
|
||||
id
|
||||
|
@ -172,7 +171,7 @@ export const RealmRoleTabs = () => {
|
|||
title={<TabTitleText>{t("details")}</TabTitleText>}
|
||||
>
|
||||
<RealmRoleForm
|
||||
reset={reset}
|
||||
reset={() => form.reset(role)}
|
||||
form={form}
|
||||
save={save}
|
||||
editMode={true}
|
||||
|
@ -182,13 +181,18 @@ export const RealmRoleTabs = () => {
|
|||
eventKey="attributes"
|
||||
title={<TabTitleText>{t("attributes")}</TabTitleText>}
|
||||
>
|
||||
<RoleAttributes form={form} save={save} reset={reset} />
|
||||
<RoleAttributes
|
||||
form={form}
|
||||
save={save}
|
||||
array={{ fields, append, remove }}
|
||||
reset={() => form.reset(role)}
|
||||
/>
|
||||
</Tab>
|
||||
</KeycloakTabs>
|
||||
)}
|
||||
{!id && (
|
||||
<RealmRoleForm
|
||||
reset={reset}
|
||||
reset={() => form.reset()}
|
||||
form={form}
|
||||
save={save}
|
||||
editMode={false}
|
||||
|
|
|
@ -7,8 +7,7 @@ import {
|
|||
ButtonVariant,
|
||||
PageSection,
|
||||
} from "@patternfly/react-core";
|
||||
import { IFormatter, IFormatterValueType } from "@patternfly/react-table";
|
||||
|
||||
import { boolFormatter } from "../util";
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
|
||||
|
@ -17,7 +16,7 @@ import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable
|
|||
import { formattedLinkTableCell } from "../components/external-link/FormattedLink";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||
import { emptyFormatter, toUpperCase } from "../util";
|
||||
import { emptyFormatter } from "../util";
|
||||
|
||||
export const RealmRolesSection = () => {
|
||||
const { t } = useTranslation("roles");
|
||||
|
@ -45,12 +44,6 @@ export const RealmRolesSection = () => {
|
|||
</>
|
||||
);
|
||||
|
||||
const boolFormatter = (): IFormatter => (data?: IFormatterValueType) => {
|
||||
const boolVal = data?.toString();
|
||||
|
||||
return (boolVal ? toUpperCase(boolVal) : undefined) as string;
|
||||
};
|
||||
|
||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
titleKey: "roles:roleDeleteConfirm",
|
||||
messageKey: t("roles:roleDeleteConfirmDialog", {
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ArrayField, UseFormMethods } from "react-hook-form";
|
||||
import { ActionGroup, Button, TextInput } from "@patternfly/react-core";
|
||||
import { useFieldArray, UseFormMethods } from "react-hook-form";
|
||||
import "./RealmRolesSection.css";
|
||||
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
|
||||
|
||||
import {
|
||||
TableComposable,
|
||||
Tbody,
|
||||
|
@ -13,34 +11,42 @@ import {
|
|||
Tr,
|
||||
} from "@patternfly/react-table";
|
||||
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { FormAccess } from "../components/form-access/FormAccess";
|
||||
import { RoleFormType } from "./RealmRoleTabs";
|
||||
|
||||
import "./RealmRolesSection.css";
|
||||
|
||||
export type KeyValueType = { key: string; value: string };
|
||||
|
||||
type RoleAttributesProps = {
|
||||
form: UseFormMethods;
|
||||
save: (role: RoleRepresentation) => void;
|
||||
form: UseFormMethods<RoleFormType>;
|
||||
save: (role: RoleFormType) => void;
|
||||
reset: () => void;
|
||||
array: {
|
||||
fields: Partial<ArrayField<Record<string, any>, "id">>[];
|
||||
append: (
|
||||
value: Partial<Record<string, any>> | Partial<Record<string, any>>[],
|
||||
shouldFocus?: boolean | undefined
|
||||
) => void;
|
||||
remove: (index?: number | number[] | undefined) => void;
|
||||
};
|
||||
};
|
||||
|
||||
export const RoleAttributes = ({ form, save, reset }: RoleAttributesProps) => {
|
||||
export const RoleAttributes = ({
|
||||
form: { handleSubmit, register, formState, errors, watch },
|
||||
save,
|
||||
array: { fields, append, remove },
|
||||
reset,
|
||||
}: RoleAttributesProps) => {
|
||||
const { t } = useTranslation("roles");
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "attributes",
|
||||
});
|
||||
|
||||
const columns = ["Key", "Value"];
|
||||
|
||||
const onAdd = () => {
|
||||
append({ key: "", value: "" });
|
||||
};
|
||||
const watchFirstKey = watch("attributes[0].key");
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormAccess role="manage-realm" onSubmit={form.handleSubmit(save)}>
|
||||
<FormAccess role="manage-realm" onSubmit={handleSubmit(save)}>
|
||||
<TableComposable
|
||||
className="kc-role-attributes__table"
|
||||
aria-label="Role attribute keys and values"
|
||||
|
@ -67,9 +73,14 @@ export const RoleAttributes = ({ form, save, reset }: RoleAttributesProps) => {
|
|||
>
|
||||
<TextInput
|
||||
name={`attributes[${rowIndex}].key`}
|
||||
ref={form.register({ required: true })}
|
||||
ref={register({ required: true })}
|
||||
aria-label="key-input"
|
||||
defaultValue={attribute.key}
|
||||
validated={
|
||||
errors.attributes && errors.attributes[rowIndex]
|
||||
? "error"
|
||||
: "default"
|
||||
}
|
||||
/>
|
||||
</Td>
|
||||
<Td
|
||||
|
@ -79,9 +90,10 @@ export const RoleAttributes = ({ form, save, reset }: RoleAttributesProps) => {
|
|||
>
|
||||
<TextInput
|
||||
name={`attributes[${rowIndex}].value`}
|
||||
ref={form.register({})}
|
||||
ref={register()}
|
||||
aria-label="value-input"
|
||||
defaultValue={attribute.value}
|
||||
validated={errors.description ? "error" : "default"}
|
||||
/>
|
||||
</Td>
|
||||
{rowIndex !== fields.length - 1 && fields.length - 1 !== 0 && (
|
||||
|
@ -103,14 +115,25 @@ export const RoleAttributes = ({ form, save, reset }: RoleAttributesProps) => {
|
|||
)}
|
||||
{rowIndex === fields.length - 1 && (
|
||||
<Td key="add-button" id="add-button" dataLabel={columns[2]}>
|
||||
{fields[rowIndex].key === "" && (
|
||||
<Button
|
||||
id={`minus-button-${rowIndex}`}
|
||||
aria-label={`remove ${attribute.key} with value ${attribute.value} `}
|
||||
variant="link"
|
||||
className="kc-role-attributes__minus-icon"
|
||||
onClick={() => remove(rowIndex)}
|
||||
>
|
||||
<MinusCircleIcon />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
aria-label={t("roles:addAttributeText")}
|
||||
id="plus-icon"
|
||||
variant="link"
|
||||
className="kc-role-attributes__plus-icon"
|
||||
onClick={onAdd}
|
||||
onClick={() => append({ key: "", value: "" })}
|
||||
icon={<PlusCircleIcon />}
|
||||
isDisabled={!form.formState.isValid}
|
||||
isDisabled={!formState.isValid}
|
||||
/>
|
||||
</Td>
|
||||
)}
|
||||
|
@ -119,15 +142,11 @@ export const RoleAttributes = ({ form, save, reset }: RoleAttributesProps) => {
|
|||
</Tbody>
|
||||
</TableComposable>
|
||||
<ActionGroup className="kc-role-attributes__action-group">
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
isDisabled={!form.formState.isValid}
|
||||
>
|
||||
<Button variant="primary" type="submit" isDisabled={!watchFirstKey}>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button variant="link" onClick={reset}>
|
||||
{t("common:reload")}{" "}
|
||||
<Button onClick={reset} variant="link">
|
||||
{t("common:reload")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
|
|
Loading…
Reference in a new issue