This commit is contained in:
chenos 2021-07-12 22:13:48 +08:00
parent af02a895c3
commit 6f54ee3d4e
48 changed files with 1000 additions and 775 deletions

View File

@ -11,25 +11,92 @@ nav:
NocoBase 核心主要围绕三点:
- 数据的存储 —— 结构和关系
- 数据的行为 —— 操作和事件
- 数据的形态 —— 页面和区块
- 数据的结构
- 数据的行为
- 数据的形态
由此构建了
由此抽象了三类配置协议
- @nocobase/database:提供灵活且强大的数据库构造器
- @nocobase/resourcer:为数据提供资源操作方法
- @nocobase/blocks:为数据提供的 UI 模块
- Collection用于描述数据的结构和关系
- Resourcer用于描述数据资源和操作方法
- UI Schema用于描述用户界面组件树结构
更近一步
## Collection
- @nocobase/database@nocobase/resourcer 组装成了 @nocobase/server,也就是最核心的 NocoBase
- @nocobase/blocks 是前端最重要部分,提供了现代化的 Block-styled Editor/Renderer
基于 Sequelize ModelOptions
<img style="width:700px;" src="./nocobase-core.png"/>
```ts
{
name: 'posts',
fields: [
{type: 'string', name: 'title'},
{type: 'text', name: 'content'},
],
}
```
## 数据的存储 —— 结构和关系
## Resourcer
## 数据的行为 —— 操作和事件
基于资源resource和操作方法action设计将 REST 和 RPC 思想融合起来
## 数据的形态 —— 页面和区块
```ts
{
name: 'posts',
actions: {
list: {
filter: {}, // 过滤
fields: [], // 输出哪些字段
sort: '-created_at', // 排序
page: 1,
perPage: 20,
// ...
},
get: {
filter: {},
fields: [],
// ...
},
create: {
fields: [],
values: {},
// ...
},
update: {
fields: [],
values: {},
// ...
},
destroy: {
filter: {},
// ...
},
},
}
```
## UI Schema
基于 Formily Schema 2.0
```ts
{
type: 'object',
// 'x-component': 'Form',
properties: {
title: {
type: 'string',
title: '标题',
'x-component': 'Input',
},
content: {
type: 'string',
title: '标题',
'x-component': 'Input.TextArea',
},
},
}
```
更进一步,构建了整个 NocoBase 架构:
<img src="https://nocobase.oss-cn-beijing.aliyuncs.com/60ffd9aff98871c643569219c2873749.png" style="max-width: 800px;width:100%"/>

View File

@ -1,74 +0,0 @@
---
title: '@nocobase/blocks'
order: 6
---
# @nocobase/blocks <Badge>未实现</Badge>
<Alert title="注意" type="warning">
因为前端代码的打包还有许多细节问题,核心代码暂时也都放在 @nocobase/app 里了。
v0.4 版本 components 分为了 Action、Field、View 三部分,下一把版本打算都整合到 Block 里。
</Alert>
在 NocoBase 中,区块是所有 HTML 元素片段(包括 React/Vue 等框架的自定义元素)的总称。区块可以任意组合或嵌套,为了方便使用,内置了常用的一些区块,大致分类有:
- Database - 数据
- Fields - 字段
- Buttons - 按钮
- Media - 多媒体
- Design - 设计
- 自定义区块
## Database - 数据
数据类型的区块需要绑定数据源。
### table
### form
### descriptions
### calendar
### kanban
## Fields - 字段
字段是一种特殊的子区块,只用于数据类型区块中,有两种形态:静态纯展示和动态可输入。
### boolean
### checkbox
### cascader
### time
### markdown
### string
### icon
### textarea
### number
### remoteSelect
### drawerSelect
### colorSelect
### subTable
### icon
## Buttons - 按钮
### create
### update
### destroy
### filter
### print
### export
### button
## Media - 多媒体
### markdown
## Design - 设计
### grid
### drawer
### page
## 自定义区块

View File

@ -1,256 +0,0 @@
---
title: '@nocobase/client'
order: 6
---
# @nocobase/client <Badge>未实现</Badge>
## 介绍
提供适配 Ant Design 组件的 NocoBase 客户端。
<Alert title="注意">
@nocobase/client 可以用于任意 React 框架中,不过还存在许多难点和细节未解决。
</Alert>
## Loaders
### TemplateLoader <Badge>待完善</Badge>
- routes路由表大杂烩。类型包括layout、page、redirect、url、menuGroup**页面就是 type=page 的 route**
- templates模板
- pathnameURL 路径
为了适应无代码需求,提供了一种 URL 和 Template 映射规则,如以下例子:
```ts
const routes = [
{
type: 'layout',
name: 'auth', // 因为 /login、/register 没有统一的前缀,所以这里没有配置 path
template: 'AuthLayout', // 用于处理 login、register 等页面的布局
children: [
{
type: 'page',
name: 'login',
path: '/login',
template: 'BlockLoader', // 用于解析 blocks 配置参数
blocks: [],
},
{
type: 'page',
name: 'register',
path: '/register',
template: 'BlockLoader',
blocks: [],
}
]
},
{
type: 'layout',
name: 'admin',
path: '/admin', // /admin 下的任意 uri 都转到这里
template: 'AdminLoader',
redirect: '/admin/welcome',
// admin layout 提供了 top/left 布局的菜单,菜单由 children 组成
// 通过解析 /admin/:name找到对应子页面
// 「菜单和页面配置」就是这部分的内容
children: [
{
type: 'page',
name: 'welcome',
template: 'BlockLoader',
title: '欢迎',
blocks: [],
},
{
type: 'url',
url: 'https://www.nocobase.com/',
title: 'xxx',
},
{
type: 'menuGroup',
title: 'xxx',
},
],
},
{
// 配置跳转
type: 'redirect',
path: '/',
redirect: '/admin',
},
];
```
<pre lang="tsx">
<TemplateLoader
pathname={pathname}
routes={routes}
templates={{
AuthLayout,
BlockLoader,
AdminLoader,
}}
/>
</pre>
### BlockLoader <Badge type="error">未实现</Badge>
区块驱动器。
### AdminLoader <Badge>待完善</Badge>
一种 top/left 菜单结构的 Admin 布局。菜单由其对应的 children 组成,通过 `/admin/:name` 映射到对应子页面,「菜单和页面配置」就是这部分的内容。
### ShareLoader <Badge type="error">未实现</Badge>
分享模块,细节待补充
## Blocks
将页面内部的各个块元素进行提炼,抽象了 block区块的概念。
### Grid - 布局
```ts
{
type: 'grid',
span: 12,
blocks: [
{
col: 1,
order: 1,
},
{
col: 2,
order: 1,
},
{
col: 1,
order: 2,
},
],
}
```
### Descriptions - 详情
```ts
{
type: 'descriptions',
fields: [],
actions: [],
}
```
### Form - 表单
```ts
{
type: 'form',
fields: [],
// 表单提交反馈信息,细节待定
returnType,
redirect,
message,
}
```
### Table - 表单
```ts
{
type: 'table',
defaultPerPage: 20,
draggable: false,
filter: {},
sort: [],
detailsOpenMode: 'drawer',
actions: [],
fields: [],
details: [],
labelField,
}
```
### Calendar - 日历
```ts
{
type: 'calendar',
filter: {},
detailsOpenMode: 'drawer',
actions: [],
details: [],
labelField,
}
```
### Kanban - 看板
```ts
{
type: 'kanban',
groupField,
labelField,
fields,
filter,
actions,
detailsOpenMode,
details,
}
```
### Markdown
```ts
{
type: 'markdown',
content: '',
}
```
## Actions
操作按钮
### create - 新增
### update - 编辑
### destroy - 删除
### filter - 筛选
### print - 打印
### export - 导出
## Fields
字段控件
### boolean
### cascader
### checkbox
### checkboxes
### colorSelect
### date
### drawerSelect
### filter
### icon
### markdown
### number
### password
### percent
### radio
### remoteSelect
### string
### select
### subTable
### textarea
### time
### upload

View File

@ -13,7 +13,7 @@ NocoBase 是一个开源免费的无代码、低代码开发平台。 无论是
## 架构
<img src="./nocobase.png" style="max-width: 800px;"/>
<img src="https://nocobase.oss-cn-beijing.aliyuncs.com/60ffd9aff98871c643569219c2873749.png" style="max-width: 800px;width:100%"/>
### 微内核
@ -41,11 +41,3 @@ NocoBase 采用微内核架构,框架只保留核心的概念,具体各类
- 直接写在代码里,多用于处理动态配置
- 保存在文件里,多用于系统表配置或纯开发配置
- 保存在数据表里,多用于业务表配置
## 核心点在哪里?
NocoBase 本质上是对数据的信息化处理,包括三点核心:
- 数据的存储 —— 结构和关系
- 数据的行为 —— 方法和事件
- 数据的形态 —— 页面和区块

6
nodemon.json Normal file
View File

@ -0,0 +1,6 @@
{
"watch": ["packages/", ".env"],
"ignore": ["packages/app"],
"ext": "ts",
"exec": "ts-node -r dotenv/config ./packages/api/src/index.ts"
}

View File

@ -7,6 +7,7 @@
"scripts": {
"bootstrap": "lerna bootstrap",
"clean": "lerna clean",
"start-server": "nodemon",
"start-docs": "dumi dev",
"build-docs": "dumi build",
"build2": "lerna run build",
@ -44,6 +45,7 @@
"koa-bodyparser": "^4.3.0",
"lerna": "^3.22.0",
"mockjs": "^1.1.0",
"nodemon": "^2.0.12",
"pg": "^8.6.0",
"pg-hstore": "^2.3.3",
"prettier": "^2.3.0",

View File

@ -19,7 +19,7 @@
"@nocobase/plugin-file-manager": "^0.4.0-alpha.7",
"@nocobase/plugin-pages": "^0.4.0-alpha.7",
"@nocobase/plugin-permissions": "^0.4.0-alpha.7",
"@nocobase/plugin-routes": "^0.4.0-alpha.7",
"@nocobase/plugin-ui-router": "^0.4.0-alpha.7",
"@nocobase/plugin-ui-schema": "^0.4.0-alpha.7",
"@nocobase/plugin-users": "^0.4.0-alpha.7",
"@nocobase/server": "^0.4.0-alpha.7",

View File

@ -39,7 +39,7 @@ const api = Api.create({
const plugins = [
'@nocobase/plugin-collections',
'@nocobase/plugin-routes',
'@nocobase/plugin-ui-router',
'@nocobase/plugin-ui-schema',
// '@nocobase/plugin-action-logs',
// '@nocobase/plugin-pages',

View File

@ -27,25 +27,27 @@ import * as uiSchema from './ui-schema';
exact: true,
},
{
type: 'route',
path: '/admin/:name(.+)?',
component: 'AdminLayout',
title: `后台`,
uiSchema: uiSchema.menu,
},
{
type: 'route',
component: 'AuthLayout',
children: [
{
name: 'login',
type: 'route',
path: '/login',
component: 'DefaultPage',
component: 'RouteSchemaRenderer',
title: `登录`,
uiSchema: uiSchema.login,
},
{
name: 'register',
type: 'route',
path: '/register',
component: 'DefaultPage',
component: 'RouteSchemaRenderer',
title: `注册`,
uiSchema: uiSchema.register,
},

View File

@ -2,7 +2,6 @@ import { ISchema } from '@formily/react';
export const login: ISchema = {
key: 'dtf9j0b8p9u',
name: 'dtf9j0b8p9u',
type: 'object',
properties: {
email: {

View File

@ -1,6 +1,5 @@
export const register = {
key: '46qlxqam3xk',
name: '46qlxqam3xk',
type: 'object',
properties: {
username: {

View File

@ -1,127 +0,0 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { MemoryRouter as Router, useParams, Link } from 'react-router-dom';
import { RouteSwitch } from '../';
const components = {
Index: () => <div>index</div>,
Home: ({ children }) => (
<div>
<h1>Home</h1>
{children}
</div>
),
Blog: ({ children }) => (
<div>
<h1>Blog</h1>
{children}
</div>
),
BlogPost: () => {
let { slug } = useParams<any>();
return <div>Now showing post {slug}</div>;
},
Login: () => <div>login</div>,
Register: () => <div>register</div>,
};
const routes = [
{
type: 'redirect',
from: '/blog/123',
to: '/blog/1234',
},
// {
// component: 'Home',
// routes: [
// {
// path: '/blog/123555',
// component: () => <div>/blog/123555</div>,
// },
// ],
// },
{
path: '/blog',
component: 'Blog',
routes: [
{
path: '/blog/:slug',
// exact: true,
component: 'BlogPost',
},
],
},
{
component: 'Home',
routes: [
// {
// path: '/blog/123555',
// component: () => <div>/blog/123555</div>,
// },
{
path: '/login',
component: 'Login',
},
{
path: '/register',
component: 'Register',
},
{
path: '/',
// exact: true,
component: 'Index',
},
],
},
];
it('route component', () => {
const t = renderer
.create(
<Router initialEntries={['/']}>
<RouteSwitch
routes={[
{
path: '/',
component: () => <div>test</div>,
},
]}
/>
</Router>,
)
.toJSON();
expect(t).toMatchSnapshot();
});
it('pathname=/', () => {
const t = renderer
.create(
<Router initialEntries={['/']}>
<RouteSwitch routes={routes} components={components} />
</Router>,
)
.toJSON();
expect(t).toMatchSnapshot();
});
it('pathname=/login', () => {
const t = renderer
.create(
<Router initialEntries={['/login']}>
<RouteSwitch routes={routes} components={components} />
</Router>,
)
.toJSON();
expect(t).toMatchSnapshot();
});
it('pathname=/blog/123', () => {
const t = renderer
.create(
<Router initialEntries={['/blog/123']}>
<RouteSwitch routes={routes} components={components} />
</Router>,
)
.toJSON();
expect(t).toMatchSnapshot();
});

View File

@ -1,41 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`pathname=/ 1`] = `
<div>
<h1>
Home
</h1>
<div>
index
</div>
</div>
`;
exports[`pathname=/blog/123 1`] = `
<div>
<h1>
Blog
</h1>
<div>
Now showing post
1234
</div>
</div>
`;
exports[`pathname=/login 1`] = `
<div>
<h1>
Home
</h1>
<div>
login
</div>
</div>
`;
exports[`route component 1`] = `
<div>
test
</div>
`;

View File

@ -1,73 +0,0 @@
import React from 'react';
import {
Link,
useLocation,
useRouteMatch,
MemoryRouter as Router,
} from 'react-router-dom';
import {
RouteSwitch,
AuthLayout,
AdminLayout,
PageTemplate,
} from '@nocobase/client';
const routes = [
{
path: '/admin/:slug(.+)?',
component: 'AdminLayout',
},
{
component: 'AuthLayout',
routes: [
{
name: 'login',
path: '/login',
component: 'PageTemplate',
title: '登录',
},
{
name: 'register',
path: '/register',
component: 'PageTemplate',
title: '注册',
},
],
},
{
type: 'redirect',
from: '/',
to: '/admin',
exact: true,
},
];
const components = {
AuthLayout,
AdminLayout,
PageTemplate,
};
function App() {
const location = useLocation();
return (
<div>
<div>{location.pathname}</div>
<ul>
<li><Link to={'/login'}>path=/login</Link></li>
<li><Link to={'/register'}>path=/register</Link></li>
<li><Link to={'/'}>path=/</Link></li>
<li><Link to={'/admin/welcome'}>path=/admin/welcome</Link></li>
</ul>
<RouteSwitch routes={routes} components={components} />
</div>
);
}
export default () => {
return (
<Router initialEntries={['/login']}>
<App />
</Router>
);
};

View File

@ -1,16 +0,0 @@
---
title: RouteSwitch - 路由转换器
nav:
title: 组件
path: /client
group:
order: 3
title: 其他
path: /client/others
---
# RouteSwitch - 路由转换器
## 代码演示
<code src="./demos/demo1.tsx"/>

View File

@ -1,64 +0,0 @@
import React, { createContext, useContext } from 'react';
import { Switch, Route as ReactRoute, Redirect } from 'react-router-dom';
export const RouteComponentContext = createContext({});
export interface RouteSwitchProps {
routes?: any[];
components?: any;
}
export function RouteSwitch(props: RouteSwitchProps) {
const { routes } = props;
if (!Array.isArray(routes)) {
return null;
}
const components = props.components || useContext(RouteComponentContext);
return (
<RouteComponentContext.Provider value={components}>
<Switch>
{routes.map((route, key) => {
if (route.type === 'redirect') {
return (
<Redirect
key={key}
exact={route.exact}
from={route.from}
to={route.to}
/>
);
}
if (route.component) {
let path = route.path;
if (!path && Array.isArray(route.routes)) {
path = route.routes.map((r) => r.path);
}
return (
<ReactRoute
key={key}
path={path}
exact={route.exact}
component={(routeProps) => {
const Component =
typeof route.component === 'string'
? components[route.component]
: route.component;
if (!Component) {
return null;
}
return (
<Component {...routeProps} route={route}>
<RouteSwitch routes={route.routes} />
</Component>
);
}}
/>
);
}
})}
</Switch>
</RouteComponentContext.Provider>
);
}
export default RouteSwitch;

View File

@ -0,0 +1,14 @@
---
title: AdminLayout - 后台布局
nav:
title: 组件
path: /client
group:
order: 2
title: Templates
path: /client/templates
---
# AdminLayout - 后台布局
内置的后台布局模板,提供了基础的菜单和路由切换。

View File

@ -0,0 +1,92 @@
import React, { useContext, useEffect, useRef, useState } from 'react';
import {
Button,
Spin,
Layout,
PageHeader,
Modal,
Menu,
Collapse,
Dropdown,
} from 'antd';
import isEmpty from 'lodash/isEmpty';
import {
Link,
useLocation,
useRouteMatch,
useHistory,
Redirect,
} from 'react-router-dom';
import { SchemaRenderer } from '../../schemas';
import { useRequest } from 'ahooks';
import './style.less';
function LayoutWithMenu({ schema }) {
const match = useRouteMatch<any>();
const location = useLocation();
const sideMenuRef = useRef();
const [activeKey, setActiveKey] = useState(match.params.name);
const onSelect = (info) => {
console.log('LayoutWithMenu', schema);
setActiveKey(info.key);
};
console.log({ match });
return (
<Layout>
<Layout.Header>
<SchemaRenderer
schema={schema}
scope={{
sideMenuRef,
onSelect,
selectedKeys: [activeKey].filter(Boolean),
}}
/>
</Layout.Header>
<Layout>
<Layout.Sider
ref={sideMenuRef}
theme={'light'}
width={200}
></Layout.Sider>
<Layout.Content>
{activeKey && <Content activeKey={activeKey} />}
</Layout.Content>
</Layout>
</Layout>
);
}
function Content({ activeKey }) {
const { data = {}, loading } = useRequest(
`ui_schemas:getTree/${activeKey}?filter[parentId]=${activeKey}`,
{
refreshDeps: [activeKey],
formatResult: (result) => result?.data,
},
);
if (loading) {
return <Spin />;
}
return <SchemaRenderer schema={data} />;
}
export function AdminLayout({ route }: any) {
const { data = {}, loading } = useRequest(
`ui_schemas:getTree/${route.uiSchemaKey}`,
{
refreshDeps: [route],
formatResult: (result) => result?.data,
},
);
if (loading) {
return <Spin />;
}
return <LayoutWithMenu schema={data} />;
}
export default AdminLayout;

View File

@ -0,0 +1,49 @@
// .fields-collapse {
// border: 1px solid #f0f0f0;
// border-bottom: 0;
// > .ant-collapse-item {
// border-bottom: 1px solid #f0f0f0;
// }
// .ant-collapse-content {
// &.ant-collapse-content-active {
// border-top: 1px solid #f0f0f0;
// }
// }
// > .ant-collapse-item > .ant-collapse-content {
// background: #fff;
// }
// > .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box {
// padding: 16px;
// padding-bottom: 1px;
// }
// }
.ant-collapse {
border: 1px solid #d9d9d9 !important;
border-bottom: 0 !important;
> .ant-collapse-item {
border-bottom: 1px solid #d9d9d9 !important;
}
.ant-collapse-content {
border-top: 1px solid #d9d9d9 !important;
}
.ant-collapse-content > .ant-collapse-content-box {
padding: 24px !important;
padding-bottom: 0 !important;
}
}
.database-sider {
background: #fafafa;
padding-top: 16px;
.ant-menu {
background: #fafafa;
border-right: 0;
}
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected {
background: #fff;
&::after {
display: none;
}
}
}

View File

@ -0,0 +1,14 @@
---
title: AuthLayout - 登录布局
nav:
title: 组件
path: /client
group:
order: 2
title: Templates
path: /client/templates
---
# AuthLayout - 登录布局
内置的登录、注册页布局

View File

@ -0,0 +1,15 @@
import React from 'react';
import { useLocation, useHistory } from 'react-router-dom';
export function AuthLayout({ children, route }: any) {
const location = useLocation();
const history = useHistory();
return (
<div style={{ maxWidth: 320, margin: '0 auto' }}>
<h1>NocoBase</h1>
{children}
</div>
);
}
export default AuthLayout;

View File

@ -0,0 +1,25 @@
import React, { useContext, useEffect, useState } from 'react';
import { Spin } from 'antd';
import { Helmet } from 'react-helmet';
import { useRequest } from 'ahooks';
import { SchemaRenderer } from '../../schemas';
export function RouteSchemaRenderer({ route }) {
const { data = {}, loading } = useRequest(
`ui_schemas:getTree/${route.uiSchemaKey}`,
{
refreshDeps: [route],
formatResult: (result) => result?.data,
},
);
if (loading) {
return <Spin />;
}
return (
<div>
<SchemaRenderer schema={data} />
</div>
);
}
export default RouteSchemaRenderer;

View File

@ -0,0 +1,49 @@
import React, { useMemo } from 'react';
import {
Link,
useLocation,
useRouteMatch,
MemoryRouter as Router,
} from 'react-router-dom';
import { createRouteSwitch, RouteRedirectProps } from '..';
const RouteSwitch = createRouteSwitch({
components: {
Home: (props) => {
console.log({ props });
return <div>Home {props.children}</div>
},
Login: () => <div>Login</div>,
},
});
export default () => {
const routes: Array<RouteRedirectProps> = [
{
type: 'route',
path: '/login',
exact: true,
component: 'Login',
},
{
type: 'route',
path: '/home',
component: 'Home',
routes: [
{
type: 'route',
path: '/home/123',
exact: true,
component: 'Login',
},
],
},
];
return (
<div>
<Router initialEntries={['/home/123']}>
<RouteSwitch routes={routes} />
</Router>
</div>
);
};

View File

@ -0,0 +1,65 @@
import React, { useMemo } from 'react';
import {
Link,
useLocation,
useRouteMatch,
MemoryRouter as Router,
} from 'react-router-dom';
import { createRouteSwitch, RouteRedirectProps } from '..';
import { AdminLayout } from '../../admin-layout';
import { AuthLayout } from '../../auth-layout';
import { RouteSchemaRenderer } from '../../route-schema-renderer';
const RouteSwitch = createRouteSwitch({
components: {
AdminLayout,
AuthLayout,
RouteSchemaRenderer,
},
});
const routes: Array<RouteRedirectProps> = [
{
type: 'redirect',
from: '/',
to: '/admin',
exact: true,
},
{
type: 'route',
path: '/admin/:name(.+)?',
component: 'AdminLayout',
title: `后台`,
uiSchemaKey: 'qqzzjakwkwl',
},
{
type: 'route',
component: 'AuthLayout',
children: [
{
type: 'route',
path: '/login',
component: 'RouteSchemaRenderer',
title: `登录`,
uiSchemaKey: 'dtf9j0b8p9u',
},
{
type: 'route',
path: '/register',
component: 'RouteSchemaRenderer',
title: `注册`,
uiSchemaKey: '46qlxqam3xk',
},
],
},
];
export default () => {
return (
<div>
<Router initialEntries={['/']}>
<RouteSwitch routes={routes} />
</Router>
</div>
);
};

View File

@ -0,0 +1,3 @@
<code src="./demos/demo1.tsx"/>
<code src="./demos/demo2.tsx"/>

View File

@ -0,0 +1,105 @@
import { get } from 'lodash';
import React from 'react';
import { useContext } from 'react';
import { createContext } from 'react';
import { Switch, Route, Redirect } from 'react-router';
export interface RedirectProps {
type: 'redirect';
to: any;
path?: string;
exact?: boolean;
strict?: boolean;
push?: boolean;
from?: string;
[key: string]: any;
}
export interface RouteProps {
type: 'route';
path?: string | string[];
exact?: boolean;
strict?: boolean;
sensitive?: boolean;
component?: any;
routes?: RouteProps[],
[key: string]: any;
}
export type RouteRedirectProps = RedirectProps | RouteProps;
export interface RouteSwitchOptions {
routes?: RouteRedirectProps[];
components?: any;
}
export interface RouteSwitchProps {
routes?: RouteRedirectProps[];
components?: any;
}
export const RouteComponentsContext = createContext(null);
export function useComponent(route: RouteProps) {
const components = useContext(RouteComponentsContext);
if (typeof route.component === 'string') {
const component = get(components, route.component);
return component;
}
return route.component || (() => null);
}
export function createRouteSwitch(options: RouteSwitchOptions) {
function ComponentRenderer(props) {
const Component = useComponent(props.route);
return (
<Component {...props}>
<RouteSwitch routes={props.route.routes} />
</Component>
);
}
function RouteSwitch(props: RouteSwitchProps) {
const { routes = [] } = props;
if (!routes.length) {
return null;
}
return (
<RouteComponentsContext.Provider
value={{ ...options.components, ...props.components }}
>
<Switch>
{routes.map((route) => {
if (route.type == 'redirect') {
return (
<Redirect
to={route.to}
push={route.push}
from={route.from}
path={route.path}
exact={route.exact}
strict={route.strict}
/>
);
}
if (!route.path && Array.isArray(route.routes)) {
route.path = route.routes.map((r) => r.path) as any;
}
return (
<Route
path={route.path}
exact={route.exact}
strict={route.strict}
sensitive={route.sensitive}
render={(props) => {
return <ComponentRenderer {...props} route={route} />;
}}
/>
);
})}
</Switch>
</RouteComponentsContext.Provider>
);
}
return RouteSwitch;
}

View File

@ -1,12 +1,4 @@
import Mock from 'mockjs';
export default [
{
type: 'redirect',
from: '/admin',
to: '/admin/item2',
exact: true,
},
{
type: 'redirect',
from: '/',
@ -14,28 +6,30 @@ export default [
exact: true,
},
{
type: 'route',
path: '/admin/:name(.+)?',
component: 'AdminLayout',
title: `后台 - ${Mock.mock('@string')}`,
schemaName: 'menu',
title: `后台`,
uiSchemaKey: 'qqzzjakwkwl',
},
{
type: 'route',
component: 'AuthLayout',
routes: [
children: [
{
name: 'login',
type: 'route',
path: '/login',
component: 'DefaultPage',
title: `登录 - ${Mock.mock('@string')}`,
schemaName: 'login',
component: 'RouteSchemaRenderer',
title: `登录`,
uiSchemaKey: 'dtf9j0b8p9u',
},
{
name: 'register',
type: 'route',
path: '/register',
component: 'DefaultPage',
title: `注册 - ${Mock.mock('@string')}`,
schemaName: 'register',
component: 'RouteSchemaRenderer',
title: `注册`,
uiSchemaKey: '46qlxqam3xk',
},
],
},
]
];

View File

@ -1,43 +1,51 @@
import React, { useEffect } from 'react';
import { useRequest } from 'ahooks';
import { Spin } from 'antd';
import React, { useMemo } from 'react';
import {
RouteSwitch,
useGlobalAction,
AdminLayout,
AuthLayout,
DefaultPage,
} from '@nocobase/client';
import {
Link,
useHistory,
useLocation,
useRouteMatch,
MemoryRouter as Router,
} from 'react-router-dom';
import { UseRequestProvider } from 'ahooks';
import { request } from './api';
const templates = {
import {
createRouteSwitch,
RouteRedirectProps,
AdminLayout,
AuthLayout,
DefaultPage,
};
RouteSchemaRenderer,
} from '../';
import { UseRequestProvider } from 'ahooks';
import { extend } from 'umi-request';
const request = extend({
prefix: 'http://localhost:23003/api/',
timeout: 1000,
});
const RouteSwitch = createRouteSwitch({
components: {
AdminLayout,
AuthLayout,
RouteSchemaRenderer,
},
});
const App = () => {
const { data, loading } = useRequest('routes:getAccessible', {
formatResult: (result) => result?.data,
});
function App() {
const { data, loading } = useGlobalAction('routes:getAccessible');
if (loading) {
return <Spin />;
return <Spin/>
}
return (
<div>
<Router initialEntries={['/login']}>
<RouteSwitch components={templates} routes={data} />
<RouteSwitch routes={data} />
</Router>
</div>
);
}
};
export default function IndexPage() {
export default () => {
return (
<UseRequestProvider
value={{

View File

@ -1,6 +1,5 @@
export * from './state';
// export * from './blocks';
export * from './templates';
// export * from './fields';
export * from './components/RouteSwitch';
export { Drawer } from './components/Drawer';
export * from './components/router-config';
export * from './components/admin-layout';
export * from './components/auth-layout';
export * from './components/route-schema-renderer';
export * from './schemas';

View File

@ -207,6 +207,9 @@ export function useDesignable(path?: any) {
if (!property.name) {
property.name = uid();
}
if (target['key']) {
property['parentKey'] = target['key'];
}
const properties = {};
properties[property.name] = property;
Object.keys(target.properties).forEach((name, index) => {
@ -231,6 +234,9 @@ export function useDesignable(path?: any) {
if (!property.name) {
property.name = uid();
}
if (target['key']) {
property['parentKey'] = target['key'];
}
target.addProperty(property.name, property);
// BUG: 空 properties 时addProperty 无反应。
const tmp = { name: uid() };
@ -251,6 +257,9 @@ export function useDesignable(path?: any) {
if (!property.name) {
property.name = uid();
}
if (target['parentKey']) {
property['parentKey'] = target['parentKey'];
}
addPropertyAfter(target, property);
refresh();
return target.parent.properties[property.name];
@ -267,6 +276,9 @@ export function useDesignable(path?: any) {
if (!property.name) {
property.name = uid();
}
if (target['parentKey']) {
property['parentKey'] = target['parentKey'];
}
addPropertyBefore(target, property);
refresh();
return target.parent.properties[property.name];

View File

@ -412,6 +412,68 @@ export default () => {
}
```
### 设计器模式 - 菜单项为空时
```tsx
import React, { useRef, useState } from 'react';
import { SchemaRenderer } from '../';
import { MenuContainerContext } from './';
import { Layout } from 'antd';
export default () => {
const sideMenuRef = useRef();
const [activeKey, setActiveKey] = useState('item3');
const onSelect = (info) => {
setActiveKey(info.key);
console.log({ info })
}
const schema = {
type: 'object',
properties: {
menu1: {
type: 'void',
'x-component': 'Menu',
'x-designable-bar': 'Menu.DesignableBar',
'x-component-props': {
defaultSelectedKeys: [activeKey],
mode: 'mix',
theme: 'dark',
sideMenuRef: '{{ sideMenuRef }}',
onSelect: '{{ onSelect }}',
},
},
},
}
return (
<div>
<Layout>
<Layout.Header>
<SchemaRenderer
schema={schema}
scope={{ onSelect, sideMenuRef }}
/>
</Layout.Header>
<Layout>
<Layout.Sider
ref={sideMenuRef}
theme={'light'}
width={200}
>
</Layout.Sider>
<Layout.Content>
{activeKey}
</Layout.Content>
</Layout>
</Layout>
</div>
)
}
```
### Menu.Action

View File

@ -94,22 +94,30 @@ function useDesignableBar() {
}
export const Menu: MenuType = observer((props: any) => {
const { sideMenuRef, onSelect, mode, defaultSelectedKeys, ...others } = props;
const {
children,
sideMenuRef,
onSelect,
mode,
defaultSelectedKeys,
...others
} = props;
let defaultSelectedKey = defaultSelectedKeys ? defaultSelectedKeys[0] : null;
const schema = useFieldSchema();
const { schema: designableSchema, refresh } = useDesignable();
// const schema = useFieldSchema();
const { schema, schema: designableSchema, refresh } = useDesignable();
const designableBar = schema['x-designable-bar'];
const history = useHistory();
const renderSideMenu = (selectedKey) => {
if (!selectedKey) {
return;
}
if ((mode as any) !== 'mix') {
return;
}
if (!sideMenuRef || !sideMenuRef.current) {
return;
}
if (!selectedKey || !schema.properties) {
sideMenuRef.current.style.display = 'none';
return;
}
const subSchema = schema.properties[selectedKey];
if (!subSchema) {
sideMenuRef.current.style.display = 'none';
@ -160,6 +168,7 @@ export const Menu: MenuType = observer((props: any) => {
...newProps,
[`${uid()}-add-new`]: {
type: 'void',
parentKey: subSchema['key'],
'x-component': 'Menu.AddNew',
},
},
@ -173,6 +182,8 @@ export const Menu: MenuType = observer((props: any) => {
console.log({ defaultSelectedKey }, schema.properties);
renderSideMenu(defaultSelectedKey);
});
const isEmpty = !Object.keys(designableSchema.properties || {}).length;
console.log({ designableSchema })
return (
<MenuContext.Provider
value={{
@ -189,13 +200,44 @@ export const Menu: MenuType = observer((props: any) => {
onSelect && onSelect(info);
}}
mode={(mode as any) === 'mix' ? 'horizontal' : mode}
/>
>
{!isEmpty ? (
<RecursionField schema={schema} onlyRenderProperties />
) : (
<SchemaRenderer
onRefresh={(subSchema: Schema) => {
Object.keys(subSchema.properties).forEach((name) => {
if (name === 'add-new') {
return;
}
designableSchema.addProperty(
name,
subSchema.properties[name].toJSON(),
);
});
refresh();
}}
schema={{
type: 'object',
properties: {
'add-new': {
parentKey: schema['key'],
type: 'void',
'x-component': 'Menu.AddNew',
},
},
}}
/>
)}
</AntdMenu>
</MenuContext.Provider>
);
});
const AddNewAction = () => {
const { insertBefore } = useDesignable();
const fieldSchema = useFieldSchema();
const { schema, insertBefore } = useDesignable();
console.log('AddNewAction', schema, fieldSchema);
return (
<Dropdown
overlayStyle={{
@ -206,11 +248,13 @@ const AddNewAction = () => {
<AntdMenu>
<AntdMenu.Item
onClick={() => {
insertBefore({
const s = insertBefore({
type: 'void',
title: uid(),
key: uid(),
'x-component': 'Menu.Item',
});
console.log('s.s.s.s.s.s', s)
}}
style={{ minWidth: 150 }}
>
@ -375,7 +419,7 @@ Menu.DesignableBar = (props) => {
const field = useField();
const fieldSchema = useFieldSchema();
const [visible, setVisible] = useState(false);
const { schema, remove, refresh } = useDesignable();
const { schema, remove, refresh, insertAfter } = useDesignable();
return (
<div className={cls('designable-bar', { active: visible })}>
<div
@ -414,6 +458,14 @@ Menu.DesignableBar = (props) => {
>
</AntdMenu.Item>
<AntdMenu.Item onClick={() => {
const s = insertAfter({
type: 'void',
key: uid(),
title: uid(),
'x-component': 'Menu.SubMenu',
});
}}></AntdMenu.Item>
<AntdMenu.Item
onClick={() => {
Modal.confirm({

View File

@ -1,6 +0,0 @@
import _ from 'lodash';
import { Model } from '@nocobase/database';
export class Route extends Model {
}

View File

@ -1,5 +1,5 @@
{
"name": "@nocobase/plugin-routes",
"name": "@nocobase/plugin-ui-router",
"version": "0.4.0-alpha.7",
"main": "lib/index.js",
"license": "MIT",
@ -7,7 +7,8 @@
"@nocobase/database": "^0.4.0-alpha.7",
"@nocobase/resourcer": "^0.4.0-alpha.7",
"@nocobase/server": "^0.4.0-alpha.7",
"deepmerge": "^4.2.2"
"deepmerge": "^4.2.2",
"flat-to-nested": "^1.1.1"
},
"devDependencies": {
"@nocobase/actions": "^0.4.0-alpha.7"

View File

@ -15,20 +15,20 @@ describe('routes', () => {
afterEach(() => app.database.close());
it.only('create route', async () => {
const Route = db.getModel('routes');
const item = {
path: '/admin/:name(.+)?',
component: 'AdminLayout',
title: `后台`,
uiSchema: {
name: 'menu',
},
};
console.log(Route.associations);
const route = await Route.create(item);
await route.updateAssociations(item);
});
// it.only('create route', async () => {
// const Route = db.getModel('routes');
// const item = {
// path: '/admin/:name(.+)?',
// component: 'AdminLayout',
// title: `后台`,
// uiSchema: {
// name: 'menu',
// },
// };
// console.log(Route.associations);
// const route = await Route.create(item);
// await route.updateAssociations(item);
// });
it('create route', async () => {
const Route = db.getModel('routes');

View File

@ -0,0 +1,18 @@
import { Model, ModelCtor } from '@nocobase/database';
import { actions } from '@nocobase/actions';
import FlatToNested from 'flat-to-nested';
const flatToNested = new FlatToNested({
id: 'key',
parent: 'parentKey',
children: 'routes',
});
export default async (ctx: actions.Context, next: actions.Next) => {
const { resourceKey } = ctx.action.params;
const Route = ctx.db.getModel('routes');
const routes = await Route.findAll();
const data = flatToNested.convert(routes.map(route => route.toProps()));
ctx.body = data.routes;
await next();
}

View File

@ -3,6 +3,7 @@ import { TableOptions } from '@nocobase/database';
export default {
name: 'routes',
title: '路由表',
model: 'Route',
fields: [
{
type: 'uid',

View File

@ -0,0 +1,33 @@
import _ from 'lodash';
import { Model } from '@nocobase/database';
export class Route extends Model {
static async create(value?: any, options?: any): Promise<any> {
// console.log({ value });
const attributes = this.toAttributes(value);
// @ts-ignore
const model: Model = await super.create(attributes, options);
return model;
}
static toAttributes(value = {}): any {
const data = _.cloneDeep(value);
const keys = [
...Object.keys(this.rawAttributes),
...Object.keys(this.associations),
];
const attrs = _.pick(data, keys);
const options = _.omit(data, keys);
return { ...attrs, options };
}
toProps() {
const json = this.toJSON();
const data: any = _.omit(json, ['options', 'created_at', 'updated_at', 'ui_schema_key']);
const options = json['options'] || {};
if (json['ui_schema_key']) {
data.uiSchemaKey = json['ui_schema_key'];
}
return { ...data, ...options }
}
}

View File

@ -2,6 +2,7 @@ import path from 'path';
import { Application } from '@nocobase/server';
import { registerModels } from '@nocobase/database';
import * as models from './models';
import getAccessible from './actions/getAccessible';
export default async function (this: Application, options = {}) {
const database = this.database;
@ -10,4 +11,6 @@ export default async function (this: Application, options = {}) {
database.import({
directory: path.resolve(__dirname, 'collections'),
});
this.resourcer.registerActionHandler('routes:getAccessible', getAccessible);
}

View File

@ -0,0 +1,15 @@
import { Model, ModelCtor } from '@nocobase/database';
import { actions } from '@nocobase/actions';
export default async (ctx: actions.Context, next: actions.Next) => {
const { resourceKey } = ctx.action.params;
const UISchema = ctx.db.getModel('ui_schemas');
const schema = await UISchema.findByPk(resourceKey);
const property = schema.toProperty();
const properties = await schema.getProperties();
if (Object.keys(properties).length) {
property.properties = properties;
}
ctx.body = property;
await next();
}

View File

@ -2,6 +2,7 @@ import path from 'path';
import { Application } from '@nocobase/server';
import { registerModels, Table } from '@nocobase/database';
import * as models from './models';
import getTree from './actions/getTree';
export default async function (this: Application, options = {}) {
const database = this.database;
@ -11,9 +12,11 @@ export default async function (this: Application, options = {}) {
directory: path.resolve(__dirname, 'collections'),
});
database.getModel('ui_schemas').beforeCreate((model) => {
if (!model.get('name')) {
model.set('name', model.get('key'));
}
});
// database.getModel('ui_schemas').beforeCreate((model) => {
// if (!model.get('name')) {
// model.set('name', model.get('key'));
// }
// });
this.resourcer.registerActionHandler('ui_schemas:getTree', getTree);
}

View File

@ -1,5 +1,5 @@
import compose from 'koa-compose';
import { pathToRegexp } from 'path-to-regexp';
import pathToRegexp from 'path-to-regexp';
import Resourcer, { getNameByParams, KoaMiddlewareOptions, parseRequest, parseQuery, ResourcerContext, ResourceType } from '@nocobase/resourcer';
import Database, { BELONGSTO, BELONGSTOMANY, HASMANY, HASONE } from '@nocobase/database';

View File

@ -1,5 +1,5 @@
import compose from 'koa-compose';
import { pathToRegexp } from 'path-to-regexp';
import pathToRegexp from 'path-to-regexp';
import Resourcer, { getNameByParams, KoaMiddlewareOptions, parseRequest, parseQuery, ResourcerContext, ResourceType } from '@nocobase/resourcer';
import Database, { BELONGSTO, BELONGSTOMANY, HASMANY, HASONE } from '@nocobase/database';

191
yarn.lock
View File

@ -5278,6 +5278,20 @@ boxen@^3.0.0:
type-fest "^0.3.0"
widest-line "^2.0.0"
boxen@^4.2.0:
version "4.2.0"
resolved "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64"
integrity sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==
dependencies:
ansi-align "^3.0.0"
camelcase "^5.3.1"
chalk "^3.0.0"
cli-boxes "^2.2.0"
string-width "^4.1.0"
term-size "^2.1.0"
type-fest "^0.8.1"
widest-line "^3.1.0"
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@ -5702,6 +5716,14 @@ chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
chalk@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^4.0.0, chalk@^4.1.0:
version "4.1.1"
resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
@ -5803,7 +5825,7 @@ chokidar@^2.0.4:
optionalDependencies:
fsevents "^1.2.7"
chokidar@^3.0.2:
chokidar@^3.0.2, chokidar@^3.2.2:
version "3.5.2"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
@ -6187,6 +6209,18 @@ configstore@^4.0.0:
write-file-atomic "^2.0.0"
xdg-basedir "^3.0.0"
configstore@^5.0.1:
version "5.0.1"
resolved "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96"
integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==
dependencies:
dot-prop "^5.2.0"
graceful-fs "^4.1.2"
make-dir "^3.0.0"
unique-string "^2.0.0"
write-file-atomic "^3.0.0"
xdg-basedir "^4.0.0"
connected-react-router@6.5.2:
version "6.5.2"
resolved "https://registry.npmjs.org/connected-react-router/-/connected-react-router-6.5.2.tgz#422af70f86cb276681e20ab4295cf27dd9b6c7e3"
@ -6534,6 +6568,11 @@ crypto-random-string@^1.0.0:
resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=
crypto-random-string@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
crypto-random-string@^3.3.0, crypto-random-string@^3.3.1:
version "3.3.1"
resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.3.1.tgz#13cee94cac8001e4842501608ef779e0ed08f82d"
@ -6878,7 +6917,7 @@ debug@3.1.0, debug@~3.1.0:
dependencies:
ms "2.0.0"
debug@3.X, debug@^3.1.0, debug@^3.2.7:
debug@3.X, debug@^3.1.0, debug@^3.2.6, debug@^3.2.7:
version "3.2.7"
resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
@ -7679,6 +7718,11 @@ escalade@^3.1.1:
resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
escape-goat@^2.0.0:
version "2.1.1"
resolved "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==
escape-html@^1.0.3:
version "1.0.3"
resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
@ -8370,6 +8414,11 @@ flat-cache@^3.0.4:
flatted "^3.1.0"
rimraf "^3.0.2"
flat-to-nested@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/flat-to-nested/-/flat-to-nested-1.1.1.tgz#ec183cd9a72f6bfbf8ca21acb0fc8235e509f581"
integrity sha512-Sym5oik6BO9JnsDEjv9Q9hPTCexG2ttk0UiM2mgLEiCiiUOQr8acBd33r8ixnoSGR0HAxPoP8WtLAL5oV46IhQ==
flatted@^3.1.0:
version "3.1.1"
resolved "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469"
@ -8811,6 +8860,13 @@ global-dirs@^0.1.0:
dependencies:
ini "^1.3.4"
global-dirs@^2.0.1:
version "2.1.0"
resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz#e9046a49c806ff04d6c1825e196c8f0091e8df4d"
integrity sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==
dependencies:
ini "1.3.7"
global@^4.3.2:
version "4.4.0"
resolved "https://registry.npmjs.org/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406"
@ -9480,6 +9536,11 @@ iferr@^0.1.5:
resolved "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
ignore-by-default@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk=
ignore-walk@^3.0.1:
version "3.0.4"
resolved "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335"
@ -9647,6 +9708,11 @@ inherits@2.0.3:
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
ini@1.3.7:
version "1.3.7"
resolved "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84"
integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==
ini@^1.3.2, ini@^1.3.4, ini@~1.3.0:
version "1.3.8"
resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
@ -9998,6 +10064,14 @@ is-installed-globally@^0.1.0:
global-dirs "^0.1.0"
is-path-inside "^1.0.0"
is-installed-globally@^0.3.1:
version "0.3.2"
resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141"
integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==
dependencies:
global-dirs "^2.0.1"
is-path-inside "^3.0.1"
is-module@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
@ -10026,6 +10100,11 @@ is-npm@^3.0.0:
resolved "https://registry.npmjs.org/is-npm/-/is-npm-3.0.0.tgz#ec9147bfb629c43f494cf67936a961edec7e8053"
integrity sha512-wsigDr1Kkschp2opC4G3yA6r9EgVA6NjRpWzIi9axXqeIaAATPRJc4uLujXe3Nd9uO8KoDyA4MD6aZSeXTADhA==
is-npm@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d"
integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==
is-number-object@^1.0.4:
version "1.0.5"
resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz#6edfaeed7950cff19afedce9fbfca9ee6dd289eb"
@ -10060,6 +10139,11 @@ is-path-inside@^1.0.0:
dependencies:
path-is-inside "^1.0.1"
is-path-inside@^3.0.1:
version "3.0.3"
resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
@ -12889,6 +12973,22 @@ node-schedule@^2.0.0:
long-timeout "0.1.1"
sorted-array-functions "^1.3.0"
nodemon@^2.0.12:
version "2.0.12"
resolved "https://registry.npmjs.org/nodemon/-/nodemon-2.0.12.tgz#5dae4e162b617b91f1873b3bfea215dd71e144d5"
integrity sha512-egCTmNZdObdBxUBw6ZNwvZ/xzk24CKRs5K6d+5zbmrMr7rOpPmfPeF6OxM3DDpaRx331CQRFEktn+wrFFfBSOA==
dependencies:
chokidar "^3.2.2"
debug "^3.2.6"
ignore-by-default "^1.0.1"
minimatch "^3.0.4"
pstree.remy "^1.1.7"
semver "^5.7.1"
supports-color "^5.5.0"
touch "^3.1.0"
undefsafe "^2.0.3"
update-notifier "^4.1.0"
nopt@^4.0.1:
version "4.0.3"
resolved "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48"
@ -12904,6 +13004,13 @@ nopt@^5.0.0:
dependencies:
abbrev "1"
nopt@~1.0.10:
version "1.0.10"
resolved "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee"
integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=
dependencies:
abbrev "1"
normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.3.5, normalize-package-data@^2.4.0, normalize-package-data@^2.5.0:
version "2.5.0"
resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
@ -14812,6 +14919,11 @@ psl@^1.1.28, psl@^1.1.33:
resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
pstree.remy@^1.1.7:
version "1.1.8"
resolved "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a"
integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==
public-encrypt@^4.0.0:
version "4.0.3"
resolved "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
@ -14864,6 +14976,13 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
pupa@^2.0.1:
version "2.1.1"
resolved "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62"
integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==
dependencies:
escape-goat "^2.0.0"
q@^1.1.2, q@^1.5.1:
version "1.5.1"
resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
@ -16771,6 +16890,13 @@ semver-diff@^2.0.0:
dependencies:
semver "^5.0.3"
semver-diff@^3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b"
integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==
dependencies:
semver "^6.3.0"
"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1:
version "5.7.1"
resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
@ -17385,7 +17511,7 @@ string-width@^3.0.0, string-width@^3.1.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0"
string-width@^4.1.0, string-width@^4.2.0:
string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0:
version "4.2.2"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==
@ -17626,7 +17752,7 @@ supports-color@^3.2.3:
dependencies:
has-flag "^1.0.0"
supports-color@^5.3.0, supports-color@^5.4.0:
supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0:
version "5.5.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
@ -17755,6 +17881,11 @@ term-size@^1.2.0:
dependencies:
execa "^0.7.0"
term-size@^2.1.0:
version "2.2.1"
resolved "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54"
integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==
terminal-link@^2.0.0:
version "2.1.1"
resolved "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"
@ -18024,6 +18155,13 @@ toposort-class@^1.0.1:
resolved "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988"
integrity sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=
touch@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==
dependencies:
nopt "~1.0.10"
tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@^2.5.0, tough-cookie@~2.5.0:
version "2.5.0"
resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
@ -18408,6 +18546,13 @@ unc-path-regex@^0.1.2:
resolved "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo=
undefsafe@^2.0.3:
version "2.0.3"
resolved "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae"
integrity sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==
dependencies:
debug "^2.2.0"
underscore@^1.13.1:
version "1.13.1"
resolved "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1"
@ -18503,6 +18648,13 @@ unique-string@^1.0.0:
dependencies:
crypto-random-string "^1.0.0"
unique-string@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d"
integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==
dependencies:
crypto-random-string "^2.0.0"
unist-builder@^2.0.0:
version "2.0.3"
resolved "https://registry.npmjs.org/unist-builder/-/unist-builder-2.0.3.tgz#77648711b5d86af0942f334397a33c5e91516436"
@ -18619,6 +18771,25 @@ update-notifier@3.0.0:
semver-diff "^2.0.0"
xdg-basedir "^3.0.0"
update-notifier@^4.1.0:
version "4.1.3"
resolved "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz#be86ee13e8ce48fb50043ff72057b5bd598e1ea3"
integrity sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==
dependencies:
boxen "^4.2.0"
chalk "^3.0.0"
configstore "^5.0.1"
has-yarn "^2.1.0"
import-lazy "^2.1.0"
is-ci "^2.0.0"
is-installed-globally "^0.3.1"
is-npm "^4.0.0"
is-yarn-global "^0.3.0"
latest-version "^5.0.0"
pupa "^2.0.1"
semver-diff "^3.1.1"
xdg-basedir "^4.0.0"
upper-case@^2.0.1:
version "2.0.2"
resolved "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz#d89810823faab1df1549b7d97a76f8662bae6f7a"
@ -19084,6 +19255,13 @@ widest-line@^2.0.0:
dependencies:
string-width "^2.1.1"
widest-line@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca"
integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==
dependencies:
string-width "^4.0.0"
wildcard@^1.1.0:
version "1.1.2"
resolved "https://registry.npmjs.org/wildcard/-/wildcard-1.1.2.tgz#a7020453084d8cd2efe70ba9d3696263de1710a5"
@ -19237,6 +19415,11 @@ xdg-basedir@^3.0.0:
resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=
xdg-basedir@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==
xml-name-validator@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"