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:
parent
2c1e326949
commit
85545d4cee
10 changed files with 141 additions and 67 deletions
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
22
packages/elements/src/utils/api.ts
Normal file
22
packages/elements/src/utils/api.ts
Normal 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>();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue