diff --git a/js/apps/admin-ui/src/components/client/ClientSelect.tsx b/js/apps/admin-ui/src/components/client/ClientSelect.tsx index be8b7f2eeb6b..b7a210dfca4b 100644 --- a/js/apps/admin-ui/src/components/client/ClientSelect.tsx +++ b/js/apps/admin-ui/src/components/client/ClientSelect.tsx @@ -2,6 +2,7 @@ import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/ import type { ClientQuery } from "@keycloak/keycloak-admin-client/lib/resources/clients"; import { SelectControl, + SelectControlOption, SelectVariant, useFetch, } from "@keycloak/keycloak-ui-shared"; @@ -11,6 +12,7 @@ import { useAdminClient } from "../../admin-client"; import type { ComponentProps } from "../dynamic/components"; import { PermissionsConfigurationTabsParams } from "../../permissions-configuration/routes/PermissionsConfigurationTabs"; import { useParams } from "react-router-dom"; +import { useFormContext, useWatch } from "react-hook-form"; type ClientSelectProps = Omit & { variant?: `${SelectVariant}`; @@ -35,9 +37,18 @@ export const ClientSelect = ({ const { t } = useTranslation(); const [clients, setClients] = useState([]); + const [selectedClients, setSelectedClients] = + useState(); const [search, setSearch] = useState(""); const { tab } = useParams(); + const { control } = useFormContext(); + const value = useWatch({ + control, + name: name!, + defaultValue: defaultValue || "", + }); + useFetch( () => { const params: ClientQuery = { @@ -53,6 +64,30 @@ export const ClientSelect = ({ [search], ); + useFetch( + () => { + const values = ((value as string[]) || []).map(async (clientId) => { + if (clientKey === "clientId") { + return (await adminClient.clients.find({ clientId }))[0]; + } else { + return adminClient.clients.findOne({ id: clientId }); + } + }); + return Promise.all(values); + }, + (clients) => { + setSelectedClients( + clients + .filter((client) => !!client) + .map((client) => ({ + key: client[clientKey] as string, + value: client.clientId!, + })), + ); + }, + [], + ); + return ( setSearch(value)} variant={variant} isDisabled={isDisabled} + selectedOptions={selectedClients} options={clients.map((client) => ({ key: client[clientKey] as string, value: client.clientId!, diff --git a/js/apps/admin-ui/test/realm-settings/events.ts b/js/apps/admin-ui/test/realm-settings/events.ts index 65a470362079..71bf822b789b 100644 --- a/js/apps/admin-ui/test/realm-settings/events.ts +++ b/js/apps/admin-ui/test/realm-settings/events.ts @@ -30,6 +30,7 @@ export async function fillEventListener(page: Page, listener: string) { await eventListener.click(); await eventListener.fill(listener); await page.getByRole("option", { name: listener }).click(); + await page.keyboard.press("Escape"); } export async function clickRevertButton(page: Page) { diff --git a/js/libs/ui-shared/src/controls/select-control/SelectControl.tsx b/js/libs/ui-shared/src/controls/select-control/SelectControl.tsx index fca77433c18b..d1e08b575531 100644 --- a/js/libs/ui-shared/src/controls/select-control/SelectControl.tsx +++ b/js/libs/ui-shared/src/controls/select-control/SelectControl.tsx @@ -41,6 +41,7 @@ export type SelectControlProps< name: string; label?: string; options: OptionType; + selectedOptions?: SelectControlOption[]; labelIcon?: string; controller: Omit; onFilter?: (value: string) => void; diff --git a/js/libs/ui-shared/src/controls/select-control/TypeaheadSelectControl.tsx b/js/libs/ui-shared/src/controls/select-control/TypeaheadSelectControl.tsx index 6bb156592f64..25440a681430 100644 --- a/js/libs/ui-shared/src/controls/select-control/TypeaheadSelectControl.tsx +++ b/js/libs/ui-shared/src/controls/select-control/TypeaheadSelectControl.tsx @@ -43,6 +43,7 @@ export const TypeaheadSelectControl = < name, label, options, + selectedOptions = [], controller, labelIcon, placeholderText, @@ -57,6 +58,9 @@ export const TypeaheadSelectControl = < const [open, setOpen] = useState(false); const [filterValue, setFilterValue] = useState(""); const [focusedItemIndex, setFocusedItemIndex] = useState(0); + const [selectedOptionsState, setSelectedOptions] = useState< + SelectControlOption[] + >([]); const textInputRef = useRef(); const required = getRuleValue(controller.rules?.required) === true; const isTypeaheadMulti = variant === SelectVariant.typeaheadMulti; @@ -79,6 +83,28 @@ export const TypeaheadSelectControl = < [focusedItemIndex, filteredOptions], ); + const updateValue = ( + option: string | string[], + field: ControllerRenderProps, + ) => { + if (field.value.includes(option)) { + field.onChange(field.value.filter((item: string) => item !== option)); + if (isSelectBasedOptions(options)) { + setSelectedOptions( + selectedOptionsState.filter((item) => item.key !== option), + ); + } + } else { + field.onChange([...field.value, option]); + if (isSelectBasedOptions(options)) { + setSelectedOptions([ + ...selectedOptionsState, + options.find((o) => o.key === option)!, + ]); + } + } + }; + const onInputKeyDown = ( event: React.KeyboardEvent, field: ControllerRenderProps, @@ -96,11 +122,8 @@ export const TypeaheadSelectControl = < setFilterValue(""); } - field.onChange( - Array.isArray(field.value) - ? [...field.value, key(focusedItem)] - : key(focusedItem), - ); + updateValue(key(focusedItem), field); + setOpen(false); setFocusedItemIndex(0); @@ -232,8 +255,11 @@ export const TypeaheadSelectControl = < }} > {isSelectBasedOptions(options) - ? options.find((o) => selection === o.key) - ?.value + ? [ + ...options, + ...selectedOptionsState, + ...selectedOptions, + ].find((o) => selection === o.key)?.value : getValue(selection)} ), @@ -263,13 +289,8 @@ export const TypeaheadSelectControl = < event?.stopPropagation(); const option = v?.toString(); if (isTypeaheadMulti && Array.isArray(field.value)) { - if (field.value.includes(option)) { - field.onChange( - field.value.filter((item: string) => item !== option), - ); - } else { - field.onChange([...field.value, option]); - } + setFilterValue(""); + updateValue(option || "", field); } else { field.onChange(Array.isArray(field.value) ? [option] : option); setOpen(false);