新增真实账户数据驱动的身份档案卡
个人资料改为“身份卡 + 编辑工作区”双栏布局 侧栏加入语义图标、产品域标题和更明确的选中状态 页面首屏增加身份控制台层级标识 用户名、邮箱、姓名、语言会随表单内容实时呈现 390px 移动端自动堆叠,无横向溢出 相关组件已由 Keycloakify 正式认领,后续同步不会覆盖
This commit is contained in:
1 parent
78743fb436
commit
c1b36b4d72
6 files changed
+1763
-262
No files matched your search
+3
-3
@@ -7,6 +7,8 @@
|
||||
# admin/index.css
|
||||
# admin/KcAdminUi.tsx
|
||||
# account/assets/logo.svg
|
||||
# account/personal-info/PersonalInfo.tsx
|
||||
# account/root/PageNav.tsx
|
||||
# admin/assets/logo.svg
|
||||
# admin/i18n/messages_en.properties
|
||||
# admin/i18n/messages_zh_Hans.properties
|
||||
@@ -15,6 +17,7 @@
|
||||
# email/messages/messages_en.properties
|
||||
# email/messages/messages_zh_CN.properties
|
||||
# email/messages/messages_zh_TW.properties
|
||||
# account/components/page/Page.tsx
|
||||
# === Owned files end =====
|
||||
|
||||
# === @keycloakify/email-native v260007.0.0 ===
|
||||
@@ -904,7 +907,6 @@
|
||||
/account/i18n/messages_zh_Hans.properties
|
||||
/account/i18n/messages_zh_Hant.properties
|
||||
/account/organizations/Organizations.tsx
|
||||
/account/personal-info/PersonalInfo.tsx
|
||||
/account/resources/EditTheResource.tsx
|
||||
/account/resources/PermissionRequest.tsx
|
||||
/account/resources/Resources.tsx
|
||||
@@ -914,7 +916,6 @@
|
||||
/account/resources/ShareTheResource.tsx
|
||||
/account/root/header.module.css
|
||||
/account/root/Header.tsx
|
||||
/account/root/PageNav.tsx
|
||||
/account/root/Root.tsx
|
||||
/account/utils/formatDate.ts
|
||||
/account/utils/joinPath.ts
|
||||
@@ -1074,4 +1075,3 @@
|
||||
/account/assets/passkeys/zoho-light.svg
|
||||
/account/assets/passkeys/ztpass-light.png
|
||||
/account/components/datalist/EmptyRow.tsx
|
||||
/account/components/page/Page.tsx
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "account/components/page/Page.tsx" --revert
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import {
|
||||
PageSection,
|
||||
Text,
|
||||
TextContent,
|
||||
Title
|
||||
} from "../../../shared/@patternfly/react-core";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
type PageProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export const Page = ({ title, description, children }: PropsWithChildren<PageProps>) => {
|
||||
return (
|
||||
<>
|
||||
<PageSection variant="light">
|
||||
<div className="nexus-page-intro">
|
||||
<div className="nexus-page-intro__copy">
|
||||
<span className="nexus-page-intro__kicker">
|
||||
NEXUS / Identity control
|
||||
</span>
|
||||
<TextContent>
|
||||
<Title headingLevel="h1" data-testid="page-heading">
|
||||
{title}
|
||||
</Title>
|
||||
<Text component="p">{description}</Text>
|
||||
</TextContent>
|
||||
</div>
|
||||
<div className="nexus-page-intro__mark" aria-hidden="true">
|
||||
<span>ID</span>
|
||||
<i />
|
||||
<span>ACCOUNT</span>
|
||||
</div>
|
||||
</div>
|
||||
</PageSection>
|
||||
<PageSection variant="light">{children}</PageSection>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "account/personal-info/PersonalInfo.tsx" --revert
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import {
|
||||
UserProfileFields,
|
||||
beerify,
|
||||
debeerify,
|
||||
setUserProfileServerError,
|
||||
useEnvironment
|
||||
} from "../../shared/keycloak-ui-shared";
|
||||
import {
|
||||
ActionGroup,
|
||||
Alert,
|
||||
AlertVariant,
|
||||
Button,
|
||||
ExpandableSection,
|
||||
Form,
|
||||
Spinner
|
||||
} from "../../shared/@patternfly/react-core";
|
||||
import { ExternalLinkSquareAltIcon } from "../../shared/@patternfly/react-icons";
|
||||
import { TFunction } from "i18next";
|
||||
import { useState } from "react";
|
||||
import { ErrorOption, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { getPersonalInfo, getSupportedLocales, savePersonalInfo } from "../api/methods";
|
||||
import { UserProfileMetadata, UserRepresentation } from "../api/representations";
|
||||
import { Page } from "../components/page/Page";
|
||||
import { type AccountEnvironment } from "..";
|
||||
import type { TFuncKey } from "../i18n-type";
|
||||
import { useAccountAlerts } from "../utils/useAccountAlerts";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
|
||||
export const PersonalInfo = () => {
|
||||
const { t } = useTranslation();
|
||||
const context = useEnvironment<AccountEnvironment>();
|
||||
const [userProfileMetadata, setUserProfileMetadata] = useState<UserProfileMetadata>();
|
||||
const [supportedLocales, setSupportedLocales] = useState<string[]>([]);
|
||||
const form = useForm<UserRepresentation>({ mode: "onChange" });
|
||||
const { handleSubmit, reset, setValue, setError } = form;
|
||||
const profile = form.watch();
|
||||
const { addAlert } = useAccountAlerts();
|
||||
|
||||
usePromise(
|
||||
signal =>
|
||||
Promise.all([
|
||||
getPersonalInfo({ signal, context }),
|
||||
getSupportedLocales({ signal, context })
|
||||
]),
|
||||
([personalInfo, supportedLocales]) => {
|
||||
setUserProfileMetadata(personalInfo.userProfileMetadata);
|
||||
setSupportedLocales(supportedLocales);
|
||||
reset(personalInfo);
|
||||
Object.entries(personalInfo.attributes || {}).forEach(([k, v]) =>
|
||||
setValue(`attributes[${beerify(k)}]`, v)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const onSubmit = async (user: UserRepresentation) => {
|
||||
try {
|
||||
const attributes = Object.fromEntries(
|
||||
Object.entries(user.attributes || {}).map(([k, v]) => [debeerify(k), v])
|
||||
);
|
||||
await savePersonalInfo(context, { ...user, attributes });
|
||||
const locale = attributes["locale"]?.toString();
|
||||
if (locale) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("languageChanged", { detail: { language: locale } })
|
||||
);
|
||||
}
|
||||
await context.keycloak.updateToken();
|
||||
addAlert(t("accountUpdatedMessage"));
|
||||
} catch (error) {
|
||||
addAlert(t("accountUpdatedError"), AlertVariant.danger);
|
||||
|
||||
setUserProfileServerError(
|
||||
{ responseData: { errors: error as any } },
|
||||
(name: string | number, error: unknown) =>
|
||||
setError(name as string, error as ErrorOption),
|
||||
((key: TFuncKey, param?: object) => t(key, param as any)) as TFunction
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (!userProfileMetadata) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const allFieldsReadOnly = () =>
|
||||
userProfileMetadata.attributes
|
||||
.map(a => a.readOnly)
|
||||
.reduce((p, c) => p && c, true);
|
||||
|
||||
const {
|
||||
updateEmailFeatureEnabled,
|
||||
updateEmailActionEnabled,
|
||||
isRegistrationEmailAsUsername,
|
||||
isEditUserNameAllowed
|
||||
} = context.environment.features;
|
||||
|
||||
const displayName =
|
||||
[profile.firstName, profile.lastName].filter(Boolean).join(" ").trim() ||
|
||||
profile.username ||
|
||||
t("personalInfo");
|
||||
const initials =
|
||||
[profile.firstName, profile.lastName]
|
||||
.filter(Boolean)
|
||||
.map(value => value!.trim().charAt(0))
|
||||
.join("")
|
||||
.slice(0, 2)
|
||||
.toUpperCase() || displayName.slice(0, 2).toUpperCase();
|
||||
const profileLocale =
|
||||
profile.attributes?.locale?.toString() || context.environment.locale;
|
||||
|
||||
return (
|
||||
<Page title={t("personalInfo")} description={t("personalInfoDescription")}>
|
||||
<div className="nexus-profile-layout">
|
||||
<aside className="nexus-identity-card" aria-label={t("personalInfo")}>
|
||||
<div className="nexus-identity-card__topline">
|
||||
<span>Identity record</span>
|
||||
<i aria-hidden="true" />
|
||||
</div>
|
||||
<div className="nexus-identity-card__avatar" aria-hidden="true">
|
||||
<span>{initials}</span>
|
||||
</div>
|
||||
<div className="nexus-identity-card__name">
|
||||
<span>{t("personalInfo")}</span>
|
||||
<h2>{displayName}</h2>
|
||||
<p>{profile.email || profile.username}</p>
|
||||
</div>
|
||||
<dl className="nexus-identity-card__facts">
|
||||
<div>
|
||||
<dt>{t("username")}</dt>
|
||||
<dd>{profile.username || "—"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{t("email")}</dt>
|
||||
<dd>{profile.email || "—"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Locale</dt>
|
||||
<dd>{profileLocale}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="nexus-identity-card__footer">
|
||||
<span aria-hidden="true" />
|
||||
NEXUS identity directory
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section className="nexus-profile-editor">
|
||||
<header className="nexus-profile-editor__header">
|
||||
<span>Profile data</span>
|
||||
<div>
|
||||
<h2>{t("personalInfo")}</h2>
|
||||
<p>{t("personalInfoDescription")}</p>
|
||||
</div>
|
||||
</header>
|
||||
<Form isHorizontal onSubmit={handleSubmit(onSubmit)}>
|
||||
<UserProfileFields
|
||||
form={form}
|
||||
userProfileMetadata={userProfileMetadata}
|
||||
supportedLocales={supportedLocales}
|
||||
currentLocale={context.environment.locale}
|
||||
t={
|
||||
((key: unknown, params) =>
|
||||
t(key as TFuncKey, params as any)) as TFunction
|
||||
}
|
||||
renderer={attribute => {
|
||||
const annotations = attribute.annotations
|
||||
? attribute.annotations
|
||||
: {};
|
||||
return attribute.name === "email" &&
|
||||
updateEmailFeatureEnabled &&
|
||||
updateEmailActionEnabled &&
|
||||
annotations["kc.required.action.supported"] &&
|
||||
(!isRegistrationEmailAsUsername ||
|
||||
isEditUserNameAllowed) ? (
|
||||
<Button
|
||||
id="update-email-btn"
|
||||
variant="link"
|
||||
onClick={() =>
|
||||
context.keycloak.login({
|
||||
action: "UPDATE_EMAIL"
|
||||
})
|
||||
}
|
||||
icon={<ExternalLinkSquareAltIcon />}
|
||||
iconPosition="right"
|
||||
>
|
||||
{t("updateEmail")}
|
||||
</Button>
|
||||
) : undefined;
|
||||
}}
|
||||
/>
|
||||
{!allFieldsReadOnly() && (
|
||||
<ActionGroup>
|
||||
<Button
|
||||
data-testid="save"
|
||||
type="submit"
|
||||
id="save-btn"
|
||||
variant="primary"
|
||||
>
|
||||
{t("save")}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="cancel"
|
||||
id="cancel-btn"
|
||||
variant="link"
|
||||
onClick={() => reset()}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
)}
|
||||
{context.environment.features.deleteAccountAllowed && (
|
||||
<ExpandableSection
|
||||
data-testid="delete-account"
|
||||
toggleText={t("deleteAccount")}
|
||||
>
|
||||
<Alert
|
||||
isInline
|
||||
title={t("deleteAccount")}
|
||||
variant="danger"
|
||||
actionLinks={
|
||||
<Button
|
||||
id="delete-account-btn"
|
||||
variant="danger"
|
||||
onClick={() =>
|
||||
context.keycloak.login({
|
||||
action: "delete_account"
|
||||
})
|
||||
}
|
||||
className="delete-button"
|
||||
>
|
||||
{t("delete")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{t("deleteAccountWarning")}
|
||||
</Alert>
|
||||
</ExpandableSection>
|
||||
)}
|
||||
</Form>
|
||||
</section>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default PersonalInfo;
|
||||
Loaded 3 of 6 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user