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",
});