Use string[] directly removed need for converstion (#2175)

This commit is contained in:
Erik Jan de Wit 2022-03-09 17:42:23 +01:00 committed by GitHub
parent ae133d675b
commit 37d1087e7e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 86 additions and 221 deletions

View file

@ -96,7 +96,7 @@ export default class AuthorizationTab {
if (Array.isArray(value)) {
for (let index = 0; index < value.length; index++) {
const v = value[index];
cy.get(`input[name="${key}[${index}].value"]`).type(v);
cy.get(`input[name="${key}[${index}]"]`).type(v);
cy.findByTestId("addValue").click();
}
} else {

View file

@ -144,11 +144,6 @@ const WebauthnSelect = ({
);
};
const MULTILINE_INPUTS = [
"webAuthnPolicyAcceptableAaguids",
"webAuthnPolicyPasswordlessAcceptableAaguids",
];
type WebauthnPolicyProps = {
realm: RealmRepresentation;
realmUpdated: (realm: RealmRepresentation) => void;
@ -180,12 +175,12 @@ export const WebauthnPolicy = ({
: "webAuthnPolicy";
const setupForm = (realm: RealmRepresentation) =>
convertToFormValues(realm, setValue, MULTILINE_INPUTS);
convertToFormValues(realm, setValue);
useEffect(() => setupForm(realm), []);
const save = async (realm: RealmRepresentation) => {
const submittedRealm = convertFormValuesToObject(realm, MULTILINE_INPUTS);
const submittedRealm = convertFormValuesToObject(realm);
try {
await adminClient.realms.update({ realm: realmName }, submittedRealm);
realmUpdated(submittedRealm);

View file

@ -9,10 +9,9 @@ import {
ValidatedOptions,
} from "@patternfly/react-core";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import { HelpItem } from "../components/help-enabler/HelpItem";
import { FormAccess } from "../components/form-access/FormAccess";
import type { ClientForm } from "./ClientDetails";
type ClientDescriptionProps = {
protocol?: string;
@ -20,7 +19,7 @@ type ClientDescriptionProps = {
export const ClientDescription = ({ protocol }: ClientDescriptionProps) => {
const { t } = useTranslation("clients");
const { register, errors, control } = useFormContext<ClientForm>();
const { register, errors, control } = useFormContext<ClientRepresentation>();
return (
<FormAccess role="manage-clients" unWrap>
<FormGroup

View file

@ -24,7 +24,6 @@ import {
} from "../components/confirm-dialog/ConfirmDialog";
import { DownloadDialog } from "../components/download-dialog/DownloadDialog";
import {
MultiLine,
stringToMultiline,
toStringValue,
} from "../components/multi-line-input/multi-line-convert";
@ -168,15 +167,6 @@ const ClientDetailHeader = ({
);
};
export type ClientForm = Omit<
ClientRepresentation,
"redirectUris" | "webOrigins"
> & {
redirectUris: MultiLine[];
webOrigins: MultiLine[];
requestUris?: MultiLine[];
};
export type SaveOptions = {
confirmed?: boolean;
messageKey?: string;
@ -193,7 +183,7 @@ export default function ClientDetails() {
const [downloadDialogOpen, toggleDownloadDialogOpen] = useToggle();
const [changeAuthenticatorOpen, toggleChangeAuthenticatorOpen] = useToggle();
const form = useForm<ClientForm>({ shouldUnregister: false });
const form = useForm<ClientRepresentation>({ shouldUnregister: false });
const { clientId } = useParams<ClientParams>();
const clientAuthenticatorType = useWatch({
@ -226,9 +216,9 @@ export default function ClientDetails() {
});
const setupForm = (client: ClientRepresentation) => {
convertToFormValues(client, form.setValue, ["redirectUris", "webOrigins"]);
convertToFormValues(client, form.setValue);
form.setValue(
"requestUris",
"attributes.request.uris",
stringToMultiline(client.attributes?.["request.uris"])
);
};
@ -263,15 +253,14 @@ export default function ClientDetails() {
const values = form.getValues();
if (values.requestUris) {
values.attributes!["request.uris"] = toStringValue(values.requestUris);
delete values.requestUris;
if (values.attributes?.request.uris) {
values.attributes["request.uris"] = toStringValue(
values.attributes.request.uris
);
}
const submittedClient = convertFormValuesToObject<
ClientForm,
ClientRepresentation
>(values, ["redirectUris", "webOrigins"]);
const submittedClient =
convertFormValuesToObject<ClientRepresentation>(values);
try {
const newClient: ClientRepresentation = {

View file

@ -24,7 +24,6 @@ import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { SaveReset } from "./advanced/SaveReset";
import { SamlConfig } from "./add/SamlConfig";
import { SamlSignature } from "./add/SamlSignature";
import type { ClientForm } from "./ClientDetails";
import environment from "../environment";
import { useRealm } from "../context/realm-context/RealmContext";
@ -39,7 +38,8 @@ export const ClientSettings = ({
save,
reset,
}: ClientSettingsProps) => {
const { register, control, watch, errors } = useFormContext<ClientForm>();
const { register, control, watch, errors } =
useFormContext<ClientRepresentation>();
const { t } = useTranslation("clients");
const { realm } = useRealm();

View file

@ -10,8 +10,8 @@ import {
InputGroup,
} from "@patternfly/react-core";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import { FormAccess } from "../../components/form-access/FormAccess";
import type { ClientForm } from "../ClientDetails";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import "./capability-config.css";
@ -26,7 +26,7 @@ export const CapabilityConfig = ({
protocol: type,
}: CapabilityConfigProps) => {
const { t } = useTranslation("clients");
const { control, watch, setValue } = useFormContext<ClientForm>();
const { control, watch, setValue } = useFormContext<ClientRepresentation>();
const protocol = type || watch("protocol");
const clientAuthentication = watch("publicClient");
const authorization = watch("authorizationServicesEnabled");

View file

@ -9,13 +9,13 @@ import {
Switch,
} from "@patternfly/react-core";
import type { ClientForm } from "../ClientDetails";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
export const Toggle = ({ name, label }: { name: string; label: string }) => {
const { t } = useTranslation("clients");
const { control } = useFormContext<ClientForm>();
const { control } = useFormContext<ClientRepresentation>();
return (
<FormGroup
@ -50,7 +50,7 @@ export const Toggle = ({ name, label }: { name: string; label: string }) => {
export const SamlConfig = () => {
const { t } = useTranslation("clients");
const { control } = useFormContext<ClientForm>();
const { control } = useFormContext<ClientRepresentation>();
const [nameFormatOpen, setNameFormatOpen] = useState(false);
return (

View file

@ -8,7 +8,7 @@ import {
SelectVariant,
} from "@patternfly/react-core";
import type { ClientForm } from "../ClientDetails";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { Toggle } from "./SamlConfig";
@ -46,7 +46,7 @@ export const SamlSignature = () => {
const [keyOpen, setKeyOpen] = useState(false);
const [canOpen, setCanOpen] = useState(false);
const { control, watch } = useFormContext<ClientForm>();
const { control, watch } = useFormContext<ClientRepresentation>();
const signDocs = watch("attributes.saml.server.signature");
const signAssertion = watch("attributes.saml.assertion.signature");

View file

@ -486,7 +486,7 @@ export const FineGrainOpenIdConnect = ({
}
>
<MultiLineInput
name="requestUris"
name="attributes.request.uris"
aria-label={t("validRequestURIs")}
addButtonLabel="clients:addRequestUri"
/>

View file

@ -27,7 +27,6 @@ import { ViewHeader } from "../../components/view-header/ViewHeader";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
import { useAlerts } from "../../components/alert/Alerts";
import { FormAccess } from "../../components/form-access/FormAccess";
import type { MultiLine } from "../../components/multi-line-input/multi-line-convert";
import type { KeyValueType } from "../../components/attribute-form/attribute-convert";
import { convertFormValuesToObject, convertToFormValues } from "../../util";
import { MultiLineInput } from "../../components/multi-line-input/MultiLineInput";
@ -37,9 +36,8 @@ import { AttributeInput } from "../../components/attribute-input/AttributeInput"
import "./resource-details.css";
type SubmittedResource = Omit<ResourceRepresentation, "attributes" | "uris"> & {
type SubmittedResource = Omit<ResourceRepresentation, "attributes"> & {
attributes: KeyValueType[];
uris: MultiLine[];
};
export default function ResourceDetails() {
@ -62,7 +60,7 @@ export default function ResourceDetails() {
const history = useHistory();
const setupForm = (resource: ResourceRepresentation = {}) => {
convertToFormValues(resource, setValue, ["uris"]);
convertToFormValues(resource, setValue);
};
useFetch(
@ -92,7 +90,7 @@ export default function ResourceDetails() {
const resource = convertFormValuesToObject<
SubmittedResource,
ResourceRepresentation
>(submitted, ["uris"]);
>(submitted);
try {
if (resourceId) {

View file

@ -8,7 +8,7 @@ import {
SplitItem,
} from "@patternfly/react-core";
import { useFormContext } from "react-hook-form";
import type { ClientForm } from "../ClientDetails";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
export type ClientSecretProps = {
secret: string;
@ -17,7 +17,7 @@ export type ClientSecretProps = {
export const ClientSecret = ({ secret, toggle }: ClientSecretProps) => {
const { t } = useTranslation("clients");
const { formState } = useFormContext<ClientForm>();
const { formState } = useFormContext<ClientRepresentation>();
return (
<FormGroup label={t("clientSecret")} fieldId="kc-client-secret">
<Split hasGutter>

View file

@ -17,12 +17,12 @@ import {
TextInput,
} from "@patternfly/react-core";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import type CertificateRepresentation from "@keycloak/keycloak-admin-client/lib/defs/certificateRepresentation";
import type KeyStoreConfig from "@keycloak/keycloak-admin-client/lib/defs/keystoreConfig";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { FormAccess } from "../../components/form-access/FormAccess";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import type { ClientForm } from "../ClientDetails";
import { GenerateKeyDialog } from "./GenerateKeyDialog";
import { useFetch, useAdminClient } from "../../context/auth/AdminClient";
import { useAlerts } from "../../components/alert/Alerts";
@ -43,7 +43,7 @@ export const Keys = ({ clientId, save }: KeysProps) => {
control,
register,
formState: { isDirty },
} = useFormContext<ClientForm>();
} = useFormContext<ClientRepresentation>();
const adminClient = useAdminClient();
const { addAlert, addError } = useAlerts();

View file

@ -16,11 +16,11 @@ import {
AlertVariant,
} from "@patternfly/react-core";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import type CertificateRepresentation from "@keycloak/keycloak-admin-client/lib/defs/certificateRepresentation";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import type { ClientForm } from "../ClientDetails";
import { SamlKeysDialog } from "./SamlKeysDialog";
import { FormPanel } from "../../components/scroll-form/FormPanel";
import { Certificate } from "./Certificate";
@ -65,7 +65,7 @@ const KeySection = ({
onImport,
}: KeySectionProps) => {
const { t } = useTranslation("clients");
const { control, watch } = useFormContext<ClientForm>();
const { control, watch } = useFormContext<ClientRepresentation>();
const title = KEYS_MAPPING[attr].title;
const key = KEYS_MAPPING[attr].key;
const name = KEYS_MAPPING[attr].name;

View file

@ -1,17 +1,10 @@
import React, { Fragment, useEffect } from "react";
import React from "react";
import { useTranslation } from "react-i18next";
import { useFormContext } from "react-hook-form";
import {
Button,
ButtonVariant,
FormGroup,
InputGroup,
TextInput,
} from "@patternfly/react-core";
import { FormGroup } from "@patternfly/react-core";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import type { ComponentProps } from "./components";
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { MultiLineInput } from "../multi-line-input/MultiLineInput";
export const MultiValuedStringComponent = ({
name,
@ -22,20 +15,6 @@ export const MultiValuedStringComponent = ({
}: ComponentProps) => {
const { t } = useTranslation("dynamic");
const fieldName = `config.${name}`;
const { register, setValue, watch } = useFormContext();
const fields = watch(fieldName, [defaultValue]);
const remove = (id: number) => {
fields.splice(id, 1);
setValue(fieldName, [...fields]);
};
const append = () => {
setValue(fieldName, [...fields, ""]);
};
useEffect(() => register(`config.${name}`), [register]);
return (
<FormGroup
@ -45,46 +24,14 @@ export const MultiValuedStringComponent = ({
}
fieldId={name!}
>
{fields.map((value: string, index: number) => (
<Fragment key={index}>
<InputGroup>
<TextInput
id={fieldName + index}
onChange={(value) => {
fields[index] = value;
setValue(fieldName, [...fields]);
}}
name={`${fieldName}[${index}]`}
value={value}
isDisabled={isDisabled}
/>
<Button
variant={ButtonVariant.link}
onClick={() => remove(index)}
tabIndex={-1}
aria-label={t("common:remove")}
isDisabled={index === fields.length - 1}
>
<MinusCircleIcon />
</Button>
</InputGroup>
{index === fields.length - 1 && (
<Button
variant={ButtonVariant.link}
onClick={append}
tabIndex={-1}
aria-label={t("common:add")}
data-testid="addValue"
isDisabled={!value}
>
<PlusCircleIcon />{" "}
{t("addMultivaluedLabel", {
fieldLabel: t(label!).toLowerCase(),
})}
</Button>
)}
</Fragment>
))}
<MultiLineInput
name={fieldName}
isDisabled={isDisabled}
defaultValue={[defaultValue]}
addButtonLabel={t("addMultivaluedLabel", {
fieldLabel: t(label!).toLowerCase(),
})}
/>
</FormGroup>
);
};

View file

@ -1,5 +1,5 @@
import React, { Fragment } from "react";
import { useFieldArray, useFormContext, useWatch } from "react-hook-form";
import React, { Fragment, useEffect } from "react";
import { useFormContext } from "react-hook-form";
import {
TextInput,
Button,
@ -13,32 +13,50 @@ import { useTranslation } from "react-i18next";
export type MultiLineInputProps = Omit<TextInputProps, "form"> & {
name: string;
addButtonLabel?: string;
isDisabled?: boolean;
defaultValue?: string[];
};
export const MultiLineInput = ({
name,
addButtonLabel,
isDisabled = false,
defaultValue,
...rest
}: MultiLineInputProps) => {
const { t } = useTranslation();
const { register, control } = useFormContext();
const { fields, append, remove } = useFieldArray({
name,
control,
});
const currentValues: { [name: string]: { value: string } } | undefined =
useWatch({ control, name });
const { register, watch, setValue } = useFormContext();
const value = watch(name, defaultValue);
const fields = Array.isArray(value) && value.length !== 0 ? value : [""];
const remove = (index: number) => {
setValue(name, [...fields.slice(0, index), ...fields.slice(index + 1)]);
};
const append = () => {
setValue(name, [...fields, ""]);
};
useEffect(() => register(name), [register]);
return (
<>
{fields.map(({ id, value }, index) => (
<Fragment key={id}>
{fields.map((value: string, index: number) => (
<Fragment key={index}>
<InputGroup>
<TextInput
id={id}
ref={register()}
name={`${name}[${index}].value`}
defaultValue={value}
id={name + index}
onChange={(value) => {
setValue(name, [
...fields.slice(0, index),
value,
...fields.slice(index + 1),
]);
}}
name={`${name}[${index}]`}
value={value}
isDisabled={isDisabled}
{...rest}
/>
<Button
@ -54,12 +72,11 @@ export const MultiLineInput = ({
{index === fields.length - 1 && (
<Button
variant={ButtonVariant.link}
onClick={() => append({})}
onClick={append}
tabIndex={-1}
aria-label={t("common:add")}
data-testid="addValue"
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
isDisabled={rest.isDisabled || !currentValues?.[index]?.value}
isDisabled={!value}
>
<PlusCircleIcon /> {t(addButtonLabel || "common:add")}
</Button>

View file

@ -1,19 +1,7 @@
export type MultiLine = {
value: string;
};
export function convertToMultiline(fields: string[]): MultiLine[] {
return (fields.length > 0 ? fields : [""]).map((field) => ({ value: field }));
export function stringToMultiline(value?: string): string[] {
return (value || "").split("##");
}
export function stringToMultiline(value?: string): MultiLine[] {
return (value || "").split("##").map((v) => ({ value: v }));
}
export function toStringValue(formValue: MultiLine[]): string {
return formValue.map((field) => field.value).join("##");
}
export function toValue(formValue: MultiLine[]): string[] {
return formValue.map((field) => field.value);
export function toStringValue(formValue: string[]): string {
return formValue.join("##");
}

View file

@ -99,55 +99,4 @@ describe("Tests the form convert util functions", () => {
},
});
});
it("convert arrays to form values", () => {
const given = {
name: "test",
description: "",
redirectUris: ["http://bla.nl", "http://test.nl/bla", "http://test.nl"],
};
const values: { [index: string]: any } = {};
const spy = (name: string, value: any) => (values[name] = value);
//when
convertToFormValues(given, spy, ["redirectUris"]);
//then
expect(values).toEqual({
name: "test",
description: "",
redirectUris: [
{ value: "http://bla.nl" },
{ value: "http://test.nl/bla" },
{ value: "http://test.nl" },
],
});
});
it("convert form values to object", () => {
const given = {
redirectUris: [{ value: "http://bla.nl" }, { value: "http://test.nl" }],
};
//when
const values = convertFormValuesToObject(given, ["redirectUris"]);
//then
expect(values).toEqual({
redirectUris: ["http://bla.nl", "http://test.nl"],
});
});
it("convert empty multi-lines", () => {
const values: { [index: string]: any } = {};
const spy = (name: string, value: any) => (values[name] = value);
//when
convertToFormValues({}, spy, ["redirectUris"]);
//then
expect(values).toEqual({
redirectUris: [{ value: "" }],
});
});
});

View file

@ -12,10 +12,6 @@ import {
attributesToArray,
KeyValueType,
} from "./components/attribute-form/attribute-convert";
import {
convertToMultiline,
toValue,
} from "./components/multi-line-input/multi-line-convert";
export const sortProviders = (providers: {
[index: string]: ProviderRepresentation;
@ -84,37 +80,24 @@ const isEmpty = (obj: any) => Object.keys(obj).length === 0;
export const convertToFormValues = (
obj: any,
setValue: (name: string, value: any) => void,
multiline?: string[]
setValue: (name: string, value: any) => void
) => {
Object.entries(obj).map(([key, value]) => {
if (key === "attributes" && isAttributesObject(value)) {
setValue(key, attributesToArray(value as Record<string, string[]>));
} else if (key === "config" || key === "attributes") {
setValue(key, !isEmpty(value) ? unflatten(value) : undefined);
} else if (multiline?.includes(key)) {
setValue(key, convertToMultiline(value as string[]));
} else {
setValue(key, value);
}
});
multiline?.map((line) => {
if (!Object.keys(obj).includes(line)) {
setValue(line, convertToMultiline([""]));
}
});
};
export function convertFormValuesToObject<T, G = T>(
obj: T,
multiline: string[] | undefined = []
): G {
export function convertFormValuesToObject<T, G = T>(obj: T): G {
const result: any = {};
Object.entries(obj).map(([key, value]) => {
if (isAttributeArray(value)) {
result[key] = arrayToAttributes(value as KeyValueType[]);
} else if (multiline.includes(key)) {
result[key] = toValue(value);
} else if (key === "config" || key === "attributes") {
result[key] = flatten(value as Record<string, any>, { safe: true });
} else {