0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

feat(elements): update name

This commit is contained in:
Gao Sun 2024-07-19 17:46:55 +08:00
parent 2c1e326949
commit 85545d4cee
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
10 changed files with 141 additions and 67 deletions

View file

@ -36,7 +36,7 @@ if (isDevFeaturesEnabled) {
initLocalization();
}
const { LogtoProfileCard, LogtoThemeProvider } = createReactComponents(React);
const { LogtoProfileCard, LogtoThemeProvider, LogtoUserProvider } = createReactComponents(React);
function Profile() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
@ -70,60 +70,62 @@ function Profile() {
return (
<AppBoundary>
<LogtoThemeProvider theme="dark">
<div className={styles.pageContainer}>
<Topbar hideTenantSelector hideTitle />
<OverlayScrollbar className={styles.scrollable}>
<div className={styles.wrapper}>
<PageMeta titleKey="profile.page_title" />
<div className={pageLayout.headline}>
<CardTitle title="profile.title" subtitle="profile.description" />
</div>
{showLoadingSkeleton && <Skeleton />}
{isDevFeaturesEnabled && <LogtoProfileCard />}
{user && !showLoadingSkeleton && (
<div className={styles.content}>
<BasicUserInfoSection user={user} onUpdate={reload} />
{isCloud && (
<LinkAccountSection user={user} connectors={connectors} onUpdate={reload} />
)}
<FormCard title="profile.password.title">
<CardContent
title="profile.password.password_setting"
data={[
{
key: 'password',
label: 'profile.password.password',
value: user.hasPassword,
renderer: (value) => (value ? <span>********</span> : <NotSet />),
action: {
name: 'profile.change',
handler: () => {
navigate(user.hasPassword ? 'verify-password' : 'change-password', {
state: { email: user.primaryEmail, action: 'changePassword' },
});
<LogtoUserProvider api={api}>
<div className={styles.pageContainer}>
<Topbar hideTenantSelector hideTitle />
<OverlayScrollbar className={styles.scrollable}>
<div className={styles.wrapper}>
<PageMeta titleKey="profile.page_title" />
<div className={pageLayout.headline}>
<CardTitle title="profile.title" subtitle="profile.description" />
</div>
{showLoadingSkeleton && <Skeleton />}
{isDevFeaturesEnabled && <LogtoProfileCard />}
{user && !showLoadingSkeleton && (
<div className={styles.content}>
<BasicUserInfoSection user={user} onUpdate={reload} />
{isCloud && (
<LinkAccountSection user={user} connectors={connectors} onUpdate={reload} />
)}
<FormCard title="profile.password.title">
<CardContent
title="profile.password.password_setting"
data={[
{
key: 'password',
label: 'profile.password.password',
value: user.hasPassword,
renderer: (value) => (value ? <span>********</span> : <NotSet />),
action: {
name: 'profile.change',
handler: () => {
navigate(user.hasPassword ? 'verify-password' : 'change-password', {
state: { email: user.primaryEmail, action: 'changePassword' },
});
},
},
},
},
]}
/>
</FormCard>
{isCloud && (
<FormCard title="profile.delete_account.title">
<div className={styles.deleteAccount}>
<div className={styles.description}>
{t('profile.delete_account.description')}
</div>
<Button title="profile.delete_account.button" onClick={show} />
</div>
<DeleteAccountModal isOpen={showDeleteAccountModal} onClose={hide} />
]}
/>
</FormCard>
)}
</div>
)}
</div>
</OverlayScrollbar>
{childrenRoutes}
</div>
{isCloud && (
<FormCard title="profile.delete_account.title">
<div className={styles.deleteAccount}>
<div className={styles.description}>
{t('profile.delete_account.description')}
</div>
<Button title="profile.delete_account.button" onClick={show} />
</div>
<DeleteAccountModal isOpen={showDeleteAccountModal} onClose={hide} />
</FormCard>
)}
</div>
)}
</div>
</OverlayScrollbar>
{childrenRoutes}
</div>
</LogtoUserProvider>
</LogtoThemeProvider>
</AppBoundary>
);

View file

@ -11,6 +11,10 @@ export default function koaCors<StateT, ContextT, ResponseBodyT>(
origin: (ctx) => {
const { origin } = ctx.request.headers;
if (!EnvSet.values.isProduction) {
return origin ?? '';
}
if (
origin &&
urlSets.some((set) => {

View file

@ -10,7 +10,7 @@
<body style="background: #111;">
<logto-theme-provider theme="dark">
<logto-user-provider user='{"name": "Johnny Silverhand", "avatar": "https://github.com/logto-io.png"}'>
<logto-user-provider>
<logto-profile-card></logto-profile-card>
</logto-user-provider>
</logto-theme-provider>

View file

@ -51,6 +51,7 @@
"@lit/localize": "^0.12.1",
"@lit/react": "^1.0.5",
"@silverhand/essentials": "^2.9.1",
"ky": "^1.2.3",
"lit": "^3.1.4"
},
"devDependencies": {

View file

@ -25,6 +25,9 @@ export class LogtoProfileCard extends LitElement {
@state()
updateNameOpened = false;
@state()
name = '';
render() {
const user = this.userContext?.user;
@ -74,6 +77,7 @@ export class LogtoProfileCard extends LitElement {
size="small"
@click=${() => {
this.updateNameOpened = true;
this.name = user.name ?? '';
}}
>
${msg('Update', { id: 'general.update' })}
@ -100,10 +104,22 @@ export class LogtoProfileCard extends LitElement {
id: 'account.profile.personal-info.name-placeholder',
desc: 'The placeholder for the name input field.',
})}
value=""
.value=${this.name}
@input=${(event: InputEvent) => {
// eslint-disable-next-line no-restricted-syntax
this.name = (event.target as HTMLInputElement).value;
}}
/>
</logto-text-input>
<logto-button slot="footer" size="large" type="primary">
<logto-button
slot="footer"
size="large"
type="primary"
@click=${async () => {
await this.userContext?.updateUser({ name: this.name });
this.updateNameOpened = false;
}}
>
${msg('Save', { id: 'general.save' })}
</logto-button>
</logto-modal-layout>

View file

@ -10,5 +10,9 @@ export * from './components/logto-modal-layout.js';
export * from './components/logto-modal.js';
export * from './components/logto-text-input.js';
export * from './elements/logto-profile-card.js';
export * from './utils/locale.js';
export * from './providers/logto-theme-provider.js';
export * from './providers/logto-user-provider.js';
export * from './utils/api.js';
export * from './utils/locale.js';

View file

@ -1,10 +1,16 @@
import { createContext, provide } from '@lit/context';
import { type UserInfo } from '@logto/schemas';
import { noop } from '@silverhand/essentials';
import { LitElement, type PropertyValues, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { LogtoAccountApi } from '../utils/api.js';
/** @see {@link UserContext} */
export type UserContextType = { user?: UserInfo };
export type UserContextType = Readonly<{
user?: UserInfo;
updateUser: (user: Partial<UserInfo>) => void | Promise<void>;
}>;
/**
* Context for the current user. It's a fundamental context for the account-related elements.
@ -12,7 +18,9 @@ export type UserContextType = { user?: UserInfo };
export const UserContext = createContext<UserContextType>('modal-context');
/** The default value for the user context. */
export const userContext: UserContextType = {};
export const userContext: UserContextType = Object.freeze({
updateUser: noop,
});
const tagName = 'logto-user-provider';
@ -24,23 +32,30 @@ export class LogtoUserProvider extends LitElement {
context = userContext;
@property({ type: Object })
user?: UserInfo;
api!: LogtoAccountApi | ConstructorParameters<typeof LogtoAccountApi>[0];
render() {
return html`<slot></slot>`;
}
protected handlePropertiesChange(changedProperties: PropertyValues) {
if (changedProperties.has('user')) {
this.context.user = this.user;
protected updateContext(context: Partial<UserContextType>) {
this.context = Object.freeze({ ...this.context, ...context });
}
protected async handlePropertiesChange(changedProperties: PropertyValues) {
if (changedProperties.has('api')) {
const api = this.api instanceof LogtoAccountApi ? this.api : new LogtoAccountApi(this.api);
this.updateContext({
updateUser: async (user) => {
const updated = await api.updateUser(user);
this.updateContext({ user: updated });
},
user: await api.getUser(),
});
}
}
protected firstUpdated(changedProperties: PropertyValues): void {
this.handlePropertiesChange(changedProperties);
}
protected updated(changedProperties: PropertyValues): void {
this.handlePropertiesChange(changedProperties);
void this.handlePropertiesChange(changedProperties);
}
}

View file

@ -6,9 +6,11 @@ import {
LogtoFormCard,
LogtoProfileCard,
LogtoList,
LogtoUserProvider,
} from './index.js';
export * from './utils/locale.js';
export * from './utils/api.js';
export const createReactComponents = (react: Parameters<typeof createComponent>[0]['react']) => {
return {
@ -37,5 +39,10 @@ export const createReactComponents = (react: Parameters<typeof createComponent>[
elementClass: LogtoThemeProvider,
react,
}),
LogtoUserProvider: createComponent({
tagName: LogtoUserProvider.tagName,
elementClass: LogtoUserProvider,
react,
}),
};
};

View file

@ -0,0 +1,22 @@
import { type UserInfo } from '@logto/schemas';
import originalKy, { type Options, type KyInstance } from 'ky';
/**
* CAUTION: The current implementation is based on the admin tenant's `/me` API which is interim.
* The final implementation should be based on the Account API.
*/
export class LogtoAccountApi {
protected ky: KyInstance;
constructor(init: KyInstance | Options) {
this.ky = 'create' in init ? init : originalKy.create(init);
}
async getUser() {
return this.ky('me').json<UserInfo>();
}
async updateUser(user: Partial<UserInfo>) {
return this.ky.patch('me', { json: user }).json<UserInfo>();
}
}

View file

@ -3572,6 +3572,9 @@ importers:
'@silverhand/essentials':
specifier: ^2.9.1
version: 2.9.1
ky:
specifier: ^1.2.3
version: 1.2.3
lit:
specifier: ^3.1.4
version: 3.1.4