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

View File

@ -145,10 +145,13 @@ export default (apiClient: APIClient) => {
routes: [
{
type: 'route',
uiSchemaUid: 'dtf9j0b8p9u',
path: '/signin',
component: 'RouteSchemaComponent',
title: '{{t("Sign in")}}',
component: 'SigninPage',
},
{
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';
interface DocumentTitleContextProps {
@ -35,3 +35,10 @@ export const DocumentTitleProvider: React.FC<{ addonBefore?: string; addonAfter?
export const useDocumentTitle = () => {
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 './board';
export * from './collection-manager';
export * from './current-user';
export * from './document-title';
export * from './i18n';
export * from './icon';
@ -17,4 +16,5 @@ export * from './record-provider';
export * from './route-switch';
export * from './schema-component';
export * from './system-settings';
export * from './user';

View File

@ -34,15 +34,27 @@ interface ToolbarItemProps {
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) => {
const { components } = useContext(PluginManagerContext);
const { items = [] } = props;
const [pinned, unpinned] = splitItems(items);
return (
<div style={{ display: 'inline-block' }}>
<Menu style={{ width: '100%' }} selectable={false} mode={'horizontal'} theme={'dark'}>
{items
.filter((item) => item.pin)
.map((item, index) => {
{pinned.map((item, index) => {
const Action = get(components, item.component);
return (
Action && (
@ -52,10 +64,9 @@ PluginManager.Toolbar = (props: ToolbarProps) => {
)
);
})}
<Menu.SubMenu key={'more'} title={<MoreOutlined />}>
{items
.filter((item) => !item.pin)
.map((item, index) => {
{unpinned.length > 0 && (
<Menu.SubMenu popupClassName={'pm-sub-menu'} key={'more'} title={<MoreOutlined />}>
{unpinned.map((item, index) => {
const Action = get(components, item.component);
return (
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>
</Menu.SubMenu>
)}
</Menu>
</div>
);

View File

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

View File

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

View File

@ -1,2 +1,4 @@
export * from './admin-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 { useA } from './hooks';
import { VisibleContext } from './context';
import { ComposedAction } from './types';
import { Button } from 'antd';
import React, { useState } from 'react';
import { ActionDrawer } from './Action.Drawer';
import { VisibleContext } from './context';
import { useA } from './hooks';
import { ComposedAction } from './types';
export const Action: ComposedAction = observer((props) => {
const { useAction = useA, onClick, ...others } = props;
@ -22,7 +22,7 @@ export const Action: ComposedAction = observer((props) => {
run();
}}
>
{schema.title}
{field.title}
</Button>
<RecursionField basePath={field.address} schema={schema} onlyRenderProperties />
</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
---
# SystemSettings <Badge>待定</Badge>
# SystemSettings
## SystemSettingsProvider
## SystemSettingsAction
## SystemSettingsShortcut
## useSystemSettings
```ts
const { data, loading, refresh } = useSystemSettings();
```
## Examples
<code src="./demos/demo1.tsx"/>

View File

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