mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 05:29:26 +08:00
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:
parent
c9c7531946
commit
9eb9d4e000
@ -2,9 +2,7 @@
|
||||
"version": "1.6.0-beta.9",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"npmClientArgs": [
|
||||
"--ignore-engines"
|
||||
],
|
||||
"npmClientArgs": ["--ignore-engines"],
|
||||
"command": {
|
||||
"version": {
|
||||
"forcePublish": true,
|
||||
|
@ -101,7 +101,7 @@ const Demo = () => {
|
||||
return (
|
||||
<div>
|
||||
<div>{render()}</div>
|
||||
<div>可以进行参数的二次覆盖:{render({ style: { color: 'red' } })}</div>
|
||||
<div>可以进行参数的二次覆盖:{render({ mode: 'inline', style: { color: 'red' } })}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -65,7 +65,8 @@
|
||||
"react-router-dom": "^6.11.2",
|
||||
"react-to-print": "^2.14.7",
|
||||
"sanitize-html": "2.13.0",
|
||||
"use-deep-compare-effect": "^1.8.1"
|
||||
"use-deep-compare-effect": "^1.8.1",
|
||||
"ignore": "^5.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
|
@ -17,7 +17,6 @@ import React, { ComponentType, FC, ReactElement, ReactNode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Link, NavLink, Navigate } from 'react-router-dom';
|
||||
|
||||
import { APIClient, APIClientProvider } from '../api-client';
|
||||
import { CSSVariableProvider } from '../css-variable';
|
||||
import { AntdAppProvider, GlobalThemeProvider } from '../global-theme';
|
||||
@ -29,7 +28,8 @@ import { WebSocketClient, WebSocketClientOptions } from './WebSocketClient';
|
||||
import { AppComponent, BlankComponent, defaultAppComponents } from './components';
|
||||
import { SchemaInitializer, SchemaInitializerManager } from './schema-initializer';
|
||||
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 { defineGlobalDeps } from './utils/globalDeps';
|
||||
import { getRequireJs } from './utils/requirejs';
|
||||
@ -46,6 +46,7 @@ import { AppSchemaComponentProvider } from './AppSchemaComponentProvider';
|
||||
import type { Plugin } from './Plugin';
|
||||
import { getOperators } from './globalOperators';
|
||||
import type { RequireJS } from './utils/requirejs';
|
||||
import { useAclSnippets } from './hooks/useAclSnippets';
|
||||
|
||||
type JsonLogic = {
|
||||
addOperation: (name: string, fn?: any) => void;
|
||||
@ -495,4 +496,20 @@ export class Application {
|
||||
getGlobalVar(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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
25
packages/core/client/src/application/hooks/useAclSnippets.ts
Normal file
25
packages/core/client/src/application/hooks/useAclSnippets.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
};
|
@ -7,16 +7,15 @@
|
||||
* 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 { Schema } from '@formily/json-schema';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Designable } from '../../../schema-component';
|
||||
import { SchemaSettingsProps } from '../../../schema-settings';
|
||||
import { useApp } from '../../hooks';
|
||||
import { SchemaSettingsWrapper } from '../components';
|
||||
import { SchemaSettings } from '../SchemaSettings';
|
||||
import { SchemaSettingOptions } from '../types';
|
||||
|
||||
type UseSchemaSettingsRenderOptions<T = {}> = Omit<SchemaSettingOptions<T>, 'name' | 'items'> &
|
||||
Omit<SchemaSettingsProps, 'title' | 'children'> & {
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
|
||||
export interface SchemaSettingOptions<T = {}> {
|
||||
name: string;
|
||||
mode?: 'inline' | 'dropdown';
|
||||
Component?: ComponentType<T>;
|
||||
componentProps?: T;
|
||||
items: SchemaSettingsItemType[];
|
||||
|
@ -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();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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>;
|
||||
}
|
@ -27,7 +27,6 @@ import { Outlet } from 'react-router-dom';
|
||||
import {
|
||||
ACLRolesCheckProvider,
|
||||
CurrentAppInfoProvider,
|
||||
CurrentUser,
|
||||
findByUid,
|
||||
findMenuItem,
|
||||
NavigateIfNotSignIn,
|
||||
@ -58,7 +57,8 @@ import { useMenuTranslation } from '../../../schema-component/antd/menu/locale';
|
||||
import { Help } from '../../../user/Help';
|
||||
import { KeepAlive } from './KeepAlive';
|
||||
import { convertRoutesToSchema, NocoBaseDesktopRoute, NocoBaseDesktopRouteType } from './convertRoutesToSchema';
|
||||
|
||||
import { userCenterSettings } from './userCenterSettings';
|
||||
import { UserCenter } from './UserCenterButton';
|
||||
export { KeepAlive, NocoBaseDesktopRouteType };
|
||||
|
||||
const RouteContext = createContext<NocoBaseDesktopRoute | null>(null);
|
||||
@ -529,7 +529,7 @@ export const InternalAdminLayout = () => {
|
||||
<Divider type="vertical" />
|
||||
</ConfigProvider>
|
||||
<Help />
|
||||
<CurrentUser />
|
||||
<UserCenter />
|
||||
</div>
|
||||
</div>
|
||||
</Layout.Header>
|
||||
@ -574,6 +574,7 @@ export class AdminLayoutPlugin extends Plugin {
|
||||
await this.app.pm.add(RemoteSchemaTemplateManagerPlugin);
|
||||
}
|
||||
async load() {
|
||||
this.app.schemaSettingsManager.add(userCenterSettings);
|
||||
this.app.addComponents({ AdminLayout, AdminDynamicPage });
|
||||
}
|
||||
}
|
||||
|
@ -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 };
|
@ -22,6 +22,7 @@ import {
|
||||
CascaderProps,
|
||||
ConfigProvider,
|
||||
Dropdown,
|
||||
Menu,
|
||||
MenuItemProps,
|
||||
MenuProps,
|
||||
Modal,
|
||||
@ -119,6 +120,7 @@ export interface SchemaSettingsProps {
|
||||
field?: GeneralField;
|
||||
fieldSchema?: Schema;
|
||||
children?: ReactNode;
|
||||
mode?: 'inline' | 'dropdown';
|
||||
}
|
||||
|
||||
interface SchemaSettingsContextProps<T = any> {
|
||||
@ -167,7 +169,7 @@ export const SchemaSettingsProvider: React.FC<SchemaSettingsProviderProps> = (pr
|
||||
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 [visible, setVisible] = useState(false);
|
||||
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';
|
||||
|
||||
const findGridSchema = (fieldSchema) => {
|
||||
|
@ -7,219 +7,12 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
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';
|
||||
import { createContext } from 'react';
|
||||
import { SelectWithTitle } from '../common';
|
||||
|
||||
const useNickname = () => {
|
||||
const { data } = useCurrentUserContext();
|
||||
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 SettingsMenuProvider = (props) => {
|
||||
return SelectWithTitle;
|
||||
};
|
||||
|
||||
export const DropdownVisibleContext = createContext(null);
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
@ -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;
|
||||
};
|
@ -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);
|
||||
// });
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -9,9 +9,10 @@
|
||||
|
||||
import { Plugin, lazy } from '@nocobase/client';
|
||||
import { ACLSettingsUI } from './ACLSettingsUI';
|
||||
// import { RolesManagement } from './RolesManagement';
|
||||
const { RolesManagement } = lazy(() => import('./RolesManagement'), 'RolesManagement');
|
||||
import { RolesManager } from './roles-manager';
|
||||
import { SwitchRole } from './SwitchRole';
|
||||
|
||||
const { RolesManagement } = lazy(() => import('./RolesManagement'), 'RolesManagement');
|
||||
|
||||
export class PluginACLClient extends Plugin {
|
||||
rolesManager = new RolesManager();
|
||||
@ -25,6 +26,18 @@ export class PluginACLClient extends Plugin {
|
||||
aclSnippet: 'pm.acl.roles',
|
||||
sort: 3,
|
||||
});
|
||||
|
||||
// 个人中心注册 切换角色
|
||||
this.app.addUserCenterSettingsItem({
|
||||
name: 'divider_switchRole',
|
||||
type: 'divider',
|
||||
sort: 200,
|
||||
});
|
||||
this.app.addUserCenterSettingsItem({
|
||||
name: 'switchRole',
|
||||
Component: SwitchRole,
|
||||
sort: 300,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -7,25 +7,48 @@
|
||||
* 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 { lang as t } from './locale';
|
||||
import { MobileRoutesManager } from './MobileRoutesManager';
|
||||
import { ClearCache } from './ClearCache';
|
||||
import { RestartApplication } from './RestartApplication';
|
||||
|
||||
class PluginClient extends Plugin {
|
||||
async load() {
|
||||
this.app.pluginSettingsManager.add('routes', {
|
||||
title: t('Routes'),
|
||||
icon: 'ApartmentOutlined',
|
||||
aclSnippet: 'pm.routes',
|
||||
aclSnippet: 'ui.*',
|
||||
});
|
||||
this.app.pluginSettingsManager.add(`routes.desktop`, {
|
||||
title: t('Desktop routes'),
|
||||
Component: DesktopRoutesManager,
|
||||
aclSnippet: 'pm.routes.desktop',
|
||||
aclSnippet: 'ui.*',
|
||||
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');
|
||||
|
||||
if (mobilePlugin?.options?.enabled) {
|
||||
|
@ -7,9 +7,14 @@
|
||||
* 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 { 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 = () => {
|
||||
const Designer = useDesigner();
|
||||
return (
|
||||
@ -22,7 +27,7 @@ export const InternalSettings = () => {
|
||||
)}
|
||||
>
|
||||
<Designer />
|
||||
<SettingsMenu redirectUrl="/mobile" />
|
||||
<UserCenter />
|
||||
</SortableItem>
|
||||
);
|
||||
};
|
||||
|
@ -7,9 +7,14 @@
|
||||
* 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';
|
||||
|
||||
export function UserCenter() {
|
||||
const { render } = useSchemaSettingsRender('userCenterSettings');
|
||||
return <div style={{ display: 'inline-block', width: '100%' }}>{render({ mode: 'inline' })}</div>;
|
||||
}
|
||||
|
||||
export const InternalSettings = () => {
|
||||
const Designer = useDesigner();
|
||||
const { token } = useToken();
|
||||
@ -22,7 +27,7 @@ export const InternalSettings = () => {
|
||||
return (
|
||||
<SortableItem className={cx('nb-mobile-setting')} style={style}>
|
||||
<Designer />
|
||||
<SettingsMenu />
|
||||
<UserCenter />
|
||||
</SortableItem>
|
||||
);
|
||||
};
|
||||
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -7,7 +7,14 @@
|
||||
* 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 _ from 'lodash';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
@ -23,8 +30,8 @@ const ThemeList = lazy(() => import('./components/ThemeList'));
|
||||
const { ThemeListProvider } = lazy(() => import('./components/ThemeListProvider'), 'ThemeListProvider');
|
||||
const CustomTheme = lazy(() => import('./components/theme-editor'));
|
||||
|
||||
import { useThemeSettings } from './hooks/useThemeSettings';
|
||||
import { NAMESPACE } from './locale';
|
||||
import { ThemeSettings } from './components/ThemeSettings';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => {
|
||||
return {
|
||||
@ -45,17 +52,10 @@ const useStyles = createStyles(({ css, token }) => {
|
||||
});
|
||||
|
||||
const CustomThemeProvider = React.memo((props) => {
|
||||
const { addMenuItem } = useCurrentUserSettingsMenu();
|
||||
const themeItem = useThemeSettings();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const { theme, setTheme } = useGlobalTheme();
|
||||
const { styles } = useStyles();
|
||||
|
||||
useEffect(() => {
|
||||
// 在页面右上角中添加一个 Theme 菜单项
|
||||
addMenuItem(themeItem, { before: 'divider_3' });
|
||||
}, [addMenuItem, themeItem]);
|
||||
|
||||
const contentStyle = useMemo(() => {
|
||||
return open
|
||||
? { transform: 'rotate(0)', flexGrow: 1, width: 0, height: '100%' }
|
||||
@ -100,6 +100,12 @@ export class PluginThemeEditorClient extends Plugin {
|
||||
Component: ThemeList,
|
||||
aclSnippet: 'pm.theme-editor.themes',
|
||||
});
|
||||
// 个人中心注册 Theme 菜单项
|
||||
this.app.addUserCenterSettingsItem({
|
||||
name: 'theme',
|
||||
sort: 310,
|
||||
Component: ThemeSettings,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ export class PluginThemeEditorServer extends Plugin {
|
||||
|
||||
this.app.acl.allow('themeConfig', 'list', 'public');
|
||||
this.app.acl.registerSnippet({
|
||||
name: `pm.${this.name}.themeConfig`,
|
||||
name: `pm.${this.name}.themes`,
|
||||
actions: ['themeConfig:*'],
|
||||
});
|
||||
}
|
||||
|
@ -9,18 +9,21 @@
|
||||
|
||||
import { ISchema, useForm } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { MenuProps } from 'antd';
|
||||
import React, { useContext, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import React, { useContext, useEffect, useMemo, useState, useCallback } from 'react';
|
||||
import {
|
||||
ActionContextProvider,
|
||||
DropdownVisibleContext,
|
||||
SchemaComponent,
|
||||
useActionContext,
|
||||
useSystemSettings,
|
||||
} from '../';
|
||||
import { useAPIClient } from '../api-client';
|
||||
zIndexContext,
|
||||
useZIndexContext,
|
||||
SchemaComponentContext,
|
||||
useAPIClient,
|
||||
SchemaSettingsItem,
|
||||
} from '@nocobase/client';
|
||||
|
||||
const useCloseAction = () => {
|
||||
const { setVisible } = useActionContext();
|
||||
@ -133,36 +136,44 @@ const schema: ISchema = {
|
||||
},
|
||||
};
|
||||
|
||||
export const useChangePassword = () => {
|
||||
export const ChangePassword = () => {
|
||||
const ctx = useContext(DropdownVisibleContext);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const { data } = useSystemSettings() || {};
|
||||
const { enableChangePassword } = data?.data || {};
|
||||
const parentZIndex = useZIndexContext();
|
||||
const zIndex = parentZIndex + 10;
|
||||
|
||||
const result = useMemo<MenuProps['items'][0]>(() => {
|
||||
return {
|
||||
key: 'password',
|
||||
eventKey: 'ChangePassword',
|
||||
onClick: () => {
|
||||
setVisible(true);
|
||||
ctx?.setVisible(false);
|
||||
// 避免重复渲染的 click 处理
|
||||
const handleClick = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
ctx?.setVisible?.(false);
|
||||
setVisible((prev) => (prev ? prev : true)); // 只有 `visible` 变化时才触发更新
|
||||
},
|
||||
label: (
|
||||
<>
|
||||
{t('Change password')}
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
[ctx],
|
||||
);
|
||||
|
||||
const schemaComponent = useMemo(() => {
|
||||
return (
|
||||
<SchemaComponentContext.Provider value={{ designable: false }}>
|
||||
<SchemaComponent scope={{ useCloseAction, useSaveCurrentUserValues }} schema={schema} />
|
||||
</div>
|
||||
</ActionContextProvider>
|
||||
</>
|
||||
),
|
||||
};
|
||||
}, [visible]);
|
||||
</SchemaComponentContext.Provider>
|
||||
);
|
||||
}, [zIndex]);
|
||||
|
||||
if (enableChangePassword === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
@ -9,8 +9,7 @@
|
||||
|
||||
import { useField, useFieldSchema, useForm } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { MenuProps } from 'antd';
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useContext, useEffect, useMemo, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActionContextProvider,
|
||||
@ -23,8 +22,11 @@ import {
|
||||
useCollectionManager,
|
||||
useCurrentUserContext,
|
||||
useSystemSettings,
|
||||
} from '../';
|
||||
import { useAPIClient } from '../api-client';
|
||||
zIndexContext,
|
||||
useZIndexContext,
|
||||
useAPIClient,
|
||||
SchemaSettingsItem,
|
||||
} from '@nocobase/client';
|
||||
|
||||
const useUpdateProfileActionProps = () => {
|
||||
const ctx = useCurrentUserContext();
|
||||
@ -70,9 +72,9 @@ const useUpdateProfileActionProps = () => {
|
||||
};
|
||||
|
||||
const useEditProfileFormBlockDecoratorProps = () => {
|
||||
const { data } = useCurrentUserContext();
|
||||
const { data } = useCurrentUserContext() || {};
|
||||
return {
|
||||
filterByTk: data.data?.id,
|
||||
filterByTk: data?.data?.id,
|
||||
};
|
||||
};
|
||||
|
||||
@ -116,25 +118,28 @@ const ProfileEditForm = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const useEditProfile = () => {
|
||||
export const EditProfile = () => {
|
||||
const ctx = useContext(DropdownVisibleContext);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const { data } = useSystemSettings() || {};
|
||||
const { enableEditProfile } = data?.data || {};
|
||||
const result = useMemo<MenuProps['items'][0]>(() => {
|
||||
return {
|
||||
key: 'profile',
|
||||
eventKey: 'EditProfile',
|
||||
onClick: () => {
|
||||
ctx?.setVisible(false);
|
||||
setVisible(true);
|
||||
const { enableEditProfile } = 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` 变化时才触发更新
|
||||
},
|
||||
label: (
|
||||
<div>
|
||||
{t('Edit profile')}
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
[ctx],
|
||||
);
|
||||
|
||||
// 避免 `SchemaComponent` 结构重新创建
|
||||
const schemaComponent = useMemo(() => {
|
||||
return (
|
||||
<SchemaComponent
|
||||
components={{ ProfileEditForm }}
|
||||
schema={{
|
||||
@ -142,9 +147,7 @@ export const useEditProfile = () => {
|
||||
properties: {
|
||||
[uid()]: {
|
||||
'x-component': 'Action.Drawer',
|
||||
'x-component-props': {
|
||||
zIndex: 2000,
|
||||
},
|
||||
'x-component-props': { zIndex },
|
||||
type: 'void',
|
||||
title: '{{t("Edit profile")}}',
|
||||
properties: {
|
||||
@ -157,16 +160,19 @@ export const useEditProfile = () => {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ActionContextProvider>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}, [visible]);
|
||||
|
||||
);
|
||||
}, [zIndex]);
|
||||
if (enableEditProfile === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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 = () => {
|
||||
const ctx = useContext(DropdownVisibleContext);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
@ -11,10 +11,14 @@ import { Plugin } from '@nocobase/client';
|
||||
import { tval } from '@nocobase/utils/client';
|
||||
import ACLPlugin from '@nocobase/plugin-acl/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 { UsersManagement } = lazy(() => import('./UsersManagement'), 'UsersManagement');
|
||||
const { RoleUsersManager } = lazy(() => import('./RoleUsersManager'), 'RoleUsersManager');
|
||||
|
||||
class PluginUsersClient extends Plugin {
|
||||
async load() {
|
||||
this.app.pluginSettingsManager.add('users-permissions', {
|
||||
@ -33,6 +37,37 @@ class PluginUsersClient extends Plugin {
|
||||
title: tval('Users'),
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user