diff --git a/js/apps/admin-ui/cypress/e2e/users_test.spec.ts b/js/apps/admin-ui/cypress/e2e/users_test.spec.ts index 28cd391dd1..3f019a2af9 100644 --- a/js/apps/admin-ui/cypress/e2e/users_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/users_test.spec.ts @@ -163,6 +163,20 @@ describe("User creation", () => { masthead.checkNotificationMessage("The user has been saved"); + attributesTab + .addAttribute("LDAP_ID", "value_test") + .addAttribute("LDAP_ID", "another_value_test") + .addAttribute("c", "d") + .save(); + + masthead.checkNotificationMessage("The user has not been saved: "); + + cy.get(".pf-c-helper-text__item-text") + .filter(':contains("Update of read-only attribute rejected")') + .should("have.length", 2); + + cy.reload(); + userDetailsPage.goToDetailsTab(); attributesTab .goToAttributesTab() diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/AttributesTab.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/AttributesTab.ts index 9f9f539eb5..9f9956f4dc 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/AttributesTab.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/AttributesTab.ts @@ -44,7 +44,7 @@ export default class AttributesTab { cy.findByTestId(this.#keyInput).should((exist ? "" : "not.") + "exist"); if (exist) { - cy.findAllByTestId(this.#keyInput).invoke("val").should("eq", "key_test"); + cy.findAllByTestId(this.#keyInput).invoke("val").should("eq", key); } return this; diff --git a/js/apps/admin-ui/src/components/key-value-form/KeyValueInput.tsx b/js/apps/admin-ui/src/components/key-value-form/KeyValueInput.tsx index d829d2b618..a1c4d9ca00 100644 --- a/js/apps/admin-ui/src/components/key-value-form/KeyValueInput.tsx +++ b/js/apps/admin-ui/src/components/key-value-form/KeyValueInput.tsx @@ -73,9 +73,10 @@ export const KeyValueInput = ({ {t("value")} {fields.map((attribute, index) => { - const keyError = !!(errors as any)[name]?.[index]?.key; - const valueError = !!(errors as any)[name]?.[index]?.value; - + const error = (errors as any)[name]?.[index]; + const keyError = !!error?.key; + const valueErrorPresent = !!error?.value || !!error?.message; + const valueError = error?.message || t("valueError"); return ( @@ -118,15 +119,15 @@ export const KeyValueInput = ({ aria-label={t("value")} data-testid={`${name}-value`} {...register(`${name}.${index}.value`, { required: true })} - validated={valueError ? "error" : "default"} + validated={valueErrorPresent ? "error" : "default"} isRequired isDisabled={isDisabled} /> )} - {valueError && ( + {valueErrorPresent && ( - {t("valueError")} + {valueError} )} diff --git a/js/apps/admin-ui/src/user/EditUser.tsx b/js/apps/admin-ui/src/user/EditUser.tsx index d4627ae910..53723172a3 100644 --- a/js/apps/admin-ui/src/user/EditUser.tsx +++ b/js/apps/admin-ui/src/user/EditUser.tsx @@ -55,6 +55,7 @@ import { toUsers } from "./routes/Users"; import { isLightweightUser } from "./utils"; import { getUnmanagedAttributes } from "../components/users/resource"; import "./user-section.css"; +import { KeyValueType } from "../components/key-value-form/key-value-convert"; export default function EditUser() { const { t } = useTranslation(); @@ -63,7 +64,15 @@ export default function EditUser() { const { hasAccess } = useAccess(); const { id } = useParams(); const { realm: realmName } = useRealm(); - const form = useForm({ mode: "onChange" }); + // Validation of form fields is performed on server, thus we need to clear all errors before submit + const clearAllErrorsBeforeSubmit = async (values: UserFormFields) => ({ + values, + errors: {}, + }); + const form = useForm({ + mode: "onChange", + resolver: clearAllErrorsBeforeSubmit, + }); const [realm, setRealm] = useState(); const [user, setUser] = useState(); const [bruteForced, setBruteForced] = useState(); @@ -147,9 +156,46 @@ export default function EditUser() { refresh(); } catch (error) { if (isUserProfileError(error)) { - setUserProfileServerError(error, form.setError, ((key, param) => - t(key as string, param as any)) as TFunction); - addError("userNotSaved", error); + if ( + isUnmanagedAttributesEnabled && + Array.isArray(data.unmanagedAttributes) + ) { + const unmanagedAttributeErrors: object[] = new Array( + data.unmanagedAttributes.length, + ); + let someUnmanagedAttributeError = false; + setUserProfileServerError( + error, + (field, params) => { + if (field.startsWith("attributes.")) { + const attributeName = field.substring("attributes.".length); + (data.unmanagedAttributes as KeyValueType[]).forEach( + (attr, index) => { + if (attr.key === attributeName) { + unmanagedAttributeErrors[index] = params; + someUnmanagedAttributeError = true; + } + }, + ); + } else { + form.setError(field, params); + } + }, + ((key, param) => t(key as string, param as any)) as TFunction, + ); + if (someUnmanagedAttributeError) { + form.setError( + "unmanagedAttributes", + unmanagedAttributeErrors as any, + ); + } + } else { + setUserProfileServerError(error, form.setError, (( + key, + param, + ) => t(key as string, param as any)) as TFunction); + } + addError("userNotSaved", ""); } else { addError("userCreateError", error); } diff --git a/js/apps/admin-ui/src/user/form-state.ts b/js/apps/admin-ui/src/user/form-state.ts index edb91a8668..b1672b8b45 100644 --- a/js/apps/admin-ui/src/user/form-state.ts +++ b/js/apps/admin-ui/src/user/form-state.ts @@ -8,7 +8,7 @@ import { beerify, debeerify } from "../util"; export type UserFormFields = Omit< UIUserRepresentation, - "attributes" | "userProfileMetadata | unmanagedAttributes" + "attributes" | "userProfileMetadata" | "unmanagedAttributes" > & { attributes?: KeyValueType[] | Record; unmanagedAttributes?: KeyValueType[] | Record; diff --git a/js/libs/ui-shared/src/user-profile/utils.ts b/js/libs/ui-shared/src/user-profile/utils.ts index 329bab7fbc..2f1ae49117 100644 --- a/js/libs/ui-shared/src/user-profile/utils.ts +++ b/js/libs/ui-shared/src/user-profile/utils.ts @@ -69,7 +69,7 @@ export function setUserProfileServerError( setError(fieldName(e.field) as keyof T, { message: t(e.errorMessage, { ...params, - defaultValue: e.field, + defaultValue: e.errorMessage || e.field, }), type: "server", });