feat: improve the user module

This commit is contained in:
chenos 2022-02-05 17:06:33 +08:00
parent ef24f7cfea
commit 5b4dd1800e
20 changed files with 464 additions and 122 deletions

View File

@ -1,6 +1,5 @@
import { import {
ACLShortcut, ACLShortcut,
Action,
AdminLayout, AdminLayout,
AntdConfigProvider, AntdConfigProvider,
AntdSchemaComponentProvider, AntdSchemaComponentProvider,
@ -11,12 +10,13 @@ import {
DesignableSwitch, DesignableSwitch,
DocumentTitleProvider, DocumentTitleProvider,
i18n, i18n,
Menu,
Page,
PluginManagerProvider, PluginManagerProvider,
RouteSchemaComponent,
RouteSwitch, RouteSwitch,
RouteSwitchProvider, RouteSwitchProvider,
SchemaComponentProvider, SchemaComponentProvider,
SigninPage,
SignupPage,
SystemSettingsProvider, SystemSettingsProvider,
SystemSettingsShortcut, SystemSettingsShortcut,
useRequest useRequest
@ -24,7 +24,7 @@ import {
import { Spin } from 'antd'; import { Spin } from 'antd';
import React from 'react'; import React from 'react';
import { I18nextProvider } from 'react-i18next'; import { I18nextProvider } from 'react-i18next';
import { MemoryRouter } from 'react-router-dom'; import { Link, MemoryRouter, NavLink } from 'react-router-dom';
import apiClient from './apiClient'; import apiClient from './apiClient';
const providers = [ const providers = [
@ -38,10 +38,10 @@ const providers = [
PluginManagerProvider, PluginManagerProvider,
{ components: { ACLShortcut, DesignableSwitch, CollectionManagerShortcut, SystemSettingsShortcut } }, { components: { ACLShortcut, DesignableSwitch, CollectionManagerShortcut, SystemSettingsShortcut } },
], ],
[SchemaComponentProvider, { components: { Page, Menu, Action } }], [SchemaComponentProvider, { components: { Link, NavLink } }],
AntdSchemaComponentProvider, AntdSchemaComponentProvider,
[DocumentTitleProvider, { addonAfter: 'NocoBase' }], [DocumentTitleProvider, { addonAfter: 'NocoBase' }],
[RouteSwitchProvider, { components: { AuthLayout, AdminLayout } }], [RouteSwitchProvider, { components: { AuthLayout, AdminLayout, RouteSchemaComponent, SigninPage, SignupPage } }],
]; ];
const App = compose(...providers)(() => { const App = compose(...providers)(() => {

View File

@ -145,10 +145,13 @@ export default (apiClient: APIClient) => {
routes: [ routes: [
{ {
type: 'route', type: 'route',
uiSchemaUid: 'dtf9j0b8p9u',
path: '/signin', path: '/signin',
component: 'RouteSchemaComponent', component: 'SigninPage',
title: '{{t("Sign in")}}', },
{
type: 'route',
path: '/signup',
component: 'SignupPage',
}, },
], ],
}, },

View File

@ -1,65 +0,0 @@
import { Menu } from 'antd';
import React, { useState } from 'react';
import { DatabaseOutlined } from '@ant-design/icons';
import { SchemaComponent, useActionVisible, VisibleContext } from '../schema-component';
import { ISchema, useForm } from '@formily/react';
const useCloseAction = () => {
const { setVisible } = useActionVisible();
const form = useForm();
return {
async run() {
setVisible(false);
form.submit((values) => {
console.log(values);
});
},
};
};
const schema: ISchema = {
type: 'object',
properties: {
drawer1: {
'x-component': 'Action.Drawer',
type: 'void',
title: 'Drawer Title',
properties: {
hello1: {
'x-content': 'Hello',
title: 'T1',
},
footer1: {
'x-component': 'Action.Drawer.Footer',
type: 'void',
properties: {
close1: {
title: 'Close',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCloseAction }}',
},
},
},
},
},
},
},
};
export const ProfileAction = () => {
const [visible, setVisible] = useState(false);
return (
<VisibleContext.Provider value={[visible, setVisible]}>
<Menu.Item
eventKey={'ProfileAction'}
onClick={() => {
setVisible(true);
}}
>
</Menu.Item>
<SchemaComponent scope={{ useCloseAction }} schema={schema} />
</VisibleContext.Provider>
);
};

View File

@ -1,9 +0,0 @@
---
nav:
path: /client
group:
title: Client
path: /client
---
# CurrentUser <Badge>待定</Badge>

View File

@ -1,4 +1,4 @@
import React, { createContext, useContext, useState } from 'react'; import React, { createContext, useContext, useEffect, useState } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
interface DocumentTitleContextProps { interface DocumentTitleContextProps {
@ -35,3 +35,10 @@ export const DocumentTitleProvider: React.FC<{ addonBefore?: string; addonAfter?
export const useDocumentTitle = () => { export const useDocumentTitle = () => {
return useContext(DocumentTitleContext); return useContext(DocumentTitleContext);
}; };
export const useCurrentDocumentTitle = (title: string) => {
const { setTitle } = useDocumentTitle();
useEffect(() => {
setTitle(title);
}, []);
};

View File

@ -8,7 +8,6 @@ export * from './application';
export * from './async-data-provider'; export * from './async-data-provider';
export * from './board'; export * from './board';
export * from './collection-manager'; export * from './collection-manager';
export * from './current-user';
export * from './document-title'; export * from './document-title';
export * from './i18n'; export * from './i18n';
export * from './icon'; export * from './icon';
@ -17,4 +16,5 @@ export * from './record-provider';
export * from './route-switch'; export * from './route-switch';
export * from './schema-component'; export * from './schema-component';
export * from './system-settings'; export * from './system-settings';
export * from './user';

View File

@ -34,28 +34,39 @@ interface ToolbarItemProps {
pin?: boolean; pin?: boolean;
} }
const splitItems = (items: ToolbarItemProps[]) => {
const pinned = [];
const unpinned = [];
for (const item of items) {
if (item.pin) {
pinned.push(item);
} else {
unpinned.push(item);
}
}
return [pinned, unpinned];
};
PluginManager.Toolbar = (props: ToolbarProps) => { PluginManager.Toolbar = (props: ToolbarProps) => {
const { components } = useContext(PluginManagerContext); const { components } = useContext(PluginManagerContext);
const { items = [] } = props; const { items = [] } = props;
const [pinned, unpinned] = splitItems(items);
return ( return (
<div style={{ display: 'inline-block' }}> <div style={{ display: 'inline-block' }}>
<Menu style={{ width: '100%' }} selectable={false} mode={'horizontal'} theme={'dark'}> <Menu style={{ width: '100%' }} selectable={false} mode={'horizontal'} theme={'dark'}>
{items {pinned.map((item, index) => {
.filter((item) => item.pin) const Action = get(components, item.component);
.map((item, index) => { return (
const Action = get(components, item.component); Action && (
return ( <ToolbarItemContext.Provider key={index} value={item}>
Action && ( <Action />
<ToolbarItemContext.Provider key={index} value={item}> </ToolbarItemContext.Provider>
<Action /> )
</ToolbarItemContext.Provider> );
) })}
); {unpinned.length > 0 && (
})} <Menu.SubMenu popupClassName={'pm-sub-menu'} key={'more'} title={<MoreOutlined />}>
<Menu.SubMenu key={'more'} title={<MoreOutlined />}> {unpinned.map((item, index) => {
{items
.filter((item) => !item.pin)
.map((item, index) => {
const Action = get(components, item.component); const Action = get(components, item.component);
return ( return (
Action && ( Action && (
@ -65,11 +76,12 @@ PluginManager.Toolbar = (props: ToolbarProps) => {
) )
); );
})} })}
<Menu.Divider key={'divider'}></Menu.Divider> {unpinned.length > 0 && <Menu.Divider key={'divider'}></Menu.Divider>}
<Menu.Item key={'plugins'} disabled icon={<SettingOutlined />}> <Menu.Item key={'plugins'} disabled icon={<SettingOutlined />}>
</Menu.Item> </Menu.Item>
</Menu.SubMenu> </Menu.SubMenu>
)}
</Menu> </Menu>
</div> </div>
); );

View File

@ -7,9 +7,9 @@ import {
PluginManager, PluginManager,
RemoteSchemaComponent, RemoteSchemaComponent,
useDocumentTitle, useDocumentTitle,
useRoute,
useSystemSettings useSystemSettings
} from '../../../'; } from '../../../';
import { useRoute } from '../../hooks';
export function AdminLayout(props: any) { export function AdminLayout(props: any) {
const route = useRoute(); const route = useRoute();
@ -64,7 +64,7 @@ export function AdminLayout(props: any) {
{ component: 'SystemSettingsShortcut' }, { component: 'SystemSettingsShortcut' },
]} ]}
/> />
<CurrentUser.Dropdown /> <CurrentUser />
</div> </div>
</Layout.Header> </Layout.Header>
<Layout> <Layout>

View File

@ -2,7 +2,13 @@ import React from 'react';
export function AuthLayout(props: any) { export function AuthLayout(props: any) {
return ( return (
<div style={{ maxWidth: 320, margin: '0 auto', paddingTop: '20vh' }}> <div
style={{
maxWidth: 320,
margin: '0 auto',
// paddingTop: '20vh'
}}
>
<h1>NocoBase</h1> <h1>NocoBase</h1>
{props.children} {props.children}
</div> </div>

View File

@ -1,2 +1,4 @@
export * from './admin-layout'; export * from './admin-layout';
export * from './auth-layout'; export * from './auth-layout';
export * from './route-schema-component';

View File

@ -0,0 +1,7 @@
import React from 'react';
import { RemoteSchemaComponent, useRoute } from '../../../';
export function RouteSchemaComponent(props: any) {
const route = useRoute();
return <RemoteSchemaComponent uid={route.uiSchemaUid} />;
}

View File

@ -1,10 +1,10 @@
import React, { useState } from 'react';
import { Button, ButtonProps } from 'antd';
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react'; import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
import { useA } from './hooks'; import { Button } from 'antd';
import { VisibleContext } from './context'; import React, { useState } from 'react';
import { ComposedAction } from './types';
import { ActionDrawer } from './Action.Drawer'; import { ActionDrawer } from './Action.Drawer';
import { VisibleContext } from './context';
import { useA } from './hooks';
import { ComposedAction } from './types';
export const Action: ComposedAction = observer((props) => { export const Action: ComposedAction = observer((props) => {
const { useAction = useA, onClick, ...others } = props; const { useAction = useA, onClick, ...others } = props;
@ -22,7 +22,7 @@ export const Action: ComposedAction = observer((props) => {
run(); run();
}} }}
> >
{schema.title} {field.title}
</Button> </Button>
<RecursionField basePath={field.address} schema={schema} onlyRenderProperties /> <RecursionField basePath={field.address} schema={schema} onlyRenderProperties />
</VisibleContext.Provider> </VisibleContext.Provider>

View File

@ -0,0 +1,55 @@
import {
AntdSchemaComponentProvider,
APIClient,
APIClientProvider,
PluginManager,
PluginManagerProvider,
SchemaComponentProvider,
SystemSettingsProvider,
SystemSettingsShortcut,
useSystemSettings
} from '@nocobase/client';
import MockAdapter from 'axios-mock-adapter';
import React from 'react';
const apiClient = new APIClient();
const mock = new MockAdapter(apiClient.axios);
mock.onGet('/system_settings:get').reply(200, {
data: {
title: 'NocoBase',
},
});
const Demo = () => {
const { data } = useSystemSettings();
return <div>
<h3></h3>
System title: {data?.data?.title}
</div>;
};
export default () => {
return (
<APIClientProvider apiClient={apiClient}>
<SystemSettingsProvider>
<SchemaComponentProvider>
<AntdSchemaComponentProvider>
<PluginManagerProvider components={{ SystemSettingsShortcut }}>
<PluginManager.Toolbar
items={[
{
component: 'SystemSettingsShortcut',
pin: true,
},
]}
/>
<Demo />
</PluginManagerProvider>
</AntdSchemaComponentProvider>
</SchemaComponentProvider>
</SystemSettingsProvider>
</APIClientProvider>
);
};

View File

@ -5,11 +5,18 @@ group:
path: /client path: /client
--- ---
# SystemSettings <Badge>待定</Badge> # SystemSettings
## SystemSettingsProvider ## SystemSettingsProvider
## SystemSettingsAction ## SystemSettingsShortcut
## useSystemSettings ## useSystemSettings
```ts
const { data, loading, refresh } = useSystemSettings();
```
## Examples
<code src="./demos/demo1.tsx"/>

View File

@ -1,11 +1,10 @@
import { Menu } from 'antd'; import { Menu } from 'antd';
import React from 'react'; import React from 'react';
import { MoreOutlined, DesktopOutlined } from '@ant-design/icons'; import { useHistory } from 'react-router-dom';
import { ProfileAction } from './ProfileAction'; import { ProfileAction } from './ProfileAction';
export const CurrentUser = () => null; export const CurrentUser = () => {
const history = useHistory();
CurrentUser.Dropdown = () => {
return ( return (
<div style={{ display: 'inline-block' }}> <div style={{ display: 'inline-block' }}>
<Menu selectable={false} mode={'horizontal'} theme={'dark'}> <Menu selectable={false} mode={'horizontal'} theme={'dark'}>
@ -14,7 +13,13 @@ CurrentUser.Dropdown = () => {
<Menu.Item></Menu.Item> <Menu.Item></Menu.Item>
<Menu.Item></Menu.Item> <Menu.Item></Menu.Item>
<Menu.Divider /> <Menu.Divider />
<Menu.Item></Menu.Item> <Menu.Item
onClick={() => {
history.push('/signin');
}}
>
</Menu.Item>
</Menu.SubMenu> </Menu.SubMenu>
</Menu> </Menu>
</div> </div>

View File

@ -0,0 +1,105 @@
import { ISchema, useForm } from '@formily/react';
import { uid } from '@formily/shared';
import { Menu } from 'antd';
import React, { useState } from 'react';
import { SchemaComponent, useActionVisible, useRequest, VisibleContext } from '../';
const useCloseAction = () => {
const { setVisible } = useActionVisible();
const form = useForm();
return {
async run() {
setVisible(false);
form.submit((values) => {
console.log(values);
});
},
};
};
const useCurrentUserValues = (props, options) => {
return useRequest(
() =>
Promise.resolve({
data: {},
}),
{
...options,
},
);
};
const useSaveCurrentUserValues = () => {
const { setVisible } = useActionVisible();
const form = useForm();
return {
async run() {
form.submit((values) => {
setVisible(false);
console.log(values);
});
},
};
};
const schema: ISchema = {
type: 'object',
properties: {
[uid()]: {
'x-decorator': 'Form',
'x-decorator-props': {
useValues: '{{ useCurrentUserValues }}',
},
'x-component': 'Action.Drawer',
type: 'void',
title: '个人资料',
properties: {
nickname: {
type: 'string',
title: "{{t('Nickname')}}",
'x-decorator': 'FormItem',
'x-component': 'Input',
required: true,
},
footer: {
'x-component': 'Action.Drawer.Footer',
type: 'void',
properties: {
cancel: {
title: 'Cancel',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCloseAction }}',
},
},
submit: {
title: 'Submit',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: '{{ useSaveCurrentUserValues }}',
},
},
},
},
},
},
},
};
export const ProfileAction = () => {
const [visible, setVisible] = useState(false);
return (
<VisibleContext.Provider value={[visible, setVisible]}>
<Menu.Item
eventKey={'ProfileAction'}
onClick={() => {
setVisible(true);
}}
>
</Menu.Item>
<SchemaComponent scope={{ useCurrentUserValues, useCloseAction, useSaveCurrentUserValues }} schema={schema} />
</VisibleContext.Provider>
);
};

View File

@ -0,0 +1,79 @@
import { ISchema, useForm } from '@formily/react';
import { uid } from '@formily/shared';
import React from 'react';
import { useHistory } from 'react-router-dom';
import { SchemaComponent, useCurrentDocumentTitle } from '..';
const schema: ISchema = {
type: 'object',
name: uid(),
'x-component': 'Form',
properties: {
email: {
type: 'string',
required: true,
'x-component': 'Input',
'x-validator': 'email',
'x-decorator': 'FormItem',
'x-component-props': { placeholder: '{{t("Email")}}', style: {} },
},
password: {
type: 'string',
'x-component': 'Password',
required: true,
'x-decorator': 'FormItem',
'x-component-props': { placeholder: '{{t("Password")}}', style: {} },
},
actions: {
type: 'void',
'x-component': 'div',
properties: {
submit: {
title: '{{t("Sign in")}}',
type: 'void',
'x-component': 'Action',
'x-component-props': {
block: true,
type: 'primary',
useAction: '{{ useSignin }}',
style: { width: '100%' },
},
},
},
},
link: {
type: 'void',
'x-component': 'div',
properties: {
link: {
title: '{{t("Create an account")}}',
type: 'void',
'x-component': 'Link',
'x-content': '{{t("Create an account")}}',
'x-component-props': { to: '/signup' },
},
},
},
},
};
const useSignin = () => {
const history = useHistory();
const form = useForm();
return {
async run() {
await form.submit();
console.log(form.values);
history.push('/admin');
},
};
};
export const SigninPage = () => {
useCurrentDocumentTitle('Signin');
return (
<div>
<SchemaComponent scope={{ useSignin }} schema={schema} />
</div>
);
};

View File

@ -0,0 +1,101 @@
import { ISchema, useForm } from '@formily/react';
import { uid } from '@formily/shared';
import React from 'react';
import { useHistory } from 'react-router-dom';
import { SchemaComponent, useCurrentDocumentTitle } from '..';
const schema: ISchema = {
type: 'object',
name: uid(),
'x-component': 'Form',
properties: {
email: {
type: 'string',
required: true,
'x-component': 'Input',
'x-validator': 'email',
'x-decorator': 'FormItem',
'x-component-props': { placeholder: '{{t("Email")}}', style: {} },
},
password: {
type: 'string',
required: true,
'x-component': 'Password',
'x-decorator': 'FormItem',
'x-component-props': { placeholder: '{{t("Password")}}', checkStrength: true, style: {} },
'x-reactions': [
{
dependencies: ['.confirm_password'],
fulfill: {
state: {
selfErrors: '{{$deps[0] && $self.value && $self.value !== $deps[0] ? t("Password mismatch") : ""}}',
},
},
},
],
},
confirm_password: {
type: 'string',
required: true,
'x-component': 'Password',
'x-decorator': 'FormItem',
'x-component-props': { placeholder: '{{t("Confirm password")}}', checkStrength: true, style: {} },
'x-reactions': [
{
dependencies: ['.password'],
fulfill: {
state: {
selfErrors: '{{$deps[0] && $self.value && $self.value !== $deps[0] ? t("Password mismatch") : ""}}',
},
},
},
],
},
actions: {
type: 'void',
'x-component': 'div',
properties: {
submit: {
title: '{{t("Sign up")}}',
type: 'void',
'x-component': 'Action',
'x-component-props': {
block: true,
type: 'primary',
useAction: '{{ useSignup }}',
style: { width: '100%' },
},
},
},
},
link: {
type: 'void',
'x-component': 'div',
properties: {
link: {
type: 'void',
'x-component': 'Link',
'x-component-props': { to: '/signin' },
'x-content': '{{t("Log in with an existing account")}}',
},
},
},
},
};
const useSignup = () => {
const history = useHistory();
const form = useForm();
return {
async run() {
await form.submit();
console.log(form.values);
// history.push('/signin');
},
};
};
export const SignupPage = () => {
useCurrentDocumentTitle('Signup');
return <SchemaComponent schema={schema} scope={{ useSignup }} />;
};

View File

@ -0,0 +1,23 @@
---
nav:
path: /client
group:
title: Client
path: /client
---
# User
`plugin-users` 插件的前端模块
## CurrentUser
当前用户
## SigninPage
登录页
## SignupPage
注册页

View File

@ -0,0 +1,4 @@
export * from './CurrentUser';
export * from './SigninPage';
export * from './SignupPage';