Unify utils for interpolated labels under ui-shared (#25203)

Signed-off-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Jon Koops 2023-12-01 14:41:15 +01:00 committed by GitHub
parent 3b26e5d489
commit dc0455b73c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 61 additions and 98 deletions

View file

@ -7,6 +7,7 @@ import {
Spinner,
} from "@patternfly/react-core";
import { ExternalLinkSquareAltIcon } from "@patternfly/react-icons";
import { TFunction } from "i18next";
import { useKeycloak } from "keycloak-masthead";
import { useState } from "react";
import { ErrorOption, useForm } from "react-hook-form";
@ -16,6 +17,7 @@ import {
setUserProfileServerError,
useAlerts,
} from "ui-shared";
import {
getPersonalInfo,
getSupportedLocales,
@ -65,7 +67,7 @@ const PersonalInfo = () => {
{ responseData: { errors: error as any } },
(name: string | number, error: unknown) =>
setError(name as string, error as ErrorOption),
(key: TFuncKey, param?: object) => t(key, { ...param }),
((key: TFuncKey, param?: object) => t(key, param as any)) as TFunction,
);
}
};
@ -87,7 +89,10 @@ const PersonalInfo = () => {
form={form}
userProfileMetadata={userProfileMetadata}
supportedLocales={supportedLocales}
t={(key: unknown, params) => t(key as TFuncKey, { ...params })}
t={
((key: unknown, params) =>
t(key as TFuncKey, params as any)) as TFunction
}
renderer={(attribute) =>
attribute.name === "email" &&
updateEmailFeatureEnabled &&

View file

@ -1,20 +0,0 @@
import { TFunction } from "i18next";
import { TFuncKey } from "../i18n";
import { UserProfileAttributeMetadata } from "../api/representations";
export const isBundleKey = (displayName?: string) =>
displayName?.includes("${");
export const unWrap = (key: string) => key.substring(2, key.length - 1);
export const label = (attribute: UserProfileAttributeMetadata, t: TFunction) =>
(isBundleKey(attribute.displayName)
? t(unWrap(attribute.displayName!) as TFuncKey)
: attribute.displayName) || attribute.name;
const ROOT_ATTRIBUTES = ["username", "firstName", "lastName", "email"];
const isRootAttribute = (attr?: string) =>
attr && ROOT_ATTRIBUTES.includes(attr);
export const fieldName = (attribute: UserProfileAttributeMetadata) =>
`${isRootAttribute(attribute.name) ? "" : "attributes."}${attribute.name}`;

View file

@ -13,14 +13,15 @@ import {
TextContent,
TextVariants,
} from "@patternfly/react-core";
import { Form } from "react-router-dom";
import { KeycloakTextInput } from "../keycloak-text-input/KeycloakTextInput";
import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form";
import { isBundleKey, unWrap } from "../../user/utils";
import { CheckIcon } from "@patternfly/react-icons";
import { useAlerts } from "../alert/Alerts";
import { ReactNode, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Form } from "react-router-dom";
import { label } from "ui-shared";
import { useAlerts } from "../alert/Alerts";
import { KeycloakTextInput } from "../keycloak-text-input/KeycloakTextInput";
import { UserAttribute } from "./UserDataTable";
type UserDataTableAttributeSearchFormProps = {
@ -153,11 +154,7 @@ export function UserDataTableAttributeSearchForm({
{profile.attributes?.map((option) => (
<SelectOption
key={option.name}
value={
(isBundleKey(option.displayName)
? t(unWrap(option.displayName!))
: option.displayName) || option.name
}
value={label(t, option.displayName!, option.name)}
onClick={(e) => {
e.stopPropagation();
setSelectAttributeKeyOpen(false);

View file

@ -2,10 +2,12 @@ import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/g
import RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
import type { UserProfileMetadata } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata";
import { AlertVariant, PageSection } from "@patternfly/react-core";
import { TFunction } from "i18next";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { isUserProfileError, setUserProfileServerError } from "ui-shared";
import { adminClient } from "../admin-client";
import { useAlerts } from "../components/alert/Alerts";
@ -15,7 +17,6 @@ import { useRealm } from "../context/realm-context/RealmContext";
import { useFetch } from "../utils/useFetch";
import useIsFeatureEnabled, { Feature } from "../utils/useIsFeatureEnabled";
import { UserForm } from "./UserForm";
import { isUserProfileError, setUserProfileServerError } from "ui-shared";
import { UserFormFields, toUserRepresentation } from "./form-state";
import { toUser } from "./routes/User";
@ -71,9 +72,8 @@ export default function CreateUser() {
);
} catch (error) {
if (isUserProfileError(error)) {
setUserProfileServerError(error, form.setError, (key, param) =>
t(key as string, { ...param }),
);
setUserProfileServerError(error, form.setError, ((key, param) =>
t(key as string, param as any)) as TFunction);
} else {
addError("userCreateError", error);
}

View file

@ -14,11 +14,13 @@ import {
Tooltip,
} from "@patternfly/react-core";
import { InfoCircleIcon } from "@patternfly/react-icons";
import { TFunction } from "i18next";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { isUserProfileError, setUserProfileServerError } from "ui-shared";
import { adminClient } from "../admin-client";
import { useAlerts } from "../components/alert/Alerts";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
@ -158,9 +160,8 @@ export default function EditUser() {
refresh();
} catch (error) {
if (isUserProfileError(error)) {
setUserProfileServerError(error, form.setError, (key, param) =>
t(key as string, { ...param }),
);
setUserProfileServerError(error, form.setError, ((key, param) =>
t(key as string, param as any)) as TFunction);
} else {
addError("userCreateError", error);
}

View file

@ -12,11 +12,13 @@ import {
InputGroup,
Switch,
} from "@patternfly/react-core";
import { TFunction } from "i18next";
import { useState } from "react";
import { Controller, UseFormReturn } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { HelpItem, UserProfileFields } from "ui-shared";
import { adminClient } from "../admin-client";
import { useAlerts } from "../components/alert/Alerts";
import { FormAccess } from "../components/form/FormAccess";
@ -218,7 +220,10 @@ export const UserForm = ({
userProfileMetadata={userProfileMetadata}
hideReadOnly={!user}
supportedLocales={realm.supportedLocales || []}
t={(key: unknown, params) => t(key as string, { ...params })}
t={
((key: unknown, params) =>
t(key as string, params as any)) as TFunction
}
/>
</>
) : (

View file

@ -1,30 +1,2 @@
import { UserProfileAttributeMetadata } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata";
import { TFunction } from "i18next";
export const isBundleKey = (displayName?: string) =>
displayName?.includes("${");
export const unWrap = (key: string) => key.substring(2, key.length - 1);
export const label = (
text: string | undefined,
fallback: string | undefined,
t: TFunction,
) => (isBundleKey(text) ? t(unWrap(text!)) : text) || fallback;
export const labelAttribute = (
attribute: UserProfileAttributeMetadata,
t: TFunction,
) => label(attribute.displayName, attribute.name, t);
const ROOT_ATTRIBUTES = ["username", "firstName", "lastName", "email"];
export const isRootAttribute = (attr?: string) =>
attr && ROOT_ATTRIBUTES.includes(attr);
export const fieldName = (attribute: UserProfileAttributeMetadata) =>
isRootAttribute(attribute.name)
? attribute.name
: `attributes.${attribute.name}`;
export const isLightweightUser = (userId?: string) =>
userId?.startsWith("lightweight-");

View file

@ -40,13 +40,15 @@
}
},
"dependencies": {
"@keycloak/keycloak-admin-client": "workspace:*",
"@patternfly/react-core": "^4.278.0",
"@patternfly/react-icons": "^4.93.7",
"@keycloak/keycloak-admin-client": "workspace:*",
"i18next": "^23.7.6",
"lodash-es": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "7.48.2"
"react-hook-form": "7.48.2",
"react-i18next": "^13.5.0"
},
"devDependencies": {
"@types/lodash-es": "^4.17.12",

View file

@ -18,6 +18,7 @@ export { UserProfileFields } from "./user-profile/UserProfileFields";
export {
setUserProfileServerError,
isUserProfileError,
label,
} from "./user-profile/utils";
export type { UserFormFields } from "./user-profile/utils";
export { ScrollForm, mainPageContentId } from "./scroll-form/ScrollForm";

View file

@ -6,16 +6,13 @@ import {
TextInputProps,
} from "@patternfly/react-core";
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
import { type TFunction } from "i18next";
import { Fragment, useEffect, useMemo } from "react";
import { FieldPath, UseFormReturn, useWatch } from "react-hook-form";
import { UserProfileFieldProps } from "./UserProfileFields";
import { UserProfileGroup } from "./UserProfileGroup";
import {
TranslationFunction,
UserFormFields,
fieldName,
labelAttribute,
} from "./utils";
import { UserFormFields, fieldName, labelAttribute } from "./utils";
export const MultiInputComponent = ({
t,
@ -37,7 +34,7 @@ export const MultiInputComponent = ({
);
export type MultiLineInputProps = Omit<TextInputProps, "form"> & {
t: TranslationFunction;
t: TFunction;
name: FieldPath<UserFormFields>;
form: UseFormReturn<UserFormFields>;
addButtonLabel?: string;

View file

@ -4,8 +4,10 @@ import {
UserProfileMetadata,
} from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata";
import { Text } from "@patternfly/react-core";
import { TFunction } from "i18next";
import { ReactNode, useMemo } from "react";
import { FieldPath, UseFormReturn } from "react-hook-form";
import { ScrollForm } from "../main";
import { LocaleSelector } from "./LocaleSelector";
import { MultiInputComponent } from "./MultiInputComponent";
@ -13,13 +15,7 @@ import { OptionComponent } from "./OptionsComponent";
import { SelectComponent } from "./SelectComponent";
import { TextAreaComponent } from "./TextAreaComponent";
import { TextComponent } from "./TextComponent";
import {
TranslationFunction,
UserFormFields,
fieldName,
isRootAttribute,
label,
} from "./utils";
import { UserFormFields, fieldName, isRootAttribute, label } from "./utils";
export type UserProfileError = {
responseData: { errors?: { errorMessage: string }[] };
@ -51,7 +47,7 @@ const INPUT_TYPES = [
export type InputType = (typeof INPUT_TYPES)[number];
export type UserProfileFieldProps = {
t: TranslationFunction;
t: TFunction;
form: UseFormReturn<UserFormFields>;
inputType: InputType;
attribute: UserProfileAttributeMetadata;
@ -80,7 +76,7 @@ export const FIELDS: {
} as const;
export type UserProfileFieldsProps = {
t: TranslationFunction;
t: TFunction;
form: UseFormReturn<UserFormFields>;
userProfileMetadata: UserProfileMetadata;
supportedLocales: string[];
@ -167,7 +163,7 @@ export const UserProfileFields = ({
};
type FormFieldProps = {
t: TranslationFunction;
t: TFunction;
form: UseFormReturn<UserFormFields>;
supportedLocales: string[];
attribute: UserProfileAttributeMetadata;

View file

@ -1,11 +1,12 @@
import { UserProfileAttributeMetadata } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata";
import { FormGroup, InputGroup } from "@patternfly/react-core";
import { TFunction } from "i18next";
import { get } from "lodash-es";
import { PropsWithChildren, ReactNode } from "react";
import { UseFormReturn } from "react-hook-form";
import { HelpItem } from "../controls/HelpItem";
import {
TranslationFunction,
UserFormFields,
fieldName,
isRequiredAttribute,
@ -13,7 +14,7 @@ import {
} from "./utils";
export type UserProfileGroupProps = {
t: TranslationFunction;
t: TFunction;
form: UseFormReturn<UserFormFields>;
attribute: UserProfileAttributeMetadata;
renderer?: (attribute: UserProfileAttributeMetadata) => ReactNode;
@ -39,7 +40,9 @@ export const UserProfileGroup = ({
fieldId={attribute.name}
isRequired={isRequiredAttribute(attribute)}
validated={get(errors, fieldName(attribute.name)) ? "error" : "default"}
helperTextInvalid={t(get(errors, fieldName(attribute.name))?.message)}
helperTextInvalid={t(
get(errors, fieldName(attribute.name))?.message as string,
)}
labelIcon={
helpText ? (
<HelpItem helpText={helpText} fieldLabelId={attribute.name!} />

View file

@ -1,5 +1,6 @@
import { UserProfileAttributeMetadata } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata";
import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import { TFunction } from "i18next";
import { FieldPath } from "react-hook-form";
export type KeyValueType = { key: string; value: string };
@ -23,18 +24,17 @@ export type UserProfileError = {
responseData: ErrorArray | FieldError;
};
export const isBundleKey = (displayName?: string) =>
displayName?.includes("${");
const isBundleKey = (displayName?: string) => displayName?.includes("${");
export const unWrap = (key: string) => key.substring(2, key.length - 1);
export const label = (
t: TranslationFunction,
t: TFunction,
text: string | undefined,
fallback: string | undefined,
) => (isBundleKey(text) ? t(unWrap(text!)) : text) || fallback;
export const labelAttribute = (
t: TranslationFunction,
t: TFunction,
attribute: UserProfileAttributeMetadata,
) => label(t, attribute.displayName, attribute.name);
@ -51,7 +51,7 @@ export const fieldName = (name?: string) =>
export function setUserProfileServerError<T>(
error: UserProfileError,
setError: (field: keyof T, params: object) => void,
t: TranslationFunction,
t: TFunction,
) {
(
((error.responseData as ErrorArray).errors !== undefined
@ -152,5 +152,3 @@ function isFieldError(error: unknown): error is FieldError {
return true;
}
export type TranslationFunction = (key: unknown, params?: object) => string;

View file

@ -439,6 +439,9 @@ importers:
'@patternfly/react-icons':
specifier: ^4.93.7
version: 4.93.7(react-dom@18.2.0)(react@18.2.0)
i18next:
specifier: ^23.7.6
version: 23.7.7
lodash-es:
specifier: ^4.17.21
version: 4.17.21
@ -451,6 +454,9 @@ importers:
react-hook-form:
specifier: 7.48.2
version: 7.48.2(react@18.2.0)
react-i18next:
specifier: ^13.5.0
version: 13.5.0(i18next@23.7.7)(react-dom@18.2.0)(react@18.2.0)
devDependencies:
'@types/lodash-es':
specifier: ^4.17.12