diff --git a/js/apps/admin-ui/cypress/e2e/realm_settings_user_profile_enabled.spec.ts b/js/apps/admin-ui/cypress/e2e/realm_settings_user_profile_enabled.spec.ts index 15ab840b7e..6bccaebf5f 100644 --- a/js/apps/admin-ui/cypress/e2e/realm_settings_user_profile_enabled.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/realm_settings_user_profile_enabled.spec.ts @@ -9,7 +9,6 @@ import { keycloakBefore } from "../support/util/keycloak_hooks"; import ModalUtils from "../support/util/ModalUtils"; import RealmSettingsPage from "../support/pages/admin-ui/manage/realm_settings/RealmSettingsPage"; import CreateUserPage from "../support/pages/admin-ui/manage/users/CreateUserPage"; -import { UserProfileConfig } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata"; const loginPage = new LoginPage(); const sidebarPage = new SidebarPage(); @@ -28,37 +27,18 @@ const getAttributesGroupTab = () => userProfileTab.goToAttributesGroupTab(); const usernameAttributeName = "username"; const emailAttributeName = "email"; -let defaultUserProfile: UserProfileConfig; - describe("User profile tabs", () => { const realmName = "Realm_" + uuid(); const attributeName = "Test"; const attributeDisplayName = "Test display name"; - before(() => { - cy.wrap(null).then(async () => { - await adminClient.createRealm(realmName); + before(() => adminClient.createRealm(realmName)); - defaultUserProfile = await adminClient.getUserProfile(realmName); - }); - }); - - after(() => - cy.wrap(null).then(async () => await adminClient.deleteRealm(realmName)), - ); + after(() => adminClient.deleteRealm(realmName)); beforeEach(() => { loginPage.logIn(); keycloakBefore(); - cy.wrap(null).then(async () => { - await adminClient.updateUserProfile(realmName, defaultUserProfile); - - await adminClient.updateRealm(realmName, { - registrationEmailAsUsername: false, - editUsernameAllowed: false, - }); - }); - sidebarPage.goToRealm(realmName); sidebarPage.goToRealmSettings(); }); @@ -91,11 +71,12 @@ describe("User profile tabs", () => { }); it("Modifies existing attribute and performs save", () => { + const attrName = "ModifyTest"; getUserProfileTab(); - createAttributeDefinition(attributeName); + createAttributeDefinition(attrName); userProfileTab - .selectElementInList(attributeName) + .selectElementInList(attrName) .editAttribute("Edited display name") .saveAttributeCreation() .assertNotificationSaved(); @@ -103,7 +84,6 @@ describe("User profile tabs", () => { it("Adds and removes validator to/from existing attribute and performs save", () => { getUserProfileTab(); - createAttributeDefinition(attributeName); userProfileTab .selectElementInList(attributeName) @@ -120,19 +100,16 @@ describe("User profile tabs", () => { }); describe("Attribute groups sub tab tests", () => { - it.skip("Deletes an attributes group", () => { - const group = "Test"; - cy.wrap(null).then(() => - adminClient.patchUserProfile(realmName, { - groups: [{ name: group }], - }), - ); + const group = "Test" + uuid(); + before(() => adminClient.addGroupToProfile(realmName, group)); + + it("Deletes an attributes group", () => { getUserProfileTab(); getAttributesGroupTab(); listingPage.deleteItem(group); modalUtils.confirmModal(); - listingPage.checkEmptyList(); + listingPage.itemExist(group, false); }); }); @@ -202,6 +179,8 @@ describe("User profile tabs", () => { .setAttributeValue(emailAttributeName, `testuser9-${uuid()}@gmail.com`) .update() .assertNotificationUpdated(); + + deleteAttributeDefinition(attrName); }); it("Checks that not required attribute with permissions to view/edit is present when user is created", () => { @@ -216,7 +195,9 @@ describe("User profile tabs", () => { realmSettingsPage.goToLoginTab(); cy.wait(1000); realmSettingsPage + .setSwitch(realmSettingsPage.emailAsUsernameSwitch, false) .assertSwitch(realmSettingsPage.emailAsUsernameSwitch, false) + .setSwitch(realmSettingsPage.editUsernameSwitch, false) .assertSwitch(realmSettingsPage.editUsernameSwitch, false); // Create user @@ -228,6 +209,8 @@ describe("User profile tabs", () => { .create() .assertNotificationCreated() .assertAttributeFieldExists(attrName, true); + + deleteAttributeDefinition(attrName); }); it("Checks that required attribute with permissions to view/edit is present and required when user is created", () => { @@ -251,6 +234,8 @@ describe("User profile tabs", () => { .setAttributeValue(attrName, "MyAttribute") .create() .assertNotificationCreated(); + + deleteAttributeDefinition(attrName); }); it("Checks that required attribute with permissions to view/edit is accepted when user is created", () => { @@ -270,6 +255,8 @@ describe("User profile tabs", () => { .setAttributeValue(attrName, "MyAttribute") .create() .assertNotificationCreated(); + + deleteAttributeDefinition(attrName); }); it("Checks that attribute group is visible when user with existing attribute is created", () => { @@ -358,6 +345,8 @@ describe("User profile tabs", () => { .resetAttributeGroup() .saveAttributeCreation() .assertNotificationSaved(); + + deleteAttributeDefinition(attrName); }); it("Checks that attribute with select-annotation is displayed and editable when user is created/edited", () => { @@ -382,7 +371,7 @@ describe("User profile tabs", () => { createUserPage .goToCreateUser() .assertAttributeLabel(attrName, attrName) - .assertAttributeSelect(attrName, supportedOptions, "") + .assertAttributeSelect(attrName, supportedOptions, "Choose...") .setUsername(userName) .setAttributeValueOnSelect(attrName, opt1) .create() @@ -400,6 +389,15 @@ describe("User profile tabs", () => { }); }); + function deleteAttributeDefinition(attrName: string) { + sidebarPage.goToRealmSettings(); + getUserProfileTab(); + getAttributesTab(); + listingPage.deleteItem(attrName); + modalUtils.confirmModal(); + masthead.checkNotificationMessage("Attribute deleted"); + } + function createAttributeDefinition( attrName: string, attrConfigurer?: (attrConfigurer: UserProfile) => void, diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/CreateUserPage.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/CreateUserPage.ts index b2df86a64f..854a6beb29 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/CreateUserPage.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/users/CreateUserPage.ts @@ -1,5 +1,6 @@ import Masthead from "../../Masthead"; import FormValidation from "../../../../forms/FormValidation"; +import Select from "../../../../forms/Select"; export default class CreateUserPage { readonly masthead = new Masthead(); @@ -84,28 +85,11 @@ export default class CreateUserPage { expectedOptionsWithoutEmptyOption: string[], expectedValue: string, ) { - this.#getSelectFieldButton(attrName).should( - "have.class", - "pf-v5-c-select__toggle", + Select.assertSelectedItem( + this.#getSelectFieldButton(attrName), + expectedValue, ); - const valueToCheck = expectedValue ? expectedValue : this.emptyOptionValue; - this.#getSelectFieldButton(attrName) - .find(".pf-v5-c-select__toggle-text") - .invoke("text") - .should("eq", valueToCheck); - - const expectedOptions = [this.emptyOptionValue].concat( - expectedOptionsWithoutEmptyOption, - ); - this.#withSelectExpanded(attrName, () => { - this.#getSelectOptions(attrName) - .should("have.length", expectedOptions.length) - .each(($option, index) => - cy.wrap($option).should("have.text", expectedOptions[index]), - ); - }); - return this; } @@ -132,33 +116,6 @@ export default class CreateUserPage { }); } - /** - * Runs the given function in the context of an expanded select field. - * - * This method makes sure that the initial expanded/collapsed state is preserved: - * The select field will be expanded before running the function, if it is not already expanded. - * It will be collapsed after running the function, when it was not expanded before running the function. - * - * @param attrName the attribute name of the select field - * @param func the function to be applied - * @private - */ - #withSelectExpanded(attrName: string, func: () => any) { - let wasInitiallyExpanded = false; - this.#toggleSelectField(attrName, (currentlyExpanded) => { - wasInitiallyExpanded = currentlyExpanded; - return !currentlyExpanded; - }) - .then(() => func()) - .then(() => - // click again on the dropdown to hide the values list, when necessary - this.#toggleSelectField( - attrName, - (currentlyExpanded) => currentlyExpanded && !wasInitiallyExpanded, - ), - ); - } - assertAttributeLabel(attrName: string, expectedText: string) { cy.get(`.pf-v5-c-form__label[for='${attrName}'] .pf-v5-c-form__label-text`) .contains(expectedText) @@ -193,9 +150,7 @@ export default class CreateUserPage { } setAttributeValueOnSelect(attrName: string, value: string) { - this.#withSelectExpanded(attrName, () => { - this.#getSelectOptions(attrName).contains(value).click(); - }); + Select.selectItem(this.#getSelectFieldButton(attrName), value); return this; } diff --git a/js/apps/admin-ui/cypress/support/util/AdminClient.ts b/js/apps/admin-ui/cypress/support/util/AdminClient.ts index f094c5e7dc..01b9956143 100644 --- a/js/apps/admin-ui/cypress/support/util/AdminClient.ts +++ b/js/apps/admin-ui/cypress/support/util/AdminClient.ts @@ -260,14 +260,16 @@ class AdminClient { await this.#client.users.updateProfile(merge(userProfile, { realm })); } - async patchUserProfile(realm: string, payload: UserProfileConfig) { + async addGroupToProfile(realm: string, groupName: string) { await this.#login(); const currentProfile = await this.#client.users.getProfile({ realm }); - await this.#client.users.updateProfile( - merge(currentProfile, payload, { realm }), - ); + await this.#client.users.updateProfile({ + ...currentProfile, + realm, + ...{ groups: [...currentProfile.groups!, { name: groupName }] }, + }); } async createRealmRole(payload: RoleRepresentation) { diff --git a/js/apps/admin-ui/src/components/key-value-form/ValueSelect.tsx b/js/apps/admin-ui/src/components/key-value-form/ValueSelect.tsx index 963c889049..2c6e1eed20 100644 --- a/js/apps/admin-ui/src/components/key-value-form/ValueSelect.tsx +++ b/js/apps/admin-ui/src/components/key-value-form/ValueSelect.tsx @@ -36,7 +36,9 @@ export const ValueSelect = ({ placeholderText={t("valuePlaceholder")} > {defaultItem.values.map((item) => ( - + + {item} + ))} ) : ( diff --git a/js/libs/ui-shared/src/user-profile/SelectComponent.tsx b/js/libs/ui-shared/src/user-profile/SelectComponent.tsx index 70519354bf..9306efe722 100644 --- a/js/libs/ui-shared/src/user-profile/SelectComponent.tsx +++ b/js/libs/ui-shared/src/user-profile/SelectComponent.tsx @@ -1,4 +1,11 @@ -import { Select, SelectOption } from "@patternfly/react-core/deprecated"; +import { + Chip, + ChipGroup, + MenuToggle, + Select, + SelectList, + SelectOption, +} from "@patternfly/react-core"; import { useState } from "react"; import { Controller, ControllerRenderProps } from "react-hook-form"; import { @@ -7,12 +14,11 @@ import { UserProfileFieldProps, } from "./UserProfileFields"; import { UserProfileGroup } from "./UserProfileGroup"; -import { UserFormFields, fieldName, isRequiredAttribute, label } from "./utils"; +import { UserFormFields, fieldName, label } from "./utils"; export const SelectComponent = (props: UserProfileFieldProps) => { const { t, form, inputType, attribute } = props; const [open, setOpen] = useState(false); - const isRequired = isRequiredAttribute(attribute); const isMultiValue = inputType === "multiselect"; const setValue = ( @@ -23,7 +29,11 @@ export const SelectComponent = (props: UserProfileFieldProps) => { if (field.value.includes(value)) { field.onChange(field.value.filter((item: string) => item !== value)); } else { - field.onChange([...field.value, value]); + if (Array.isArray(field.value)) { + field.onChange([...field.value, value]); + } else { + field.onChange([value]); + } } } else { field.onChange(value); @@ -46,39 +56,56 @@ export const SelectComponent = (props: UserProfileFieldProps) => { control={form.control} render={({ field }) => ( )} /> diff --git a/js/libs/ui-shared/src/user-profile/UserProfileFields.tsx b/js/libs/ui-shared/src/user-profile/UserProfileFields.tsx index fbf3336d78..a0106693f4 100644 --- a/js/libs/ui-shared/src/user-profile/UserProfileFields.tsx +++ b/js/libs/ui-shared/src/user-profile/UserProfileFields.tsx @@ -192,7 +192,8 @@ const FormField = ({ const inputType = useMemo(() => determineInputType(attribute), [attribute]); const Component = - attribute.multivalued || isMultiValue(value) + attribute.multivalued || + (isMultiValue(value) && attribute.annotations?.inputType === undefined) ? FIELDS["multi-input"] : FIELDS[inputType];