chore: user center extension (#6213)

* chore(versions): 😊 publish v1.6.0-alpha.24

* chore(versions): 😊 publish v1.6.0-alpha.25

* chore: user center extension

* refactor: change password

* chore: theme

* chore: language

* chore: code improve

* feat: support extending frontend filter operators (#6085)

* feat: operator extension

* fix: bug

* refactor: code improve

* fix: jsonLogic

---------

Co-authored-by: chenos <chenlinxh@gmail.com>

* refactor: remove registerOperators (#6224)

* refactor(plugin-workflow): trigger workflow action settings (#6143)

* refactor(plugin-workflow): move bind workflow settings to plugin

* refactor(plugin-block-workbench): move component to core

* refactor(plugin-block-workbench): adjust component api

* fix(plugin-workflow-action-trigger): fix test cases

* fix(plugin-workflow): fix component scope

* fix(plugin-workflow-action-trigger): fix test cases

* chore(versions): 😊 publish v1.6.0-alpha.26

* feat: support the extension of preset fields in collections (#6183)

* feat: support the extension of preset fields in collections

* fix: bug

* fix: bug

* fix: bug

* refactor: create collection

* fix: config

* fix: test case

* refactor: code improve

* refactor: code improve

* fix: bug

* fix: bug

---------

Co-authored-by: chenos <chenlinxh@gmail.com>

* feat: support for the extension of optional fields for Kanban, Calendar, and Formula Field plugins (#6076)

* feat: kanban field extention

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* feat: calender title fields

* feat: background color fields

* fix: bug

* fix: bug

* feat: formula field expression support field

* feat: preset fields

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* refactor: code improve

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* refactor: code improve

* revert: preset fields

* refactor: code improve

* refactor: code improve

* fix: bug

* fix: bug

* fix: bug

* refactor: code improve

* fix: bug

* refactor: code improve

* refactor: code improve

* fix: bug

* fix: locale

* refactor: code improve

* fix: bug

* refactor: code improve

* refactor: code improve

* refactor: code improve

* refactor: locale

* fix: test

* fix: bug

* fix: test

* fix: test

---------

Co-authored-by: chenos <chenlinxh@gmail.com>

* feat: inline mode

* chore(versions): 😊 publish v1.6.0-alpha.27

* fix(data-source-main): update order

* fix: bug

* fix: bug

* refactor: code improve

* fix: bug

* fix: code improve

* fix: bug

* fix: improve code

* fix: getFontColor (#6241)

* chore(versions): 😊 publish v1.6.0-alpha.28

* refactor: code improve

* fix: bug

* refactor: code improve

* fix: bug

* fix: print action e2e test (#6256)

* fix: print action e2e test

* fix: test

* fix: merge bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* refactor: update  package.json

* fix: bug

* feat: code improve

---------

Co-authored-by: nocobase[bot] <179432756+nocobase[bot]@users.noreply.github.com>
Co-authored-by: chenos <chenlinxh@gmail.com>
Co-authored-by: Junyi <mytharcher@users.noreply.github.com>
This commit is contained in:
Katherine 2025-02-24 10:11:42 +08:00 committed by GitHub
parent c9c7531946
commit 9eb9d4e000
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 1749 additions and 1218 deletions

View File

@ -2,9 +2,7 @@
"version": "1.6.0-beta.9", "version": "1.6.0-beta.9",
"npmClient": "yarn", "npmClient": "yarn",
"useWorkspaces": true, "useWorkspaces": true,
"npmClientArgs": [ "npmClientArgs": ["--ignore-engines"],
"--ignore-engines"
],
"command": { "command": {
"version": { "version": {
"forcePublish": true, "forcePublish": true,

View File

@ -101,7 +101,7 @@ const Demo = () => {
return ( return (
<div> <div>
<div>{render()}</div> <div>{render()}</div>
<div>{render({ style: { color: 'red' } })}</div> <div>{render({ mode: 'inline', style: { color: 'red' } })}</div>
</div> </div>
); );
}; };

View File

@ -65,7 +65,8 @@
"react-router-dom": "^6.11.2", "react-router-dom": "^6.11.2",
"react-to-print": "^2.14.7", "react-to-print": "^2.14.7",
"sanitize-html": "2.13.0", "sanitize-html": "2.13.0",
"use-deep-compare-effect": "^1.8.1" "use-deep-compare-effect": "^1.8.1",
"ignore": "^5.2.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=18.0.0", "react": ">=18.0.0",

View File

@ -17,7 +17,6 @@ import React, { ComponentType, FC, ReactElement, ReactNode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { I18nextProvider } from 'react-i18next'; import { I18nextProvider } from 'react-i18next';
import { Link, NavLink, Navigate } from 'react-router-dom'; import { Link, NavLink, Navigate } from 'react-router-dom';
import { APIClient, APIClientProvider } from '../api-client'; import { APIClient, APIClientProvider } from '../api-client';
import { CSSVariableProvider } from '../css-variable'; import { CSSVariableProvider } from '../css-variable';
import { AntdAppProvider, GlobalThemeProvider } from '../global-theme'; import { AntdAppProvider, GlobalThemeProvider } from '../global-theme';
@ -29,7 +28,8 @@ import { WebSocketClient, WebSocketClientOptions } from './WebSocketClient';
import { AppComponent, BlankComponent, defaultAppComponents } from './components'; import { AppComponent, BlankComponent, defaultAppComponents } from './components';
import { SchemaInitializer, SchemaInitializerManager } from './schema-initializer'; import { SchemaInitializer, SchemaInitializerManager } from './schema-initializer';
import * as schemaInitializerComponents from './schema-initializer/components'; import * as schemaInitializerComponents from './schema-initializer/components';
import { SchemaSettings, SchemaSettingsManager } from './schema-settings'; import { SchemaSettings, SchemaSettingsManager, SchemaSettingsItemType } from './schema-settings';
import { compose, normalizeContainer } from './utils'; import { compose, normalizeContainer } from './utils';
import { defineGlobalDeps } from './utils/globalDeps'; import { defineGlobalDeps } from './utils/globalDeps';
import { getRequireJs } from './utils/requirejs'; import { getRequireJs } from './utils/requirejs';
@ -46,6 +46,7 @@ import { AppSchemaComponentProvider } from './AppSchemaComponentProvider';
import type { Plugin } from './Plugin'; import type { Plugin } from './Plugin';
import { getOperators } from './globalOperators'; import { getOperators } from './globalOperators';
import type { RequireJS } from './utils/requirejs'; import type { RequireJS } from './utils/requirejs';
import { useAclSnippets } from './hooks/useAclSnippets';
type JsonLogic = { type JsonLogic = {
addOperation: (name: string, fn?: any) => void; addOperation: (name: string, fn?: any) => void;
@ -495,4 +496,20 @@ export class Application {
getGlobalVar(key) { getGlobalVar(key) {
return get(this.globalVars, key); return get(this.globalVars, key);
} }
addUserCenterSettingsItem(item: SchemaSettingsItemType & { aclSnippet?: string }) {
const useVisibleProp = item.useVisible || (() => true);
const useVisible = () => {
const { allow } = useAclSnippets();
const visible = useVisibleProp();
if (!visible) {
return false;
}
return item.aclSnippet ? allow(item.aclSnippet) : true;
};
this.schemaSettingsManager.addItem('userCenterSettings', item.name, {
...item,
useVisible: useVisible,
});
}
} }

View File

@ -0,0 +1,25 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useACLRoleContext } from '../../acl/ACLProvider';
import ignore from 'ignore';
export const useAclSnippets = () => {
const { allowAll, snippets } = useACLRoleContext();
return {
allow: (aclSnippet) => {
if (aclSnippet) {
const ig = ignore().add(snippets);
const appAllowed = allowAll || ig.ignores(aclSnippet);
return appAllowed;
}
return true;
},
};
};

View File

@ -7,16 +7,15 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { useMemo } from 'react';
import { useApp } from '../../hooks';
import { SchemaSettingOptions } from '../types';
import React from 'react';
import { SchemaSettingsWrapper } from '../components';
import { SchemaSettingsProps } from '../../../schema-settings';
import { Schema } from '@formily/json-schema';
import { GeneralField } from '@formily/core'; import { GeneralField } from '@formily/core';
import { Schema } from '@formily/json-schema';
import React, { useMemo } from 'react';
import { Designable } from '../../../schema-component'; import { Designable } from '../../../schema-component';
import { SchemaSettingsProps } from '../../../schema-settings';
import { useApp } from '../../hooks';
import { SchemaSettingsWrapper } from '../components';
import { SchemaSettings } from '../SchemaSettings'; import { SchemaSettings } from '../SchemaSettings';
import { SchemaSettingOptions } from '../types';
type UseSchemaSettingsRenderOptions<T = {}> = Omit<SchemaSettingOptions<T>, 'name' | 'items'> & type UseSchemaSettingsRenderOptions<T = {}> = Omit<SchemaSettingOptions<T>, 'name' | 'items'> &
Omit<SchemaSettingsProps, 'title' | 'children'> & { Omit<SchemaSettingsProps, 'title' | 'children'> & {

View File

@ -22,6 +22,7 @@ import {
export interface SchemaSettingOptions<T = {}> { export interface SchemaSettingOptions<T = {}> {
name: string; name: string;
mode?: 'inline' | 'dropdown';
Component?: ComponentType<T>; Component?: ComponentType<T>;
componentProps?: T; componentProps?: T;
items: SchemaSettingsItemType[]; items: SchemaSettingsItemType[];

View File

@ -0,0 +1,46 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useAPIClient, useSystemSettings, SchemaSettingsSelectItem } from '../../..';
import locale from '../../../locale';
export const LanguageSettings = () => {
const { t, i18n } = useTranslation();
const api = useAPIClient();
const { data } = useSystemSettings() || {};
const enabledLanguages: string[] = useMemo(() => data?.data?.enabledLanguages || [], [data?.data?.enabledLanguages]);
if (enabledLanguages.length < 2) {
return null;
}
return (
<SchemaSettingsSelectItem
title={t('Language')}
options={Object.keys(locale)
.filter((lang) => enabledLanguages.includes(lang))
.map((lang) => {
return {
label: locale[lang].label,
value: lang,
};
})}
value={i18n.language}
onChange={async (lang) => {
await api.resource('users').updateLang({
values: {
appLang: lang,
},
});
api.auth.setLocale(lang);
window.location.reload();
}}
/>
);
};

View File

@ -0,0 +1,43 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React from 'react';
import { UserOutlined } from '@ant-design/icons';
import { css } from '@emotion/css';
import { useToken, useSchemaSettingsRender } from '../../../';
export const UserCenterButton = () => {
const { token } = useToken();
return (
<div
className="nb-user-center"
style={{ display: 'inline-block', verticalAlign: 'top', width: '46px', height: '46px' }}
>
<span
data-testid="user-center-button"
className={css`
max-width: 160px;
overflow: hidden;
display: inline-block;
line-height: 12px;
white-space: nowrap;
text-overflow: ellipsis;
`}
style={{ cursor: 'pointer', padding: '16px', color: token.colorTextHeaderMenu }}
>
<UserOutlined />
</span>
</div>
);
};
export function UserCenter() {
const { render } = useSchemaSettingsRender('userCenterSettings');
return <div style={{ display: 'inline-block' }}>{render()}</div>;
}

View File

@ -27,7 +27,6 @@ import { Outlet } from 'react-router-dom';
import { import {
ACLRolesCheckProvider, ACLRolesCheckProvider,
CurrentAppInfoProvider, CurrentAppInfoProvider,
CurrentUser,
findByUid, findByUid,
findMenuItem, findMenuItem,
NavigateIfNotSignIn, NavigateIfNotSignIn,
@ -58,7 +57,8 @@ import { useMenuTranslation } from '../../../schema-component/antd/menu/locale';
import { Help } from '../../../user/Help'; import { Help } from '../../../user/Help';
import { KeepAlive } from './KeepAlive'; import { KeepAlive } from './KeepAlive';
import { convertRoutesToSchema, NocoBaseDesktopRoute, NocoBaseDesktopRouteType } from './convertRoutesToSchema'; import { convertRoutesToSchema, NocoBaseDesktopRoute, NocoBaseDesktopRouteType } from './convertRoutesToSchema';
import { userCenterSettings } from './userCenterSettings';
import { UserCenter } from './UserCenterButton';
export { KeepAlive, NocoBaseDesktopRouteType }; export { KeepAlive, NocoBaseDesktopRouteType };
const RouteContext = createContext<NocoBaseDesktopRoute | null>(null); const RouteContext = createContext<NocoBaseDesktopRoute | null>(null);
@ -529,7 +529,7 @@ export const InternalAdminLayout = () => {
<Divider type="vertical" /> <Divider type="vertical" />
</ConfigProvider> </ConfigProvider>
<Help /> <Help />
<CurrentUser /> <UserCenter />
</div> </div>
</div> </div>
</Layout.Header> </Layout.Header>
@ -574,6 +574,7 @@ export class AdminLayoutPlugin extends Plugin {
await this.app.pm.add(RemoteSchemaTemplateManagerPlugin); await this.app.pm.add(RemoteSchemaTemplateManagerPlugin);
} }
async load() { async load() {
this.app.schemaSettingsManager.add(userCenterSettings);
this.app.addComponents({ AdminLayout, AdminDynamicPage }); this.app.addComponents({ AdminLayout, AdminDynamicPage });
} }
} }

View File

@ -0,0 +1,26 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { UserCenterButton } from './UserCenterButton';
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
import { LanguageSettings } from './LanguageSettings';
const userCenterSettings = new SchemaSettings({
name: 'userCenterSettings',
Component: UserCenterButton,
items: [
{
name: 'langue',
Component: LanguageSettings,
sort: 350,
},
],
});
export { userCenterSettings };

View File

@ -22,6 +22,7 @@ import {
CascaderProps, CascaderProps,
ConfigProvider, ConfigProvider,
Dropdown, Dropdown,
Menu,
MenuItemProps, MenuItemProps,
MenuProps, MenuProps,
Modal, Modal,
@ -119,6 +120,7 @@ export interface SchemaSettingsProps {
field?: GeneralField; field?: GeneralField;
fieldSchema?: Schema; fieldSchema?: Schema;
children?: ReactNode; children?: ReactNode;
mode?: 'inline' | 'dropdown';
} }
interface SchemaSettingsContextProps<T = any> { interface SchemaSettingsContextProps<T = any> {
@ -167,7 +169,7 @@ export const SchemaSettingsProvider: React.FC<SchemaSettingsProviderProps> = (pr
return <SchemaSettingsContext.Provider value={value}>{children}</SchemaSettingsContext.Provider>; return <SchemaSettingsContext.Provider value={value}>{children}</SchemaSettingsContext.Provider>;
}; };
export const SchemaSettingsDropdown: React.FC<SchemaSettingsProps> = React.memo((props) => { const InternalSchemaSettingsDropdown: React.FC<SchemaSettingsProps> = React.memo((props) => {
const { title, dn, ...others } = props; const { title, dn, ...others } = props;
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { Component, getMenuItems } = useMenuItem(); const { Component, getMenuItems } = useMenuItem();
@ -232,6 +234,25 @@ export const SchemaSettingsDropdown: React.FC<SchemaSettingsProps> = React.memo(
); );
}); });
const InternalSchemaSettingsMenu: React.FC<SchemaSettingsProps> = React.memo((props) => {
const { title, dn, ...others } = props;
const [visible, setVisible] = useState(true);
const { Component, getMenuItems } = useMenuItem();
const items = getMenuItems(() => props.children);
return (
<SchemaSettingsProvider visible={visible} setVisible={setVisible} dn={dn} {...others}>
<Component />
<Menu items={items} />
</SchemaSettingsProvider>
);
});
export const SchemaSettingsDropdown: React.FC<SchemaSettingsProps> = React.memo((props) => {
const { mode } = props;
return mode === 'inline' ? <InternalSchemaSettingsMenu {...props} /> : <InternalSchemaSettingsDropdown {...props} />;
});
SchemaSettingsDropdown.displayName = 'SchemaSettingsDropdown'; SchemaSettingsDropdown.displayName = 'SchemaSettingsDropdown';
const findGridSchema = (fieldSchema) => { const findGridSchema = (fieldSchema) => {

View File

@ -7,219 +7,12 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { UserOutlined } from '@ant-design/icons'; import { createContext } from 'react';
import { css } from '@emotion/css'; import { SelectWithTitle } from '../common';
import { error } from '@nocobase/utils/client';
import { App, Dropdown, Menu, MenuProps } from 'antd';
import React, { createContext, useCallback, useMemo as useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useACLRoleContext, useAPIClient, useCurrentUserContext, useToken } from '..';
import { useNavigateNoUpdate } from '../application/CustomRouterContextProvider';
import { useChangePassword } from './ChangePassword';
import { useCurrentUserSettingsMenu } from './CurrentUserSettingsMenuProvider';
import { useEditProfile } from './EditProfile';
import { useLanguageSettings } from './LanguageSettings';
import { useSwitchRole } from './SwitchRole';
const useNickname = () => { export const SettingsMenuProvider = (props) => {
const { data } = useCurrentUserContext(); return SelectWithTitle;
const { token } = useToken();
return useEffect(() => {
return {
key: 'nickname',
disabled: true,
label: (
<span aria-disabled="false" style={{ cursor: 'text', color: token.colorTextDescription }}>
{data?.data?.nickname || data?.data?.username || data?.data?.email}
</span>
),
};
}, [data?.data?.email, data?.data?.nickname, data?.data?.username, data?.data.version, token.colorTextDescription]);
};
/**
* @note If you want to change here, Note the Setting block on the mobile side
*/
export const SettingsMenu: React.FC<{
redirectUrl?: string;
}> = (props) => {
const { addMenuItem, getMenuItems } = useCurrentUserSettingsMenu();
const { redirectUrl = '' } = props;
const { allowAll, snippets } = useACLRoleContext();
const appAllowed = allowAll || snippets?.includes('app');
const navigate = useNavigateNoUpdate();
const api = useAPIClient();
const { t } = useTranslation();
const silenceApi = useAPIClient();
const check = useCallback(async () => {
return await new Promise((resolve) => {
const heartbeat = setInterval(() => {
silenceApi
.silent()
.resource('app')
.getInfo()
.then((res) => {
if (res?.status === 200) {
resolve('ok');
clearInterval(heartbeat);
}
return res;
})
.catch((err) => {
error(err);
});
}, 3000);
});
}, [silenceApi]);
const nickname = useNickname();
const editProfile = useEditProfile();
const changePassword = useChangePassword();
const switchRole = useSwitchRole();
const languageSettings = useLanguageSettings();
const { modal } = App.useApp();
const controlApp = useEffect<MenuProps['items']>(() => {
if (!appAllowed) {
return [];
}
return [
{
key: 'cache',
label: t('Clear cache'),
onClick: async () => {
await api.resource('app').clearCache();
window.location.reload();
},
},
{
key: 'reboot',
label: t('Restart application'),
onClick: async () => {
modal.confirm({
title: t('Restart application'),
// content: t('The will interrupt service, it may take a few seconds to restart. Are you sure to continue?'),
okText: t('Restart'),
okButtonProps: {
danger: true,
},
onOk: async () => {
await api.resource('app').restart();
},
});
},
},
{
key: 'divider_4',
type: 'divider',
},
];
}, [api, appAllowed, check, modal, t]);
useEffect(() => {
const items = [
nickname,
{
key: 'divider_1',
type: 'divider',
},
editProfile,
changePassword,
editProfile ||
(changePassword && {
key: 'divider_2',
type: 'divider',
}),
switchRole,
{
key: 'divider_3',
type: 'divider',
},
...controlApp,
{
key: 'signout',
label: t('Sign out'),
onClick: async () => {
const { data } = await api.auth.signOut();
if (data?.data?.redirect) {
window.location.href = data.data.redirect;
} else {
navigate(`/signin?redirect=${encodeURIComponent(redirectUrl)}`);
}
},
},
];
items.forEach((item) => {
if (item) {
addMenuItem(item);
}
});
if (languageSettings) {
addMenuItem(languageSettings, { before: 'divider_3' });
}
}, [
addMenuItem,
api.auth,
editProfile,
changePassword,
controlApp,
languageSettings,
navigate,
redirectUrl,
switchRole,
t,
nickname,
]);
return <Menu items={getMenuItems()} />;
}; };
export const DropdownVisibleContext = createContext(null); export const DropdownVisibleContext = createContext(null);
DropdownVisibleContext.displayName = 'DropdownVisibleContext'; DropdownVisibleContext.displayName = 'DropdownVisibleContext';
export const CurrentUser = () => {
const [visible, setVisible] = useState(false);
const { token } = useToken();
return (
<div
className={css`
display: inline-block;
vertical-align: top;
width: 46px;
height: 46px;
&:hover {
background: rgba(255, 255, 255, 0.1) !important;
}
`}
>
<DropdownVisibleContext.Provider value={{ visible, setVisible }}>
<Dropdown
open={visible}
onOpenChange={(visible) => {
setVisible(visible);
}}
dropdownRender={() => {
return <SettingsMenu />;
}}
>
<span
data-testid="user-center-button"
className={css`
max-width: 160px;
overflow: hidden;
display: inline-block;
line-height: 12px;
white-space: nowrap;
text-overflow: ellipsis;
`}
style={{ cursor: 'pointer', padding: '16px', color: token.colorTextHeaderMenu }}
>
<UserOutlined />
</span>
</Dropdown>
</DropdownVisibleContext.Provider>
</div>
);
};

View File

@ -1,56 +0,0 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { MenuProps } from 'antd';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { SelectWithTitle, useAPIClient, useSystemSettings } from '..';
import locale from '../locale';
export const useLanguageSettings = () => {
const { t, i18n } = useTranslation();
const api = useAPIClient();
const { data } = useSystemSettings() || {};
const enabledLanguages: string[] = useMemo(() => data?.data?.enabledLanguages || [], [data?.data?.enabledLanguages]);
const result = useMemo<MenuProps['items'][0]>(() => {
return {
key: 'language',
eventKey: 'LanguageSettings',
label: (
<SelectWithTitle
title={t('Language')}
options={Object.keys(locale)
.filter((lang) => enabledLanguages.includes(lang))
.map((lang) => {
return {
label: locale[lang].label,
value: lang,
};
})}
defaultValue={i18n.language}
onChange={async (lang) => {
await api.resource('users').updateLang({
values: {
appLang: lang,
},
});
api.auth.setLocale(lang);
window.location.reload();
}}
/>
),
};
}, [api, enabledLanguages, i18n, t]);
if (enabledLanguages.length < 2) {
return null;
}
return result;
};

View File

@ -1,50 +0,0 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { MenuProps } from 'antd';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useAPIClient } from '../api-client';
import { SelectWithTitle } from '../common';
import { useCurrentRoles } from './CurrentUserProvider';
export const useSwitchRole = () => {
const api = useAPIClient();
const roles = useCurrentRoles();
const { t } = useTranslation();
const result = useMemo<MenuProps['items'][0]>(() => {
return {
key: 'role',
eventKey: 'SwitchRole',
label: (
<SelectWithTitle
title={t('Switch role')}
fieldNames={{
label: 'title',
value: 'name',
}}
options={roles}
defaultValue={api.auth.role}
onChange={async (roleName) => {
api.auth.setRole(roleName);
await api.resource('users').setDefaultRole({ values: { roleName } });
location.reload();
window.location.reload();
}}
/>
),
};
}, [api, roles, t]);
if (roles.length <= 1) {
return null;
}
return result;
};

View File

@ -1,58 +0,0 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { render } from '@nocobase/test/client';
import React from 'react';
import { SettingsMenu } from '../CurrentUser';
import { useCurrentUserSettingsMenu } from '../CurrentUserSettingsMenuProvider';
const AppContextProvider = (props) => {
return <div></div>;
};
// TODO: AppContextProvider 没有提供足够的上下文环境
describe.skip('CurrentUserSettingsMenuProvider', () => {
const wrapper = ({ children }) => {
return (
<AppContextProvider>
<SettingsMenu />
{children}
</AppContextProvider>
);
};
const TestComponent = () => {
const { getMenuItems } = useCurrentUserSettingsMenu();
getMenuItems();
return <div>Test</div>;
};
it('should throw error when CurrentUserSettingsMenuProvider is not provided', () => {
expect(() => {
render(<TestComponent />);
}).toThrowErrorMatchingInlineSnapshot(
'"CurrentUser: You should use `CurrentUserSettingsMenuProvider` in the root of your app."',
);
});
it('should not throw error when providing context', () => {
expect(() => {
render(<TestComponent />, { wrapper });
}).not.toThrow();
});
// TODO: result.current 是 null会报错暂时不知道哪里出了问题
// it.skip('add menu item', () => {
// const { result } = renderHook(() => useCurrentUserSettingsMenu(), {
// wrapper,
// });
// expect(result.current.getMenuItems()).not.toHaveLength(0);
// });
});

View File

@ -0,0 +1,40 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useCurrentRoles, useAPIClient, SchemaSettingsItem, SelectWithTitle } from '@nocobase/client';
export const SwitchRole = () => {
const { t } = useTranslation();
const api = useAPIClient();
const roles = useCurrentRoles();
if (roles.length <= 1) {
return null;
}
return (
<SchemaSettingsItem eventKey="SwitchRole" title="SwitchRole">
<SelectWithTitle
title={t('Switch role')}
fieldNames={{
label: 'title',
value: 'name',
}}
options={roles}
defaultValue={api.auth.role}
onChange={async (roleName) => {
api.auth.setRole(roleName);
await api.resource('users').setDefaultRole({ values: { roleName } });
location.reload();
window.location.reload();
}}
/>
</SchemaSettingsItem>
);
};

View File

@ -9,9 +9,10 @@
import { Plugin, lazy } from '@nocobase/client'; import { Plugin, lazy } from '@nocobase/client';
import { ACLSettingsUI } from './ACLSettingsUI'; import { ACLSettingsUI } from './ACLSettingsUI';
// import { RolesManagement } from './RolesManagement';
const { RolesManagement } = lazy(() => import('./RolesManagement'), 'RolesManagement');
import { RolesManager } from './roles-manager'; import { RolesManager } from './roles-manager';
import { SwitchRole } from './SwitchRole';
const { RolesManagement } = lazy(() => import('./RolesManagement'), 'RolesManagement');
export class PluginACLClient extends Plugin { export class PluginACLClient extends Plugin {
rolesManager = new RolesManager(); rolesManager = new RolesManager();
@ -25,6 +26,18 @@ export class PluginACLClient extends Plugin {
aclSnippet: 'pm.acl.roles', aclSnippet: 'pm.acl.roles',
sort: 3, sort: 3,
}); });
// 个人中心注册 切换角色
this.app.addUserCenterSettingsItem({
name: 'divider_switchRole',
type: 'divider',
sort: 200,
});
this.app.addUserCenterSettingsItem({
name: 'switchRole',
Component: SwitchRole,
sort: 300,
});
} }
} }

View File

@ -0,0 +1,29 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useAPIClient, SchemaSettingsItem } from '@nocobase/client';
export const ClearCache = () => {
const { t } = useTranslation();
const api = useAPIClient();
return (
<SchemaSettingsItem
eventKey="cache"
title="cache"
onClick={async () => {
await api.resource('app').clearCache();
window.location.reload();
}}
>
{t('Clear cache')}
</SchemaSettingsItem>
);
};

View File

@ -0,0 +1,40 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React from 'react';
import { useTranslation } from 'react-i18next';
import { App } from 'antd';
import { useAPIClient, SchemaSettingsItem } from '@nocobase/client';
export const RestartApplication = () => {
const { t } = useTranslation();
const api = useAPIClient();
const { modal } = App.useApp();
return (
<SchemaSettingsItem
eventKey="restartApplication"
title="restartApplication"
onClick={async () => {
modal.confirm({
title: t('Restart application'),
// content: t('The will interrupt service, it may take a few seconds to restart. Are you sure to continue?'),
okText: t('Restart'),
okButtonProps: {
danger: true,
},
onOk: async () => {
await api.resource('app').restart();
},
});
}}
>
{t('Restart application')}
</SchemaSettingsItem>
);
};

View File

@ -7,25 +7,48 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { Plugin } from '@nocobase/client'; import { Plugin, useACLRoleContext } from '@nocobase/client';
import ignore from 'ignore';
import { DesktopRoutesManager } from './DesktopRoutesManager'; import { DesktopRoutesManager } from './DesktopRoutesManager';
import { lang as t } from './locale'; import { lang as t } from './locale';
import { MobileRoutesManager } from './MobileRoutesManager'; import { MobileRoutesManager } from './MobileRoutesManager';
import { ClearCache } from './ClearCache';
import { RestartApplication } from './RestartApplication';
class PluginClient extends Plugin { class PluginClient extends Plugin {
async load() { async load() {
this.app.pluginSettingsManager.add('routes', { this.app.pluginSettingsManager.add('routes', {
title: t('Routes'), title: t('Routes'),
icon: 'ApartmentOutlined', icon: 'ApartmentOutlined',
aclSnippet: 'pm.routes', aclSnippet: 'ui.*',
}); });
this.app.pluginSettingsManager.add(`routes.desktop`, { this.app.pluginSettingsManager.add(`routes.desktop`, {
title: t('Desktop routes'), title: t('Desktop routes'),
Component: DesktopRoutesManager, Component: DesktopRoutesManager,
aclSnippet: 'pm.routes.desktop', aclSnippet: 'ui.*',
sort: 1, sort: 1,
}); });
// 个人中心注册
this.app.addUserCenterSettingsItem({
name: 'divider4',
sort: 499,
type: 'divider',
aclSnippet: 'app',
});
this.app.addUserCenterSettingsItem({
name: 'cache',
sort: 500,
Component: ClearCache,
aclSnippet: 'app',
});
this.app.addUserCenterSettingsItem({
name: 'restartApplication',
Component: RestartApplication,
sort: 510,
aclSnippet: 'app',
});
const mobilePlugin: any = this.app.pluginManager.get('@nocobase/plugin-mobile'); const mobilePlugin: any = this.app.pluginManager.get('@nocobase/plugin-mobile');
if (mobilePlugin?.options?.enabled) { if (mobilePlugin?.options?.enabled) {

View File

@ -7,9 +7,14 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { css, cx, SettingsMenu, SortableItem, useDesigner } from '@nocobase/client'; import { css, cx, useSchemaSettingsRender, SortableItem, useDesigner } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { SettingsDesigner } from './Settings.Designer'; import { SettingsDesigner } from './Settings.Designer';
export function UserCenter() {
const { render } = useSchemaSettingsRender('userCenterSettings');
return <div style={{ display: 'inline-block' }}>{render({ mode: 'inline', style: { width: '100%' } })}</div>;
}
export const InternalSettings = () => { export const InternalSettings = () => {
const Designer = useDesigner(); const Designer = useDesigner();
return ( return (
@ -22,7 +27,7 @@ export const InternalSettings = () => {
)} )}
> >
<Designer /> <Designer />
<SettingsMenu redirectUrl="/mobile" /> <UserCenter />
</SortableItem> </SortableItem>
); );
}; };

View File

@ -7,9 +7,14 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { cx, SettingsMenu, SortableItem, useDesigner, useToken } from '@nocobase/client'; import { cx, SortableItem, useDesigner, useToken, useSchemaSettingsRender } from '@nocobase/client';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
export function UserCenter() {
const { render } = useSchemaSettingsRender('userCenterSettings');
return <div style={{ display: 'inline-block', width: '100%' }}>{render({ mode: 'inline' })}</div>;
}
export const InternalSettings = () => { export const InternalSettings = () => {
const Designer = useDesigner(); const Designer = useDesigner();
const { token } = useToken(); const { token } = useToken();
@ -22,7 +27,7 @@ export const InternalSettings = () => {
return ( return (
<SortableItem className={cx('nb-mobile-setting')} style={style}> <SortableItem className={cx('nb-mobile-setting')} style={style}>
<Designer /> <Designer />
<SettingsMenu /> <UserCenter />
</SortableItem> </SortableItem>
); );
}; };

View File

@ -0,0 +1,54 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { SchemaSettingsSelectItem } from '@nocobase/client';
import { error } from '@nocobase/utils/client';
import React, { useEffect, useMemo } from 'react';
import { useThemeId } from '../components/InitializeTheme';
import { useThemeListContext } from '../components/ThemeListProvider';
import { useTranslation } from '../locale';
import { useUpdateThemeSettings } from '../hooks/useUpdateThemeSettings';
export const ThemeSettings = () => {
const { t } = useTranslation();
const { run, error: err, data } = useThemeListContext();
const { updateUserThemeSettings } = useUpdateThemeSettings();
const { currentThemeId } = useThemeId();
const options = useMemo(() => {
return data
?.filter((item) => item.optional)
.map((item) => {
return {
label: t(item.config.name),
value: item.id,
};
});
}, [data, t]);
useEffect(() => {
if (!data) {
run();
}
}, []);
if (err) {
error(err);
return null;
}
return (
<SchemaSettingsSelectItem
title={t('Theme')}
options={options}
value={currentThemeId}
onChange={(value) => {
updateUserThemeSettings(value);
}}
/>
);
};

View File

@ -7,7 +7,14 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { Plugin, createStyles, defaultTheme, useCurrentUserSettingsMenu, useGlobalTheme } from '@nocobase/client'; import {
Plugin,
createStyles,
defaultTheme,
useCurrentUserSettingsMenu,
useGlobalTheme,
useACLContext,
} from '@nocobase/client';
import { ConfigProvider } from 'antd'; import { ConfigProvider } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { useEffect, useMemo } from 'react'; import React, { useEffect, useMemo } from 'react';
@ -23,8 +30,8 @@ const ThemeList = lazy(() => import('./components/ThemeList'));
const { ThemeListProvider } = lazy(() => import('./components/ThemeListProvider'), 'ThemeListProvider'); const { ThemeListProvider } = lazy(() => import('./components/ThemeListProvider'), 'ThemeListProvider');
const CustomTheme = lazy(() => import('./components/theme-editor')); const CustomTheme = lazy(() => import('./components/theme-editor'));
import { useThemeSettings } from './hooks/useThemeSettings';
import { NAMESPACE } from './locale'; import { NAMESPACE } from './locale';
import { ThemeSettings } from './components/ThemeSettings';
const useStyles = createStyles(({ css, token }) => { const useStyles = createStyles(({ css, token }) => {
return { return {
@ -45,17 +52,10 @@ const useStyles = createStyles(({ css, token }) => {
}); });
const CustomThemeProvider = React.memo((props) => { const CustomThemeProvider = React.memo((props) => {
const { addMenuItem } = useCurrentUserSettingsMenu();
const themeItem = useThemeSettings();
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const { theme, setTheme } = useGlobalTheme(); const { theme, setTheme } = useGlobalTheme();
const { styles } = useStyles(); const { styles } = useStyles();
useEffect(() => {
// 在页面右上角中添加一个 Theme 菜单项
addMenuItem(themeItem, { before: 'divider_3' });
}, [addMenuItem, themeItem]);
const contentStyle = useMemo(() => { const contentStyle = useMemo(() => {
return open return open
? { transform: 'rotate(0)', flexGrow: 1, width: 0, height: '100%' } ? { transform: 'rotate(0)', flexGrow: 1, width: 0, height: '100%' }
@ -100,6 +100,12 @@ export class PluginThemeEditorClient extends Plugin {
Component: ThemeList, Component: ThemeList,
aclSnippet: 'pm.theme-editor.themes', aclSnippet: 'pm.theme-editor.themes',
}); });
// 个人中心注册 Theme 菜单项
this.app.addUserCenterSettingsItem({
name: 'theme',
sort: 310,
Component: ThemeSettings,
});
} }
} }

View File

@ -24,7 +24,7 @@ export class PluginThemeEditorServer extends Plugin {
this.app.acl.allow('themeConfig', 'list', 'public'); this.app.acl.allow('themeConfig', 'list', 'public');
this.app.acl.registerSnippet({ this.app.acl.registerSnippet({
name: `pm.${this.name}.themeConfig`, name: `pm.${this.name}.themes`,
actions: ['themeConfig:*'], actions: ['themeConfig:*'],
}); });
} }

View File

@ -9,18 +9,21 @@
import { ISchema, useForm } from '@formily/react'; import { ISchema, useForm } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { MenuProps } from 'antd';
import React, { useContext, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import React, { useContext, useEffect, useMemo, useState, useCallback } from 'react';
import { import {
ActionContextProvider, ActionContextProvider,
DropdownVisibleContext, DropdownVisibleContext,
SchemaComponent, SchemaComponent,
useActionContext, useActionContext,
useSystemSettings, useSystemSettings,
} from '../'; zIndexContext,
import { useAPIClient } from '../api-client'; useZIndexContext,
SchemaComponentContext,
useAPIClient,
SchemaSettingsItem,
} from '@nocobase/client';
const useCloseAction = () => { const useCloseAction = () => {
const { setVisible } = useActionContext(); const { setVisible } = useActionContext();
@ -133,36 +136,44 @@ const schema: ISchema = {
}, },
}; };
export const useChangePassword = () => { export const ChangePassword = () => {
const ctx = useContext(DropdownVisibleContext); const ctx = useContext(DropdownVisibleContext);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const { data } = useSystemSettings() || {}; const { data } = useSystemSettings() || {};
const { enableChangePassword } = data?.data || {}; const { enableChangePassword } = data?.data || {};
const parentZIndex = useZIndexContext();
const zIndex = parentZIndex + 10;
// 避免重复渲染的 click 处理
const handleClick = useCallback(
(e) => {
e.stopPropagation();
ctx?.setVisible?.(false);
setVisible((prev) => (prev ? prev : true)); // 只有 `visible` 变化时才触发更新
},
[ctx],
);
const schemaComponent = useMemo(() => {
return (
<SchemaComponentContext.Provider value={{ designable: false }}>
<SchemaComponent scope={{ useCloseAction, useSaveCurrentUserValues }} schema={schema} />
</SchemaComponentContext.Provider>
);
}, [zIndex]);
const result = useMemo<MenuProps['items'][0]>(() => {
return {
key: 'password',
eventKey: 'ChangePassword',
onClick: () => {
setVisible(true);
ctx?.setVisible(false);
},
label: (
<>
{t('Change password')}
<ActionContextProvider value={{ visible, setVisible }}>
<div onClick={(e) => e.stopPropagation()}>
<SchemaComponent scope={{ useCloseAction, useSaveCurrentUserValues }} schema={schema} />
</div>
</ActionContextProvider>
</>
),
};
}, [visible]);
if (enableChangePassword === false) { if (enableChangePassword === false) {
return null; return null;
} }
return (
return result; <zIndexContext.Provider value={zIndex}>
<SchemaSettingsItem eventKey="changePassword" title="changePassword">
<div onClick={handleClick}>{t('Change password')}</div>
</SchemaSettingsItem>
<ActionContextProvider value={{ visible, setVisible }}>
{visible && <div onClick={(e) => e.stopPropagation()}>{schemaComponent}</div>}
</ActionContextProvider>
</zIndexContext.Provider>
);
}; };

View File

@ -9,8 +9,7 @@
import { useField, useFieldSchema, useForm } from '@formily/react'; import { useField, useFieldSchema, useForm } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { MenuProps } from 'antd'; import React, { useContext, useEffect, useMemo, useState, useCallback } from 'react';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
ActionContextProvider, ActionContextProvider,
@ -23,8 +22,11 @@ import {
useCollectionManager, useCollectionManager,
useCurrentUserContext, useCurrentUserContext,
useSystemSettings, useSystemSettings,
} from '../'; zIndexContext,
import { useAPIClient } from '../api-client'; useZIndexContext,
useAPIClient,
SchemaSettingsItem,
} from '@nocobase/client';
const useUpdateProfileActionProps = () => { const useUpdateProfileActionProps = () => {
const ctx = useCurrentUserContext(); const ctx = useCurrentUserContext();
@ -70,9 +72,9 @@ const useUpdateProfileActionProps = () => {
}; };
const useEditProfileFormBlockDecoratorProps = () => { const useEditProfileFormBlockDecoratorProps = () => {
const { data } = useCurrentUserContext(); const { data } = useCurrentUserContext() || {};
return { return {
filterByTk: data.data?.id, filterByTk: data?.data?.id,
}; };
}; };
@ -116,57 +118,61 @@ const ProfileEditForm = () => {
); );
}; };
export const useEditProfile = () => { export const EditProfile = () => {
const ctx = useContext(DropdownVisibleContext); const ctx = useContext(DropdownVisibleContext);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const { data } = useSystemSettings() || {}; const { data } = useSystemSettings() || {};
const { enableEditProfile } = data?.data || {}; const { enableEditProfile } = data?.data ?? {};
const result = useMemo<MenuProps['items'][0]>(() => { const parentZIndex = useZIndexContext();
return { const zIndex = parentZIndex + 10;
key: 'profile',
eventKey: 'EditProfile',
onClick: () => {
ctx?.setVisible(false);
setVisible(true);
},
label: (
<div>
{t('Edit profile')}
<ActionContextProvider value={{ visible, setVisible }}>
<div onClick={(e) => e.stopPropagation()}>
<SchemaComponent
components={{ ProfileEditForm }}
schema={{
type: 'object',
properties: {
[uid()]: {
'x-component': 'Action.Drawer',
'x-component-props': {
zIndex: 2000,
},
type: 'void',
title: '{{t("Edit profile")}}',
properties: {
form: {
type: 'void',
'x-component': 'ProfileEditForm',
},
},
},
},
}}
/>
</div>
</ActionContextProvider>
</div>
),
};
}, [visible]);
// 避免重复渲染的 click 处理
const handleClick = useCallback(
(e) => {
e.stopPropagation();
ctx?.setVisible?.(false);
setVisible((prev) => (prev ? prev : true)); // 只有 `visible` 变化时才触发更新
},
[ctx],
);
// 避免 `SchemaComponent` 结构重新创建
const schemaComponent = useMemo(() => {
return (
<SchemaComponent
components={{ ProfileEditForm }}
schema={{
type: 'object',
properties: {
[uid()]: {
'x-component': 'Action.Drawer',
'x-component-props': { zIndex },
type: 'void',
title: '{{t("Edit profile")}}',
properties: {
form: {
type: 'void',
'x-component': 'ProfileEditForm',
},
},
},
},
}}
/>
);
}, [zIndex]);
if (enableEditProfile === false) { if (enableEditProfile === false) {
return null; return null;
} }
return (
return result; <zIndexContext.Provider value={zIndex}>
<SchemaSettingsItem eventKey="EditProfile" title="EditProfile">
<div onClick={handleClick}>{t('Edit profile')}</div>
</SchemaSettingsItem>
<ActionContextProvider value={{ visible, setVisible }}>
{visible && <div onClick={(e) => e.stopPropagation()}>{schemaComponent}</div>}
</ActionContextProvider>
</zIndexContext.Provider>
);
}; };

View File

@ -0,0 +1,23 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React from 'react';
import { SchemaSettingsItem, useToken, useCurrentUserContext, SchemaSettings } from '@nocobase/client';
export const NickName = () => {
const { data } = useCurrentUserContext();
const { token } = useToken();
return (
<SchemaSettingsItem disabled={true} eventKey="nickname" title="nickname">
<span aria-disabled="false" style={{ cursor: 'text', color: token.colorTextDescription }}>
{data?.data?.nickname || data?.data?.username || data?.data?.email}
</span>
</SchemaSettingsItem>
);
};

View File

@ -0,0 +1,34 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React from 'react';
import { SchemaSettingsItem, useNavigateNoUpdate, useAPIClient } from '@nocobase/client';
import { useTranslation } from 'react-i18next';
export const SignOut = () => {
const { t } = useTranslation();
const navigate = useNavigateNoUpdate();
const api = useAPIClient();
return (
<SchemaSettingsItem
title="signOut"
eventKey="signOut"
onClick={async () => {
const { data } = await api.auth.signOut();
if (data?.data?.redirect) {
window.location.href = data.data.redirect;
} else {
navigate(`/signin?redirect=${encodeURIComponent('')}`);
}
}}
>
{t('Sign out')}
</SchemaSettingsItem>
);
};

View File

@ -96,37 +96,6 @@ const ProfileEditForm = () => {
); );
}; };
const EditProfile = ({ visible, setVisible }) => {
return (
<ActionContextProvider value={{ visible, setVisible }}>
<div onClick={(e) => e.stopPropagation()}>
<SchemaComponent
components={{ ProfileEditForm }}
schema={{
type: 'object',
properties: {
[uid()]: {
'x-component': 'Action.Drawer',
'x-component-props': {
// zIndex: 10000,
},
type: 'void',
title: '{{t("Edit profile")}}',
properties: {
form: {
type: 'void',
'x-component': 'ProfileEditForm',
},
},
},
},
}}
/>
</div>
</ActionContextProvider>
);
};
export const useEditProfile = () => { export const useEditProfile = () => {
const ctx = useContext(DropdownVisibleContext); const ctx = useContext(DropdownVisibleContext);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);

View File

@ -11,10 +11,14 @@ import { Plugin } from '@nocobase/client';
import { tval } from '@nocobase/utils/client'; import { tval } from '@nocobase/utils/client';
import ACLPlugin from '@nocobase/plugin-acl/client'; import ACLPlugin from '@nocobase/plugin-acl/client';
import { lazy } from '@nocobase/client'; import { lazy } from '@nocobase/client';
import { ChangePassword } from './ChangePassword';
import { EditProfile } from './EditProfile';
import { NickName } from './NickName';
import { SignOut } from './SignOut';
const { UsersProvider } = lazy(() => import('./UsersProvider'), 'UsersProvider'); const { UsersProvider } = lazy(() => import('./UsersProvider'), 'UsersProvider');
const { UsersManagement } = lazy(() => import('./UsersManagement'), 'UsersManagement'); const { UsersManagement } = lazy(() => import('./UsersManagement'), 'UsersManagement');
const { RoleUsersManager } = lazy(() => import('./RoleUsersManager'), 'RoleUsersManager'); const { RoleUsersManager } = lazy(() => import('./RoleUsersManager'), 'RoleUsersManager');
class PluginUsersClient extends Plugin { class PluginUsersClient extends Plugin {
async load() { async load() {
this.app.pluginSettingsManager.add('users-permissions', { this.app.pluginSettingsManager.add('users-permissions', {
@ -33,6 +37,37 @@ class PluginUsersClient extends Plugin {
title: tval('Users'), title: tval('Users'),
Component: RoleUsersManager, Component: RoleUsersManager,
}); });
// 个人中心注册 注册设置项
this.app.addUserCenterSettingsItem({
name: 'nickName',
Component: NickName,
sort: 0,
});
this.app.addUserCenterSettingsItem({
name: 'divider1',
type: 'divider',
sort: 10,
});
this.app.addUserCenterSettingsItem({
name: 'editProfile',
Component: EditProfile,
sort: 50,
});
this.app.addUserCenterSettingsItem({
name: 'changePassword',
Component: ChangePassword,
sort: 100,
});
this.app.addUserCenterSettingsItem({
name: 'divider_signOut',
type: 'divider',
sort: 900,
});
this.app.addUserCenterSettingsItem({
name: 'signOut',
Component: SignOut,
sort: 1000,
});
} }
} }

1823
yarn.lock

File diff suppressed because it is too large Load Diff