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

View file

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

View file

@ -10,7 +10,7 @@
<body style="background: #111;"> <body style="background: #111;">
<logto-theme-provider theme="dark"> <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-profile-card></logto-profile-card>
</logto-user-provider> </logto-user-provider>
</logto-theme-provider> </logto-theme-provider>

View file

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

View file

@ -25,6 +25,9 @@ export class LogtoProfileCard extends LitElement {
@state() @state()
updateNameOpened = false; updateNameOpened = false;
@state()
name = '';
render() { render() {
const user = this.userContext?.user; const user = this.userContext?.user;
@ -74,6 +77,7 @@ export class LogtoProfileCard extends LitElement {
size="small" size="small"
@click=${() => { @click=${() => {
this.updateNameOpened = true; this.updateNameOpened = true;
this.name = user.name ?? '';
}} }}
> >
${msg('Update', { id: 'general.update' })} ${msg('Update', { id: 'general.update' })}
@ -100,10 +104,22 @@ export class LogtoProfileCard extends LitElement {
id: 'account.profile.personal-info.name-placeholder', id: 'account.profile.personal-info.name-placeholder',
desc: 'The placeholder for the name input field.', 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-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' })} ${msg('Save', { id: 'general.save' })}
</logto-button> </logto-button>
</logto-modal-layout> </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-modal.js';
export * from './components/logto-text-input.js'; export * from './components/logto-text-input.js';
export * from './elements/logto-profile-card.js'; export * from './elements/logto-profile-card.js';
export * from './utils/locale.js';
export * from './providers/logto-theme-provider.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 { createContext, provide } from '@lit/context';
import { type UserInfo } from '@logto/schemas'; import { type UserInfo } from '@logto/schemas';
import { noop } from '@silverhand/essentials';
import { LitElement, type PropertyValues, html } from 'lit'; import { LitElement, type PropertyValues, html } from 'lit';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import { LogtoAccountApi } from '../utils/api.js';
/** @see {@link UserContext} */ /** @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. * 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'); export const UserContext = createContext<UserContextType>('modal-context');
/** The default value for the user 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'; const tagName = 'logto-user-provider';
@ -24,23 +32,30 @@ export class LogtoUserProvider extends LitElement {
context = userContext; context = userContext;
@property({ type: Object }) @property({ type: Object })
user?: UserInfo; api!: LogtoAccountApi | ConstructorParameters<typeof LogtoAccountApi>[0];
render() { render() {
return html`<slot></slot>`; return html`<slot></slot>`;
} }
protected handlePropertiesChange(changedProperties: PropertyValues) { protected updateContext(context: Partial<UserContextType>) {
if (changedProperties.has('user')) { this.context = Object.freeze({ ...this.context, ...context });
this.context.user = this.user; }
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 { protected firstUpdated(changedProperties: PropertyValues): void {
this.handlePropertiesChange(changedProperties); void this.handlePropertiesChange(changedProperties);
}
protected updated(changedProperties: PropertyValues): void {
this.handlePropertiesChange(changedProperties);
} }
} }

View file

@ -6,9 +6,11 @@ import {
LogtoFormCard, LogtoFormCard,
LogtoProfileCard, LogtoProfileCard,
LogtoList, LogtoList,
LogtoUserProvider,
} from './index.js'; } from './index.js';
export * from './utils/locale.js'; export * from './utils/locale.js';
export * from './utils/api.js';
export const createReactComponents = (react: Parameters<typeof createComponent>[0]['react']) => { export const createReactComponents = (react: Parameters<typeof createComponent>[0]['react']) => {
return { return {
@ -37,5 +39,10 @@ export const createReactComponents = (react: Parameters<typeof createComponent>[
elementClass: LogtoThemeProvider, elementClass: LogtoThemeProvider,
react, 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': '@silverhand/essentials':
specifier: ^2.9.1 specifier: ^2.9.1
version: 2.9.1 version: 2.9.1
ky:
specifier: ^1.2.3
version: 1.2.3
lit: lit:
specifier: ^3.1.4 specifier: ^3.1.4
version: 3.1.4 version: 3.1.4