feat: client v0.6 (#150)
* v0.6 * update... * feat: improve code * improve code * action & form * update... * improve code * improve code * improve code * designable * update * update... * api client * RecordProvider * collection manager * update... * update api client * update use request * update * update doc * test cases for compose * docs: improve documentation
This commit is contained in:
parent
72a968b29d
commit
a87a089acf
@ -6,5 +6,9 @@ export default defineConfig({
|
||||
logo: 'https://user-images.githubusercontent.com/9554297/83762004-a0761b00-a6a9-11ea-83b4-9c8ff721d4b8.png',
|
||||
outputPath: 'docs-dist',
|
||||
mode: 'site',
|
||||
resolve: {
|
||||
includes: ['docs', 'packages/client'],
|
||||
},
|
||||
hash: true,
|
||||
// more config: https://d.umijs.org/config
|
||||
});
|
||||
|
@ -1,21 +0,0 @@
|
||||
---
|
||||
hero:
|
||||
title: umilib
|
||||
desc: umilib site example
|
||||
actions:
|
||||
- text: Getting Started
|
||||
link: /components
|
||||
features:
|
||||
- icon: https://gw.alipayobjects.com/zos/bmw-prod/881dc458-f20b-407b-947a-95104b5ec82b/k79dm8ih_w144_h144.png
|
||||
title: Feature 1
|
||||
desc: Balabala
|
||||
- icon: https://gw.alipayobjects.com/zos/bmw-prod/d60657df-0822-4631-9d7c-e7a869c2f21c/k79dmz3q_w126_h126.png
|
||||
title: Feature 2
|
||||
desc: Balabala
|
||||
- icon: https://gw.alipayobjects.com/zos/bmw-prod/d1ee0c6f-5aed-4a45-a507-339a4bfe076c/k7bjsocq_w144_h144.png
|
||||
title: Feature 3
|
||||
desc: Balabala
|
||||
footer: Open-source MIT Licensed | Copyright © 2020<br />Powered by [dumi](https://d.umijs.org)
|
||||
---
|
||||
|
||||
## Hello umilib!
|
@ -9,6 +9,7 @@
|
||||
"bootstrap": "lerna bootstrap",
|
||||
"clean": "rimraf -rf packages/*/{lib,esm,dist} && lerna clean",
|
||||
"build": "lerna run build",
|
||||
"build-docs": "dumi build",
|
||||
"test": "jest",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
@ -17,6 +18,7 @@
|
||||
"@types/react-dom": "^17.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@types/jest": "^26.0.0",
|
||||
"@types/koa": "^2.13.4",
|
||||
"@types/koa-bodyparser": "^4.3.4",
|
||||
@ -26,6 +28,7 @@
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.9.1",
|
||||
"@typescript-eslint/parser": "^4.8.2",
|
||||
"antd": "^4.16.11",
|
||||
"cross-env": "^5.2.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"dumi": "^1.1.33",
|
||||
|
61
packages/client/contributing.md
Normal file
61
packages/client/contributing.md
Normal file
@ -0,0 +1,61 @@
|
||||
---
|
||||
toc: menu
|
||||
---
|
||||
|
||||
# 参与贡献
|
||||
|
||||
客户端的模块都在 `src` 目录下,各自模块独立。
|
||||
|
||||
## 客户端模块
|
||||
|
||||
最小单元模块化,一个完整的模块包含:
|
||||
|
||||
```bash
|
||||
|- /__tests__/ # 测试文件目录
|
||||
|- /demos/ # demo 目录
|
||||
|- index.ts # 最好不要直接在 index.ts 文件里写代码,index 文件只负责 export
|
||||
|- index.md # 文档,默认为英文
|
||||
|- index.zh-CN.md # 中文文档,可缺失,可以先都写到 index.md 里
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
详细文档见 [@testing-library/react](https://testing-library.com/docs/react-testing-library/intro/)
|
||||
|
||||
```tsx | pure
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { compose } from '../';
|
||||
|
||||
describe('compose', () => {
|
||||
it('case 1', () => {
|
||||
const A: React.FC = (props) => (
|
||||
<div>
|
||||
<h1>A</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const App = compose(A)();
|
||||
const { container } = render(<App />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Demo
|
||||
|
||||
组件 Demo 见 [dumi](https://d.umijs.org/guide/basic#write-component-demo),可以直接写在文档里,如:
|
||||
|
||||
<pre lang="markdown">
|
||||
```jsx
|
||||
import React from 'react';
|
||||
|
||||
export default () => <h1>Hello NocoBase!</h1>;
|
||||
```
|
||||
</pre>
|
||||
|
||||
也可以引用 demo 文件
|
||||
|
||||
```markdown
|
||||
<code src="./demos/dmeo1.tsx"/>
|
||||
```
|
854
packages/client/intro.md
Normal file
854
packages/client/intro.md
Normal file
@ -0,0 +1,854 @@
|
||||
---
|
||||
order: 1
|
||||
---
|
||||
|
||||
# 客户端内核
|
||||
|
||||
<img src="https://nocobase.oss-cn-beijing.aliyuncs.com/5be7ebc2f47effef85be7a0c75cf76f9.png" style="max-width: 800px;" />
|
||||
|
||||
示例:
|
||||
|
||||
```tsx | pure
|
||||
const app = new Application();
|
||||
|
||||
app.use([MemoryRouter, { initialEntries: ['/'] }]);
|
||||
|
||||
app.use(({ children }) => {
|
||||
const location = useLocation();
|
||||
if (location.pathname === '/hello') {
|
||||
return <div>Hello NocoBase!</div>;
|
||||
}
|
||||
return children;
|
||||
});
|
||||
|
||||
export default app.compose();
|
||||
```
|
||||
|
||||
## RouteSwitch
|
||||
|
||||
稍微复杂的应用都会用到路由来管理前端的页面,如下:
|
||||
|
||||
```jsx
|
||||
/**
|
||||
* defaultShowCode: true
|
||||
* title: Router
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Route, Switch, Link, MemoryRouter as Router } from 'react-router-dom';
|
||||
|
||||
const Home = () => <h1>Home</h1>;
|
||||
const About = () => <h1>About</h1>;
|
||||
|
||||
const App = () => (
|
||||
<Router initialEntries={['/']}>
|
||||
<Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link>
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<Home />
|
||||
</Route>
|
||||
<Route path="/about">
|
||||
<About />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
);
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
上述例子,组件经由路由转发,`/` 转发给 `Home`,`/about` 转发给 `About`。这种 JSX 的写法,对于熟悉 JSX 的开发来说,十分便捷,但需要开发来编写和维护,不符合 NocoBase 低代码、无代码的设计理念。所以将 Route 做了封装和配置化改造,如下:
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* defaultShowCode: true
|
||||
* title: RouteSwitch
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Link, MemoryRouter as Router } from 'react-router-dom';
|
||||
import { RouteRedirectProps, RouteSwitchProvider, RouteSwitch } from '@nocobase/client';
|
||||
|
||||
const Home = () => <h1>Home</h1>;
|
||||
const About = () => <h1>About</h1>;
|
||||
|
||||
const routes: RouteRedirectProps[] = [
|
||||
{
|
||||
type: 'route',
|
||||
path: '/',
|
||||
exact: true,
|
||||
component: 'Home',
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/about',
|
||||
component: 'About',
|
||||
},
|
||||
];
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<RouteSwitchProvider components={{ Home, About }}>
|
||||
<Router initialEntries={['/']}>
|
||||
<Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link>
|
||||
<RouteSwitch routes={routes} />
|
||||
</Router>
|
||||
</RouteSwitchProvider>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
- 由 RouteSwitchProvider 配置 components,由开发编写,以 Layout 或 Template 的方式提供给 RouteSwitch 使用。
|
||||
- 由 RouteSwitch 配置 routes,JSON 的方式,可以由后端获取,方便后续的动态化、无代码的支持。
|
||||
|
||||
## SchemaComponent
|
||||
|
||||
路由可以通过 JSON 的方式配置,可以注册诸多可供路由使用的组件模板,以方便各种场景支持,但是这些组件还是需要开发编写和维护,所以进一步将组件抽象,转换成配置化的方式。如:
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* defaultShowCode: true
|
||||
* title: Schema Component
|
||||
*/
|
||||
import React from 'react';
|
||||
import { ISchema } from '@formily/react';
|
||||
import { SchemaComponentProvider, SchemaComponent } from '@nocobase/client';
|
||||
|
||||
const schema: ISchema = {
|
||||
name: 'hello',
|
||||
'x-component': 'Hello',
|
||||
'x-component-props': {
|
||||
name: 'World',
|
||||
},
|
||||
};
|
||||
|
||||
const Hello = ({ name }) => <h1>Hello {name}!</h1>;
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ Hello }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
)
|
||||
};
|
||||
```
|
||||
|
||||
可以通过 schema 方式配置的组件,称之为 schema 组件。在 SchemaComponentProvider 里注册各种 JSX 组件,编写相应的 schema,再通过 SchemaComponent 渲染。SchemaComponent 的 schema 就是 Formily 的 [Schema](https://react.formilyjs.org/zh-CN/api/shared/schema)。实际上 SchemaComponent 就是 Formily 的 [SchemaField](https://react.formilyjs.org/zh-CN/api/components/schema-field),之所以叫 SchemaComponent,是因为 SchemaComponent 可用于构建页面的各个部分,不局限于表单场景。
|
||||
|
||||
**思维转换:**
|
||||
|
||||
虽然 Formily 的核心是致力于解决表单的复杂问题,但是随着不断的演变,已经不局限在表单层面了。Formily 核心提供了 Form 和 Field 两个非常重要的模型,Form 提供了路径系统和联动模型也同样适用于页面视图,Field 实际上也可以理解为组件,分为有值组件和无值组件两类,有值组件例如 Input、Select 等,无值组件例如 Drawer、Button 等。有值组件的数值又可以分不同类型:String、Number、Boolean、Object、Array 等等,无值组件没有数值,所以用 Void 表示,和 Formily 的 Field、ArrayField、ObjectField、VoidField 都对应上了。
|
||||
|
||||
为了适应动态的配置化表单解决方案,Formily 又提炼了 Schema 协议(DSL),这个协议完全适用于描述组件模型,用类 JSON Schema 的语法描述组件结构和对应的数值类型,这个 Schema 也是 SchemaComponent 的重要组成部分。
|
||||
|
||||
- Schema 是一个树结构,多个节点以树形结构连接起来,其中的一个 property 表示的就是其中的一个 Schema 节点。
|
||||
- 单 Schema 节点(不包括 properties)由核心 `x-component`、包装器 `x-decorator`、设计器 `x-designable` 三个组件构成。
|
||||
- `x-component` 核心组件
|
||||
- `x-decorator` 包装器,不同场景中,同一个核心组件,可能使用不同的包装器,如 FormItem、CardItem、BlockItem、Editable 等
|
||||
- `x-designable` 节点设计器(NocoBase 的扩展参数),一般为当前 schema 节点的配置表单。与 Formily 提供的 Designable 解决方案不同,`x-designable` 直接作用于当前 schema 节点,使用和配置不分离。
|
||||
|
||||
理论上,很多现有组件都可以直接转为 schema 组件,但是并不一定好用。以 Drawer 为例,常规 JSX 的写法一般是这样的:
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* defaultShowCode: true
|
||||
* title: JSX Drawer
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { Drawer, Button } from 'antd';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const showDrawer = () => {
|
||||
setVisible(true);
|
||||
};
|
||||
const onClose = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Button type="primary" onClick={showDrawer}>
|
||||
Open
|
||||
</Button>
|
||||
<Drawer
|
||||
title="Basic Drawer"
|
||||
placement="right"
|
||||
onClose={onClose}
|
||||
visible={visible}
|
||||
footer={
|
||||
<Button onClick={onClose}>关闭</Button>
|
||||
}
|
||||
>
|
||||
<p>Some contents...</p>
|
||||
<p>Some contents...</p>
|
||||
<p>Some contents...</p>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
将组件转换成 schema,如果 1:1 转换是这样的:
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* defaultShowCode: true
|
||||
* title: Drawer Schema
|
||||
*/
|
||||
import React, { useMemo } from 'react';
|
||||
import { SchemaComponentProvider, SchemaComponent } from '@nocobase/client';
|
||||
import { Drawer as AntdDrawer, Button } from 'antd';
|
||||
import { createForm } from '@formily/core';
|
||||
import { RecursionField } from '@formily/react';
|
||||
|
||||
const Drawer = (props) => {
|
||||
const { footerSchema, ...others } = props;
|
||||
return (
|
||||
<AntdDrawer
|
||||
footer={
|
||||
footerSchema && <RecursionField schema={footerSchema} onlyRenderProperties />
|
||||
}
|
||||
{...others}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
b1: {
|
||||
type: 'void',
|
||||
'x-component': 'Button',
|
||||
'x-component-props': {
|
||||
children: 'Open',
|
||||
type: 'primary',
|
||||
onClick: '{{showDrawer}}',
|
||||
},
|
||||
},
|
||||
d1: {
|
||||
type: 'void',
|
||||
'x-component': 'Drawer',
|
||||
'x-component-props': {
|
||||
title: 'Basic Drawer',
|
||||
onClose: '{{onClose}}',
|
||||
footerSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fb1: {
|
||||
type: 'void',
|
||||
'x-component': 'Button',
|
||||
'x-component-props': {
|
||||
children: 'Close',
|
||||
onClick: '{{onClose}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const form = useMemo(() => createForm(), []);
|
||||
const showDrawer = () => {
|
||||
form.query('d1').take((field) => {
|
||||
field.componentProps.visible = true;
|
||||
});
|
||||
};
|
||||
const onClose = () => {
|
||||
form.query('d1').take((field) => {
|
||||
field.componentProps.visible = false;
|
||||
});
|
||||
};
|
||||
return (
|
||||
<SchemaComponentProvider
|
||||
form={form}
|
||||
components={{ Drawer, Button }}
|
||||
>
|
||||
<SchemaComponent schema={schema} scope={{ showDrawer, onClose }} />
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
这个例子讲述了怎么将组件转换为可 Schema 配置,虽然达成了某种效果,但并不是一个很好的示例。
|
||||
|
||||
- 一个 property 就是一个 Schema 节点,Drawer 的 Schema 由平行的两个 schema 节点组成,不利于管理;
|
||||
- 需要额外的自定义 scope 支持 drawer 组件 visible 的状态管理,而且这里自定义的 scope 复用性差;
|
||||
- footer 需要特殊处理。在 x-component-props 里加了个 footerSchema 参数。但这个 footerSchema 并不是一个常规的 schema 节点,因为不是在 properties 里,不利于后端 schema 存储的统一规划;
|
||||
- 删除 drawer,需要删除两个 schema 节点;
|
||||
- 后端如何输出 drawer 这部分的 schema 也非常不方便,因为 drawer 由平行的两个节点组成。
|
||||
|
||||
为了解决上述问题,从结构上做了一些改良:
|
||||
|
||||
```ts
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
a1: {
|
||||
'x-component': 'Action',
|
||||
title: 'Open',
|
||||
properties: {
|
||||
d1: {
|
||||
'x-component': 'Action.Drawer',
|
||||
title: 'Drawer Title',
|
||||
properties: {
|
||||
c1: {
|
||||
'x-content': 'Hello',
|
||||
},
|
||||
f1: {
|
||||
'x-component': 'Action.Drawer.Footer',
|
||||
properties: {
|
||||
a1: {
|
||||
'x-component': 'Action',
|
||||
title: 'Close',
|
||||
'x-component-props': {
|
||||
useAction: '{{ useCloseAction }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
以上示例,自定义了一个 Action 组件,用于配置按钮操作,又扩展了 Action.Drawer 和 Action.Drawer.Footer 两个特殊节点,分别用于配置抽屉弹框和抽屉的 footer。以上 schema 是个标准的组件树结构,层次十分分明。组件树的各个节点层次分明,schema 的增删改查就完全是标准流程了。
|
||||
|
||||
- 查询 Drawer 的 schema,只需要把 a1 节点的 json 全部输出就可以。
|
||||
- 修改各个节点和子节点都可以单节点独立处理,逻辑一致。
|
||||
- 删除时,直接删除不需要的节点就可以了。
|
||||
- 有利于扩展,比如继续增加 Action.Modal、Action.Modal.Footer 两个节点用于配置对话框。
|
||||
|
||||
Action.Drawer 完整的例子如下:
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* title: Action.Drawer
|
||||
*/
|
||||
import React, { createContext, useContext, useMemo, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Button, Drawer } from 'antd';
|
||||
import { SchemaComponentProvider, SchemaComponent } from '@nocobase/client';
|
||||
import { observer, RecursionField, useField, useFieldSchema, ISchema } from '@formily/react';
|
||||
|
||||
const VisibleContext = createContext(null);
|
||||
|
||||
const useA = () => {
|
||||
return {
|
||||
async run() {},
|
||||
};
|
||||
};
|
||||
|
||||
function useCloseAction() {
|
||||
const [, setVisible] = useContext(VisibleContext);
|
||||
return {
|
||||
async run() {
|
||||
setVisible(false);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const Action: any = observer((props: any) => {
|
||||
const { useAction = useA, onClick, ...others } = props;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const schema = useFieldSchema();
|
||||
const field = useField();
|
||||
const { run } = useAction();
|
||||
return (
|
||||
<VisibleContext.Provider value={[visible, setVisible]}>
|
||||
<Button
|
||||
{...others}
|
||||
onClick={() => {
|
||||
onClick && onClick();
|
||||
setVisible(true);
|
||||
run();
|
||||
}}
|
||||
>
|
||||
{schema.title}
|
||||
</Button>
|
||||
<RecursionField basePath={field.address} schema={schema} onlyRenderProperties />
|
||||
</VisibleContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
Action.Drawer = observer((props: any) => {
|
||||
const [visible, setVisible] = useContext(VisibleContext);
|
||||
const schema = useFieldSchema();
|
||||
const field = useField();
|
||||
return (
|
||||
<>
|
||||
{createPortal(
|
||||
<Drawer
|
||||
title={schema.title}
|
||||
visible={visible}
|
||||
onClose={() => setVisible(false)}
|
||||
footer={
|
||||
<RecursionField
|
||||
basePath={field.address}
|
||||
schema={schema}
|
||||
onlyRenderProperties
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] === 'Action.Drawer.Footer';
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<RecursionField
|
||||
basePath={field.address}
|
||||
schema={schema}
|
||||
onlyRenderProperties
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] !== 'Action.Drawer.Footer';
|
||||
}}
|
||||
/>
|
||||
</Drawer>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
Action.Drawer.Footer = observer((props: any) => {
|
||||
const field = useField();
|
||||
const schema = useFieldSchema();
|
||||
return <RecursionField basePath={field.address} schema={schema} onlyRenderProperties />;
|
||||
});
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
a1: {
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
},
|
||||
title: 'Open',
|
||||
properties: {
|
||||
d1: {
|
||||
'x-component': 'Action.Drawer',
|
||||
title: 'Drawer Title',
|
||||
properties: {
|
||||
c1: {
|
||||
'x-content': 'Hello',
|
||||
},
|
||||
f1: {
|
||||
'x-component': 'Action.Drawer.Footer',
|
||||
properties: {
|
||||
a1: {
|
||||
'x-component': 'Action',
|
||||
title: 'Close',
|
||||
'x-component-props': {
|
||||
useAction: '{{ useCloseAction }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ Action }} scope={{ useCloseAction }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## RouteSwitch + SchemaComponent
|
||||
|
||||
当路由和组件都可以配置之后,可以进一步将二者结合,例子如下:
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* defaultShowCode: true
|
||||
* title: RouteSwitch + SchemaComponent
|
||||
*/
|
||||
import React, { useMemo } from 'react';
|
||||
import { Link, MemoryRouter as Router } from 'react-router-dom';
|
||||
import {
|
||||
RouteRedirectProps,
|
||||
RouteSwitchProvider,
|
||||
RouteSwitch,
|
||||
useRoute,
|
||||
SchemaComponentProvider,
|
||||
SchemaComponent,
|
||||
useDesignable,
|
||||
} from '@nocobase/client';
|
||||
import { Spin, Button } from 'antd';
|
||||
import { observer, Schema } from '@formily/react';
|
||||
|
||||
const Hello = observer(({ name }) => {
|
||||
const { patch, remove } = useDesignable();
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello {name}!</h1>
|
||||
<Button
|
||||
onClick={() => {
|
||||
patch('x-component-props.name', Math.random());
|
||||
}}
|
||||
>更新</Button>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
const RouteSchemaComponent = (props) => {
|
||||
const route = useRoute();
|
||||
return <SchemaComponent schema={route.schema}/>
|
||||
}
|
||||
|
||||
const routes: RouteRedirectProps[] = [
|
||||
{
|
||||
type: 'route',
|
||||
path: '/',
|
||||
exact: true,
|
||||
component: 'RouteSchemaComponent',
|
||||
schema: {
|
||||
name: 'home',
|
||||
'x-component': 'Hello',
|
||||
'x-component-props': {
|
||||
name: 'Home',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/about',
|
||||
component: 'RouteSchemaComponent',
|
||||
schema: {
|
||||
name: 'home',
|
||||
'x-component': 'Hello',
|
||||
'x-component-props': {
|
||||
name: 'About',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ Hello }}>
|
||||
<RouteSwitchProvider components={{ RouteSchemaComponent }}>
|
||||
<Router initialEntries={['/']}>
|
||||
<Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link>
|
||||
<RouteSwitch routes={routes} />
|
||||
</Router>
|
||||
</RouteSwitchProvider>
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
以上例子实现了路由和组件层面的配置化,在开发层面配置了两个组件:
|
||||
|
||||
- `<RouteSchemaComponent/>` 简易的可以在路由里配置 schema 的方案
|
||||
- `<Hello/>` 自定义的 Schema 组件
|
||||
|
||||
为了让大家更加能感受到 Schema 组件的不一样之处,例子添加了一个简易的随机更新 `x-component-props.name` 值的按钮,当路由切换后,更新后的 name 并不会被重置。这也是 Schema 组件的 Designable 的能力,可以任意的动态更新 schema 配置,实时更新,实时渲染。
|
||||
|
||||
## Designable
|
||||
|
||||
SchemaComponent 基于 Formily 的 SchemaField,Formily 提供了 [Designable](https://github.com/alibaba/designable) 来解决 Schema 的配置问题,但是这套方案:
|
||||
|
||||
- 需要维护两套代码,以 antd 为例,需要同时维护 @formily/antd 和 @designable/formily-antd 两套代码
|
||||
- 使用和设计分离,在设计器界面表单无法正常工作
|
||||
|
||||
另辟蹊径,NocoBase 构想了一种更为便捷的配置方案,使用和配置也可以兼顾,只需要维护一套代码。为此,提炼了一个简易的 `useDesignable()` Hook,可用于任意 Schema 组件中,动态配置 Schema,实时更新,实时渲染。
|
||||
|
||||
Hook API:
|
||||
|
||||
```ts
|
||||
const {
|
||||
designable, // 是否可以配置
|
||||
patch, // 更新当前节点配置
|
||||
remove, // 移除当前节点
|
||||
insertAdjacent, // 在当前节点的相邻位置插入,四个位置:beforeBegin、afterBegin、beforeEnd、afterEnd
|
||||
insertBeforeBegin, // 在当前节点的前面插入
|
||||
insertAfterBegin, // 在当前节点的第一个子节点前面插入
|
||||
insertBeforeEnd, // 在当前节点的最后一个子节点后面
|
||||
insertAfterEnd, // 在当前节点的后面
|
||||
} = useDesignable();
|
||||
|
||||
const schema = {
|
||||
'x-component': 'Hello',
|
||||
};
|
||||
|
||||
// 在当前节点的前面插入
|
||||
insertBeforeBegin(schema);
|
||||
// 等同于
|
||||
insertAdjacent('beforeBegin', schema);
|
||||
|
||||
// 在当前节点的第一个子节点前面插入
|
||||
insertAfterBegin(schema);
|
||||
// 等同于
|
||||
insertAdjacent('afterBegin', schema);
|
||||
|
||||
// 在当前节点的最后一个子节点后面
|
||||
insertBeforeEnd(schema);
|
||||
// 等同于
|
||||
insertAdjacent('beforeEnd', schema);
|
||||
|
||||
// 在当前节点的后面
|
||||
insertAfterEnd(schema);
|
||||
// 等同于
|
||||
insertAdjacent('afterEnd', schema);
|
||||
```
|
||||
|
||||
insertAdjacent 的几个插入的位置:
|
||||
|
||||
```ts
|
||||
{
|
||||
properties: {
|
||||
// beforeBegin 在当前节点的前面插入
|
||||
node1: {
|
||||
properties: {
|
||||
// afterBegin 在当前节点的第一个子节点前面插入
|
||||
// ...
|
||||
// beforeEnd 在当前节点的最后一个子节点后面
|
||||
},
|
||||
},
|
||||
// afterEnd 在当前节点的后面
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
并不是所有场景都能使用 hook,所以提供了 `createDesignable()` 方法(实际上 `useDesignable()` 也是基于它来实现):
|
||||
|
||||
```ts
|
||||
const dn = createDesignable({
|
||||
current: schema,
|
||||
});
|
||||
|
||||
dn.on('afterInsertAdjacent', (position, schema) => {
|
||||
|
||||
});
|
||||
|
||||
dn.insertAfterEnd(schema);
|
||||
```
|
||||
|
||||
相关例子如下:
|
||||
|
||||
<code src="./src/schema-component/demos/demo1.tsx" />
|
||||
|
||||
insertAdjacent 操作不仅可以用于新增节点,也可以用于现有节点的位置移动,如以下拖拽排序的例子:
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* title: 拖拽排序
|
||||
*/
|
||||
import React from 'react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { observer, useField, useFieldSchema } from '@formily/react';
|
||||
import { DndContext, DragEndEvent, useDraggable, useDroppable } from '@dnd-kit/core';
|
||||
import { SchemaComponent, SchemaComponentProvider, createDesignable, useDesignable } from '@nocobase/client';
|
||||
|
||||
const useDragEnd = () => {
|
||||
const { refresh } = useDesignable();
|
||||
|
||||
return ({ active, over }: DragEndEvent) => {
|
||||
const activeSchema = active?.data?.current?.schema;
|
||||
const overSchema = over?.data?.current?.schema;
|
||||
|
||||
if (!activeSchema || !overSchema) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dn = createDesignable({
|
||||
current: overSchema,
|
||||
});
|
||||
|
||||
dn.on('afterInsertAdjacent', refresh);
|
||||
dn.insertBeforeBeginOrAfterEnd(activeSchema);
|
||||
};
|
||||
};
|
||||
|
||||
const Page = observer((props) => {
|
||||
return <DndContext onDragEnd={useDragEnd()}>{props.children}</DndContext>;
|
||||
});
|
||||
|
||||
function Draggable(props) {
|
||||
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
||||
id: props.id,
|
||||
data: props.data,
|
||||
});
|
||||
const style = transform
|
||||
? {
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<button ref={setNodeRef} style={style} {...listeners} {...attributes}>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function Droppable(props) {
|
||||
const { isOver, setNodeRef } = useDroppable({
|
||||
id: props.id,
|
||||
data: props.data,
|
||||
});
|
||||
const style = {
|
||||
color: isOver ? 'green' : undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Block = observer((props) => {
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
return (
|
||||
<Droppable id={field.address.toString()} data={{ schema: fieldSchema }}>
|
||||
<div style={{ marginBottom: 20, padding: '20px', background: '#f1f1f1' }}>
|
||||
Block {fieldSchema.name}{' '}
|
||||
<Draggable id={field.address.toString()} data={{ schema: fieldSchema }}>
|
||||
Drag
|
||||
</Draggable>
|
||||
</div>
|
||||
</Droppable>
|
||||
);
|
||||
});
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ Page, Block }}>
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
type: 'void',
|
||||
name: 'page',
|
||||
'x-component': 'Page',
|
||||
properties: {
|
||||
block1: {
|
||||
'x-component': 'Block',
|
||||
},
|
||||
block2: {
|
||||
'x-component': 'Block',
|
||||
},
|
||||
block3: {
|
||||
'x-component': 'Block',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## APIClient
|
||||
|
||||
在 WEB 应用里,客户端请求无处不在。为了便于客户端请求,提供的 API 有:
|
||||
|
||||
- APIClient:客户端 SDK
|
||||
- APIClientProvider:提供 APIClient 实例的 Context,全局共享
|
||||
- useRequest():需要结合 APIClientProvider 来使用
|
||||
- useApiClient():获取到当前配置的 apiClient 实例
|
||||
|
||||
```tsx | pure
|
||||
const api = new APIClient({
|
||||
request, // 将 request 抛出去,方便各种自定义适配
|
||||
});
|
||||
api.request(options);
|
||||
api.resource(name);
|
||||
|
||||
<APIClientProvider apiClient={api}>
|
||||
{/* children */}
|
||||
</APIClientProvider>
|
||||
```
|
||||
|
||||
useRequest() 需要结合 APIClientProvider 一起使用,是对 ahooks 的 useRequest 的封装,支持 resource 请求。
|
||||
|
||||
```ts
|
||||
const { data, loading } = useRequest();
|
||||
```
|
||||
|
||||
|
||||
## Providers
|
||||
|
||||
客户端的扩展以 Providers 的形式存在,提供各种可供组件使用的 Context,可全局也可以局部使用。上文我们已经介绍了核心的三个 Providers:
|
||||
|
||||
- RouteSwitchProvider,提供配置路由所需的 Layout 和 Template 组件
|
||||
- SchemaComponentProvider,提供配置 Schema 所需的各种组件
|
||||
- ApiClientProvider,提供客户端 SDK
|
||||
|
||||
除此之外,还有:
|
||||
|
||||
- Router,实际也是 Provider,提供 History 的 Context,对应的有 BrowserRouter,HashRouter、MemoryRouter、NativeRouter、StaticRouter 几种可选方案
|
||||
- AntdConfigProvider,为 antd 组件提供统一的全局化配置
|
||||
- I18nextProvider,提供国际化解决方案
|
||||
- ACLProvider,提供权限配置,plugin-acl 的前端模块
|
||||
- CollectionManagerProvider,提供全局的数据表配置,plugin-collection-manager 的前端模块
|
||||
- SystemSettingsProvider,提供系统设置,plugin-system-settings 的前端模块
|
||||
- 其他扩展
|
||||
|
||||
多个 Providers 需要嵌套使用:
|
||||
|
||||
```tsx | pure
|
||||
<ApiClientProvider>
|
||||
<SchemaComponentProvider>
|
||||
<RouteSwitchProvider>
|
||||
{...}
|
||||
</RouteSwitchProvider>
|
||||
</SchemaComponentProvider>
|
||||
</ApiClientProvider>
|
||||
```
|
||||
|
||||
但是这样的方式不利于 Providers 的管理和扩展,为此提炼了 `compose()` 函数用于配置多个 providers,如下:
|
||||
|
||||
<code defaultShowCode="true" titile="compose" src="./src/application/demos/demo1.tsx"/>
|
||||
|
||||
## Application
|
||||
|
||||
上文例子的 Providers 还是差点意思,再进一步封装改造:
|
||||
|
||||
```tsx | pure
|
||||
const app = new Application({});
|
||||
|
||||
app.use(ApiClientProvider);
|
||||
app.use([SchemaComponentProvider, { components: { Hello } }]);
|
||||
app.use((props) => {
|
||||
return (
|
||||
<div>
|
||||
<Link to={'/'}>Home</Link>,<Link to={'/about'}>About</Link>
|
||||
<RouteSwitch routes={routes} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
app.mount('#root');
|
||||
// 等于
|
||||
ReactDOM.render(<App/>, document.getElementById('root'));
|
||||
```
|
||||
|
||||
对比 NocoBase Server Application 中间件的核心实现:
|
||||
|
||||
```ts
|
||||
app.use((ctx, next) => {});
|
||||
const ctx = this.createContext(req, res)
|
||||
await compose(app.middleware)(ctx);
|
||||
await respond(ctx);
|
||||
```
|
||||
|
||||
通过 app.use() 方法注册各种中间件插件,最后由 compose 来处理中间件,如果有需要也可以往 app.context 里添加各种东西待用。前端在处理 Provider 时也是类似的机制,这也是为什么客户端的扩展是以 Provider 的形式存在的原因。
|
||||
|
||||
从例子来看,以 Provider 的形式扩展是个不错的方案,但是还有两个问题没有解决:
|
||||
|
||||
- Provider 的顺序怎么处理
|
||||
- 如何动态的加载前端模块
|
||||
|
||||
未完待续...
|
@ -16,18 +16,33 @@
|
||||
"engines": {
|
||||
"npm": ">=3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^4.0.3",
|
||||
"@dnd-kit/sortable": "^5.1.0",
|
||||
"@emotion/css": "^11.7.1",
|
||||
"@formily/antd": "^2.0.7",
|
||||
"@formily/core": "^2.0.7",
|
||||
"@formily/react": "^2.0.7",
|
||||
"ahooks": "^3.0.5",
|
||||
"axios": "^0.24.0",
|
||||
"i18next": "^21.6.0",
|
||||
"react-i18next": "^11.15.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8.0 || >=17.0.0",
|
||||
"@types/react-dom": ">=16.8.0 || >=17.0.0",
|
||||
"antd": "^4.0.0",
|
||||
"react": ">=16.8.0 || >=17.0.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
"react-is": ">=16.8.0 || >=17.0.0"
|
||||
"react-is": ">=16.8.0 || >=17.0.0",
|
||||
"react-router-dom": "^5.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rimraf -rf lib esm dist && npm run build:cjs && npm run build:esm",
|
||||
"build:cjs": "tsc --project tsconfig.build.json",
|
||||
"build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm"
|
||||
},
|
||||
"dependencies": {}
|
||||
"devDependencies": {
|
||||
"axios-mock-adapter": "^1.20.0"
|
||||
}
|
||||
}
|
||||
|
13
packages/client/src/acl/ACLProvider.tsx
Normal file
13
packages/client/src/acl/ACLProvider.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
|
||||
export const ACLContext = createContext(null);
|
||||
|
||||
export function ACLProvider() {
|
||||
return (
|
||||
<ACLContext.Provider value={{}}>
|
||||
|
||||
</ACLContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ACLProvider;
|
1
packages/client/src/acl/RolePermissionManager.tsx
Normal file
1
packages/client/src/acl/RolePermissionManager.tsx
Normal file
@ -0,0 +1 @@
|
||||
export function RoleManager() {}
|
12
packages/client/src/acl/index.md
Normal file
12
packages/client/src/acl/index.md
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
nav:
|
||||
title: Client
|
||||
path: /client
|
||||
group:
|
||||
title: Client
|
||||
path: /client
|
||||
---
|
||||
|
||||
# ACL <Badge>待定</Badge>
|
||||
|
||||
访问控制列表,plugin-acl 的前端模块
|
2
packages/client/src/acl/index.tsx
Normal file
2
packages/client/src/acl/index.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './ACLProvider';
|
||||
export * from './RolePermissionManager';
|
53
packages/client/src/action/Action.Drawer.tsx
Normal file
53
packages/client/src/action/Action.Drawer.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Drawer } from 'antd';
|
||||
import { VisibleContext } from './context';
|
||||
import { ComposedActionDrawer } from './types';
|
||||
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||
|
||||
export const ActionDrawer: ComposedActionDrawer = observer((props) => {
|
||||
const [visible, setVisible] = useContext(VisibleContext);
|
||||
const schema = useFieldSchema();
|
||||
const field = useField();
|
||||
return (
|
||||
<>
|
||||
{createPortal(
|
||||
<Drawer
|
||||
title={schema.title}
|
||||
{...props}
|
||||
destroyOnClose
|
||||
visible={visible}
|
||||
onClose={() => setVisible(false)}
|
||||
footer={
|
||||
<RecursionField
|
||||
basePath={field.address}
|
||||
schema={schema}
|
||||
onlyRenderProperties
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] === 'Action.Drawer.Footer';
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<RecursionField
|
||||
basePath={field.address}
|
||||
schema={schema}
|
||||
onlyRenderProperties
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] !== 'Action.Drawer.Footer';
|
||||
}}
|
||||
/>
|
||||
</Drawer>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ActionDrawer.Footer = observer(() => {
|
||||
const field = useField();
|
||||
const schema = useFieldSchema();
|
||||
return <RecursionField basePath={field.address} schema={schema} onlyRenderProperties />;
|
||||
});
|
||||
|
||||
export default ActionDrawer;
|
34
packages/client/src/action/Action.tsx
Normal file
34
packages/client/src/action/Action.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
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 { ActionDrawer } from './Action.Drawer';
|
||||
|
||||
export const Action: ComposedAction = observer((props) => {
|
||||
const { useAction = useA, onClick, ...others } = props;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const schema = useFieldSchema();
|
||||
const field = useField();
|
||||
const { run } = useAction();
|
||||
return (
|
||||
<VisibleContext.Provider value={[visible, setVisible]}>
|
||||
<Button
|
||||
{...others}
|
||||
onClick={(e) => {
|
||||
onClick && onClick(e);
|
||||
setVisible(true);
|
||||
run();
|
||||
}}
|
||||
>
|
||||
{schema.title}
|
||||
</Button>
|
||||
<RecursionField basePath={field.address} schema={schema} onlyRenderProperties />
|
||||
</VisibleContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
Action.Drawer = ActionDrawer;
|
||||
|
||||
export default Action;
|
3
packages/client/src/action/context.tsx
Normal file
3
packages/client/src/action/context.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const VisibleContext = createContext(null);
|
66
packages/client/src/action/demos/demo1.tsx
Normal file
66
packages/client/src/action/demos/demo1.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { observer, ISchema, useForm } from '@formily/react';
|
||||
import { SchemaComponent, SchemaComponentProvider, Form, Action, useActionVisible } from '@nocobase/client';
|
||||
import { FormItem, Input } from '@formily/antd';
|
||||
import 'antd/dist/antd.css';
|
||||
|
||||
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: {
|
||||
action1: {
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
},
|
||||
type: 'void',
|
||||
title: 'Open',
|
||||
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 default observer(() => {
|
||||
return (
|
||||
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Form, Action, Input, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
});
|
27
packages/client/src/action/hooks.ts
Normal file
27
packages/client/src/action/hooks.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { useForm } from '@formily/react';
|
||||
import { useContext } from 'react';
|
||||
import { VisibleContext } from './context';
|
||||
|
||||
export const useA = () => {
|
||||
return {
|
||||
async run() {},
|
||||
};
|
||||
};
|
||||
|
||||
export const useActionVisible = () => {
|
||||
const [visible, setVisible] = useContext(VisibleContext);
|
||||
return { visible, setVisible };
|
||||
};
|
||||
|
||||
export const useCloseAction = () => {
|
||||
const [, setVisible] = useContext(VisibleContext);
|
||||
const form = useForm();
|
||||
return {
|
||||
async run() {
|
||||
setVisible(false);
|
||||
form.submit((values) => {
|
||||
console.log(values);
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
33
packages/client/src/action/index.md
Normal file
33
packages/client/src/action/index.md
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
title: Schema Components
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# Action <Badge>待定</Badge>
|
||||
|
||||
## Nodes
|
||||
|
||||
已确定的节点:
|
||||
|
||||
- Action
|
||||
- Action.URL
|
||||
- Action.Link
|
||||
- Action.Drawer
|
||||
- Action.Drawer.Footer
|
||||
- Action.Modal
|
||||
- Action.Modal.Footer
|
||||
- Action.Popover
|
||||
|
||||
不确定的节点:
|
||||
|
||||
- Action.Group
|
||||
- Action.Dropdown
|
||||
- Action.Window
|
||||
- ActionBar
|
||||
|
||||
## Examples
|
||||
|
||||
<code src="./demos/demo1.tsx"/>
|
3
packages/client/src/action/index.tsx
Normal file
3
packages/client/src/action/index.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './Action';
|
||||
export * from './context';
|
||||
export * from './hooks';
|
16
packages/client/src/action/types.ts
Normal file
16
packages/client/src/action/types.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { ButtonProps, DrawerProps } from 'antd';
|
||||
|
||||
export type ActionProps = ButtonProps & {
|
||||
useAction?: () => {
|
||||
run(): Promise<void>;
|
||||
};
|
||||
};
|
||||
|
||||
export type ComposedAction = React.FC<ActionProps> & {
|
||||
Drawer?: ComposedActionDrawer;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type ComposedActionDrawer = React.FC<DrawerProps> & {
|
||||
Footer?: React.FC;
|
||||
};
|
27
packages/client/src/admin-layout/index.md
Normal file
27
packages/client/src/admin-layout/index.md
Normal file
@ -0,0 +1,27 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
title: Route Components
|
||||
path: /route-components
|
||||
---
|
||||
|
||||
# AdminLayout <Badge>待定</Badge>
|
||||
|
||||
结构
|
||||
|
||||
- Layout
|
||||
- Layout.Header
|
||||
- SiteTitle
|
||||
- Menu(SchemaComponent)
|
||||
- PluginActionBar
|
||||
- Designable.Action(引用)
|
||||
- CollectionManager.Action(引用)
|
||||
- ACL.Action(引用)
|
||||
- SystemSettings.Action(引用)
|
||||
- CurrentUser.Dropdown(引用)
|
||||
- Layout.Sider
|
||||
- SideMenu(随 Menu 联动的)
|
||||
- Layout.Content
|
||||
- PageTitle
|
||||
- SchemaComponent
|
12
packages/client/src/admin-layout/index.tsx
Normal file
12
packages/client/src/admin-layout/index.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
export function AdminLayout(props: any) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdminLayout;
|
10
packages/client/src/antd-config-provider/index.md
Normal file
10
packages/client/src/antd-config-provider/index.md
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /client
|
||||
---
|
||||
|
||||
# AntdConfigProvider
|
||||
|
||||
为 antd 组件提供统一的全局化配置
|
10
packages/client/src/antd-config-provider/index.tsx
Normal file
10
packages/client/src/antd-config-provider/index.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React, { createContext } from 'react';
|
||||
import { I18nextProvider, useTranslation } from 'react-i18next';
|
||||
import { ConfigProvider, Spin } from 'antd';
|
||||
import enUS from 'antd/lib/locale/en_US';
|
||||
import zhCN from 'antd/lib/locale/zh_CN';
|
||||
|
||||
export function AntdConfigProvider(props) {
|
||||
const { i18n } = useTranslation();
|
||||
return <ConfigProvider locale={i18n.language === 'zh-CN' ? zhCN : enUS}>{props.children}</ConfigProvider>;
|
||||
}
|
62
packages/client/src/api-client/APIClient.ts
Normal file
62
packages/client/src/api-client/APIClient.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import axios, { Axios, AxiosInstance, AxiosResponse, AxiosRequestConfig } from 'axios';
|
||||
|
||||
export interface ActionParams {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
type ResourceActionOptions<P = any> = {
|
||||
resource?: string;
|
||||
resourceOf?: any;
|
||||
action?: string;
|
||||
params?: P;
|
||||
};
|
||||
|
||||
export interface IResource {
|
||||
list?: (params?: ActionParams) => Promise<any>;
|
||||
get?: (params?: ActionParams) => Promise<any>;
|
||||
create?: (params?: ActionParams) => Promise<any>;
|
||||
update?: (params?: ActionParams) => Promise<any>;
|
||||
destroy?: (params?: ActionParams) => Promise<any>;
|
||||
}
|
||||
|
||||
export class APIClient {
|
||||
axios: AxiosInstance;
|
||||
|
||||
constructor(instance?: AxiosInstance | AxiosRequestConfig) {
|
||||
if (typeof instance === 'function') {
|
||||
this.axios = instance;
|
||||
} else {
|
||||
this.axios = axios.create(instance);
|
||||
}
|
||||
}
|
||||
|
||||
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D> | ResourceActionOptions): Promise<R> {
|
||||
const { resource, resourceOf, action, params } = config as any;
|
||||
if (resource) {
|
||||
return this.resource(resource, resourceOf)[action](params);
|
||||
}
|
||||
return this.axios.request<T, R, D>(config);
|
||||
}
|
||||
|
||||
resource(name: string, of?: any): IResource {
|
||||
const target = {};
|
||||
const handler = {
|
||||
get: (_: any, actionName: string) => {
|
||||
let url = name.split('.').join(`/${of || '_'}/`);
|
||||
url += `:${actionName}`;
|
||||
const config: AxiosRequestConfig = { url };
|
||||
if (['get', 'list'].includes(actionName)) {
|
||||
config['method'] = 'get';
|
||||
} else {
|
||||
config['method'] = 'post';
|
||||
}
|
||||
return (params?: ActionParams) => {
|
||||
config[config.method === 'get' ? 'params' : 'data'] = { __params__: params };
|
||||
console.log({ config });
|
||||
return this.request(config);
|
||||
};
|
||||
},
|
||||
};
|
||||
return new Proxy(target, handler);
|
||||
}
|
||||
}
|
12
packages/client/src/api-client/APIClientProvider.tsx
Normal file
12
packages/client/src/api-client/APIClientProvider.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { APIClient } from './APIClient';
|
||||
import { APIClientContext } from './context';
|
||||
|
||||
export interface APIClientProviderProps {
|
||||
apiClient: APIClient;
|
||||
}
|
||||
|
||||
export const APIClientProvider: React.FC<APIClientProviderProps> = (props) => {
|
||||
const { apiClient, children } = props;
|
||||
return <APIClientContext.Provider value={apiClient}>{children}</APIClientContext.Provider>;
|
||||
};
|
26
packages/client/src/api-client/__tests__/APIClient.test.tsx
Normal file
26
packages/client/src/api-client/__tests__/APIClient.test.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import axios from 'axios';
|
||||
import { APIClient } from '../APIClient';
|
||||
|
||||
describe('APIClient', () => {
|
||||
describe('axios', () => {
|
||||
it('case 1', () => {
|
||||
const apiClient = new APIClient();
|
||||
expect(apiClient.axios).toBeDefined();
|
||||
expect(typeof apiClient.axios).toBe('function');
|
||||
expect(typeof apiClient.axios.request).toBe('function');
|
||||
});
|
||||
|
||||
it('case 2', () => {
|
||||
const apiClient = new APIClient({
|
||||
baseURL: 'http://localhost/api/',
|
||||
});
|
||||
expect(apiClient.axios.defaults.baseURL).toBe('http://localhost/api/');
|
||||
});
|
||||
|
||||
it('case 3', () => {
|
||||
const instance = axios.create();
|
||||
const apiClient = new APIClient(instance);
|
||||
expect(apiClient.axios).toBe(instance);
|
||||
});
|
||||
})
|
||||
});
|
4
packages/client/src/api-client/context.ts
Normal file
4
packages/client/src/api-client/context.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { createContext } from 'react';
|
||||
import { APIClient } from './APIClient';
|
||||
|
||||
export const APIClientContext = createContext<APIClient>(new APIClient());
|
24
packages/client/src/api-client/demos/demo1.tsx
Normal file
24
packages/client/src/api-client/demos/demo1.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { APIClient, APIClientProvider, useRequest, compose } from '@nocobase/client';
|
||||
|
||||
const apiClient = new APIClient();
|
||||
|
||||
const mock = new MockAdapter(apiClient.axios);
|
||||
|
||||
mock.onGet('/users:get').reply(200, {
|
||||
data: { id: 1, name: 'John Smith' },
|
||||
});
|
||||
|
||||
const providers = [
|
||||
[APIClientProvider, { apiClient }]
|
||||
];
|
||||
|
||||
export default compose(...providers)(() => {
|
||||
const { data } = useRequest({
|
||||
resource: 'users',
|
||||
action: 'get',
|
||||
params: {},
|
||||
});
|
||||
return <div>{data?.name}</div>;
|
||||
});
|
3
packages/client/src/api-client/hooks/index.ts
Normal file
3
packages/client/src/api-client/hooks/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './useAPIClient';
|
||||
export * from './useRequest';
|
||||
export * from './useResource';
|
6
packages/client/src/api-client/hooks/useAPIClient.ts
Normal file
6
packages/client/src/api-client/hooks/useAPIClient.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { useContext } from 'react';
|
||||
import { APIClientContext } from '../context';
|
||||
|
||||
export function useAPIClient() {
|
||||
return useContext(APIClientContext);
|
||||
}
|
36
packages/client/src/api-client/hooks/useRequest.ts
Normal file
36
packages/client/src/api-client/hooks/useRequest.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { useContext } from 'react';
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
import { Options } from 'ahooks/lib/useRequest/src/types';
|
||||
import { default as useReq } from 'ahooks/lib/useRequest';
|
||||
import { APIClientContext } from '../context';
|
||||
|
||||
type FunctionService = (...args: any[]) => Promise<any>;
|
||||
|
||||
type ResourceActionOptions<P = any> = {
|
||||
resource?: string;
|
||||
resourceOf?: any;
|
||||
action?: string;
|
||||
params?: P;
|
||||
};
|
||||
|
||||
export function useRequest<P>(
|
||||
service: AxiosRequestConfig<P> | ResourceActionOptions<P> | FunctionService,
|
||||
options?: Options<any, any>,
|
||||
) {
|
||||
const api = useContext(APIClientContext);
|
||||
if (typeof service === 'function') {
|
||||
return useReq(service, options);
|
||||
}
|
||||
return useReq(async () => {
|
||||
const { url, resource, resourceOf, action, params } = service as any;
|
||||
if (url) {
|
||||
const response = await api.request(service);
|
||||
return response?.data;
|
||||
}
|
||||
if (resource) {
|
||||
const response = await api.resource(resource, resourceOf)[action](params);
|
||||
return response?.data?.data;
|
||||
}
|
||||
return;
|
||||
}, options);
|
||||
}
|
7
packages/client/src/api-client/hooks/useResource.ts
Normal file
7
packages/client/src/api-client/hooks/useResource.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { useContext } from 'react';
|
||||
import { APIClientContext } from '../context';
|
||||
|
||||
export function useResource(name: string, of?: string | number) {
|
||||
const apiClient = useContext(APIClientContext);
|
||||
return apiClient.resource(name, of);
|
||||
}
|
139
packages/client/src/api-client/index.md
Normal file
139
packages/client/src/api-client/index.md
Normal file
@ -0,0 +1,139 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /client
|
||||
---
|
||||
|
||||
# APIClient
|
||||
|
||||
## APIClient
|
||||
|
||||
```ts
|
||||
class APIClient {
|
||||
// axios 实例
|
||||
axios: AxiosInstance;
|
||||
// 构造器
|
||||
constructor(instance?: AxiosInstance | AxiosRequestConfig);
|
||||
// 客户端请求,支持 AxiosRequestConfig 和 ResourceActionOptions
|
||||
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D> | ResourceActionOptions): Promise<R>;
|
||||
// 获取资源
|
||||
resource<R = IResource>(name: string, of?: any): R;
|
||||
}
|
||||
```
|
||||
|
||||
示例
|
||||
|
||||
```ts
|
||||
import axios from 'axios';
|
||||
|
||||
// 不传参时,内部直接创建 axios 实例
|
||||
const apiClient = new APIClient();
|
||||
|
||||
// 提供 AxiosRequestConfig 配置参数
|
||||
const apiClient = new APIClient({
|
||||
baseURL: '',
|
||||
});
|
||||
|
||||
// 提供 AxiosInstance
|
||||
const instance = axios.create({
|
||||
baseURL: '',
|
||||
});
|
||||
const apiClient = new APIClient(instance);
|
||||
```
|
||||
|
||||
## APIClientProvider
|
||||
|
||||
提供 APIClient 实例的上下文。
|
||||
|
||||
```tsx | pure
|
||||
const apiClient = new APIClient();
|
||||
|
||||
<APIClientProvider apiClient={apiClient}></APIClientProvider>
|
||||
```
|
||||
|
||||
## useAPIClient
|
||||
|
||||
获取当前上下文的 APIClient 实例。
|
||||
|
||||
```ts
|
||||
const apiClient = useAPIClient();
|
||||
```
|
||||
|
||||
## useRequest
|
||||
|
||||
```ts
|
||||
function useRequest<P>(
|
||||
service: AxiosRequestConfig<P> | ResourceActionOptions<P> | FunctionService,
|
||||
options?: Options<any, any>,
|
||||
);
|
||||
```
|
||||
|
||||
支持 `axios.request(config)`,config 详情查看 [axios](https://github.com/axios/axios#request-config)
|
||||
|
||||
```ts
|
||||
const { data, loading, refresh } = useRequest({ url: '/users' });
|
||||
```
|
||||
|
||||
或者是 NocoBase 的 resource & action 请求:
|
||||
|
||||
```ts
|
||||
const { data } = useRequest({
|
||||
resource: 'users',
|
||||
action: 'get',
|
||||
params: {},
|
||||
});
|
||||
```
|
||||
|
||||
例子如下:
|
||||
|
||||
<code src="./demos/demo1.tsx" />
|
||||
|
||||
也可以是自定义的异步函数:
|
||||
|
||||
```ts
|
||||
const { data, loading, refresh } = useRequest(() => Promise.resolve({}));
|
||||
```
|
||||
|
||||
更多用法查看 ahooks 的 [useRequest()](https://ahooks.js.org/hooks/use-request/index)
|
||||
|
||||
## useResource
|
||||
|
||||
```ts
|
||||
function useResource(name: string, of?: string | number): IResource;
|
||||
```
|
||||
|
||||
资源是 NocoBase 的核心概念,包括:
|
||||
|
||||
- 独立资源,如 `posts`
|
||||
- 关系资源,如 `posts.tags` `posts.user` `posts.comments`
|
||||
|
||||
资源 URI
|
||||
|
||||
```bash
|
||||
# 独立资源,文章
|
||||
/api/posts
|
||||
# 关系资源,文章 ID=1 的评论
|
||||
/api/posts/1/comments
|
||||
```
|
||||
|
||||
通过 APIClient 获取资源
|
||||
|
||||
```ts
|
||||
const api = new APIClient();
|
||||
|
||||
api.resource('posts');
|
||||
api.resource('posts.comments', 1);
|
||||
```
|
||||
|
||||
useResource 用法:
|
||||
|
||||
```ts
|
||||
const resource = useResource('posts');
|
||||
const resource = useResource('posts.comments', 1);
|
||||
```
|
||||
|
||||
resource 的实际场景用例参见:
|
||||
|
||||
- [useCollection()](collection-manager#usecollection)
|
||||
- [useCollectionField()](collection-manager#usecollectionfield)
|
3
packages/client/src/api-client/index.tsx
Normal file
3
packages/client/src/api-client/index.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './hooks';
|
||||
export * from './APIClient';
|
||||
export * from './APIClientProvider';
|
@ -0,0 +1,77 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`compose case 1 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<h1>
|
||||
A
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`compose case 2 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<h1>
|
||||
A
|
||||
</h1>
|
||||
<div>
|
||||
<h1>
|
||||
B
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`compose case 3 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<h1>
|
||||
A
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`compose case 4 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<h1>
|
||||
A
|
||||
</h1>
|
||||
<div>
|
||||
<h1>
|
||||
B
|
||||
</h1>
|
||||
<div>
|
||||
<h1>
|
||||
C
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`compose case 5 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<h1>
|
||||
A
|
||||
</h1>
|
||||
<div>
|
||||
<h1>
|
||||
B
|
||||
1
|
||||
</h1>
|
||||
<div>
|
||||
<h1>
|
||||
C
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
95
packages/client/src/application/__tests__/compose.test.tsx
Normal file
95
packages/client/src/application/__tests__/compose.test.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { compose } from '../';
|
||||
|
||||
describe('compose', () => {
|
||||
it('case 1', () => {
|
||||
const A: React.FC = (props) => (
|
||||
<div>
|
||||
<h1>A</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const App = compose(A)();
|
||||
const { container } = render(<App />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('case 2', () => {
|
||||
const A: React.FC = (props) => (
|
||||
<div>
|
||||
<h1>A</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const B: React.FC = (props) => (
|
||||
<div>
|
||||
<h1>B</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const App = compose(A)(B);
|
||||
const { container } = render(<App />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('case 3', () => {
|
||||
const A: React.FC = (props) => (
|
||||
<div>
|
||||
<h1>A</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const App = compose([A])();
|
||||
const { container } = render(<App />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('case 4', () => {
|
||||
const A: React.FC = (props) => (
|
||||
<div>
|
||||
<h1>A</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const B: React.FC = (props) => (
|
||||
<div>
|
||||
<h1>B</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const C: React.FC = (props) => (
|
||||
<div>
|
||||
<h1>C</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const App = compose(A, B)(C);
|
||||
const { container } = render(<App />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('case 5', () => {
|
||||
const A: React.FC<any> = (props) => (
|
||||
<div>
|
||||
<h1>A {props.name}</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const B: React.FC<any> = (props) => (
|
||||
<div>
|
||||
<h1>B {props.name}</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const C: React.FC<any> = (props) => (
|
||||
<div>
|
||||
<h1>C</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const App = compose(A, [B, { name: '1' }])(C);
|
||||
const { container } = render(<App />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
18
packages/client/src/application/compose.tsx
Normal file
18
packages/client/src/application/compose.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
const Blank = ({ children }) => children || null;
|
||||
|
||||
export const compose = (...components: any[]) => {
|
||||
const Root = [...components, Blank].reduce((parent, child) => {
|
||||
const [Parent, parentProps] = Array.isArray(parent) ? parent : [parent];
|
||||
const [Child, childProps] = Array.isArray(child) ? child : [child];
|
||||
return ({ children }) => (
|
||||
<Parent {...parentProps}>
|
||||
<Child {...childProps}>{children}</Child>
|
||||
</Parent>
|
||||
);
|
||||
});
|
||||
return (LastChild?: any) => (props?: any) => {
|
||||
return <Root>{LastChild && <LastChild {...props} />}</Root>;
|
||||
};
|
||||
};
|
20
packages/client/src/application/demos/Hello.tsx
Normal file
20
packages/client/src/application/demos/Hello.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { useDesignable } from '@nocobase/client';
|
||||
import { Button } from 'antd';
|
||||
import { observer } from '@formily/react';
|
||||
|
||||
export const Hello: React.FC<any> = observer(({ name }) => {
|
||||
const { patch, remove } = useDesignable();
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello {name}!</h1>
|
||||
<Button
|
||||
onClick={() => {
|
||||
patch('x-component-props.name', Math.random());
|
||||
}}
|
||||
>
|
||||
更新
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import { SchemaComponent, useRoute } from '@nocobase/client';
|
||||
|
||||
export const RouteSchemaComponent = () => {
|
||||
const route = useRoute();
|
||||
return <SchemaComponent schema={route.schema} />;
|
||||
};
|
23
packages/client/src/application/demos/demo1.tsx
Normal file
23
packages/client/src/application/demos/demo1.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Link, MemoryRouter } from 'react-router-dom';
|
||||
import { RouteSwitchProvider, RouteSwitch, SchemaComponentProvider, compose } from '@nocobase/client';
|
||||
import { Hello } from './Hello';
|
||||
import { RouteSchemaComponent } from './RouteSchemaComponent';
|
||||
import routes from './routes';
|
||||
|
||||
const providers = [
|
||||
[MemoryRouter, { initialEntries: ['/'] }],
|
||||
[SchemaComponentProvider, { components: { Hello } }],
|
||||
[RouteSwitchProvider, { components: { RouteSchemaComponent } }],
|
||||
];
|
||||
|
||||
const App = compose(...providers)(() => {
|
||||
return (
|
||||
<div>
|
||||
<Link to={'/'}>Home</Link>,<Link to={'/about'}>About</Link>
|
||||
<RouteSwitch routes={routes} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default App;
|
29
packages/client/src/application/demos/routes.ts
Normal file
29
packages/client/src/application/demos/routes.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { RouteRedirectProps } from '@nocobase/client';
|
||||
|
||||
export default [
|
||||
{
|
||||
type: 'route',
|
||||
path: '/',
|
||||
exact: true,
|
||||
component: 'RouteSchemaComponent',
|
||||
schema: {
|
||||
name: 'home',
|
||||
'x-component': 'Hello',
|
||||
'x-component-props': {
|
||||
name: 'Home',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/about',
|
||||
component: 'RouteSchemaComponent',
|
||||
schema: {
|
||||
name: 'home',
|
||||
'x-component': 'Hello',
|
||||
'x-component-props': {
|
||||
name: 'About',
|
||||
},
|
||||
},
|
||||
},
|
||||
] as Array<RouteRedirectProps>;
|
14
packages/client/src/application/index.md
Normal file
14
packages/client/src/application/index.md
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /client
|
||||
---
|
||||
|
||||
# Application <Badge>待定</Badge>
|
||||
|
||||
<img src="https://nocobase.oss-cn-beijing.aliyuncs.com/5be7ebc2f47effef85be7a0c75cf76f9.png" style="max-width: 800px;" />
|
||||
|
||||
## compose
|
||||
|
||||
<code src="./demos/demo1.tsx"/>
|
1
packages/client/src/application/index.tsx
Normal file
1
packages/client/src/application/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export * from './compose';
|
9
packages/client/src/auth-layout/index.md
Normal file
9
packages/client/src/auth-layout/index.md
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /route-components
|
||||
---
|
||||
|
||||
# AuthLayout
|
||||
|
10
packages/client/src/auth-layout/index.tsx
Normal file
10
packages/client/src/auth-layout/index.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
export function AuthLayout(props: any) {
|
||||
return (
|
||||
<div style={{ maxWidth: 320, margin: '0 auto', paddingTop: '20vh' }}>
|
||||
<h1>NocoBase</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
86
packages/client/src/block-item/demos/demo1.tsx
Normal file
86
packages/client/src/block-item/demos/demo1.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { observer, useField, useFieldSchema } from '@formily/react';
|
||||
import { useDraggable, useDroppable } from '@dnd-kit/core';
|
||||
import { SchemaComponent, SchemaComponentProvider, BlockItem, DndContext } from '@nocobase/client';
|
||||
|
||||
function Draggable(props) {
|
||||
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
||||
id: props.id,
|
||||
data: props.data,
|
||||
});
|
||||
const style = transform
|
||||
? {
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<button ref={setNodeRef} style={style} {...listeners} {...attributes}>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function Droppable(props) {
|
||||
const { isOver, setNodeRef } = useDroppable({
|
||||
id: props.id,
|
||||
data: props.data,
|
||||
});
|
||||
const style = {
|
||||
color: isOver ? 'green' : undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Block = observer((props) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const field = useField();
|
||||
return (
|
||||
<Droppable id={field.address.toString()} data={{ schema: fieldSchema }}>
|
||||
<div style={{ marginBottom: 20, padding: '0 20px', height: 50, lineHeight: '50px', background: '#f1f1f1' }}>
|
||||
Block {fieldSchema.name}
|
||||
<Draggable id={field.address.toString()} data={{ schema: fieldSchema }}>
|
||||
Drag
|
||||
</Draggable>
|
||||
</div>
|
||||
</Droppable>
|
||||
);
|
||||
});
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ DndContext, BlockItem, Block }}>
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
type: 'void',
|
||||
name: 'page',
|
||||
'x-component': 'DndContext',
|
||||
'x-uid': uid(),
|
||||
properties: {
|
||||
block1: {
|
||||
'x-decorator': 'BlockItem',
|
||||
'x-component': 'Block',
|
||||
'x-uid': uid(),
|
||||
},
|
||||
block2: {
|
||||
'x-decorator': 'BlockItem',
|
||||
'x-component': 'Block',
|
||||
'x-uid': uid(),
|
||||
},
|
||||
block3: {
|
||||
'x-decorator': 'BlockItem',
|
||||
'x-component': 'Block',
|
||||
'x-uid': uid(),
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
}
|
12
packages/client/src/block-item/index.md
Normal file
12
packages/client/src/block-item/index.md
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# BlockItem <Badge>待定</Badge>
|
||||
|
||||
普通的装饰器(Decorator)组件,无特殊 UI 效果,一般用在 x-decorator 中。用于提供区块的管理,如拖拽功能、当前节点的 SettingsForm。CardItem 和 FormItem 组件都是基于 BlockItem 实现,也具备以上相同功能。
|
||||
|
||||
<code src="./demos/demo1.tsx" />
|
12
packages/client/src/block-item/index.tsx
Normal file
12
packages/client/src/block-item/index.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { useDesignable } from '../schema-component';
|
||||
|
||||
export const BlockItem: React.FC<any> = (props) => {
|
||||
const { DesignableBar } = useDesignable();
|
||||
return (
|
||||
<div className="nb-block-item">
|
||||
{props.children}
|
||||
<DesignableBar />
|
||||
</div>
|
||||
);
|
||||
};
|
8
packages/client/src/calendar/index.md
Normal file
8
packages/client/src/calendar/index.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# Calendar <Badge>待定</Badge>
|
10
packages/client/src/card-item/index.md
Normal file
10
packages/client/src/card-item/index.md
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# CardItem
|
||||
|
||||
卡片装饰器。除此之外,也继承了 BlockItem 的功能。
|
13
packages/client/src/card-item/index.tsx
Normal file
13
packages/client/src/card-item/index.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Card } from 'antd';
|
||||
import { BlockItem } from '../block-item';
|
||||
|
||||
export const CardItem: React.FC = (props) => {
|
||||
return (
|
||||
<BlockItem className={'noco-card-item'}>
|
||||
<Card bordered={false} {...props}>
|
||||
{props.children}
|
||||
</Card>
|
||||
</BlockItem>
|
||||
);
|
||||
};
|
8
packages/client/src/cascader/index.md
Normal file
8
packages/client/src/cascader/index.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# Cascader
|
8
packages/client/src/chart/index.md
Normal file
8
packages/client/src/chart/index.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# Chart <Badge>待定</Badge>
|
8
packages/client/src/checkbox/index.md
Normal file
8
packages/client/src/checkbox/index.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# Checkbox
|
42
packages/client/src/collection-manager/CollectionField.tsx
Normal file
42
packages/client/src/collection-manager/CollectionField.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { connect, SchemaOptionsContext, useField, useFieldSchema, useForm } from '@formily/react';
|
||||
import { useCollectionField } from './hooks';
|
||||
import { CollectionFieldProvider } from './CollectionFieldProvider';
|
||||
import { Field, FormPath } from '@formily/core';
|
||||
|
||||
// TODO: 初步适配
|
||||
const InternalField: React.FC = (props) => {
|
||||
const field = useField<Field>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { uiSchema } = useCollectionField();
|
||||
const options = useContext(SchemaOptionsContext);
|
||||
const component = FormPath.getIn(options?.components, uiSchema['x-component']);
|
||||
const setFieldProps = (key, value) => {
|
||||
field[key] = typeof field[key] === 'undefined' ? value : field[key];
|
||||
};
|
||||
const setRequired = () => {
|
||||
if (typeof fieldSchema['required'] === 'undefined') {
|
||||
field.required = !!uiSchema['required'];
|
||||
}
|
||||
};
|
||||
// TODO: 初步适配
|
||||
useEffect(() => {
|
||||
setFieldProps('title', uiSchema.title);
|
||||
setFieldProps('description', uiSchema.description);
|
||||
setFieldProps('initialValue', uiSchema.default);
|
||||
setRequired();
|
||||
field.component = [component, uiSchema['x-component-props']];
|
||||
}, [uiSchema.title, uiSchema.description, uiSchema.required]);
|
||||
return React.createElement(component, props);
|
||||
};
|
||||
|
||||
export const CollectionField = connect((props) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
return (
|
||||
<CollectionFieldProvider name={fieldSchema.name}>
|
||||
<InternalField {...props} />
|
||||
</CollectionFieldProvider>
|
||||
);
|
||||
});
|
||||
|
||||
export default CollectionField;
|
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { merge } from '@formily/shared';
|
||||
import { useCollection } from './hooks';
|
||||
import { CollectionFieldOptions } from './types';
|
||||
import { CollectionFieldContext } from './context';
|
||||
import { SchemaKey } from '@formily/react';
|
||||
|
||||
export const CollectionFieldProvider: React.FC<{ name?: SchemaKey; field?: CollectionFieldOptions }> = (props) => {
|
||||
const { name, field, children } = props;
|
||||
const { getField } = useCollection();
|
||||
return (
|
||||
<CollectionFieldContext.Provider value={field || getField(field?.name || name)}>
|
||||
{children}
|
||||
</CollectionFieldContext.Provider>
|
||||
);
|
||||
};
|
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { CollectionManagerOptions } from './types';
|
||||
import { CollectionManagerContext } from './context';
|
||||
|
||||
export const CollectionManagerProvider: React.FC<CollectionManagerOptions> = (props) => {
|
||||
const { interfaces, collections } = props;
|
||||
return (
|
||||
<CollectionManagerContext.Provider value={{ interfaces, collections }}>
|
||||
{props.children}
|
||||
</CollectionManagerContext.Provider>
|
||||
);
|
||||
};
|
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { useCollectionManager } from './hooks';
|
||||
import { CollectionOptions } from './types';
|
||||
import { CollectionContext } from './context';
|
||||
|
||||
export const CollectionProvider: React.FC<{ name?: string; collection: CollectionOptions }> = (props) => {
|
||||
const { name, collection, children } = props;
|
||||
const { get } = useCollectionManager();
|
||||
return (
|
||||
<CollectionContext.Provider value={collection || get(collection?.name || name)}>
|
||||
{children}
|
||||
</CollectionContext.Provider>
|
||||
);
|
||||
};
|
11
packages/client/src/collection-manager/context.ts
Normal file
11
packages/client/src/collection-manager/context.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { createContext } from 'react';
|
||||
import { CollectionFieldOptions, CollectionManagerOptions, CollectionOptions } from './types';
|
||||
|
||||
export const CollectionManagerContext = createContext<CollectionManagerOptions>({
|
||||
collections: [],
|
||||
interfaces: {},
|
||||
});
|
||||
|
||||
export const CollectionContext = createContext<CollectionOptions>({});
|
||||
|
||||
export const CollectionFieldContext = createContext<CollectionFieldOptions>({});
|
124
packages/client/src/collection-manager/demos/demo2.tsx
Normal file
124
packages/client/src/collection-manager/demos/demo2.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import React from 'react';
|
||||
import { observer, ISchema, useForm } from '@formily/react';
|
||||
import {
|
||||
SchemaComponent,
|
||||
SchemaComponentProvider,
|
||||
Form,
|
||||
Action,
|
||||
CollectionProvider,
|
||||
CollectionField,
|
||||
} from '@nocobase/client';
|
||||
import 'antd/dist/antd.css';
|
||||
import { FormItem, Input } from '@formily/antd';
|
||||
|
||||
export default observer(() => {
|
||||
const collection = {
|
||||
name: 'tests',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title1',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: 'Title1',
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
description: 'description1',
|
||||
} as ISchema,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title2',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: 'Title2',
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
description: 'description',
|
||||
default: 'ttt',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title3',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
form1: {
|
||||
type: 'void',
|
||||
'x-component': 'Form',
|
||||
properties: {
|
||||
// 字段 title1 直接使用全局提供的 uiSchema
|
||||
title1: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
default: '111',
|
||||
},
|
||||
// 等同于
|
||||
// title1: {
|
||||
// type: 'string',
|
||||
// title: 'Title',
|
||||
// required: true,
|
||||
// 'x-component': 'Input',
|
||||
// 'x-decorator': 'FormItem',
|
||||
// },
|
||||
title2: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
title: 'Title4', // 覆盖全局已定义的 Title2
|
||||
required: true, // 扩展的配置参数
|
||||
description: 'description4',
|
||||
},
|
||||
// 等同于
|
||||
// title2: {
|
||||
// type: 'string',
|
||||
// title: 'Title22',
|
||||
// required: true,
|
||||
// 'x-component': 'Input',
|
||||
// 'x-decorator': 'FormItem',
|
||||
// },
|
||||
// 字段 title3 没有提供 uiSchema,自行处理
|
||||
title3: {
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
title: 'Title3',
|
||||
required: true,
|
||||
},
|
||||
action1: {
|
||||
// type: 'void',
|
||||
'x-component': 'Action',
|
||||
title: 'Submit',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
useAction: '{{ useSubmit }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const useSubmit = () => {
|
||||
const form = useForm();
|
||||
return {
|
||||
async run() {
|
||||
form.submit(() => {
|
||||
console.log(form.values);
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<SchemaComponentProvider scope={{ useSubmit }} components={{ Action, Form, CollectionField, Input, FormItem }}>
|
||||
<CollectionProvider collection={collection}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</CollectionProvider>
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
});
|
67
packages/client/src/collection-manager/demos/demo3.tsx
Normal file
67
packages/client/src/collection-manager/demos/demo3.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
APIClient,
|
||||
APIClientProvider,
|
||||
CollectionFieldProvider,
|
||||
CollectionProvider,
|
||||
compose,
|
||||
RecordProvider,
|
||||
useCollectionField,
|
||||
} from '@nocobase/client';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
const apiClient = new APIClient();
|
||||
|
||||
const mock = new MockAdapter(apiClient.axios);
|
||||
|
||||
mock.onGet('/posts/1/tags:list').reply(200, {
|
||||
data: [
|
||||
{ id: 1, name: 'Tag 1' },
|
||||
{ id: 2, name: 'Tag 2' },
|
||||
],
|
||||
});
|
||||
|
||||
const collection = {
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{
|
||||
type: 'belongsToMany',
|
||||
name: 'tags',
|
||||
targetKey: 'id',
|
||||
sourceKey: 'id',
|
||||
foreignKey: 'postId',
|
||||
otherKey: 'tagId',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const record = { id: 1 };
|
||||
|
||||
const providers = [
|
||||
// API 客户端
|
||||
[APIClientProvider, { apiClient }],
|
||||
// 提供当前数据表行记录的上下文
|
||||
[RecordProvider, { record }],
|
||||
// 提供数据表配置的上下文
|
||||
[CollectionProvider, { collection }],
|
||||
// 提供字段配置上的下文
|
||||
[CollectionFieldProvider, { name: 'tags' }],
|
||||
];
|
||||
|
||||
export default compose(...providers)(() => {
|
||||
const { resource } = useCollectionField();
|
||||
const [items, setItems] = useState([]);
|
||||
useEffect(() => {
|
||||
// 请求地址为 /posts/1/tags:list
|
||||
resource.list().then((response) => {
|
||||
setItems(response?.data?.data);
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<div>
|
||||
{items?.map((item, key) => (
|
||||
<div key={key}>{item.name}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
3
packages/client/src/collection-manager/hooks/index.ts
Normal file
3
packages/client/src/collection-manager/hooks/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './useCollection';
|
||||
export * from './useCollectionField';
|
||||
export * from './useCollectionManager';
|
@ -0,0 +1,18 @@
|
||||
import { useContext } from 'react';
|
||||
import { SchemaKey } from '@formily/react';
|
||||
import { CollectionFieldOptions } from '../types';
|
||||
import { CollectionContext } from '../context';
|
||||
import { useAPIClient } from '../../api-client';
|
||||
|
||||
export const useCollection = () => {
|
||||
const collection = useContext(CollectionContext);
|
||||
const api = useAPIClient();
|
||||
const resource = api?.resource(collection.name);
|
||||
return {
|
||||
...collection,
|
||||
resource,
|
||||
getField(name: SchemaKey): CollectionFieldOptions {
|
||||
return collection?.fields?.find((field) => field.name === name);
|
||||
},
|
||||
};
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
import { useContext } from 'react';
|
||||
import { CollectionFieldContext } from '../context';
|
||||
import { useRecord } from '../../record-provider';
|
||||
import { useCollection } from './useCollection';
|
||||
import { useAPIClient } from '../../api-client';
|
||||
|
||||
export const useCollectionField = () => {
|
||||
const collection = useCollection();
|
||||
const record = useRecord();
|
||||
const api = useAPIClient();
|
||||
const ctx = useContext(CollectionFieldContext);
|
||||
const resource = api?.resource(`${collection?.name || ctx?.collectinName}.${ctx.name}`, record[ctx.sourceKey]);
|
||||
return {
|
||||
...ctx,
|
||||
resource,
|
||||
};
|
||||
};
|
@ -0,0 +1,11 @@
|
||||
import { useContext } from 'react';
|
||||
import { CollectionManagerContext } from '../context';
|
||||
|
||||
export const useCollectionManager = () => {
|
||||
const { collections } = useContext(CollectionManagerContext);
|
||||
return {
|
||||
get(name: string) {
|
||||
return collections?.find((collection) => collection.name === name);
|
||||
},
|
||||
};
|
||||
};
|
144
packages/client/src/collection-manager/index.md
Normal file
144
packages/client/src/collection-manager/index.md
Normal file
@ -0,0 +1,144 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /client
|
||||
---
|
||||
|
||||
# CollectionManager
|
||||
|
||||
## Components
|
||||
|
||||
### CollectionManagerProvider
|
||||
|
||||
```jsx | pure
|
||||
<CollectionManagerProvider interfaces={{}} collections={[]}></CollectionManagerProvider>
|
||||
```
|
||||
|
||||
### CollectionProvider
|
||||
|
||||
```jsx | pure
|
||||
const collection = {
|
||||
name: 'tests',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
'x-component': 'Input'
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
<CollectionProvider collection={collection}></CollectionProvider>
|
||||
```
|
||||
|
||||
如果没有传 collection 参数,从 CollectionManagerProvider 里取对应 name 的 collection。
|
||||
|
||||
```jsx | pure
|
||||
const collections = [
|
||||
{
|
||||
name: 'tests',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
'x-component': 'Input'
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
];
|
||||
<CollectionManagerProvider collections={collections}>
|
||||
<CollectionProvider name={'tests'}></CollectionProvider>
|
||||
</CollectionManagerProvider>
|
||||
```
|
||||
|
||||
### CollectionFieldProvider
|
||||
|
||||
```jsx | pure
|
||||
const field = {
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
'x-component': 'Input'
|
||||
},
|
||||
};
|
||||
<CollectionFieldProvider field={field}></CollectionFieldProvider>
|
||||
```
|
||||
|
||||
如果没有传 field 参数,从 CollectionProvider 里取对应 name 的 field。
|
||||
|
||||
```jsx | pure
|
||||
const collection = {
|
||||
name: 'tests',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
'x-component': 'Input'
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
<CollectionProvider collection={collection}>
|
||||
<CollectionFieldProvider name={'title'}></CollectionFieldProvider>
|
||||
</CollectionProvider>
|
||||
```
|
||||
|
||||
### CollectionField
|
||||
|
||||
万能字段组件,需要与 `<CollectionProvider/>` 搭配使用,仅限于在 Schema 场景使用。从 CollectionProvider 里取对应 name 的 field schema。可通过 CollectionField 所在的 schema 扩展配置。
|
||||
|
||||
```ts
|
||||
{
|
||||
name: 'title',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-decorator-props': {},
|
||||
'x-component': 'CollectionField',
|
||||
'x-component-props': {},
|
||||
properties: {},
|
||||
}
|
||||
```
|
||||
|
||||
<code src="./demos/demo2.tsx"/>
|
||||
|
||||
## Hooks
|
||||
|
||||
### useCollectionManager()
|
||||
|
||||
与 `<CollectionManagerProvider/>` 搭配使用
|
||||
|
||||
```jsx | pure
|
||||
const { collections, get } = useCollectionManager();
|
||||
```
|
||||
|
||||
### useCollection()
|
||||
|
||||
与 `<CollectionProvider/>` 搭配使用
|
||||
|
||||
```jsx | pure
|
||||
const { name, fields, getField, findField, resource } = useCollection();
|
||||
```
|
||||
|
||||
### useCollectionField()
|
||||
|
||||
与 `<CollectionFieldProvider/>` 搭配使用
|
||||
|
||||
```jsx | pure
|
||||
const { name, uiSchema, resource } = useCollectionField();
|
||||
```
|
||||
|
||||
resource 需要与 `<RecordProvider/>` 搭配使用,用于提供当前数据表行记录的上下文。如:
|
||||
|
||||
<code src="./demos/demo3.tsx"/>
|
7
packages/client/src/collection-manager/index.tsx
Normal file
7
packages/client/src/collection-manager/index.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
export * from './CollectionFieldProvider';
|
||||
export * from './CollectionManagerProvider';
|
||||
export * from './CollectionProvider';
|
||||
export * from './context';
|
||||
export * from './hooks';
|
||||
export * from './types';
|
||||
export * from './CollectionField';
|
23
packages/client/src/collection-manager/types.ts
Normal file
23
packages/client/src/collection-manager/types.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
|
||||
export interface CollectionManagerOptions {
|
||||
interfaces?: any;
|
||||
collections?: any[];
|
||||
}
|
||||
|
||||
export interface CollectionOptions {
|
||||
name?: string;
|
||||
fields?: any[];
|
||||
}
|
||||
|
||||
export interface ICollectionProviderProps {
|
||||
name?: string;
|
||||
fields?: any;
|
||||
}
|
||||
|
||||
export interface CollectionFieldOptions {
|
||||
name?: any;
|
||||
collectinName?: string;
|
||||
sourceKey?: string; // association field
|
||||
uiSchema?: ISchema;
|
||||
}
|
8
packages/client/src/color-select/index.md
Normal file
8
packages/client/src/color-select/index.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# ColorSelect
|
9
packages/client/src/current-user/index.md
Normal file
9
packages/client/src/current-user/index.md
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
title: Client
|
||||
path: /client
|
||||
---
|
||||
|
||||
# CurrentUser <Badge>待定</Badge>
|
3
packages/client/src/current-user/index.tsx
Normal file
3
packages/client/src/current-user/index.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export function CurrentUser() {
|
||||
|
||||
}
|
8
packages/client/src/date-picker/index.md
Normal file
8
packages/client/src/date-picker/index.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# DatePicker
|
8
packages/client/src/dnd-context/index.md
Normal file
8
packages/client/src/dnd-context/index.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# DndContext <Badge>待定</Badge>
|
29
packages/client/src/dnd-context/index.tsx
Normal file
29
packages/client/src/dnd-context/index.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { DndContext as DndKitContext, DragEndEvent } from '@dnd-kit/core';
|
||||
import { observer } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { createDesignable, useDesignable } from '../schema-component';
|
||||
|
||||
const useDragEnd = () => {
|
||||
const { refresh } = useDesignable();
|
||||
|
||||
return ({ active, over }: DragEndEvent) => {
|
||||
console.log({ active, over });
|
||||
const activeSchema = active?.data?.current?.schema;
|
||||
const overSchema = over?.data?.current?.schema;
|
||||
|
||||
if (!activeSchema || !overSchema) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dn = createDesignable({
|
||||
current: overSchema,
|
||||
});
|
||||
|
||||
dn.on('afterInsertAdjacent', refresh);
|
||||
dn.insertBeforeBeginOrAfterEnd(activeSchema);
|
||||
};
|
||||
};
|
||||
|
||||
export const DndContext = observer((props) => {
|
||||
return <DndKitContext onDragEnd={useDragEnd()}>{props.children}</DndKitContext>;
|
||||
});
|
8
packages/client/src/document-title/index.md
Normal file
8
packages/client/src/document-title/index.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /client
|
||||
---
|
||||
|
||||
# DocumentTitle <Badge>待定</Badge>
|
3
packages/client/src/document-title/index.tsx
Normal file
3
packages/client/src/document-title/index.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export function DocumentTitleProvider() {
|
||||
|
||||
}
|
8
packages/client/src/filter/index.md
Normal file
8
packages/client/src/filter/index.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# Filter
|
10
packages/client/src/form-item/index.md
Normal file
10
packages/client/src/form-item/index.md
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# FormItem
|
||||
|
||||
表单字段装饰器。除此之外,也继承了 BlockItem 的功能。
|
11
packages/client/src/form-item/index.tsx
Normal file
11
packages/client/src/form-item/index.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { BlockItem } from '../block-item';
|
||||
import { FormItem as FormilyFormItem } from '@formily/antd';
|
||||
|
||||
export const FormItem: React.FC = (props) => {
|
||||
return (
|
||||
<BlockItem className={'nb-form-item'}>
|
||||
<FormilyFormItem {...props} />
|
||||
</BlockItem>
|
||||
);
|
||||
};
|
58
packages/client/src/form/Form.tsx
Normal file
58
packages/client/src/form/Form.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { toJS } from '@formily/reactive';
|
||||
import { createForm, FormPath } from '@formily/core';
|
||||
import {
|
||||
FieldContext,
|
||||
FormContext,
|
||||
FormProvider,
|
||||
observer,
|
||||
SchemaContext,
|
||||
SchemaOptionsContext,
|
||||
useField,
|
||||
useFieldSchema,
|
||||
} from '@formily/react';
|
||||
import { SchemaComponent, useAttach } from '../schema-component';
|
||||
|
||||
type ComposedForm = React.FC<any> & {
|
||||
__NOCOBASE_FORM?: boolean;
|
||||
};
|
||||
|
||||
const useFormDecorator = () => {
|
||||
const field = useField();
|
||||
const options = useContext(SchemaOptionsContext);
|
||||
const decorator = field.decoratorType ? FormPath.getIn(options?.components, field.decoratorType) : null;
|
||||
return decorator?.__NOCOBASE_FORM ? decorator : null;
|
||||
};
|
||||
|
||||
const FormDecorator: React.FC<any> = (props) => {
|
||||
const { form } = props;
|
||||
const options = useContext(SchemaOptionsContext);
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const f = useAttach(form.createVoidField({ ...field.props, basePath: '' }));
|
||||
const finalComponent = FormPath.getIn(options?.components, field.componentType) ?? field.componentType;
|
||||
return (
|
||||
<FormContext.Provider value={form}>
|
||||
<SchemaContext.Provider value={fieldSchema}>
|
||||
<FieldContext.Provider value={f}>
|
||||
{React.createElement(finalComponent, toJS(field.componentProps))}
|
||||
</FieldContext.Provider>
|
||||
</SchemaContext.Provider>
|
||||
</FormContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const Form: ComposedForm = observer((props) => {
|
||||
const form = useMemo(() => createForm(), []);
|
||||
const fieldSchema = useFieldSchema();
|
||||
const decorator = useFormDecorator();
|
||||
return decorator ? (
|
||||
<FormDecorator form={form} />
|
||||
) : (
|
||||
<FormProvider form={form}>
|
||||
<SchemaComponent schema={fieldSchema} />
|
||||
</FormProvider>
|
||||
);
|
||||
});
|
||||
|
||||
Form.__NOCOBASE_FORM = true;
|
54
packages/client/src/form/demos/demo1.tsx
Normal file
54
packages/client/src/form/demos/demo1.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { observer, ISchema } from '@formily/react';
|
||||
import { SchemaComponent, SchemaComponentProvider, Form, Action, useCloseAction } from '@nocobase/client';
|
||||
import { FormItem, Input } from '@formily/antd';
|
||||
import 'antd/dist/antd.css';
|
||||
|
||||
export default observer(() => {
|
||||
const schema: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
a1: {
|
||||
type: 'void',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
},
|
||||
title: 'Open',
|
||||
properties: {
|
||||
d1: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer',
|
||||
'x-decorator': 'Form',
|
||||
title: 'Drawer Title',
|
||||
properties: {
|
||||
field1: {
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
title: 'T1',
|
||||
},
|
||||
f1: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer.Footer',
|
||||
properties: {
|
||||
a1: {
|
||||
'x-component': 'Action',
|
||||
title: 'Close',
|
||||
'x-component-props': {
|
||||
useAction: '{{ useCloseAction }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return (
|
||||
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Form, Action, Input, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
});
|
47
packages/client/src/form/demos/demo2.tsx
Normal file
47
packages/client/src/form/demos/demo2.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { observer, ISchema, useForm } from '@formily/react';
|
||||
import { SchemaComponent, SchemaComponentProvider, Form, Action } from '@nocobase/client';
|
||||
import 'antd/dist/antd.css';
|
||||
import { FormItem, Input } from '@formily/antd';
|
||||
|
||||
export default observer(() => {
|
||||
const schema: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
form1: {
|
||||
type: 'void',
|
||||
'x-component': 'Form',
|
||||
properties: {
|
||||
field1: {
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
title: 'T1',
|
||||
},
|
||||
action1: {
|
||||
// type: 'void',
|
||||
'x-component': 'Action',
|
||||
title: 'Submit',
|
||||
'x-component-props': {
|
||||
useAction: '{{ useSubmit }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const useSubmit = () => {
|
||||
const form = useForm();
|
||||
return {
|
||||
async run() {
|
||||
console.log(form.values);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<SchemaComponentProvider scope={{ useSubmit }} components={{ Action, Form, Input, FormItem }}>
|
||||
<SchemaComponent schema={schema} />
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
});
|
14
packages/client/src/form/index.md
Normal file
14
packages/client/src/form/index.md
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# Form
|
||||
|
||||
<code src="./demos/demo2.tsx"/>
|
||||
|
||||
Form 也可以作 decorator 存在,和 Action.Drawer 结合就是 DrawerForm 了。
|
||||
|
||||
<code src="./demos/demo1.tsx"/>
|
1
packages/client/src/form/index.tsx
Normal file
1
packages/client/src/form/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export * from './Form';
|
124
packages/client/src/grid/demos/demo1.tsx
Normal file
124
packages/client/src/grid/demos/demo1.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import React from 'react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { observer, useFieldSchema } from '@formily/react';
|
||||
import { useDraggable, useDroppable } from '@dnd-kit/core';
|
||||
import { SchemaComponent, SchemaComponentProvider, Grid } from '@nocobase/client';
|
||||
|
||||
function Draggable(props) {
|
||||
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
||||
id: props.id,
|
||||
data: props.data,
|
||||
});
|
||||
const style = transform
|
||||
? {
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<button ref={setNodeRef} style={style} {...listeners} {...attributes}>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function Droppable(props) {
|
||||
const { isOver, setNodeRef } = useDroppable({
|
||||
id: props.id,
|
||||
data: props.data,
|
||||
});
|
||||
const style = {
|
||||
color: isOver ? 'green' : undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Block = observer((props) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
return (
|
||||
<Droppable id={fieldSchema.name} data={{ schema: fieldSchema }}>
|
||||
<div style={{ marginBottom: 20, padding: '0 20px', height: 50, lineHeight: '50px', background: '#f1f1f1' }}>
|
||||
Block {fieldSchema.name}
|
||||
<Draggable id={fieldSchema.name} data={{ schema: fieldSchema }}>
|
||||
Drag
|
||||
</Draggable>
|
||||
</div>
|
||||
</Droppable>
|
||||
);
|
||||
});
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ Grid, Block }}>
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
type: 'void',
|
||||
name: 'page',
|
||||
'x-component': 'Grid',
|
||||
'x-uid': uid(),
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-uid': uid(),
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Block',
|
||||
},
|
||||
},
|
||||
},
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Block',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-uid': uid(),
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Block',
|
||||
},
|
||||
},
|
||||
},
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Block',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
}
|
10
packages/client/src/grid/index.md
Normal file
10
packages/client/src/grid/index.md
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# Grid <Badge>待定</Badge>
|
||||
|
||||
<code src="./demos/demo1.tsx" />
|
19
packages/client/src/grid/index.tsx
Normal file
19
packages/client/src/grid/index.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { observer } from '@formily/react';
|
||||
import { DndContext } from '../dnd-context';
|
||||
|
||||
export const Grid: any = observer((props) => {
|
||||
return (
|
||||
<div>
|
||||
<DndContext>{props.children}</DndContext>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Grid.Row = observer((props) => {
|
||||
return <div>{props.children}</div>;
|
||||
});
|
||||
|
||||
Grid.Col = observer((props) => {
|
||||
return <div>{props.children}</div>;
|
||||
});
|
10
packages/client/src/i18n/index.md
Normal file
10
packages/client/src/i18n/index.md
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /client
|
||||
---
|
||||
|
||||
# I18n
|
||||
|
||||
提供国际化解决方案。
|
54
packages/client/src/i18n/index.ts
Normal file
54
packages/client/src/i18n/index.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import i18next from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import moment from 'moment';
|
||||
|
||||
const zhCN = require('../locale/zh_CN');
|
||||
const enUS = require('../locale/en_US');
|
||||
const log = require('debug')('i18next');
|
||||
|
||||
export const i18n = i18next.createInstance();
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
lng: localStorage.getItem('locale') || 'en-US',
|
||||
debug: false,
|
||||
defaultNS: 'client',
|
||||
// parseMissingKeyHandler: (key) => {
|
||||
// console.log('parseMissingKeyHandler', `'${key}': '${key}',`);
|
||||
// return key;
|
||||
// },
|
||||
// ns: ['client'],
|
||||
resources: {
|
||||
'en-US': {
|
||||
client: {
|
||||
...enUS,
|
||||
},
|
||||
},
|
||||
'zh-CN': {
|
||||
client: {
|
||||
...zhCN,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const momentLngs = {
|
||||
'en-US': 'en',
|
||||
'zh-CN': 'zh-cn',
|
||||
};
|
||||
|
||||
function setMomentLng(language) {
|
||||
const lng = momentLngs[language || 'en-US'] || 'en';
|
||||
log(lng);
|
||||
moment.locale(lng);
|
||||
}
|
||||
|
||||
setMomentLng(localStorage.getItem('locale'));
|
||||
|
||||
i18n.on('languageChanged', (lng) => {
|
||||
localStorage.setItem('locale', lng);
|
||||
setMomentLng(lng);
|
||||
});
|
||||
|
||||
// export const t = i18n.t;
|
||||
|
||||
export default i18n;
|
8
packages/client/src/icon-picker/index.md
Normal file
8
packages/client/src/icon-picker/index.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# IconPicker
|
@ -1,5 +1,23 @@
|
||||
import React from 'react';
|
||||
import debug from 'debug';
|
||||
debug.log = console.log.bind(console);
|
||||
|
||||
export function Hello() {
|
||||
return <div>aaa</div>;
|
||||
}
|
||||
export * from './i18n';
|
||||
export * from './acl';
|
||||
export * from './admin-layout';
|
||||
export * from './antd-config-provider';
|
||||
export * from './api-client';
|
||||
export * from './auth-layout';
|
||||
export * from './collection-manager';
|
||||
export * from './current-user';
|
||||
export * from './document-title';
|
||||
export * from './route-switch';
|
||||
export * from './schema-component';
|
||||
export * from './system-settings';
|
||||
export * from './action';
|
||||
export * from './form';
|
||||
export * from './form-item';
|
||||
export * from './block-item';
|
||||
export * from './dnd-context';
|
||||
export * from './grid';
|
||||
export * from './application';
|
||||
export * from './record-provider';
|
||||
|
8
packages/client/src/input-number/index.md
Normal file
8
packages/client/src/input-number/index.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# InputNumber
|
8
packages/client/src/input/index.md
Normal file
8
packages/client/src/input/index.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# Input
|
8
packages/client/src/kanban/index.md
Normal file
8
packages/client/src/kanban/index.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# Kanban <Badge>待定</Badge>
|
280
packages/client/src/locale/en_US.json
Normal file
280
packages/client/src/locale/en_US.json
Normal file
@ -0,0 +1,280 @@
|
||||
{
|
||||
"{{count}} filter items_one": "{{count}} filter item",
|
||||
"{{count}} filter items_other": "{{count}} filter items",
|
||||
"{{count}} more items_one": "{{count}} more item",
|
||||
"{{count}} more items_other": "{{count}} more items",
|
||||
"Delete menu item": "Delete menu item",
|
||||
"Today": "Today",
|
||||
"Month": "Month",
|
||||
"Week": "Week",
|
||||
"Work week": "Work week",
|
||||
"Day": "Day",
|
||||
"Agenda": "Agenda",
|
||||
"Date": "Date",
|
||||
"Time": "Time",
|
||||
"Event": "Event",
|
||||
"None": "None",
|
||||
"System settings": "System settings",
|
||||
"System title": "System title",
|
||||
"Logo": "Logo",
|
||||
"Add menu item": "Add menu item",
|
||||
"Page": "Page",
|
||||
"Name": "Name",
|
||||
"Icon": "Icon",
|
||||
"Group": "Group",
|
||||
"Link": "Link",
|
||||
"Edit menu item": "Edit menu item",
|
||||
"Move to": "Move to",
|
||||
"Insert left": "Insert left",
|
||||
"Insert right": "Insert right",
|
||||
"Insert inner": "Insert inner",
|
||||
"Delete": "Delete",
|
||||
"UI editor": "UI editor",
|
||||
"Collections & Fields": "Collections & Fields",
|
||||
"Roles & Permissions": "Roles & Permissions",
|
||||
"Edit profile": "Edit profile",
|
||||
"Change password": "Change password",
|
||||
"Old password": "Old password",
|
||||
"New password": "New password",
|
||||
"Switch role": "Switch role",
|
||||
"Super admin": "Super admin",
|
||||
"Language": "Language",
|
||||
"Allow sign up": "Allow sign up",
|
||||
"Sign out": "Sign out",
|
||||
"Cancel": "Cancel",
|
||||
"Submit": "Submit",
|
||||
"Set the data scope": "Set the data scope",
|
||||
"Data blocks": "Data blocks",
|
||||
"Table": "Table",
|
||||
"Form": "Form",
|
||||
"Select data source": "Select data source",
|
||||
"Calendar": "Calendar",
|
||||
"Kanban": "Kanban",
|
||||
"Select group field": "Select group field",
|
||||
"Media": "Media",
|
||||
"Markdown": "Markdown",
|
||||
"Wysiwyg": "Wysiwyg",
|
||||
"Charts": "Charts",
|
||||
"Column chart": "Column chart",
|
||||
"Bar chart": "Bar chart",
|
||||
"Line chart": "Line chart",
|
||||
"Pie chart": "Pie chart",
|
||||
"Select template": "Select template",
|
||||
"Action logs": "Action logs",
|
||||
"Create template": "Create template",
|
||||
"Edit markdown": "Edit markdown",
|
||||
"Add block": "Add block",
|
||||
"Add new": "Add new",
|
||||
"Add record": "Add record",
|
||||
"Custom field display name": "Custom field display name",
|
||||
"Display fields": "Display fields",
|
||||
"Edit record": "Edit record",
|
||||
|
||||
"Add page": "Add page",
|
||||
"Add group": "Add group",
|
||||
"Add link": "Add link",
|
||||
"Insert above": "Insert above",
|
||||
"Insert below": "Insert below",
|
||||
"Save": "Save",
|
||||
"Delete block": "Delete block",
|
||||
"Are you sure you want to delete it?": "Are you sure you want to delete it?",
|
||||
"This is a demo text, **supports Markdown syntax**.": "This is a demo text, **supports Markdown syntax**.",
|
||||
|
||||
"Filter": "Filter",
|
||||
"Action type": "Action type",
|
||||
"Actions": "Actions",
|
||||
"Insert": "Insert",
|
||||
"Update": "Update",
|
||||
"View": "View",
|
||||
"View record": "View record",
|
||||
|
||||
"Data changes": "Data changes",
|
||||
"Field name": "Field name",
|
||||
"Before change": "Before change",
|
||||
"After change": "After change",
|
||||
|
||||
"Delete record": "Delete record",
|
||||
"Create collection": "Create collection",
|
||||
"Collection display name": "Collection display name",
|
||||
"Collection name": "Collection name",
|
||||
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.",
|
||||
"Storage type": "Storage type",
|
||||
"Edit": "Edit",
|
||||
"Edit collection": "Edit collection",
|
||||
"Configure fields": "Configure fields",
|
||||
"Edit field": "Edit field",
|
||||
"Configure fields of {{title}}": "Configure fields of {{title}}",
|
||||
"Basic": "Basic",
|
||||
"Single line text": "Single line text",
|
||||
"Long text": "Long text",
|
||||
"Phone": "Phone",
|
||||
"Email": "Email",
|
||||
"Number": "Number",
|
||||
"Percent": "Percent",
|
||||
"Password": "Password",
|
||||
"Choices": "Choices",
|
||||
"Checkbox": "Checkbox",
|
||||
"Single select": "Single select",
|
||||
"Multiple select": "Multiple select",
|
||||
"Radio group": "Radio group",
|
||||
"Checkbox group": "Checkbox group",
|
||||
"China region": "China region",
|
||||
"Attachment": "Attachment",
|
||||
"Date & Time": "Date & Time",
|
||||
"Datetime": "Datetime",
|
||||
"Relation": "Relation",
|
||||
"Link to": "Link to",
|
||||
"Sub-table": "Sub-table",
|
||||
"System info": "System info",
|
||||
"Created at": "Created at",
|
||||
"Last updated at": "Last updated at",
|
||||
"Created by": "Created by",
|
||||
"Last updated by": "Last updated by",
|
||||
"Add field": "Add field",
|
||||
"Field display name": "Field display name",
|
||||
"Field type": "Field type",
|
||||
|
||||
"Date format": "Date format",
|
||||
"Year/Month/Day": "Year/Month/Day",
|
||||
"Year-Month-Day": "Year-Month-Day",
|
||||
"Day/Month/Year": "Day/Month/Year",
|
||||
"Show time": "Show time",
|
||||
"Time format": "Time format",
|
||||
"12 hour": "12 hour",
|
||||
"24 hour": "24 hour",
|
||||
|
||||
"Meet <1><0>All</0><1>Any</1></1> conditions in the group": "Meet <1><0>All</0><1>Any</1></1> conditions in the group",
|
||||
"Add filter": "Add filter",
|
||||
"Add filter group": "Add filter group",
|
||||
"is": "is",
|
||||
"is not": "is not",
|
||||
"contains": "contains",
|
||||
"does not contain": "does not contain",
|
||||
"is empty": "is empty",
|
||||
"is not empty": "is not empty",
|
||||
"Display <1><0>10</0><1>20</1><2>50</2><3>100</3></1> items per page": "Display <1><0>10</0><1>20</1><2>50</2><3>100</3></1> items per page",
|
||||
"Edit chart": "Edit chart",
|
||||
"Add text": "Add text",
|
||||
"Filterable fields": "Filterable fields",
|
||||
"Edit button": "Edit button",
|
||||
"Hide": "Hide",
|
||||
"Enable actions": "Enable actions",
|
||||
"Export": "Export",
|
||||
"Customize": "Customize",
|
||||
"Function": "Function",
|
||||
"Popup form": "Popup form",
|
||||
"Flexible popup": "Flexible popup",
|
||||
"Configure actions": "Configure actions",
|
||||
"Display order number": "Display order number",
|
||||
"Enable drag and drop sorting": "Enable drag and drop sorting",
|
||||
"Triggered when the row is clicked": "Triggered when the row is clicked",
|
||||
"Add tab": "Add tab",
|
||||
"Disable tabs": "Disable tabs",
|
||||
"Details": "Details",
|
||||
"Edit tab": "Edit tab",
|
||||
"Relationship blocks": "Relationship blocks",
|
||||
"Select record": "Select record",
|
||||
"Display name": "Display name",
|
||||
"Select icon": "Select icon",
|
||||
"Custom column name": "Custom column name",
|
||||
"Edit description": "Edit description",
|
||||
"Required": "Required",
|
||||
"Label field": "Label field",
|
||||
"Default is the ID field": "Default is the ID field",
|
||||
"Set default sorting rules": "Set default sorting rules",
|
||||
|
||||
"is before": "is before",
|
||||
"is after": "is after",
|
||||
"is on or after": "is on or after",
|
||||
"is on or before": "is on or before",
|
||||
|
||||
"Upload": "Upload",
|
||||
|
||||
"Select level": "Select level",
|
||||
"Province": "Province",
|
||||
"City": "City",
|
||||
"Area": "Area",
|
||||
"Street": "Street",
|
||||
"Village": "Village",
|
||||
"Must select to the last level": "Must select to the last level",
|
||||
|
||||
"Move {{title}} to": "Move {{title}} to",
|
||||
"Target position": "Target position",
|
||||
"After": "After",
|
||||
"Before": "Before",
|
||||
|
||||
"Add {{type}} before \"{{title}}\"": "Add {{type}} before \"{{title}}\"",
|
||||
"Add {{type}} after \"{{title}}\"": "Add {{type}} after \"{{title}}\"",
|
||||
"Add {{type}} in \"{{title}}\"": "Add {{type}} in \"{{title}}\"",
|
||||
|
||||
"Original name": "Original name",
|
||||
"Custom name": "Custom name",
|
||||
"Custom Title": "Custom Title",
|
||||
|
||||
"Options": "Options",
|
||||
"Option value": "Option value",
|
||||
"Option label": "Option label",
|
||||
"Color": "Color",
|
||||
"Add option": "Add option",
|
||||
"Related collection": "Related collection",
|
||||
"Allow linking to multiple records": "Allow linking to multiple records",
|
||||
"Allow uploading multiple files": "Allow uploading multiple files",
|
||||
|
||||
"Configure calendar": "Configure calendar",
|
||||
"Title field": "Title field",
|
||||
"Start date field": "Start date field",
|
||||
"End date field": "End date field",
|
||||
"Navigate": "Navigate",
|
||||
"Title": "Title",
|
||||
"Select view": "View",
|
||||
"Reset": "Reset",
|
||||
"Export fields": "Export fields",
|
||||
"Saved successfully": "Saved successfully",
|
||||
"Templates": "Templates",
|
||||
"Nickname": "Nickname",
|
||||
"Sign in": "Sign in",
|
||||
"Create an account": "Create an account",
|
||||
"Sign up": "Sign up",
|
||||
"Confirm password": "Confirm password",
|
||||
"Log in with an existing account": "Log in with an existing account",
|
||||
"Password mismatch": "Password mismatch",
|
||||
"Signed up successfully. It will jump to the login page.": "Signed up successfully. It will jump to the login page.",
|
||||
|
||||
"Users": "Users",
|
||||
"Roles": "Roles",
|
||||
"Add role": "Add role",
|
||||
"Role name": "Role name",
|
||||
"Configure": "Configure",
|
||||
"Configure permissions": "Configure permissions",
|
||||
"Edit role": "Edit role",
|
||||
|
||||
"Action permissions": "Action permissions",
|
||||
"Menu permissions": "Menu permissions",
|
||||
|
||||
"Menu item name": "Menu item name",
|
||||
"Allow access": "Allow access",
|
||||
|
||||
"Action name": "Action name",
|
||||
"Allow action": "Allow action",
|
||||
"Action scope": "Action scope",
|
||||
"Operate on new data": "Operate on new data",
|
||||
"Operate on existing data": "Operate on existing data",
|
||||
|
||||
"Yes": "Yes",
|
||||
"No": "No",
|
||||
|
||||
"Red": "Red",
|
||||
"Magenta": "Magenta",
|
||||
"Volcano": "Volcano",
|
||||
"Orange": "Orange",
|
||||
"Gold": "Gold",
|
||||
"Lime": "Lime",
|
||||
"Green": "Green",
|
||||
"Cyan": "Cyan",
|
||||
"Blue": "Blue",
|
||||
"Geek Blue": "Geek Blue",
|
||||
"Purple": "Purple",
|
||||
"Default": "Default",
|
||||
"Add card": "Add card",
|
||||
"edit title": "Edit Title"
|
||||
}
|
280
packages/client/src/locale/zh_CN.json
Normal file
280
packages/client/src/locale/zh_CN.json
Normal file
@ -0,0 +1,280 @@
|
||||
{
|
||||
"Display <1><0>10</0><1>20</1><2>50</2><3>100</3></1> items per page": "每页显示 <1><0>10</0><1>20</1><2>50</2><3>100</3></1> 条",
|
||||
"Meet <1><0>All</0><1>Any</1></1> conditions in the group": "满足组内 <1><0>全部</0><1>任意</1></1> 条件",
|
||||
"Open in<1><0>Modal</0><1>Drawer</1><2>Window</2></1>": "在 <1><0>对话框</0><1>抽屉</1><2>窗口</2></1> 内打开",
|
||||
"{{count}} filter items": "{{count}} 个筛选项",
|
||||
"{{count}} more items": "还有 {{count}} 项",
|
||||
|
||||
"Today": "今天",
|
||||
"Month": "月",
|
||||
"Week": "周",
|
||||
"Work week": "工作日",
|
||||
"Day": "天",
|
||||
"Agenda": "列表",
|
||||
"Date": "日期",
|
||||
"Time": "时间",
|
||||
"Event": "事件",
|
||||
"None": "无",
|
||||
"System settings": "系统设置",
|
||||
"System title": "系统名称",
|
||||
"Logo": "Logo",
|
||||
"Add menu item": "添加菜单项",
|
||||
"Page": "页面",
|
||||
"Name": "名称",
|
||||
"Icon": "图标",
|
||||
"Group": "分组",
|
||||
"Link": "链接",
|
||||
"Edit menu item": "编辑菜单项",
|
||||
"Move to": "移动到",
|
||||
"Insert left": "在左边插入",
|
||||
"Insert right": "在右边插入",
|
||||
"Insert inner": "在里面插入",
|
||||
"Delete": "删除",
|
||||
"UI editor": "界面配置",
|
||||
"Collections & Fields": "数据表配置",
|
||||
"Roles & Permissions": "角色和权限",
|
||||
"Edit profile": "个人资料",
|
||||
"Change password": "修改密码",
|
||||
"Old password": "旧密码",
|
||||
"New password": "新密码",
|
||||
"Switch role": "切换角色",
|
||||
"Super admin": "超级管理员",
|
||||
"Language": "语言设置",
|
||||
"Allow sign up": "允许注册",
|
||||
"Sign out": "注销",
|
||||
"Cancel": "取消",
|
||||
"Submit": "提交",
|
||||
"Set the data scope": "设置数据范围",
|
||||
"Data blocks": "数据区块",
|
||||
"Table": "表格",
|
||||
"Form": "表单",
|
||||
"Select data source": "选择数据源",
|
||||
"Calendar": "日历",
|
||||
"Kanban": "看板",
|
||||
"Select group field": "选择分组字段",
|
||||
"Media": "多媒体",
|
||||
"Markdown": "Markdown",
|
||||
"Wysiwyg": "富文本",
|
||||
"Charts": "图表",
|
||||
"Column chart": "柱状图",
|
||||
"Bar chart": "条形图",
|
||||
"Line chart": "折线图",
|
||||
"Pie chart": "饼图",
|
||||
"Templates": "模板",
|
||||
"Select template": "选择模板",
|
||||
"Action logs": "操作日志",
|
||||
"Create template": "创建模板",
|
||||
"Edit markdown": "编辑 Markdown",
|
||||
"Add block": "创建区块",
|
||||
"Add new": "添加",
|
||||
"Add record": "添加数据",
|
||||
"Custom field display name": "自定义字段名称",
|
||||
"Display fields": "显示字段",
|
||||
"Edit record": "编辑数据",
|
||||
"Delete menu item": "删除菜单项",
|
||||
"Add page": "添加页面",
|
||||
"Add group": "添加分组",
|
||||
"Add link": "添加链接",
|
||||
"Insert above": "在上面插入",
|
||||
"Insert below": "在下面插入",
|
||||
"Save": "保存",
|
||||
"Delete block": "删除区块",
|
||||
"Are you sure you want to delete it?": "你确定要删除吗?",
|
||||
"This is a demo text, **supports Markdown syntax**.": "这是一段演示文本,**支持 Markdown 语法**。",
|
||||
|
||||
"Filter": "筛选",
|
||||
"Action type": "操作类型",
|
||||
"Actions": "操作",
|
||||
"Insert": "新增",
|
||||
"Update": "更新",
|
||||
"View": "查看",
|
||||
"View record": "查看数据",
|
||||
|
||||
"Data changes": "数据变更",
|
||||
"Field name": "字段标识",
|
||||
"Before change": "变更前",
|
||||
"After change": "变更后",
|
||||
|
||||
"Delete record": "删除数据",
|
||||
"Create collection": "创建数据表",
|
||||
"Collection display name": "数据表名称",
|
||||
"Collection name": "数据表标识",
|
||||
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.": "随机生成,可修改。支持英文、数字和下划线,必须以英文字母开头。",
|
||||
"Storage type": "存储类型",
|
||||
"Edit": "编辑",
|
||||
"Edit collection": "编辑数据表",
|
||||
"Configure fields": "配置字段",
|
||||
"Edit field": "编辑字段",
|
||||
"Configure fields of {{title}}": "「{{title}}」的字段配置",
|
||||
"Basic": "基本类型",
|
||||
"Single line text": "单行文本",
|
||||
"Long text": "多行文本",
|
||||
"Phone": "手机号码",
|
||||
"Email": "电子邮箱",
|
||||
"Number": "数字",
|
||||
"Percent": "百分比",
|
||||
"Password": "密码",
|
||||
"Choices": "选择类型",
|
||||
"Checkbox": "勾选",
|
||||
"Single select": "下拉菜单(单选)",
|
||||
"Multiple select": "下拉菜单(多选)",
|
||||
"Radio group": "单选框",
|
||||
"Checkbox group": "复选框",
|
||||
"China region": "中国行政区",
|
||||
"Attachment": "附件",
|
||||
"Date & Time": "日期 & 时间",
|
||||
"Datetime": "日期",
|
||||
"Relation": "关系类型",
|
||||
"Link to": "关联",
|
||||
"Sub-table": "子表格",
|
||||
"System info": "系统信息",
|
||||
"Created at": "创建日期",
|
||||
"Last updated at": "最后修改日期",
|
||||
"Created by": "创建人",
|
||||
"Last updated by": "最后更新日期",
|
||||
"Add field": "添加字段",
|
||||
"Field display name": "字段名称",
|
||||
"Field type": "字段类型",
|
||||
|
||||
"Date format": "日期格式",
|
||||
"Year/Month/Day": "年/月/日",
|
||||
"Year-Month-Day": "年-月-日",
|
||||
"Day/Month/Year": "日/月/年",
|
||||
"Show time": "显示时间",
|
||||
"Time format": "时间格式",
|
||||
"12 hour": "12 小时制",
|
||||
"24 hour": "24 小时制",
|
||||
|
||||
"Add filter": "添加筛选条件",
|
||||
"Add filter group": "添加筛选分组",
|
||||
"is": "等于",
|
||||
"is not": "不等于",
|
||||
"contains": "包含",
|
||||
"does not contain": "不包含",
|
||||
"is empty": "为空",
|
||||
"is not empty": "不为空",
|
||||
"Edit chart": "编辑图表",
|
||||
"Add text": "添加文本",
|
||||
"Filterable fields": "可筛选字段",
|
||||
"Edit button": "编辑按钮",
|
||||
"Hide": "隐藏",
|
||||
"Enable actions": "启用操作",
|
||||
"Export": "导出",
|
||||
"Customize": "自定义",
|
||||
"Function": "Function",
|
||||
"Popup form": "Popup form",
|
||||
"Flexible popup": "Flexible popup",
|
||||
"Configure actions": "配置操作",
|
||||
"Display order number": "显示序号",
|
||||
"Enable drag and drop sorting": "启用拖拽排序",
|
||||
"Triggered when the row is clicked": "点击表格行时触发",
|
||||
"Add tab": "添加标签页",
|
||||
"Disable tabs": "禁用标签页",
|
||||
"Details": "详情",
|
||||
"Edit tab": "编辑标签页",
|
||||
"Relationship blocks": "关系数据区块",
|
||||
"Select record": "选择数据",
|
||||
"Display name": "显示名称",
|
||||
"Select icon": "选择图标",
|
||||
"Custom column name": "自定义列名称",
|
||||
"Edit description": "编辑描述",
|
||||
"Required": "必填",
|
||||
"Label field": "标签字段",
|
||||
"Default is the ID field": "默认为 ID 字段",
|
||||
"Set default sorting rules": "设置排序规则",
|
||||
|
||||
"is before": "早于",
|
||||
"is after": "晚于",
|
||||
"is on or after": "不早于",
|
||||
"is on or before": "不晚于",
|
||||
|
||||
"Upload": "上传",
|
||||
|
||||
"Select level": "选择层级",
|
||||
"Province": "省",
|
||||
"City": "市",
|
||||
"Area": "区/县",
|
||||
"Street": "乡镇/街道",
|
||||
"Village": "村/居委会",
|
||||
"Must select to the last level": "必须选到最后一级",
|
||||
|
||||
"Move {{title}} to": "将 {{title}} 移动到",
|
||||
"Target position": "目标位置",
|
||||
"After": "之后",
|
||||
"Before": "之前",
|
||||
|
||||
"Add {{type}} before \"{{title}}\"": "在 \"{{title}}\" 前插入{{type}}",
|
||||
"Add {{type}} after \"{{title}}\"": "在 \"{{title}}\" 前插入{{type}}",
|
||||
"Add {{type}} in \"{{title}}\"": "在 \"{{title}}\" 里插入{{type}}",
|
||||
|
||||
"Original name": "原名称",
|
||||
"Custom name": "自定义名称",
|
||||
"Custom Title": "自定义标题",
|
||||
|
||||
"Options": "选项",
|
||||
"Option value": "选项值",
|
||||
"Option label": "选项",
|
||||
"Color": "颜色",
|
||||
"Add option": "添加选项",
|
||||
"Related collection": "关系表",
|
||||
"Allow linking to multiple records": "允许关联多条记录",
|
||||
"Allow uploading multiple files": "允许上传多个文件",
|
||||
|
||||
"Configure calendar": "配置日历",
|
||||
"Title field": "标题字段",
|
||||
"Start date field": "开始日期字段",
|
||||
"End date field": "结束日期字段",
|
||||
"Navigate": "分页",
|
||||
"Title": "标题",
|
||||
"Select view": "切换视图",
|
||||
"Reset": "重置",
|
||||
"Export fields": "导出字段",
|
||||
"Saved successfully": "保存成功",
|
||||
"Nickname": "昵称",
|
||||
"Sign in": "登录",
|
||||
"Create an account": "注册账号",
|
||||
"Sign up": "注册",
|
||||
"Confirm password": "确认密码",
|
||||
"Log in with an existing account": "使用已有账号登录",
|
||||
"Signed up successfully. It will jump to the login page.": "注册成功,将跳转登录页。",
|
||||
"Password mismatch": "确认密码不匹配",
|
||||
"Users": "用户",
|
||||
|
||||
"Roles": "角色",
|
||||
"Add role": "添加角色",
|
||||
"Role name": "角色名称",
|
||||
"Configure": "配置",
|
||||
"Configure permissions": "配置权限",
|
||||
"Edit role": "编辑角色",
|
||||
|
||||
"Action permissions": "数据表操作权限",
|
||||
"Menu permissions": "菜单访问权限",
|
||||
|
||||
"Menu item name": "菜单名称",
|
||||
"Allow access": "允许访问",
|
||||
|
||||
"Action name": "操作名称",
|
||||
"Allow action": "允许操作",
|
||||
"Action scope": "可操作数据范围",
|
||||
"Operate on new data": "对新增数据操作",
|
||||
"Operate on existing data": "对已有数据操作",
|
||||
|
||||
"Yes": "是",
|
||||
"No": "否",
|
||||
|
||||
"Red": "薄暮",
|
||||
"Magenta": "法式洋红",
|
||||
"Volcano": "火山",
|
||||
"Orange": "日暮",
|
||||
"Gold": "金盏花",
|
||||
"Lime": "青柠",
|
||||
"Green": "极光绿",
|
||||
"Cyan": "明青",
|
||||
"Blue": "拂晓蓝",
|
||||
"Geek Blue": "极客蓝",
|
||||
"Purple": "酱紫",
|
||||
"Default": "默认",
|
||||
|
||||
"Add card": "添加卡片",
|
||||
"edit title": "修改标题"
|
||||
}
|
8
packages/client/src/markdown/index.md
Normal file
8
packages/client/src/markdown/index.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# Markdown
|
19
packages/client/src/menu/index.md
Normal file
19
packages/client/src/menu/index.md
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# Menu
|
||||
|
||||
## Nodes
|
||||
|
||||
- Menu
|
||||
- Menu.ItemGroup
|
||||
- Menu.Item
|
||||
- Menu.SubMenu
|
||||
- Menu.Divider
|
||||
- Menu.URL
|
||||
- Menu.Link
|
||||
- Menu.Action
|
8
packages/client/src/password/index.md
Normal file
8
packages/client/src/password/index.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# Password
|
8
packages/client/src/radio/index.md
Normal file
8
packages/client/src/radio/index.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
nav:
|
||||
path: /client
|
||||
group:
|
||||
path: /schema-components
|
||||
---
|
||||
|
||||
# Radio
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user