From dc0455b73c94e5a992fbccd4547b7c5aaa6ef3a1 Mon Sep 17 00:00:00 2001 From: Jon Koops Date: Fri, 1 Dec 2023 14:41:15 +0100 Subject: [PATCH] Unify utils for interpolated labels under `ui-shared` (#25203) Signed-off-by: Jon Koops --- .../src/personal-info/PersonalInfo.tsx | 9 ++++-- js/apps/account-ui/src/personal-info/utils.ts | 20 ------------- .../UserDataTableAttributeSearchForm.tsx | 19 ++++++------- js/apps/admin-ui/src/user/CreateUser.tsx | 8 +++--- js/apps/admin-ui/src/user/EditUser.tsx | 7 +++-- js/apps/admin-ui/src/user/UserForm.tsx | 7 ++++- js/apps/admin-ui/src/user/utils.ts | 28 ------------------- js/libs/ui-shared/package.json | 6 ++-- js/libs/ui-shared/src/main.ts | 1 + .../src/user-profile/MultiInputComponent.tsx | 11 +++----- .../src/user-profile/UserProfileFields.tsx | 16 ++++------- .../src/user-profile/UserProfileGroup.tsx | 9 ++++-- js/libs/ui-shared/src/user-profile/utils.ts | 12 ++++---- js/pnpm-lock.yaml | 6 ++++ 14 files changed, 61 insertions(+), 98 deletions(-) delete mode 100644 js/apps/account-ui/src/personal-info/utils.ts diff --git a/js/apps/account-ui/src/personal-info/PersonalInfo.tsx b/js/apps/account-ui/src/personal-info/PersonalInfo.tsx index 51ec04c3b4..c7923a74c2 100644 --- a/js/apps/account-ui/src/personal-info/PersonalInfo.tsx +++ b/js/apps/account-ui/src/personal-info/PersonalInfo.tsx @@ -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 && diff --git a/js/apps/account-ui/src/personal-info/utils.ts b/js/apps/account-ui/src/personal-info/utils.ts deleted file mode 100644 index 8ed2742221..0000000000 --- a/js/apps/account-ui/src/personal-info/utils.ts +++ /dev/null @@ -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}`; diff --git a/js/apps/admin-ui/src/components/users/UserDataTableAttributeSearchForm.tsx b/js/apps/admin-ui/src/components/users/UserDataTableAttributeSearchForm.tsx index b1bc8275e7..47fcc34c44 100644 --- a/js/apps/admin-ui/src/components/users/UserDataTableAttributeSearchForm.tsx +++ b/js/apps/admin-ui/src/components/users/UserDataTableAttributeSearchForm.tsx @@ -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) => ( { e.stopPropagation(); setSelectAttributeKeyOpen(false); diff --git a/js/apps/admin-ui/src/user/CreateUser.tsx b/js/apps/admin-ui/src/user/CreateUser.tsx index f4252c8fe7..f1266fb62f 100644 --- a/js/apps/admin-ui/src/user/CreateUser.tsx +++ b/js/apps/admin-ui/src/user/CreateUser.tsx @@ -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); } diff --git a/js/apps/admin-ui/src/user/EditUser.tsx b/js/apps/admin-ui/src/user/EditUser.tsx index 73f2b9e7c9..3523209b9d 100644 --- a/js/apps/admin-ui/src/user/EditUser.tsx +++ b/js/apps/admin-ui/src/user/EditUser.tsx @@ -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); } diff --git a/js/apps/admin-ui/src/user/UserForm.tsx b/js/apps/admin-ui/src/user/UserForm.tsx index 1bdc5f4302..5a9bab9e70 100644 --- a/js/apps/admin-ui/src/user/UserForm.tsx +++ b/js/apps/admin-ui/src/user/UserForm.tsx @@ -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 + } /> ) : ( diff --git a/js/apps/admin-ui/src/user/utils.ts b/js/apps/admin-ui/src/user/utils.ts index 375a488e02..f1d21cee79 100644 --- a/js/apps/admin-ui/src/user/utils.ts +++ b/js/apps/admin-ui/src/user/utils.ts @@ -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-"); diff --git a/js/libs/ui-shared/package.json b/js/libs/ui-shared/package.json index 5987a580de..18e7fd7738 100644 --- a/js/libs/ui-shared/package.json +++ b/js/libs/ui-shared/package.json @@ -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", diff --git a/js/libs/ui-shared/src/main.ts b/js/libs/ui-shared/src/main.ts index 7b72c71703..1daf2529e1 100644 --- a/js/libs/ui-shared/src/main.ts +++ b/js/libs/ui-shared/src/main.ts @@ -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"; diff --git a/js/libs/ui-shared/src/user-profile/MultiInputComponent.tsx b/js/libs/ui-shared/src/user-profile/MultiInputComponent.tsx index 56a2af1951..d5836c3dc1 100644 --- a/js/libs/ui-shared/src/user-profile/MultiInputComponent.tsx +++ b/js/libs/ui-shared/src/user-profile/MultiInputComponent.tsx @@ -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 & { - t: TranslationFunction; + t: TFunction; name: FieldPath; form: UseFormReturn; addButtonLabel?: string; diff --git a/js/libs/ui-shared/src/user-profile/UserProfileFields.tsx b/js/libs/ui-shared/src/user-profile/UserProfileFields.tsx index c52b9d9991..206572814b 100644 --- a/js/libs/ui-shared/src/user-profile/UserProfileFields.tsx +++ b/js/libs/ui-shared/src/user-profile/UserProfileFields.tsx @@ -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; inputType: InputType; attribute: UserProfileAttributeMetadata; @@ -80,7 +76,7 @@ export const FIELDS: { } as const; export type UserProfileFieldsProps = { - t: TranslationFunction; + t: TFunction; form: UseFormReturn; userProfileMetadata: UserProfileMetadata; supportedLocales: string[]; @@ -167,7 +163,7 @@ export const UserProfileFields = ({ }; type FormFieldProps = { - t: TranslationFunction; + t: TFunction; form: UseFormReturn; supportedLocales: string[]; attribute: UserProfileAttributeMetadata; diff --git a/js/libs/ui-shared/src/user-profile/UserProfileGroup.tsx b/js/libs/ui-shared/src/user-profile/UserProfileGroup.tsx index 1925d25ec7..23e0fc0ecf 100644 --- a/js/libs/ui-shared/src/user-profile/UserProfileGroup.tsx +++ b/js/libs/ui-shared/src/user-profile/UserProfileGroup.tsx @@ -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; 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 ? ( diff --git a/js/libs/ui-shared/src/user-profile/utils.ts b/js/libs/ui-shared/src/user-profile/utils.ts index d413fe0f1c..02b4d27074 100644 --- a/js/libs/ui-shared/src/user-profile/utils.ts +++ b/js/libs/ui-shared/src/user-profile/utils.ts @@ -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( 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; diff --git a/js/pnpm-lock.yaml b/js/pnpm-lock.yaml index 57cd1b6d3f..be1c82af1a 100644 --- a/js/pnpm-lock.yaml +++ b/js/pnpm-lock.yaml @@ -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