diff --git a/cypress/support/pages/admin_console/manage/clients/AuthorizationTab.ts b/cypress/support/pages/admin_console/manage/clients/AuthorizationTab.ts index e5f180614a..030eb4c760 100644 --- a/cypress/support/pages/admin_console/manage/clients/AuthorizationTab.ts +++ b/cypress/support/pages/admin_console/manage/clients/AuthorizationTab.ts @@ -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 { diff --git a/src/authentication/policies/WebauthnPolicy.tsx b/src/authentication/policies/WebauthnPolicy.tsx index 2cd709f90b..70afa0e324 100644 --- a/src/authentication/policies/WebauthnPolicy.tsx +++ b/src/authentication/policies/WebauthnPolicy.tsx @@ -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); diff --git a/src/clients/ClientDescription.tsx b/src/clients/ClientDescription.tsx index ccf70aaf17..f29d6ff680 100644 --- a/src/clients/ClientDescription.tsx +++ b/src/clients/ClientDescription.tsx @@ -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(); + const { register, errors, control } = useFormContext(); return ( & { - 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({ shouldUnregister: false }); + const form = useForm({ shouldUnregister: false }); const { clientId } = useParams(); 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(values); try { const newClient: ClientRepresentation = { diff --git a/src/clients/ClientSettings.tsx b/src/clients/ClientSettings.tsx index 60870e30dd..033445dde7 100644 --- a/src/clients/ClientSettings.tsx +++ b/src/clients/ClientSettings.tsx @@ -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(); + const { register, control, watch, errors } = + useFormContext(); const { t } = useTranslation("clients"); const { realm } = useRealm(); diff --git a/src/clients/add/CapabilityConfig.tsx b/src/clients/add/CapabilityConfig.tsx index 45dd31e182..3c284417de 100644 --- a/src/clients/add/CapabilityConfig.tsx +++ b/src/clients/add/CapabilityConfig.tsx @@ -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(); + const { control, watch, setValue } = useFormContext(); const protocol = type || watch("protocol"); const clientAuthentication = watch("publicClient"); const authorization = watch("authorizationServicesEnabled"); diff --git a/src/clients/add/SamlConfig.tsx b/src/clients/add/SamlConfig.tsx index aefedc68f3..9021a29a14 100644 --- a/src/clients/add/SamlConfig.tsx +++ b/src/clients/add/SamlConfig.tsx @@ -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(); + const { control } = useFormContext(); return ( { export const SamlConfig = () => { const { t } = useTranslation("clients"); - const { control } = useFormContext(); + const { control } = useFormContext(); const [nameFormatOpen, setNameFormatOpen] = useState(false); return ( diff --git a/src/clients/add/SamlSignature.tsx b/src/clients/add/SamlSignature.tsx index 0b221145e7..29c7c860d8 100644 --- a/src/clients/add/SamlSignature.tsx +++ b/src/clients/add/SamlSignature.tsx @@ -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(); + const { control, watch } = useFormContext(); const signDocs = watch("attributes.saml.server.signature"); const signAssertion = watch("attributes.saml.assertion.signature"); diff --git a/src/clients/advanced/FineGrainOpenIdConnect.tsx b/src/clients/advanced/FineGrainOpenIdConnect.tsx index d714bcca9a..c8cfaffb52 100644 --- a/src/clients/advanced/FineGrainOpenIdConnect.tsx +++ b/src/clients/advanced/FineGrainOpenIdConnect.tsx @@ -486,7 +486,7 @@ export const FineGrainOpenIdConnect = ({ } > diff --git a/src/clients/authorization/ResourceDetails.tsx b/src/clients/authorization/ResourceDetails.tsx index 0e0847b62a..a68e34b4bb 100644 --- a/src/clients/authorization/ResourceDetails.tsx +++ b/src/clients/authorization/ResourceDetails.tsx @@ -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 & { +type SubmittedResource = Omit & { 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) { diff --git a/src/clients/credentials/ClientSecret.tsx b/src/clients/credentials/ClientSecret.tsx index bf9806a4dd..afeb4f49a2 100644 --- a/src/clients/credentials/ClientSecret.tsx +++ b/src/clients/credentials/ClientSecret.tsx @@ -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(); + const { formState } = useFormContext(); return ( diff --git a/src/clients/keys/Keys.tsx b/src/clients/keys/Keys.tsx index 078b4583bf..f6331b31c1 100644 --- a/src/clients/keys/Keys.tsx +++ b/src/clients/keys/Keys.tsx @@ -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(); + } = useFormContext(); const adminClient = useAdminClient(); const { addAlert, addError } = useAlerts(); diff --git a/src/clients/keys/SamlKeys.tsx b/src/clients/keys/SamlKeys.tsx index fa8fc8a64a..811a13f67d 100644 --- a/src/clients/keys/SamlKeys.tsx +++ b/src/clients/keys/SamlKeys.tsx @@ -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(); + const { control, watch } = useFormContext(); const title = KEYS_MAPPING[attr].title; const key = KEYS_MAPPING[attr].key; const name = KEYS_MAPPING[attr].name; diff --git a/src/components/dynamic/MultivaluedStringComponent.tsx b/src/components/dynamic/MultivaluedStringComponent.tsx index 8d51014046..de5bc9fbac 100644 --- a/src/components/dynamic/MultivaluedStringComponent.tsx +++ b/src/components/dynamic/MultivaluedStringComponent.tsx @@ -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 ( - {fields.map((value: string, index: number) => ( - - - { - fields[index] = value; - setValue(fieldName, [...fields]); - }} - name={`${fieldName}[${index}]`} - value={value} - isDisabled={isDisabled} - /> - - - {index === fields.length - 1 && ( - - )} - - ))} + ); }; diff --git a/src/components/multi-line-input/MultiLineInput.tsx b/src/components/multi-line-input/MultiLineInput.tsx index c9f2eb87c2..575da5d704 100644 --- a/src/components/multi-line-input/MultiLineInput.tsx +++ b/src/components/multi-line-input/MultiLineInput.tsx @@ -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 & { 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) => ( - + {fields.map((value: string, index: number) => ( + { + setValue(name, [ + ...fields.slice(0, index), + value, + ...fields.slice(index + 1), + ]); + }} + name={`${name}[${index}]`} + value={value} + isDisabled={isDisabled} {...rest} /> diff --git a/src/components/multi-line-input/multi-line-convert.ts b/src/components/multi-line-input/multi-line-convert.ts index c7367562a8..a2cf96745d 100644 --- a/src/components/multi-line-input/multi-line-convert.ts +++ b/src/components/multi-line-input/multi-line-convert.ts @@ -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("##"); } diff --git a/src/util.test.ts b/src/util.test.ts index 716ca9d647..a25fc68a5f 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -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: "" }], - }); - }); }); diff --git a/src/util.ts b/src/util.ts index 238910b8ce..75806c5121 100644 --- a/src/util.ts +++ b/src/util.ts @@ -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)); } 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( - obj: T, - multiline: string[] | undefined = [] -): G { +export function convertFormValuesToObject(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, { safe: true }); } else {