mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-11-29 18:58:26 +08:00
lots of updates
This commit is contained in:
parent
d39a7d822d
commit
af02a895c3
@ -3,6 +3,13 @@ networks:
|
||||
nocobase:
|
||||
driver: bridge
|
||||
services:
|
||||
adminer:
|
||||
image: adminer
|
||||
restart: always
|
||||
networks:
|
||||
- nocobase
|
||||
ports:
|
||||
- ${ADMINER_PORT}:8080
|
||||
verdaccio:
|
||||
image: verdaccio/verdaccio
|
||||
networks:
|
||||
|
@ -12,6 +12,7 @@
|
||||
"build2": "lerna run build",
|
||||
"build": "npm run build-father-build && node packages/father-build/bin/father-build.js",
|
||||
"build-father-build": "cd packages/father-build && npm run build",
|
||||
"db-migrate": "ts-node -r dotenv/config ./packages/api/src/migrate.ts",
|
||||
"lint": "eslint --ext .ts,.tsx,.js \"packages/*/src/**.@(ts|tsx|js)\" --fix",
|
||||
"test": "npm run lint && jest"
|
||||
},
|
||||
|
@ -19,6 +19,8 @@
|
||||
"@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-schema": "^0.4.0-alpha.7",
|
||||
"@nocobase/plugin-users": "^0.4.0-alpha.7",
|
||||
"@nocobase/server": "^0.4.0-alpha.7",
|
||||
"koa-static": "^5.0.0"
|
||||
|
@ -39,13 +39,15 @@ const api = Api.create({
|
||||
|
||||
const plugins = [
|
||||
'@nocobase/plugin-collections',
|
||||
'@nocobase/plugin-action-logs',
|
||||
'@nocobase/plugin-pages',
|
||||
'@nocobase/plugin-users',
|
||||
'@nocobase/plugin-file-manager',
|
||||
'@nocobase/plugin-permissions',
|
||||
'@nocobase/plugin-automations',
|
||||
'@nocobase/plugin-china-region',
|
||||
'@nocobase/plugin-routes',
|
||||
'@nocobase/plugin-ui-schema',
|
||||
// '@nocobase/plugin-action-logs',
|
||||
// '@nocobase/plugin-pages',
|
||||
// '@nocobase/plugin-users',
|
||||
// '@nocobase/plugin-file-manager',
|
||||
// '@nocobase/plugin-permissions',
|
||||
// '@nocobase/plugin-automations',
|
||||
// '@nocobase/plugin-china-region',
|
||||
];
|
||||
|
||||
for (const plugin of plugins) {
|
||||
|
@ -12,12 +12,6 @@ import { middlewares } from '@nocobase/server';
|
||||
}));
|
||||
}
|
||||
|
||||
await api.database.getModel('collections').load({skipExisting: true});
|
||||
await api.database.getModel('collections').load({where: {
|
||||
name: 'users',
|
||||
}});
|
||||
await api.database.getModel('automations').load();
|
||||
|
||||
api.use(middlewares.appDistServe({
|
||||
root: process.env.APP_DIST,
|
||||
useStaticServer: !(process.env.APP_USE_STATIC_SERVER === 'false' || !process.env.APP_USE_STATIC_SERVER),
|
||||
|
@ -8,57 +8,7 @@ global.sync = {
|
||||
|
||||
import Database from '@nocobase/database';
|
||||
import api from '../app';
|
||||
|
||||
const data = [
|
||||
{
|
||||
title: '后台应用',
|
||||
path: '/',
|
||||
type: 'layout',
|
||||
template: 'TopMenuLayout',
|
||||
sort: 10,
|
||||
redirect: '/admin',
|
||||
},
|
||||
{
|
||||
title: '后台',
|
||||
path: '/admin',
|
||||
type: 'page',
|
||||
inherit: false,
|
||||
template: 'AdminLoader',
|
||||
order: 230,
|
||||
},
|
||||
{
|
||||
title: '登录页面',
|
||||
path: '/login',
|
||||
type: 'page',
|
||||
inherit: false,
|
||||
template: 'login',
|
||||
order: 120,
|
||||
},
|
||||
{
|
||||
title: '注册页面',
|
||||
path: '/register',
|
||||
type: 'page',
|
||||
inherit: false,
|
||||
template: 'register',
|
||||
order: 130,
|
||||
},
|
||||
{
|
||||
title: '忘记密码',
|
||||
path: '/lostpassword',
|
||||
type: 'page',
|
||||
inherit: false,
|
||||
template: 'lostpassword',
|
||||
order: 140,
|
||||
},
|
||||
{
|
||||
title: '重置密码',
|
||||
path: '/resetpassword',
|
||||
type: 'page',
|
||||
inherit: false,
|
||||
template: 'resetpassword',
|
||||
order: 150,
|
||||
},
|
||||
];
|
||||
import * as uiSchema from './ui-schema';
|
||||
|
||||
(async () => {
|
||||
await api.loadPlugins();
|
||||
@ -66,196 +16,47 @@ const data = [
|
||||
await database.sync({
|
||||
// tables: ['collections', 'fields', 'actions', 'views', 'tabs'],
|
||||
});
|
||||
const [Collection, Page, User] = database.getModels(['collections', 'pages', 'users']);
|
||||
const tables = database.getTables([]);
|
||||
for (let table of tables) {
|
||||
// console.log(table.getName());
|
||||
if (table.getName() === 'roles') {
|
||||
// console.log('roles', table.getOptions())
|
||||
}
|
||||
await Collection.import(table.getOptions(), { update: true, migrate: false });
|
||||
}
|
||||
await Page.import(data);
|
||||
|
||||
const user = await User.create({
|
||||
email: process.env.ADMIN_EMAIL,
|
||||
password: process.env.ADMIN_PASSWORD,
|
||||
});
|
||||
const Storage = database.getModel('storages');
|
||||
await Storage.create({
|
||||
title: '本地存储',
|
||||
name: `local`,
|
||||
type: 'local',
|
||||
baseUrl: process.env.LOCAL_STORAGE_BASE_URL,
|
||||
default: process.env.STORAGE_TYPE === 'local',
|
||||
});
|
||||
await Storage.create({
|
||||
name: `ali-oss`,
|
||||
type: 'ali-oss',
|
||||
baseUrl: process.env.ALI_OSS_STORAGE_BASE_URL,
|
||||
options: {
|
||||
region: process.env.ALI_OSS_REGION,
|
||||
accessKeyId: process.env.ALI_OSS_ACCESS_KEY_ID,
|
||||
accessKeySecret: process.env.ALI_OSS_ACCESS_KEY_SECRET,
|
||||
bucket: process.env.ALI_OSS_BUCKET,
|
||||
},
|
||||
default: process.env.STORAGE_TYPE === 'ali-oss',
|
||||
});
|
||||
const Role = database.getModel('roles');
|
||||
if (Role) {
|
||||
const roles = await Role.bulkCreate([
|
||||
{ title: '系统开发组', type: -1 },
|
||||
// { title: '匿名用户组', type: 0 },
|
||||
{ title: '普通用户组', default: true },
|
||||
]);
|
||||
await roles[0].updateAssociations({
|
||||
users: user
|
||||
});
|
||||
}
|
||||
const Route = database.getModel('routes');
|
||||
|
||||
const Action = database.getModel('actions');
|
||||
// 全局
|
||||
await Action.bulkCreate([
|
||||
]);
|
||||
|
||||
// 导入地域数据
|
||||
const ChinaRegion = database.getModel('china_regions');
|
||||
ChinaRegion && await ChinaRegion.importData();
|
||||
|
||||
const Menu = database.getModel('menus');
|
||||
|
||||
const menus = [
|
||||
const data = [
|
||||
{
|
||||
title: '仪表盘',
|
||||
icon: 'DashboardOutlined',
|
||||
type: 'group',
|
||||
type: 'redirect',
|
||||
from: '/',
|
||||
to: '/admin',
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: '/admin/:name(.+)?',
|
||||
component: 'AdminLayout',
|
||||
title: `后台`,
|
||||
uiSchema: uiSchema.menu,
|
||||
},
|
||||
{
|
||||
component: 'AuthLayout',
|
||||
children: [
|
||||
{
|
||||
title: '欢迎光临',
|
||||
icon: 'DatabaseOutlined',
|
||||
type: 'page',
|
||||
views: [],
|
||||
name: 'welcome',
|
||||
},
|
||||
],
|
||||
name: 'login',
|
||||
path: '/login',
|
||||
component: 'DefaultPage',
|
||||
title: `登录`,
|
||||
uiSchema: uiSchema.login,
|
||||
},
|
||||
{
|
||||
title: '数据',
|
||||
icon: 'DatabaseOutlined',
|
||||
type: 'group',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
icon: 'TeamOutlined',
|
||||
type: 'group',
|
||||
children: [
|
||||
{
|
||||
title: '用户管理',
|
||||
icon: 'DatabaseOutlined',
|
||||
type: 'page',
|
||||
views: ['users.table'],
|
||||
name: 'users',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '日志',
|
||||
icon: 'NotificationOutlined',
|
||||
type: 'group',
|
||||
developerMode: true,
|
||||
children: [
|
||||
{
|
||||
title: '操作记录',
|
||||
icon: 'DatabaseOutlined',
|
||||
type: 'group',
|
||||
developerMode: true,
|
||||
children: [
|
||||
{
|
||||
title: '全部数据',
|
||||
type: 'page',
|
||||
views: ['action_logs.table'],
|
||||
developerMode: true,
|
||||
name: 'auditing',
|
||||
},
|
||||
{
|
||||
title: '新增数据',
|
||||
type: 'page',
|
||||
views: ['action_logs.create'],
|
||||
developerMode: true,
|
||||
name: 'create-auditing',
|
||||
},
|
||||
{
|
||||
title: '更新数据',
|
||||
type: 'page',
|
||||
views: ['action_logs.update'],
|
||||
developerMode: true,
|
||||
name: 'update-auditing',
|
||||
},
|
||||
{
|
||||
title: '删除数据',
|
||||
type: 'page',
|
||||
views: ['action_logs.destroy'],
|
||||
developerMode: true,
|
||||
name: 'destroy-auditing',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '配置',
|
||||
icon: 'SettingOutlined',
|
||||
type: 'group',
|
||||
developerMode: true,
|
||||
children: [
|
||||
{
|
||||
name: 'menus',
|
||||
title: '菜单和页面配置',
|
||||
icon: 'MenuOutlined',
|
||||
type: 'page',
|
||||
views: ['menus.table'],
|
||||
developerMode: true,
|
||||
},
|
||||
{
|
||||
name: 'collections',
|
||||
title: '数据表配置',
|
||||
icon: 'DatabaseOutlined',
|
||||
type: 'page',
|
||||
views: ['collections.table'],
|
||||
developerMode: true,
|
||||
},
|
||||
{
|
||||
name: 'permissions',
|
||||
title: '权限配置',
|
||||
icon: 'MenuOutlined',
|
||||
type: 'page',
|
||||
views: ['roles.table'],
|
||||
developerMode: true,
|
||||
},
|
||||
{
|
||||
name: 'automations',
|
||||
title: '自动化配置',
|
||||
icon: 'MenuOutlined',
|
||||
type: 'page',
|
||||
views: ['automations.table'],
|
||||
developerMode: true,
|
||||
},
|
||||
{
|
||||
name: 'system_settings',
|
||||
title: '系统配置',
|
||||
icon: 'DatabaseOutlined',
|
||||
type: 'page',
|
||||
views: ['system_settings.descriptions'],
|
||||
developerMode: true,
|
||||
name: 'register',
|
||||
path: '/register',
|
||||
component: 'DefaultPage',
|
||||
title: `注册`,
|
||||
uiSchema: uiSchema.register,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
for (const item of menus) {
|
||||
const menu = await Menu.create(item);
|
||||
await menu.updateAssociations(item);
|
||||
for (const item of data) {
|
||||
const route = await Route.create(item);
|
||||
await route.updateAssociations(item);
|
||||
}
|
||||
|
||||
await database.close();
|
||||
})();
|
||||
|
@ -1,32 +0,0 @@
|
||||
import api from '../app';
|
||||
import Database from '@nocobase/database';
|
||||
|
||||
(async () => {
|
||||
await api.loadPlugins();
|
||||
await api.database.getModel('collections').load({ skipExisting: true });
|
||||
const database: Database = api.database;
|
||||
const [Field] = database.getModels(['fields']);
|
||||
|
||||
const fields = await Field.findAll({
|
||||
where: {
|
||||
interface: 'multipleSelect',
|
||||
},
|
||||
});
|
||||
|
||||
for (const field of fields) {
|
||||
const M = database.getModel(field.collection_name);
|
||||
const models = await M.findAll();
|
||||
for (const model of models) {
|
||||
let value = model.get(field.name);
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
if (!Array.isArray(value)) {
|
||||
value = [value];
|
||||
}
|
||||
model.set(field.name, value);
|
||||
await model.save();
|
||||
console.log(field.name, value);
|
||||
}
|
||||
}
|
||||
})();
|
@ -1,18 +0,0 @@
|
||||
import api from '../app';
|
||||
import Database from '@nocobase/database';
|
||||
|
||||
(async () => {
|
||||
await api.loadPlugins();
|
||||
const database: Database = api.database;
|
||||
await api.database.sync({
|
||||
});
|
||||
|
||||
const [Collection] = database.getModels(['collections']);
|
||||
|
||||
const tables = database.getTables();
|
||||
|
||||
for (let table of tables) {
|
||||
console.log(table.getName());
|
||||
await Collection.import(table.getOptions(), { migrate: false });
|
||||
}
|
||||
})();
|
3
packages/api/src/migrations/ui-schema/index.ts
Normal file
3
packages/api/src/migrations/ui-schema/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './login';
|
||||
export * from './menu';
|
||||
export * from './register';
|
66
packages/api/src/migrations/ui-schema/login.ts
Normal file
66
packages/api/src/migrations/ui-schema/login.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
|
||||
export const login: ISchema = {
|
||||
key: 'dtf9j0b8p9u',
|
||||
name: 'dtf9j0b8p9u',
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {
|
||||
placeholder: '邮箱或用户名',
|
||||
style: {
|
||||
// width: 240,
|
||||
},
|
||||
},
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Password',
|
||||
'x-component-props': {
|
||||
placeholder: '密码',
|
||||
style: {
|
||||
// width: 240,
|
||||
},
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-component': 'Div',
|
||||
properties: {
|
||||
submit: {
|
||||
type: 'void',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
block: true,
|
||||
type: 'primary',
|
||||
useAction: '{{ useLogin }}',
|
||||
style: {
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
title: '登录',
|
||||
},
|
||||
},
|
||||
},
|
||||
registerlink: {
|
||||
type: 'void',
|
||||
'x-component': 'Div',
|
||||
properties: {
|
||||
link: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
to: '/register',
|
||||
},
|
||||
title: '注册账号',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
14
packages/api/src/migrations/ui-schema/menu.ts
Normal file
14
packages/api/src/migrations/ui-schema/menu.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export const menu = {
|
||||
key: 'qqzzjakwkwl',
|
||||
name: 'qqzzjakwkwl',
|
||||
type: 'void',
|
||||
'x-component': 'Menu',
|
||||
'x-designable-bar': 'Menu.DesignableBar',
|
||||
'x-component-props': {
|
||||
mode: 'mix',
|
||||
theme: 'dark',
|
||||
defaultSelectedKeys: '{{ selectedKeys }}',
|
||||
sideMenuRef: '{{ sideMenuRef }}',
|
||||
onSelect: '{{ onSelect }}',
|
||||
},
|
||||
};
|
78
packages/api/src/migrations/ui-schema/register.ts
Normal file
78
packages/api/src/migrations/ui-schema/register.ts
Normal file
@ -0,0 +1,78 @@
|
||||
export const register = {
|
||||
key: '46qlxqam3xk',
|
||||
name: '46qlxqam3xk',
|
||||
type: 'object',
|
||||
properties: {
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {
|
||||
placeholder: '用户名',
|
||||
style: {
|
||||
// width: 240,
|
||||
},
|
||||
},
|
||||
},
|
||||
pwd1: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Password',
|
||||
'x-component-props': {
|
||||
placeholder: '密码',
|
||||
checkStrength: true,
|
||||
style: {
|
||||
// width: 240,
|
||||
},
|
||||
},
|
||||
},
|
||||
pwd2: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Password',
|
||||
'x-component-props': {
|
||||
placeholder: '密码',
|
||||
checkStrength: true,
|
||||
style: {
|
||||
// width: 240,
|
||||
},
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-component': 'Div',
|
||||
properties: {
|
||||
submit: {
|
||||
type: 'void',
|
||||
title: '注册',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
block: true,
|
||||
type: 'primary',
|
||||
useAction: '{{ useRegister }}',
|
||||
style: {
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
registerlink: {
|
||||
type: 'void',
|
||||
'x-component': 'Div',
|
||||
properties: {
|
||||
link: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
to: '/login',
|
||||
},
|
||||
title: '使用已有账号登录',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
@ -1,18 +1,20 @@
|
||||
const api = {
|
||||
'/api/routes:getAccessible': require('./routes-getAccessible').default,
|
||||
'/api/blocks:getSchema/login': require('./blocks-getSchema/login').default,
|
||||
'/api/blocks:getSchema/register': require('./blocks-getSchema/register').default,
|
||||
'/api/blocks:getSchema/item1': require('./blocks-getSchema/item1').default,
|
||||
'/api/blocks:getSchema/item2': require('./blocks-getSchema/item2').default,
|
||||
'/api/blocks:getSchema/item22': require('./blocks-getSchema/item22').default,
|
||||
'/api/blocks:getSchema/item3': require('./blocks-getSchema/item3').default,
|
||||
'/api/blocks:getSchema/item4': require('./blocks-getSchema/item4').default,
|
||||
'/api/blocks:getSchema/item5': require('./blocks-getSchema/item5').default,
|
||||
'/api/blocks:getSchema/menu': require('./blocks-getSchema/menu').default,
|
||||
// '/api/blocks:getSchema/login': require('./blocks-getSchema/login').default,
|
||||
// '/api/blocks:getSchema/register': require('./blocks-getSchema/register').default,
|
||||
// '/api/blocks:getSchema/item1': require('./blocks-getSchema/item1').default,
|
||||
// '/api/blocks:getSchema/item2': require('./blocks-getSchema/item2').default,
|
||||
// '/api/blocks:getSchema/item22': require('./blocks-getSchema/item22').default,
|
||||
// '/api/blocks:getSchema/item3': require('./blocks-getSchema/item3').default,
|
||||
// '/api/blocks:getSchema/item4': require('./blocks-getSchema/item4').default,
|
||||
// '/api/blocks:getSchema/item5': require('./blocks-getSchema/item5').default,
|
||||
// '/api/blocks:getSchema/menu': require('./blocks-getSchema/menu').default,
|
||||
'/api/ui-schemas:getTree/login': require('./ui-schemas-getTree/login').default,
|
||||
'/api/ui-schemas:getTree/register': require('./ui-schemas-getTree/register').default,
|
||||
'/api/ui-schemas:getTree/menu': require('./ui-schemas-getTree/menu').default,
|
||||
}
|
||||
|
||||
export function request(service) {
|
||||
console.log({ service })
|
||||
let url = null;
|
||||
if (typeof service === 'string') {
|
||||
url = service;
|
||||
@ -20,6 +22,8 @@ export function request(service) {
|
||||
url = service.url;
|
||||
}
|
||||
|
||||
console.log('request.url', url)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
if (api[url]) {
|
||||
|
@ -17,7 +17,7 @@ export default [
|
||||
path: '/admin/:name(.+)?',
|
||||
component: 'AdminLayout',
|
||||
title: `后台 - ${Mock.mock('@string')}`,
|
||||
blockId: 'menu',
|
||||
schemaName: 'menu',
|
||||
},
|
||||
{
|
||||
component: 'AuthLayout',
|
||||
@ -27,14 +27,14 @@ export default [
|
||||
path: '/login',
|
||||
component: 'DefaultPage',
|
||||
title: `登录 - ${Mock.mock('@string')}`,
|
||||
blockId: 'login',
|
||||
schemaName: 'login',
|
||||
},
|
||||
{
|
||||
name: 'register',
|
||||
path: '/register',
|
||||
component: 'DefaultPage',
|
||||
title: `注册 - ${Mock.mock('@string')}`,
|
||||
blockId: 'register',
|
||||
schemaName: 'register',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -212,6 +212,7 @@ Action.Popover = observer((props) => {
|
||||
const schema = useFieldSchema();
|
||||
return (
|
||||
<Popover
|
||||
placement={'bottom'}
|
||||
visible={visible}
|
||||
onVisibleChange={(visible) => {
|
||||
setVisible(visible);
|
||||
|
@ -58,11 +58,47 @@ const schema = {
|
||||
'x-component': 'Form',
|
||||
properties: {
|
||||
filter: {
|
||||
type: 'string',
|
||||
name: 'filter',
|
||||
type: 'object',
|
||||
'x-component': 'Filter',
|
||||
properties: {
|
||||
column1: {
|
||||
type: 'void',
|
||||
title: '字段1',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Filter.Column',
|
||||
'x-component-props': {
|
||||
operations: [
|
||||
{ label: '等于', value: 'eq' },
|
||||
{ label: '不等于', value: 'ne' },
|
||||
],
|
||||
},
|
||||
properties: {
|
||||
field1: {
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
},
|
||||
},
|
||||
column2: {
|
||||
type: 'void',
|
||||
title: '字段2',
|
||||
'x-component': 'Filter.Column',
|
||||
'x-component-props': {
|
||||
operations: [
|
||||
{ label: '大于', value: 'gt' },
|
||||
{ label: '小于', value: 'lt' },
|
||||
{ label: '非空', value: 'notNull', noValue: true },
|
||||
],
|
||||
},
|
||||
properties: {
|
||||
field1: {
|
||||
type: 'number',
|
||||
'x-component': 'InputNumber',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
action: {
|
||||
type: 'void',
|
||||
title: '提交',
|
||||
|
@ -195,7 +195,7 @@ function LayoutWithMenu({ schema }) {
|
||||
|
||||
function Content({ activeKey }) {
|
||||
const { data = {}, loading } = useRequest(
|
||||
`/api/blocks:getSchema/${activeKey}`,
|
||||
`/api/ui-schemas:getTree/${activeKey}?filter[parentId]=${activeKey}`,
|
||||
{
|
||||
refreshDeps: [activeKey],
|
||||
formatResult: (result) => result?.data,
|
||||
@ -212,12 +212,13 @@ function Content({ activeKey }) {
|
||||
export function AdminLayout({ route, children }: any) {
|
||||
|
||||
const { data = {}, loading } = useRequest(
|
||||
`/api/blocks:getSchema/${route.blockId}`,
|
||||
`/api/ui-schemas:getTree/${route.schemaName}`,
|
||||
{
|
||||
refreshDeps: [route],
|
||||
formatResult: (result) => result?.data,
|
||||
},
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import { SchemaRenderer } from '../../schemas';
|
||||
|
||||
export function DefaultPage({ route }) {
|
||||
const { data = {}, loading } = useRequest(
|
||||
`/api/blocks:getSchema/${route.blockId}`,
|
||||
`/api/ui-schemas:getTree/${route.schemaName}`,
|
||||
{
|
||||
refreshDeps: [route],
|
||||
formatResult: (result) => result?.data,
|
||||
|
@ -0,0 +1,58 @@
|
||||
import { getDatabase } from '..';
|
||||
import Database from '../..';
|
||||
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = getDatabase();
|
||||
db.table({
|
||||
name: 'routes',
|
||||
fields: [
|
||||
{
|
||||
type: 'uid',
|
||||
name: 'key',
|
||||
prefix: 'r_',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'uiSchema',
|
||||
target: 'ui_schemas',
|
||||
},
|
||||
],
|
||||
});
|
||||
db.table({
|
||||
name: 'ui_schemas',
|
||||
fields: [
|
||||
{
|
||||
type: 'uid',
|
||||
name: 'key',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
});
|
||||
await db.sync();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
describe('updateAssociations', () => {
|
||||
it('belongsTo', async () => {
|
||||
const [Route, Schema] = db.getModels(['routes', 'ui_schemas']);
|
||||
const route = await Route.create();
|
||||
await route.updateAssociations({
|
||||
uiSchema: {
|
||||
key: '6kyo0t1jnpw',
|
||||
// name: '6kyo0t1jnpw'
|
||||
},
|
||||
});
|
||||
const schema = await Schema.findByPk('6kyo0t1jnpw');
|
||||
expect(schema).toBeDefined();
|
||||
});
|
||||
});
|
@ -18,7 +18,7 @@ import { getDataTypeKey } from '.';
|
||||
import Table from '../table';
|
||||
import Database from '../database';
|
||||
import Model, { ModelCtor } from '../model';
|
||||
import { whereCompare, isNumber } from '../utils';
|
||||
import { whereCompare, isNumber, uid } from '../utils';
|
||||
|
||||
export interface IField {
|
||||
|
||||
@ -344,7 +344,7 @@ export class ARRAY extends Column {
|
||||
|
||||
export class JSON extends Column {
|
||||
public getDataType() {
|
||||
return DataTypes.JSONB;
|
||||
return DataTypes.JSON;
|
||||
}
|
||||
}
|
||||
|
||||
@ -352,6 +352,33 @@ export class JSONB extends Column {
|
||||
}
|
||||
|
||||
export class UUID extends Column {
|
||||
public getDataType() {
|
||||
return DataTypes.UUID;
|
||||
}
|
||||
}
|
||||
|
||||
export class UID extends Column {
|
||||
|
||||
constructor(options: Options.StringOptions, context: FieldContext) {
|
||||
super(options, context);
|
||||
const { name, prefix = '' } = options;
|
||||
const Model = context.sourceTable.getModel();
|
||||
Model.addHook('beforeCreate', (model) => {
|
||||
if (!model.get(name)) {
|
||||
model.set(name, `${prefix}${uid()}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getDataType() {
|
||||
return DataTypes.STRING;
|
||||
}
|
||||
}
|
||||
|
||||
export class UUIDV4 extends Column {
|
||||
public getDataType() {
|
||||
return DataTypes.UUIDV4;
|
||||
}
|
||||
}
|
||||
|
||||
export interface HasOneAccessors {
|
||||
|
@ -310,17 +310,22 @@ export abstract class Model extends SequelizeModel {
|
||||
? association.options.targetKey
|
||||
: association.options.sourceKey;
|
||||
if (data[targetAttribute]) {
|
||||
await this[accessors.set](data[targetAttribute], opts);
|
||||
if (Object.keys(data).length > 1) {
|
||||
if (Object.keys(data).length > 0) {
|
||||
const target = await Target.findOne({
|
||||
where: {
|
||||
[targetAttribute]: data[targetAttribute],
|
||||
},
|
||||
transaction
|
||||
});
|
||||
if (target) {
|
||||
await this[accessors.set](data[targetAttribute], opts);
|
||||
await target.update(data, opts);
|
||||
// @ts-ignore
|
||||
await target.updateAssociations(data, opts);
|
||||
} else {
|
||||
const t = await this[accessors.create](data, opts);
|
||||
await t.updateAssociations(data, opts);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const t = await this[accessors.create](data, opts);
|
||||
@ -513,8 +518,8 @@ export abstract class Model extends SequelizeModel {
|
||||
*/
|
||||
async updateAssociations(data: any, options: UpdateAssociationOptions = {}) {
|
||||
const { transaction = await this.sequelize.transaction() } = options;
|
||||
const table = this.database.getTable(this.constructor.name);
|
||||
for (const key of table.getAssociations().keys()) {
|
||||
// @ts-ignore 判断 Model.associations 更准确
|
||||
for (const key of Object.keys(this.constructor.associations)) {
|
||||
// 如果 key 不存在才跳过
|
||||
if (!Object.keys(data).includes(key)) {
|
||||
continue;
|
||||
|
@ -336,3 +336,14 @@ export function isNumber(num) {
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
let IDX = 36,
|
||||
HEX = ''
|
||||
while (IDX--) HEX += IDX.toString(36)
|
||||
|
||||
export function uid(len?: number) {
|
||||
let str = '',
|
||||
num = len || 11
|
||||
while (num--) str += HEX[(Math.random() * 36) | 0]
|
||||
return str
|
||||
}
|
||||
|
7
packages/plugin-collections-v04/.npmignore
Normal file
7
packages/plugin-collections-v04/.npmignore
Normal file
@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
*.log
|
||||
docs
|
||||
__tests__
|
||||
tsconfig.json
|
||||
src
|
||||
.fatherrc.ts
|
16
packages/plugin-collections-v04/package.json
Normal file
16
packages/plugin-collections-v04/package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-collections-v04",
|
||||
"version": "0.4.0-alpha.7",
|
||||
"main": "lib/index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nocobase/actions": "^0.4.0-alpha.7"
|
||||
},
|
||||
"gitHead": "f0b335ac30f29f25c95d7d137655fa64d8d67f1e"
|
||||
}
|
144
packages/plugin-collections-v04/src/__tests__/index.ts
Normal file
144
packages/plugin-collections-v04/src/__tests__/index.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import qs from 'qs';
|
||||
import plugin from '../server';
|
||||
import supertest from 'supertest';
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import { Dialect } from 'sequelize';
|
||||
import Database from '@nocobase/database';
|
||||
import { actions, middlewares } from '@nocobase/actions';
|
||||
import { Application, middleware } from '@nocobase/server';
|
||||
|
||||
function getTestKey() {
|
||||
const { id } = require.main;
|
||||
const key = id
|
||||
.replace(`${process.env.PWD}/packages`, '')
|
||||
.replace(/src\/__tests__/g, '')
|
||||
.replace('.test.ts', '')
|
||||
.replace(/[^\w]/g, '_')
|
||||
.replace(/_+/g, '_');
|
||||
return key
|
||||
}
|
||||
|
||||
const config = {
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_DATABASE,
|
||||
host: process.env.DB_HOST,
|
||||
port: Number.parseInt(process.env.DB_PORT, 10),
|
||||
dialect: process.env.DB_DIALECT as Dialect,
|
||||
define: {
|
||||
hooks: {
|
||||
beforeCreate(model, options) {
|
||||
|
||||
},
|
||||
},
|
||||
},
|
||||
logging: false,
|
||||
sync: {
|
||||
force: true,
|
||||
alter: {
|
||||
drop: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export async function getApp() {
|
||||
const app = new Application({
|
||||
database: {
|
||||
...config,
|
||||
hooks: {
|
||||
beforeDefine(columns, model) {
|
||||
model.tableName = `${getTestKey()}_${model.tableName || model.name.plural}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
resourcer: {
|
||||
prefix: '/api',
|
||||
},
|
||||
});
|
||||
app.resourcer.use(middlewares.associated);
|
||||
app.resourcer.registerActionHandlers({ ...actions.associate, ...actions.common });
|
||||
app.registerPlugin('collections', [plugin]);
|
||||
await app.loadPlugins();
|
||||
await app.database.sync();
|
||||
// 表配置信息存到数据库里
|
||||
// const tables = app.database.getTables([]);
|
||||
// for (const table of tables) {
|
||||
// const Collection = app.database.getModel('collections');
|
||||
// await Collection.import(table.getOptions(), { hooks: false });
|
||||
// }
|
||||
app.use(async (ctx, next) => {
|
||||
ctx.db = app.database;
|
||||
await next();
|
||||
});
|
||||
app.use(bodyParser());
|
||||
app.use(middleware({
|
||||
prefix: '/api',
|
||||
resourcer: app.resourcer,
|
||||
database: app.database,
|
||||
}));
|
||||
return app;
|
||||
}
|
||||
|
||||
interface ActionParams {
|
||||
resourceKey?: string | number;
|
||||
// resourceName?: string;
|
||||
// associatedName?: string;
|
||||
associatedKey?: string | number;
|
||||
fields?: any;
|
||||
filter?: any;
|
||||
values?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface Handler {
|
||||
get: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
list: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
create: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
update: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
destroy: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
[name: string]: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
}
|
||||
|
||||
export interface Agent {
|
||||
resource: (name: string) => Handler;
|
||||
}
|
||||
|
||||
export function getAgent(app: Application): Agent {
|
||||
const agent = supertest.agent(app.callback());
|
||||
return {
|
||||
resource(name: string): any {
|
||||
return new Proxy({}, {
|
||||
get(target, method, receiver) {
|
||||
return (params: ActionParams = {}) => {
|
||||
const { associatedKey, resourceKey, values = {}, ...restParams } = params;
|
||||
let url = `/api/${name}`;
|
||||
if (associatedKey) {
|
||||
url = `/api/${name.split('.').join(`/${associatedKey}/`)}`;
|
||||
}
|
||||
url += `:${method as string}`;
|
||||
if (resourceKey) {
|
||||
url += `/${resourceKey}`;
|
||||
}
|
||||
console.log(url);
|
||||
if (['list', 'get'].indexOf(method as string) !== -1) {
|
||||
return agent.get(`${url}?${qs.stringify(restParams)}`);
|
||||
} else {
|
||||
return agent.post(`${url}?${qs.stringify(restParams)}`).send(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getDatabase() {
|
||||
return new Database({
|
||||
...config,
|
||||
hooks: {
|
||||
beforeDefine(columns, model) {
|
||||
model.tableName = `${getTestKey()}_${model.tableName || model.name.plural}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
379
packages/plugin-collections-v04/src/collections/collections.ts
Normal file
379
packages/plugin-collections-v04/src/collections/collections.ts
Normal file
@ -0,0 +1,379 @@
|
||||
import { TableOptions } from '@nocobase/database';
|
||||
|
||||
export default {
|
||||
name: 'collections',
|
||||
title: '数据表配置',
|
||||
internal: true,
|
||||
sortable: true,
|
||||
draggable: true,
|
||||
model: 'CollectionModel',
|
||||
developerMode: true,
|
||||
createdAt: 'createdTime',
|
||||
updatedAt: 'updatedTime',
|
||||
fields: [
|
||||
// {
|
||||
// interface: 'sort',
|
||||
// type: 'sort',
|
||||
// name: 'sort',
|
||||
// title: '排序',
|
||||
// component: {
|
||||
// type: 'sort',
|
||||
// className: 'drag-visible',
|
||||
// width: 60,
|
||||
// showInTable: true,
|
||||
// },
|
||||
// },
|
||||
{
|
||||
interface: 'string',
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
title: '数据表名称',
|
||||
required: true,
|
||||
component: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'string',
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
createOnly: true,
|
||||
title: '标识',
|
||||
unique: true,
|
||||
required: true,
|
||||
developerMode: true,
|
||||
component: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'textarea',
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
title: '数据表描述',
|
||||
component: {
|
||||
type: 'textarea',
|
||||
},
|
||||
},
|
||||
// {
|
||||
// interface: 'boolean',
|
||||
// type: 'virtual',
|
||||
// name: 'createdAt',
|
||||
// title: '记录创建时间',
|
||||
// developerMode: true,
|
||||
// defaultValue: true,
|
||||
// component: {
|
||||
// type: 'checkbox',
|
||||
// default: true,
|
||||
// showInForm: true,
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// interface: 'boolean',
|
||||
// type: 'virtual',
|
||||
// name: 'updatedAt',
|
||||
// title: '记录修改时间',
|
||||
// developerMode: true,
|
||||
// defaultValue: true,
|
||||
// component: {
|
||||
// type: 'checkbox',
|
||||
// default: true,
|
||||
// showInForm: true,
|
||||
// },
|
||||
// },
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'virtual',
|
||||
name: 'createdBy',
|
||||
title: '记录创建人信息',
|
||||
developerMode: true,
|
||||
component: {
|
||||
type: 'checkbox',
|
||||
default: true,
|
||||
showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'virtual',
|
||||
name: 'updatedBy',
|
||||
title: '记录修改人信息',
|
||||
developerMode: true,
|
||||
component: {
|
||||
type: 'checkbox',
|
||||
default: true,
|
||||
showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'boolean',
|
||||
name: 'developerMode',
|
||||
title: '开发者模式',
|
||||
developerMode: true,
|
||||
defaultValue: false,
|
||||
component: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'json',
|
||||
type: 'json',
|
||||
name: 'options',
|
||||
title: '配置信息',
|
||||
defaultValue: {},
|
||||
component: {
|
||||
type: 'hidden',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'boolean',
|
||||
name: 'internal',
|
||||
title: '系统内置',
|
||||
defaultValue: false,
|
||||
developerMode: true,
|
||||
component: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'linkTo',
|
||||
type: 'hasMany',
|
||||
name: 'fields',
|
||||
title: '字段',
|
||||
sourceKey: 'name',
|
||||
draggable: true,
|
||||
actions: {
|
||||
list: {
|
||||
sort: 'sort',
|
||||
},
|
||||
get: {
|
||||
fields: {
|
||||
appends: ['children'],
|
||||
},
|
||||
},
|
||||
},
|
||||
component: {
|
||||
type: 'drawerSelect',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'linkTo',
|
||||
type: 'hasMany',
|
||||
name: 'actions',
|
||||
title: '动作',
|
||||
sourceKey: 'name',
|
||||
draggable: true,
|
||||
actions: {
|
||||
list: {
|
||||
sort: 'sort',
|
||||
},
|
||||
},
|
||||
component: {
|
||||
type: 'drawerSelect',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'linkTo',
|
||||
type: 'hasMany',
|
||||
name: 'views_v2',
|
||||
target: 'views_v2',
|
||||
title: '视图',
|
||||
sourceKey: 'name',
|
||||
draggable: true,
|
||||
// actions: {
|
||||
// list: {
|
||||
// sort: 'sort',
|
||||
// },
|
||||
// destroy: {
|
||||
// filter: {
|
||||
// default: false
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
component: {
|
||||
type: 'drawerSelect',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'linkTo',
|
||||
type: 'hasMany',
|
||||
name: 'scopes',
|
||||
target: 'scopes',
|
||||
title: '数据范围',
|
||||
sourceKey: 'name',
|
||||
actions: {
|
||||
list: {
|
||||
sort: 'id',
|
||||
},
|
||||
update: {
|
||||
filter: {
|
||||
locked: false
|
||||
}
|
||||
},
|
||||
destroy: {
|
||||
filter: {
|
||||
locked: false
|
||||
}
|
||||
}
|
||||
},
|
||||
component: {
|
||||
type: 'drawerSelect',
|
||||
},
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: 'list',
|
||||
name: 'list',
|
||||
title: '查看',
|
||||
},
|
||||
{
|
||||
type: 'destroy',
|
||||
name: 'destroy',
|
||||
title: '删除',
|
||||
},
|
||||
{
|
||||
type: 'create',
|
||||
name: 'create',
|
||||
title: '新增',
|
||||
viewName: 'form',
|
||||
},
|
||||
{
|
||||
type: 'update',
|
||||
name: 'update',
|
||||
title: '编辑',
|
||||
viewName: 'form',
|
||||
},
|
||||
],
|
||||
views_v2: [
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'table',
|
||||
name: 'table',
|
||||
title: '全部数据',
|
||||
labelField: 'title',
|
||||
actions: [
|
||||
{
|
||||
name: 'create',
|
||||
type: 'create',
|
||||
title: '新增',
|
||||
viewName: 'form',
|
||||
},
|
||||
{
|
||||
name: 'destroy',
|
||||
type: 'destroy',
|
||||
title: '删除',
|
||||
},
|
||||
],
|
||||
fields: ['title', 'description'],
|
||||
detailsOpenMode: 'window', // window
|
||||
details: ['descriptions', 'fields', 'views'],
|
||||
sort: ['id'],
|
||||
},
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'form',
|
||||
name: 'form',
|
||||
title: '表单',
|
||||
fields: ['title', 'description'],
|
||||
},
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'descriptions',
|
||||
name: 'descriptions',
|
||||
title: '详情',
|
||||
fields: ['title', 'description'],
|
||||
actions: [
|
||||
{
|
||||
name: 'update',
|
||||
type: 'update',
|
||||
title: '编辑',
|
||||
viewName: 'form',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'table',
|
||||
name: 'permissions_table',
|
||||
title: '权限表格',
|
||||
labelField: 'title',
|
||||
actions: [],
|
||||
fields: ['title'],
|
||||
detailsOpenMode: 'drawer', // window
|
||||
details: ['permissions_form'],
|
||||
sort: ['id'],
|
||||
},
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'form',
|
||||
name: 'permissions_form',
|
||||
title: '权限表单',
|
||||
fields: [
|
||||
{
|
||||
interface: 'json',
|
||||
type: 'json',
|
||||
title: '数据操作权限',
|
||||
name: 'actions',
|
||||
component: {
|
||||
"type": "permissions.actions",
|
||||
"title": "数据操作权限",
|
||||
"x-linkages": [{
|
||||
"type": "value:schema",
|
||||
"target": "actions",
|
||||
"schema": {
|
||||
"x-component-props": {
|
||||
"resourceKey": "{{ $form.values && $form.values.resourceKey }}"
|
||||
}
|
||||
}
|
||||
}],
|
||||
"x-component-props": {
|
||||
"dataSource": []
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'json',
|
||||
type: 'json',
|
||||
title: '字段权限',
|
||||
name: 'fields',
|
||||
component: {
|
||||
"type": "permissions.fields",
|
||||
"x-linkages": [{
|
||||
"type": "value:schema",
|
||||
"target": "fields",
|
||||
"schema": {
|
||||
"x-component-props": {
|
||||
"resourceKey": "{{ $form.values && $form.values.resourceKey }}"
|
||||
}
|
||||
}
|
||||
}],
|
||||
"x-component-props": {
|
||||
"dataSource": []
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'table',
|
||||
dataSourceType: 'association',
|
||||
name: 'fields',
|
||||
title: '字段',
|
||||
targetViewName: 'table2',
|
||||
targetFieldName: 'fields',
|
||||
},
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'table',
|
||||
dataSourceType: 'association',
|
||||
name: 'views',
|
||||
title: '视图',
|
||||
targetViewName: 'table',
|
||||
targetFieldName: 'views_v2',
|
||||
},
|
||||
],
|
||||
} as TableOptions;
|
690
packages/plugin-collections-v04/src/collections/fields.ts
Normal file
690
packages/plugin-collections-v04/src/collections/fields.ts
Normal file
@ -0,0 +1,690 @@
|
||||
import { TableOptions } from '@nocobase/database';
|
||||
import { types, getOptions } from '../interfaces';
|
||||
|
||||
export default {
|
||||
name: 'fields',
|
||||
title: '字段配置',
|
||||
internal: true,
|
||||
draggable: true,
|
||||
model: 'FieldModel',
|
||||
developerMode: true,
|
||||
fields: [
|
||||
{
|
||||
interface: 'sort',
|
||||
type: 'sort',
|
||||
name: 'sort',
|
||||
scope: ['collection'],
|
||||
title: '排序',
|
||||
component: {
|
||||
type: 'sort',
|
||||
className: 'drag-visible',
|
||||
width: 60,
|
||||
showInTable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'string',
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
title: '字段名称',
|
||||
required: true,
|
||||
component: {
|
||||
type: 'string',
|
||||
className: 'drag-visible',
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'string',
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
title: '标识',
|
||||
required: true,
|
||||
createOnly: true,
|
||||
developerMode: true,
|
||||
component: {
|
||||
type: 'string',
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'select',
|
||||
type: 'string',
|
||||
name: 'interface',
|
||||
title: '字段类型',
|
||||
required: true,
|
||||
dataSource: getOptions(),
|
||||
createOnly: true,
|
||||
component: {
|
||||
type: 'select',
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
"x-linkages": [
|
||||
// TODO(draft): 统一解决字段类型和配置参数联动的一种方式
|
||||
// {
|
||||
// type: 'value:schema',
|
||||
// target: 'options',
|
||||
// schema: {
|
||||
// 'x-component-props': {
|
||||
// fields: '{{ $self.values[1].fields || [] }}'
|
||||
// }
|
||||
// },
|
||||
// condition: '{{ !!$self.value }}'
|
||||
// },
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "precision",
|
||||
"condition": "{{ ['number', 'percent'].indexOf($self.value) !== -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "dataSource",
|
||||
"condition": "{{ ['select', 'multipleSelect', 'radio', 'checkboxes'].indexOf($self.value) !== -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "dateFormat",
|
||||
"condition": "{{ ['datetime', 'createdAt', 'updatedAt'].indexOf($self.value) !== -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "showTime",
|
||||
"condition": "{{ ['datetime', 'createdAt', 'updatedAt'].indexOf($self.value) !== -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "timeFormat",
|
||||
"condition": "{{ ['time'].indexOf($self.value) !== -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "multiple",
|
||||
"condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "target",
|
||||
"condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
|
||||
},
|
||||
// {
|
||||
// "type": "value:visible",
|
||||
// "target": "labelField",
|
||||
// "condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
|
||||
// },
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "createable",
|
||||
"condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "children",
|
||||
"condition": "{{ ['subTable'].indexOf($self.value) !== -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "component.showInTable",
|
||||
"condition": "{{ ['subTable', 'description'].indexOf($self.value) === -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "component.showInForm",
|
||||
"condition": "{{ ['createdAt', 'updatedAt', 'createdBy', 'updatedBy'].indexOf($self.value) === -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "required",
|
||||
"condition": "{{ ['createdAt', 'updatedAt', 'createdBy', 'updatedBy'].indexOf($self.value) === -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "maxLevel",
|
||||
"condition": "{{ ['chinaRegion'].includes($self.value) }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "incompletely",
|
||||
"condition": "{{ ['chinaRegion'].includes($self.value) }}"
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// TODO(draft): 将 options 作为集合字段开放出来,可以动态的解决字段参数的配置表单联动问题
|
||||
// {
|
||||
// interface: 'json',
|
||||
// type: 'json',
|
||||
// name: 'options',
|
||||
// title: '配置信息',
|
||||
// defaultValue: {},
|
||||
// component: {
|
||||
// type: 'subFields',
|
||||
// showInForm: true,
|
||||
// },
|
||||
// },
|
||||
{
|
||||
interface: 'subTable',
|
||||
type: 'virtual',
|
||||
name: 'dataSource',
|
||||
title: '可选项',
|
||||
component: {
|
||||
type: 'table',
|
||||
default: [{}],
|
||||
// showInTable: true,
|
||||
// showInDetail: true,
|
||||
showInForm: true,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
value: {
|
||||
type: "string",
|
||||
title: "值",
|
||||
required: true
|
||||
},
|
||||
label: {
|
||||
type: "string",
|
||||
title: "选项",
|
||||
required: true
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'string',
|
||||
type: 'string',
|
||||
name: 'type',
|
||||
title: '数据类型',
|
||||
developerMode: true,
|
||||
component: {
|
||||
type: 'string',
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'number',
|
||||
type: 'integer',
|
||||
name: 'parent_id',
|
||||
title: '所属分组',
|
||||
component: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'select',
|
||||
type: 'virtual',
|
||||
name: 'precision',
|
||||
title: '精度',
|
||||
dataSource: [
|
||||
{ value: 0, label: '1' },
|
||||
{ value: 1, label: '1.0' },
|
||||
{ value: 2, label: '1.00' },
|
||||
{ value: 3, label: '1.000' },
|
||||
{ value: 4, label: '1.0000' },
|
||||
],
|
||||
component: {
|
||||
type: 'number',
|
||||
showInForm: true,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'select',
|
||||
type: 'virtual',
|
||||
name: 'dateFormat',
|
||||
title: '日期格式',
|
||||
dataSource: [
|
||||
{ value: 'YYYY/MM/DD', label: '年/月/日' },
|
||||
{ value: 'YYYY-MM-DD', label: '年-月-日' },
|
||||
{ value: 'DD/MM/YYYY', label: '日/月/年' },
|
||||
],
|
||||
component: {
|
||||
type: 'string',
|
||||
showInForm: true,
|
||||
default: 'YYYY-MM-DD',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'virtual',
|
||||
name: 'showTime',
|
||||
title: '显示时间',
|
||||
component: {
|
||||
type: 'boolean',
|
||||
showInForm: true,
|
||||
default: false,
|
||||
"x-linkages": [
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "timeFormat",
|
||||
"condition": "{{ ($form.values && $form.values.interface === 'time') || $self.value === true }}"
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'select',
|
||||
type: 'virtual',
|
||||
name: 'timeFormat',
|
||||
title: '时间格式',
|
||||
dataSource: [
|
||||
{ value: 'HH:mm:ss', label: '24小时制' },
|
||||
{ value: 'hh:mm:ss a', label: '12小时制' },
|
||||
],
|
||||
component: {
|
||||
type: 'string',
|
||||
showInForm: true,
|
||||
default: 'HH:mm:ss',
|
||||
},
|
||||
},
|
||||
// TODO(refactor): 此部分类型相关的参数,后期应拆分出去
|
||||
{
|
||||
name: 'maxLevel',
|
||||
title: '可选层级',
|
||||
interface: 'radio',
|
||||
type: 'virtual',
|
||||
dataSource: [
|
||||
{ value: 1, label: '省' },
|
||||
{ value: 2, label: '市' },
|
||||
{ value: 3, label: '区/县' },
|
||||
{ value: 4, label: '乡镇/街道' },
|
||||
{ value: 5, label: '村/居委会' },
|
||||
],
|
||||
component: {
|
||||
showInForm: true,
|
||||
default: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'incompletely',
|
||||
title: '可部分选择',
|
||||
interface: 'boolean',
|
||||
type: 'virtual',
|
||||
component: {
|
||||
showInForm: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
interface: 'linkTo',
|
||||
multiple: false,
|
||||
type: 'belongsTo',
|
||||
name: 'parent',
|
||||
title: '所属分组',
|
||||
target: 'fields',
|
||||
foreignKey: 'parent_id',
|
||||
targetKey: 'id',
|
||||
component: {
|
||||
type: 'drawerSelect',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'string',
|
||||
type: 'virtual',
|
||||
name: 'target',
|
||||
title: '要关联的数据表',
|
||||
required: true,
|
||||
createOnly: true,
|
||||
component: {
|
||||
type: 'remoteSelect',
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
'x-component-props': {
|
||||
mode: 'simple',
|
||||
resourceName: 'collections',
|
||||
labelField: 'title',
|
||||
valueField: 'name',
|
||||
},
|
||||
"x-linkages": [
|
||||
{
|
||||
type: "value:state",
|
||||
target: "labelField",
|
||||
condition: "{{ $self.inputed }}",
|
||||
state: {
|
||||
value: null,
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "labelField",
|
||||
"condition": "{{ !!$self.value }}"
|
||||
},
|
||||
{
|
||||
type: "value:schema",
|
||||
target: "labelField",
|
||||
// condition: "{{ $self.value }}",
|
||||
schema: {
|
||||
"x-component-props": {
|
||||
"associatedKey": "{{ $self.value }}"
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value:visible',
|
||||
target: 'component.x-component-props.filter',
|
||||
condition: '{{ !!$self.value }}'
|
||||
},
|
||||
{
|
||||
type: "value:schema",
|
||||
target: "component.x-component-props.filter",
|
||||
schema: {
|
||||
"x-component-props": {
|
||||
"associatedKey": "{{ $self.value }}"
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'string',
|
||||
type: 'virtual',
|
||||
name: 'labelField',
|
||||
title: '要显示的字段',
|
||||
required: true,
|
||||
component: {
|
||||
type: 'remoteSelect',
|
||||
'x-component-props': {
|
||||
mode: 'simple',
|
||||
resourceName: 'collections.fields',
|
||||
labelField: 'title',
|
||||
valueField: 'name',
|
||||
},
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'virtual',
|
||||
name: 'multiple',
|
||||
title: '允许添加多条记录',
|
||||
component: {
|
||||
type: 'checkbox',
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'component.x-component-props.filter',
|
||||
interface: 'json',
|
||||
type: 'virtual',
|
||||
title: '数据范围',
|
||||
component: {
|
||||
type: 'filter',
|
||||
'x-component-props': {
|
||||
resourceName: 'collections.fields',
|
||||
},
|
||||
showInForm: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'virtual',
|
||||
name: 'createable',
|
||||
title: '允许直接在关联的数据表内新建数据',
|
||||
component: {
|
||||
type: 'checkbox',
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'subTable',
|
||||
type: 'hasMany',
|
||||
name: 'children',
|
||||
target: 'fields',
|
||||
sourceKey: 'id',
|
||||
foreignKey: 'parent_id',
|
||||
title: '子表格字段',
|
||||
viewName: 'table2',
|
||||
// visible: true,
|
||||
component: {
|
||||
type: 'subTable',
|
||||
default: [],
|
||||
// showInTable: true,
|
||||
// showInDetail: true,
|
||||
showInForm: true,
|
||||
'x-linkages': [
|
||||
{
|
||||
type: 'value:schema',
|
||||
target: 'children',
|
||||
schema: {
|
||||
'x-component-props': {
|
||||
associatedKey: "{{ $form.values && $form.values.id }}"
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// {
|
||||
// interface: 'linkTo',
|
||||
// multiple: true,
|
||||
// type: 'hasMany',
|
||||
// name: 'children',
|
||||
// title: '子字段',
|
||||
// target: 'fields',
|
||||
// foreignKey: 'parent_id',
|
||||
// sourceKey: 'id',
|
||||
// component: {
|
||||
// type: 'drawerSelect',
|
||||
// },
|
||||
// },
|
||||
{
|
||||
interface: 'textarea',
|
||||
type: 'virtual',
|
||||
name: 'component.tooltip',
|
||||
title: '提示信息',
|
||||
component: {
|
||||
type: 'textarea',
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'boolean',
|
||||
name: 'required',
|
||||
title: '必填项',
|
||||
component: {
|
||||
type: 'checkbox',
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'virtual',
|
||||
name: 'component.showInTable',
|
||||
title: '显示在表格中',
|
||||
component: {
|
||||
type: 'checkbox',
|
||||
tooltip: '若勾选,该字段将作为一列显示在数据表里',
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'virtual',
|
||||
name: 'component.showInForm',
|
||||
title: '显示在表单中',
|
||||
component: {
|
||||
type: 'checkbox',
|
||||
tooltip: '若勾选,该字段将出现在表单中',
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'virtual',
|
||||
name: 'component.showInDetail',
|
||||
title: '显示在详情中',
|
||||
component: {
|
||||
type: 'checkbox',
|
||||
tooltip: '若勾选,该字段将出现在详情中',
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'linkTo',
|
||||
type: 'belongsTo',
|
||||
name: 'collection',
|
||||
title: '所属数据表',
|
||||
target: 'collections',
|
||||
targetKey: 'name',
|
||||
labelField: 'title',
|
||||
component: {
|
||||
type: 'drawerSelect',
|
||||
// showInTable: true,
|
||||
'x-component-props': {
|
||||
resourceName: 'collections.fields',
|
||||
labelField: 'title',
|
||||
valueField: 'name',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'boolean',
|
||||
name: 'developerMode',
|
||||
title: '开发者模式',
|
||||
defaultValue: false,
|
||||
component: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'json',
|
||||
type: 'json',
|
||||
name: 'component',
|
||||
title: '前端组件',
|
||||
defaultValue: {},
|
||||
component: {
|
||||
type: 'hidden',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'json',
|
||||
type: 'json',
|
||||
name: 'options',
|
||||
title: '配置信息',
|
||||
defaultValue: {},
|
||||
component: {
|
||||
type: 'hidden',
|
||||
},
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: 'list',
|
||||
name: 'list',
|
||||
title: '查看',
|
||||
},
|
||||
{
|
||||
type: 'destroy',
|
||||
name: 'destroy',
|
||||
title: '删除',
|
||||
},
|
||||
{
|
||||
type: 'create',
|
||||
name: 'create',
|
||||
title: '新增',
|
||||
viewName: 'form',
|
||||
},
|
||||
{
|
||||
type: 'update',
|
||||
name: 'update',
|
||||
title: '编辑',
|
||||
viewName: 'form',
|
||||
},
|
||||
],
|
||||
views_v2: [
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'table',
|
||||
name: 'table',
|
||||
title: '关联的字段',
|
||||
labelField: 'title',
|
||||
actions: [
|
||||
{
|
||||
name: 'create',
|
||||
type: 'create',
|
||||
title: '新增',
|
||||
viewName: 'form',
|
||||
},
|
||||
{
|
||||
name: 'destroy',
|
||||
type: 'destroy',
|
||||
title: '删除',
|
||||
},
|
||||
],
|
||||
fields: ['title', 'interface'],
|
||||
detailsOpenMode: 'drawer', // window
|
||||
details: ['form'],
|
||||
sort: ['sort'],
|
||||
},
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'table',
|
||||
name: 'table2',
|
||||
title: '表格',
|
||||
labelField: 'title',
|
||||
actions: [
|
||||
{
|
||||
name: 'create',
|
||||
type: 'create',
|
||||
title: '新增',
|
||||
viewName: 'form',
|
||||
},
|
||||
{
|
||||
name: 'destroy',
|
||||
type: 'destroy',
|
||||
title: '删除',
|
||||
},
|
||||
],
|
||||
fields: ['sort', 'title', 'interface'],
|
||||
detailsOpenMode: 'drawer', // window
|
||||
details: ['form'],
|
||||
sort: ['sort'],
|
||||
},
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'form',
|
||||
name: 'form',
|
||||
title: '表单',
|
||||
fields: [
|
||||
'title',
|
||||
'interface',
|
||||
'dataSource',
|
||||
'precision',
|
||||
'dateFormat',
|
||||
'showTime',
|
||||
'timeFormat',
|
||||
'maxLevel',
|
||||
'incompletely',
|
||||
'target',
|
||||
'labelField',
|
||||
'children',
|
||||
'multiple',
|
||||
// 'required',
|
||||
],
|
||||
},
|
||||
],
|
||||
} as TableOptions;
|
241
packages/plugin-collections-v04/src/models/collection.ts
Normal file
241
packages/plugin-collections-v04/src/models/collection.ts
Normal file
@ -0,0 +1,241 @@
|
||||
import _ from 'lodash';
|
||||
import BaseModel from './base';
|
||||
import Field from './field';
|
||||
import { TableOptions } from '@nocobase/database';
|
||||
import { SaveOptions, Op } from 'sequelize';
|
||||
|
||||
/**
|
||||
* 生成随机数据库表名
|
||||
*
|
||||
* 策略:暂时使用 3+2
|
||||
* 1. 自增 id
|
||||
* 2. 随机字母
|
||||
* 3. 时间戳
|
||||
* 4. 转拼音
|
||||
* 5. 常见词翻译
|
||||
*
|
||||
* @param title 显示的名称
|
||||
*/
|
||||
export function generateCollectionName(title?: string): string {
|
||||
return `t_${Date.now().toString(36)}_${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
|
||||
}
|
||||
|
||||
export interface LoadOptions {
|
||||
reset?: boolean;
|
||||
where?: any;
|
||||
skipExisting?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface MigrateOptions {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class CollectionModel extends BaseModel {
|
||||
|
||||
generateName() {
|
||||
this.set('name', generateCollectionName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 name 获取 collection
|
||||
*
|
||||
* @param name
|
||||
*/
|
||||
static async findByName(name: string) {
|
||||
return this.findOne({ where: { name } });
|
||||
}
|
||||
|
||||
/**
|
||||
* DOTO:
|
||||
* - database.table 初始化可能存在一些缺陷
|
||||
* - 是否需要考虑关系数据的重载?
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
async loadTableOptions(opts: any = {}) {
|
||||
const options = await this.getOptions();
|
||||
// const prevTable = this.database.getTable(this.get('name'));
|
||||
// const prevOptions = prevTable ? prevTable.getOptions() : {};
|
||||
// table 是初始化和重新初始化
|
||||
const table = this.database.table(options);
|
||||
// console.log({options, actions: table.getOptions()['actions']})
|
||||
|
||||
// 如果关系表未加载,一起处理
|
||||
// const associationTableNames = [];
|
||||
// for (const [key, association] of table.getAssociations()) {
|
||||
// // TODO:是否需要考虑重载的情况?(暂时是跳过处理)
|
||||
// if (!this.database.isDefined(association.options.target)) {
|
||||
// continue;
|
||||
// }
|
||||
// associationTableNames.push(association.options.target);
|
||||
// }
|
||||
// console.log({associationTableNames});
|
||||
// if (associationTableNames.length) {
|
||||
// await CollectionModel.load({
|
||||
// ...opts,
|
||||
// where: {
|
||||
// name: {
|
||||
// [Op.in]: associationTableNames,
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
return table;
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移
|
||||
*/
|
||||
async migrate(options: MigrateOptions = {}) {
|
||||
const { isNewRecord } = options;
|
||||
const table = await this.loadTableOptions(options);
|
||||
// 如果不是新增数据,force 必须为 false
|
||||
if (!isNewRecord) {
|
||||
return await table.sync({
|
||||
force: false,
|
||||
alter: {
|
||||
drop: false,
|
||||
}
|
||||
});
|
||||
}
|
||||
// TODO: 暂时加了个 collectionSync 解决 collection.create 的数据不清空问题
|
||||
// @ts-ignore
|
||||
const sync = this.sequelize.options.collectionSync;
|
||||
return await table.sync(sync || {
|
||||
force: false,
|
||||
alter: {
|
||||
drop: false,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getFieldsOptions() {
|
||||
const fieldsOptions = [];
|
||||
const fields = await this.getFields();
|
||||
for (const field of fields) {
|
||||
fieldsOptions.push(await field.getOptions());
|
||||
}
|
||||
return fieldsOptions;
|
||||
}
|
||||
|
||||
async getOptions(): Promise<TableOptions> {
|
||||
const options: any = {
|
||||
...this.get(),
|
||||
actions: await this.getActions(),
|
||||
fields: await this.getFieldsOptions(),
|
||||
}
|
||||
// @ts-ignore
|
||||
// console.log(this.constructor.associations);
|
||||
// @ts-ignore
|
||||
if (this.constructor.hasAlias('views_v2')) {
|
||||
options.views_v2 = await this.getViews_v2();
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO:需要考虑是初次加载还是重载
|
||||
*
|
||||
* @param options
|
||||
*/
|
||||
static async load(options: LoadOptions = {}) {
|
||||
const { skipExisting = false, reset = false, where = {}, transaction } = options;
|
||||
const collections = await this.findAll({
|
||||
transaction,
|
||||
where,
|
||||
});
|
||||
for (const collection of collections) {
|
||||
if (skipExisting && this.database.isDefined(collection.get('name'))) {
|
||||
continue;
|
||||
}
|
||||
await collection.loadTableOptions({
|
||||
transaction,
|
||||
reset,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async import(data: TableOptions, options: SaveOptions = {}): Promise<CollectionModel> {
|
||||
data = _.cloneDeep(data);
|
||||
// @ts-ignore
|
||||
const { update } = options;
|
||||
let collection: CollectionModel;
|
||||
if (data.name) {
|
||||
collection = await this.findOne({
|
||||
...options,
|
||||
where: {
|
||||
name: data.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (collection) {
|
||||
// @ts-ignore
|
||||
await collection.update(data, options);
|
||||
}
|
||||
if (!collection) {
|
||||
// @ts-ignore
|
||||
collection = await this.create(data, options);
|
||||
}
|
||||
|
||||
const associations = ['fields', 'actions', 'views_v2'];
|
||||
for (const key of associations) {
|
||||
if (!Array.isArray(data[key])) {
|
||||
continue;
|
||||
}
|
||||
const Model = this.database.getModel(key);
|
||||
if (!Model) {
|
||||
continue;
|
||||
}
|
||||
let ids = [];
|
||||
for (const index in data[key]) {
|
||||
if (key === 'fields') {
|
||||
ids = await Model.import(data[key], {
|
||||
...options,
|
||||
collectionName: collection.name,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let model;
|
||||
const item = data[key][index];
|
||||
if (item.name) {
|
||||
model = await Model.findOne({
|
||||
...options,
|
||||
where: {
|
||||
collection_name: collection.name,
|
||||
name: item.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (model) {
|
||||
await model.update({
|
||||
...item,
|
||||
// sort: index+1
|
||||
}, options);
|
||||
}
|
||||
if (!model) {
|
||||
model = await Model.create(
|
||||
{
|
||||
...item,
|
||||
// sort: index+1,
|
||||
collection_name: collection.name,
|
||||
},
|
||||
// @ts-ignore
|
||||
options
|
||||
);
|
||||
}
|
||||
if (model) {
|
||||
ids.push(model.id);
|
||||
}
|
||||
}
|
||||
if (ids.length && collection.get('internal')) {
|
||||
await collection.updateAssociations({
|
||||
[key]: ids,
|
||||
});
|
||||
}
|
||||
}
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
|
||||
export default CollectionModel;
|
263
packages/plugin-collections-v04/src/models/field.ts
Normal file
263
packages/plugin-collections-v04/src/models/field.ts
Normal file
@ -0,0 +1,263 @@
|
||||
import _ from 'lodash';
|
||||
import BaseModel from './base';
|
||||
import { FieldOptions, BELONGSTO, BELONGSTOMANY, HASMANY } from '@nocobase/database';
|
||||
import * as types from '../interfaces/types';
|
||||
import { merge } from '../utils';
|
||||
import { BuildOptions } from 'sequelize';
|
||||
import { SaveOptions, Utils } from 'sequelize';
|
||||
import { generateCollectionName } from './collection';
|
||||
|
||||
interface FieldImportOptions extends SaveOptions {
|
||||
parentId?: number;
|
||||
collectionName?: string;
|
||||
}
|
||||
|
||||
export function generateValueName(title?: string): string {
|
||||
return `${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
|
||||
}
|
||||
|
||||
export function generateFieldName(title?: string): string {
|
||||
return `f_${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
|
||||
}
|
||||
|
||||
export class FieldModel extends BaseModel {
|
||||
|
||||
constructor(values: any = {}, options: any = {}) {
|
||||
let data = {
|
||||
...(values.options || {}),
|
||||
...values,
|
||||
// ..._.omit(values, 'options'),
|
||||
};
|
||||
const interfaceType = data.interface;
|
||||
if (interfaceType) {
|
||||
const { options } = types[interfaceType];
|
||||
let args = [options, data];
|
||||
// @ts-ignore
|
||||
data = merge(...args);
|
||||
if (['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(data.type)) {
|
||||
// 关系字段如果没有 name,相关参数都随机生成
|
||||
if (!data.name) {
|
||||
data.name = generateFieldName();
|
||||
data.paired = true;
|
||||
// 通用,关系表
|
||||
if (!data.target) {
|
||||
data.target = generateCollectionName();
|
||||
}
|
||||
// 通用,外键
|
||||
if (!data.foreignKey) {
|
||||
data.foreignKey = generateFieldName();
|
||||
}
|
||||
if (data.type !== 'belongsTo' && !data.sourceKey) {
|
||||
data.sourceKey = 'id';
|
||||
}
|
||||
if (['belongsTo', 'belongsToMany'].includes(data.type) && !data.targetKey) {
|
||||
data.targetKey = 'id';
|
||||
}
|
||||
// 多对多关联
|
||||
if (data.type === 'belongsToMany') {
|
||||
if (!data.through) {
|
||||
data.through = generateCollectionName();
|
||||
}
|
||||
if (!data.otherKey) {
|
||||
data.otherKey = generateFieldName();
|
||||
}
|
||||
}
|
||||
}
|
||||
// 有 name,但是没有 target
|
||||
if (!data.target) {
|
||||
data.target = ['hasOne', 'belongsTo'].includes(data.type) ? Utils.pluralize(data.name) : data.name;
|
||||
}
|
||||
}
|
||||
if (!data.name) {
|
||||
data.name = generateFieldName();
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
super(data, options);
|
||||
}
|
||||
|
||||
generateName() {
|
||||
this.set('name', generateFieldName());
|
||||
}
|
||||
|
||||
async generatePairField(options) {
|
||||
const { interface: control, paired, type, target, sourceKey, targetKey, foreignKey, otherKey, through, collection_name } = this.get();
|
||||
if (control !== 'linkTo' || type !== 'belongsToMany' || !collection_name || !paired) {
|
||||
return;
|
||||
}
|
||||
if (!this.database.isDefined(target)) {
|
||||
return;
|
||||
}
|
||||
const targetTable = this.database.getTable(target);
|
||||
const Field = FieldModel;
|
||||
let labelField = 'id';
|
||||
const targetField = await Field.findOne({
|
||||
...options,
|
||||
where: {
|
||||
type: 'string',
|
||||
collection_name: target,
|
||||
},
|
||||
order: [['sort', 'asc']],
|
||||
});
|
||||
if (targetField) {
|
||||
labelField = targetField.get('name');
|
||||
}
|
||||
const collection = await this.getCollection(options);
|
||||
let targetOptions: any = {
|
||||
...types.linkTo.options,
|
||||
interface: 'linkTo',
|
||||
title: collection.get('title'),
|
||||
collection_name: target,
|
||||
options: {
|
||||
paired: true,
|
||||
target: collection_name,
|
||||
labelField,
|
||||
},
|
||||
component: {
|
||||
showInTable: true,
|
||||
showInForm: true,
|
||||
showInDetail: true,
|
||||
},
|
||||
};
|
||||
// 暂时不处理 hasone
|
||||
switch (type) {
|
||||
case 'hasMany':
|
||||
targetOptions.type = 'belongsTo';
|
||||
targetOptions.options.targetKey = sourceKey;
|
||||
targetOptions.options.foreignKey = foreignKey;
|
||||
break;
|
||||
case 'belongsTo':
|
||||
targetOptions.type = 'hasMany';
|
||||
targetOptions.options.sourceKey = targetKey;
|
||||
targetOptions.options.foreignKey = foreignKey;
|
||||
break;
|
||||
case 'belongsToMany':
|
||||
targetOptions.type = 'belongsToMany';
|
||||
targetOptions.options.sourceKey = targetKey;
|
||||
targetOptions.options.foreignKey = otherKey;
|
||||
targetOptions.options.targetKey = sourceKey;
|
||||
targetOptions.options.otherKey = foreignKey;
|
||||
targetOptions.options.through = through;
|
||||
break;
|
||||
}
|
||||
const associations = targetTable.getAssociations();
|
||||
// console.log(associations);
|
||||
for (const association of associations.values()) {
|
||||
if (association instanceof BELONGSTOMANY) {
|
||||
if (
|
||||
association.options.foreignKey === otherKey
|
||||
&& association.options.sourceKey === targetKey
|
||||
&& association.options.otherKey === foreignKey
|
||||
&& association.options.targetKey === sourceKey
|
||||
&& association.options.through === through
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// if (association instanceof BELONGSTO) {
|
||||
// continue;
|
||||
// }
|
||||
// if (association instanceof HASMANY) {
|
||||
// continue;
|
||||
// }
|
||||
}
|
||||
const f = await Field.create(targetOptions, options);
|
||||
// console.log({targetOptions}, f.get('options'));
|
||||
}
|
||||
|
||||
setInterface(value) {
|
||||
const { options } = types[value];
|
||||
let args = [];
|
||||
// 如果是新数据或 interface 不相等,interface options 放后
|
||||
if (this.isNewRecord || this.get('interface') !== value) {
|
||||
args = [this.get(), options];
|
||||
} else {
|
||||
// 已存在的数据更新,不相等,interface options 放前面
|
||||
args = [options, this.get()];
|
||||
}
|
||||
// @ts-ignore
|
||||
const values = merge(...args);
|
||||
this.set(values);
|
||||
}
|
||||
|
||||
async getOptions(): Promise<FieldOptions> {
|
||||
return this.get();
|
||||
}
|
||||
|
||||
async migrate(options: any = {}) {
|
||||
const collectionName = this.get('collection_name');
|
||||
if (!collectionName) {
|
||||
return false;
|
||||
}
|
||||
if (!this.database.isDefined(collectionName)) {
|
||||
throw new Error(`${collectionName} is not defined`);
|
||||
}
|
||||
const table = this.database.getTable(collectionName);
|
||||
table.addField(await this.getOptions());
|
||||
await table.sync({
|
||||
force: false,
|
||||
alter: {
|
||||
drop: false,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async import(items: any, options: FieldImportOptions = {}): Promise<any> {
|
||||
const { parentId, collectionName } = options;
|
||||
if (!Array.isArray(items)) {
|
||||
items = [items];
|
||||
}
|
||||
const ids = [];
|
||||
for (const index in items) {
|
||||
const item = items[index];
|
||||
let model;
|
||||
const where: any = {};
|
||||
if (parentId) {
|
||||
where.parent_id = parentId
|
||||
} else {
|
||||
where.collection_name = collectionName;
|
||||
}
|
||||
if (item.name) {
|
||||
model = await this.findOne({
|
||||
...options,
|
||||
where: {
|
||||
...where,
|
||||
name: item.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!model) {
|
||||
const tmp: any = {};
|
||||
if (parentId) {
|
||||
tmp.parent_id = parentId
|
||||
} else {
|
||||
tmp.collection_name = collectionName;
|
||||
}
|
||||
model = await this.create(
|
||||
{
|
||||
...item,
|
||||
...tmp,
|
||||
},
|
||||
//@ts-ignore
|
||||
options
|
||||
);
|
||||
} else {
|
||||
//@ts-ignore
|
||||
await model.update(item, options);
|
||||
}
|
||||
if (Array.isArray(item.children)) {
|
||||
const childrenIds = await this.import(item.children, {
|
||||
...options,
|
||||
parentId: model.id,
|
||||
collectionName,
|
||||
});
|
||||
await model.updateAssociations({
|
||||
children: childrenIds,
|
||||
}, options);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
|
||||
export default FieldModel;
|
7
packages/plugin-collections-v04/src/models/index.ts
Normal file
7
packages/plugin-collections-v04/src/models/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * from './base';
|
||||
export * from './action';
|
||||
export * from './collection';
|
||||
export * from './field';
|
||||
export * from './tab';
|
||||
export * from './view';
|
||||
export * from './page';
|
77
packages/plugin-collections-v04/src/server.ts
Normal file
77
packages/plugin-collections-v04/src/server.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import path from 'path';
|
||||
import { Application } from '@nocobase/server';
|
||||
import hooks from './hooks';
|
||||
import { registerModels, Table } from '@nocobase/database';
|
||||
import * as models from './models';
|
||||
|
||||
export default async function (this: Application, options = {}) {
|
||||
const database = this.database;
|
||||
const resourcer = this.resourcer;
|
||||
// 提供全局的 models 注册机制
|
||||
registerModels(models);
|
||||
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
database.addHook('afterUpdateAssociations', async function (model, options) {
|
||||
if (model instanceof models.FieldModel) {
|
||||
if (model.get('interface') === 'subTable') {
|
||||
const { migrate = true } = options;
|
||||
const Collection = model.database.getModel('collections');
|
||||
await Collection.load({ ...options, where: { name: model.get('collection_name') } });
|
||||
migrate && await model.migrate(options);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(hooks).forEach(modelName => {
|
||||
const Model = database.getModel(modelName);
|
||||
Object.keys(hooks[modelName]).forEach(hookKey => {
|
||||
// TODO(types): 多层 map 映射类型定义较为复杂,暂时忽略
|
||||
// @ts-ignore
|
||||
Model.addHook(hookKey, hooks[modelName][hookKey]);
|
||||
});
|
||||
});
|
||||
|
||||
const Collection = database.getModel('collections');
|
||||
Collection.addHook('afterCreate', async (model: any, options) => {
|
||||
if (model.get('developerMode')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.get('statusable') === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("model.get('developerMode')", model.get('name'));
|
||||
|
||||
const { transaction = await model.sequelize.transaction() } = options;
|
||||
|
||||
await model.createField({
|
||||
interface: 'radio',
|
||||
name: 'status',
|
||||
type: 'string',
|
||||
filterable: true,
|
||||
title: '状态',
|
||||
// index: true,
|
||||
dataSource: [
|
||||
{
|
||||
label: '已发布',
|
||||
value: 'publish',
|
||||
},
|
||||
{
|
||||
label: '草稿',
|
||||
value: 'draft',
|
||||
}
|
||||
],
|
||||
component: {
|
||||
type: 'radio',
|
||||
},
|
||||
}, { transaction });
|
||||
|
||||
if (!options.transaction) {
|
||||
await transaction.commit();
|
||||
}
|
||||
});
|
||||
}
|
9
packages/plugin-collections-v04/src/utils.ts
Normal file
9
packages/plugin-collections-v04/src/utils.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
|
||||
const overwriteMerge = (destinationArray, sourceArray, options) => sourceArray
|
||||
|
||||
export function merge(obj1: any, obj2: any) {
|
||||
return deepmerge(obj1, obj2, {
|
||||
arrayMerge: overwriteMerge,
|
||||
});
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import { Agent, getAgent, getApp } from '.';
|
||||
import { Application } from '@nocobase/server';
|
||||
import Database from '@nocobase/database';
|
||||
|
||||
describe('collection hooks', () => {
|
||||
let app: Application;
|
||||
let agent: Agent;
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await getApp();
|
||||
db = app.database;
|
||||
agent = getAgent(app);
|
||||
});
|
||||
|
||||
afterEach(() => app.database.close());
|
||||
|
||||
it('create table', async () => {
|
||||
const Collection = db.getModel('collections');
|
||||
const collection = await Collection.create({
|
||||
title: 'tests',
|
||||
// name: 'tests',
|
||||
});
|
||||
await collection.updateAssociations({
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
dataType: 'string',
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
dataType: 'text',
|
||||
},
|
||||
{
|
||||
dataType: 'integer',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
@ -3,377 +3,28 @@ import { TableOptions } from '@nocobase/database';
|
||||
export default {
|
||||
name: 'collections',
|
||||
title: '数据表配置',
|
||||
internal: true,
|
||||
sortable: true,
|
||||
draggable: true,
|
||||
model: 'CollectionModel',
|
||||
developerMode: true,
|
||||
createdAt: 'createdTime',
|
||||
updatedAt: 'updatedTime',
|
||||
model: 'Collection',
|
||||
fields: [
|
||||
// {
|
||||
// interface: 'sort',
|
||||
// type: 'sort',
|
||||
// name: 'sort',
|
||||
// title: '排序',
|
||||
// component: {
|
||||
// type: 'sort',
|
||||
// className: 'drag-visible',
|
||||
// width: 60,
|
||||
// showInTable: true,
|
||||
// },
|
||||
// },
|
||||
{
|
||||
interface: 'string',
|
||||
type: 'uid',
|
||||
name: 'name',
|
||||
primaryKey: true,
|
||||
prefix: 't_',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
title: '数据表名称',
|
||||
required: true,
|
||||
component: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'string',
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
createOnly: true,
|
||||
title: '标识',
|
||||
unique: true,
|
||||
required: true,
|
||||
developerMode: true,
|
||||
component: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'textarea',
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
title: '数据表描述',
|
||||
component: {
|
||||
type: 'textarea',
|
||||
},
|
||||
},
|
||||
// {
|
||||
// interface: 'boolean',
|
||||
// type: 'virtual',
|
||||
// name: 'createdAt',
|
||||
// title: '记录创建时间',
|
||||
// developerMode: true,
|
||||
// defaultValue: true,
|
||||
// component: {
|
||||
// type: 'checkbox',
|
||||
// default: true,
|
||||
// showInForm: true,
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// interface: 'boolean',
|
||||
// type: 'virtual',
|
||||
// name: 'updatedAt',
|
||||
// title: '记录修改时间',
|
||||
// developerMode: true,
|
||||
// defaultValue: true,
|
||||
// component: {
|
||||
// type: 'checkbox',
|
||||
// default: true,
|
||||
// showInForm: true,
|
||||
// },
|
||||
// },
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'virtual',
|
||||
name: 'createdBy',
|
||||
title: '记录创建人信息',
|
||||
developerMode: true,
|
||||
component: {
|
||||
type: 'checkbox',
|
||||
default: true,
|
||||
showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'virtual',
|
||||
name: 'updatedBy',
|
||||
title: '记录修改人信息',
|
||||
developerMode: true,
|
||||
component: {
|
||||
type: 'checkbox',
|
||||
default: true,
|
||||
showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'boolean',
|
||||
name: 'developerMode',
|
||||
title: '开发者模式',
|
||||
developerMode: true,
|
||||
defaultValue: false,
|
||||
component: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'json',
|
||||
type: 'json',
|
||||
name: 'options',
|
||||
title: '配置信息',
|
||||
defaultValue: {},
|
||||
component: {
|
||||
type: 'hidden',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'boolean',
|
||||
name: 'internal',
|
||||
title: '系统内置',
|
||||
defaultValue: false,
|
||||
developerMode: true,
|
||||
component: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'linkTo',
|
||||
type: 'hasMany',
|
||||
name: 'fields',
|
||||
title: '字段',
|
||||
sourceKey: 'name',
|
||||
draggable: true,
|
||||
actions: {
|
||||
list: {
|
||||
sort: 'sort',
|
||||
},
|
||||
get: {
|
||||
fields: {
|
||||
appends: ['children'],
|
||||
},
|
||||
},
|
||||
},
|
||||
component: {
|
||||
type: 'drawerSelect',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'linkTo',
|
||||
type: 'hasMany',
|
||||
name: 'actions',
|
||||
title: '动作',
|
||||
sourceKey: 'name',
|
||||
draggable: true,
|
||||
actions: {
|
||||
list: {
|
||||
sort: 'sort',
|
||||
},
|
||||
},
|
||||
component: {
|
||||
type: 'drawerSelect',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'linkTo',
|
||||
type: 'hasMany',
|
||||
name: 'views_v2',
|
||||
target: 'views_v2',
|
||||
title: '视图',
|
||||
sourceKey: 'name',
|
||||
draggable: true,
|
||||
// actions: {
|
||||
// list: {
|
||||
// sort: 'sort',
|
||||
// },
|
||||
// destroy: {
|
||||
// filter: {
|
||||
// default: false
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
component: {
|
||||
type: 'drawerSelect',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'linkTo',
|
||||
type: 'hasMany',
|
||||
name: 'scopes',
|
||||
target: 'scopes',
|
||||
title: '数据范围',
|
||||
sourceKey: 'name',
|
||||
actions: {
|
||||
list: {
|
||||
sort: 'id',
|
||||
},
|
||||
update: {
|
||||
filter: {
|
||||
locked: false
|
||||
}
|
||||
},
|
||||
destroy: {
|
||||
filter: {
|
||||
locked: false
|
||||
}
|
||||
}
|
||||
},
|
||||
component: {
|
||||
type: 'drawerSelect',
|
||||
},
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: 'list',
|
||||
name: 'list',
|
||||
title: '查看',
|
||||
},
|
||||
{
|
||||
type: 'destroy',
|
||||
name: 'destroy',
|
||||
title: '删除',
|
||||
},
|
||||
{
|
||||
type: 'create',
|
||||
name: 'create',
|
||||
title: '新增',
|
||||
viewName: 'form',
|
||||
},
|
||||
{
|
||||
type: 'update',
|
||||
name: 'update',
|
||||
title: '编辑',
|
||||
viewName: 'form',
|
||||
},
|
||||
],
|
||||
views_v2: [
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'table',
|
||||
name: 'table',
|
||||
title: '全部数据',
|
||||
labelField: 'title',
|
||||
actions: [
|
||||
{
|
||||
name: 'create',
|
||||
type: 'create',
|
||||
title: '新增',
|
||||
viewName: 'form',
|
||||
},
|
||||
{
|
||||
name: 'destroy',
|
||||
type: 'destroy',
|
||||
title: '删除',
|
||||
},
|
||||
],
|
||||
fields: ['title', 'description'],
|
||||
detailsOpenMode: 'window', // window
|
||||
details: ['descriptions', 'fields', 'views'],
|
||||
sort: ['id'],
|
||||
},
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'form',
|
||||
name: 'form',
|
||||
title: '表单',
|
||||
fields: ['title', 'description'],
|
||||
},
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'descriptions',
|
||||
name: 'descriptions',
|
||||
title: '详情',
|
||||
fields: ['title', 'description'],
|
||||
actions: [
|
||||
{
|
||||
name: 'update',
|
||||
type: 'update',
|
||||
title: '编辑',
|
||||
viewName: 'form',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'table',
|
||||
name: 'permissions_table',
|
||||
title: '权限表格',
|
||||
labelField: 'title',
|
||||
actions: [],
|
||||
fields: ['title'],
|
||||
detailsOpenMode: 'drawer', // window
|
||||
details: ['permissions_form'],
|
||||
sort: ['id'],
|
||||
},
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'form',
|
||||
name: 'permissions_form',
|
||||
title: '权限表单',
|
||||
fields: [
|
||||
{
|
||||
interface: 'json',
|
||||
type: 'json',
|
||||
title: '数据操作权限',
|
||||
name: 'actions',
|
||||
component: {
|
||||
"type": "permissions.actions",
|
||||
"title": "数据操作权限",
|
||||
"x-linkages": [{
|
||||
"type": "value:schema",
|
||||
"target": "actions",
|
||||
"schema": {
|
||||
"x-component-props": {
|
||||
"resourceKey": "{{ $form.values && $form.values.resourceKey }}"
|
||||
}
|
||||
}
|
||||
}],
|
||||
"x-component-props": {
|
||||
"dataSource": []
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'json',
|
||||
type: 'json',
|
||||
title: '字段权限',
|
||||
name: 'fields',
|
||||
component: {
|
||||
"type": "permissions.fields",
|
||||
"x-linkages": [{
|
||||
"type": "value:schema",
|
||||
"target": "fields",
|
||||
"schema": {
|
||||
"x-component-props": {
|
||||
"resourceKey": "{{ $form.values && $form.values.resourceKey }}"
|
||||
}
|
||||
}
|
||||
}],
|
||||
"x-component-props": {
|
||||
"dataSource": []
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'table',
|
||||
dataSourceType: 'association',
|
||||
name: 'fields',
|
||||
title: '字段',
|
||||
targetViewName: 'table2',
|
||||
targetFieldName: 'fields',
|
||||
},
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'table',
|
||||
dataSourceType: 'association',
|
||||
name: 'views',
|
||||
title: '视图',
|
||||
targetViewName: 'table',
|
||||
targetFieldName: 'views_v2',
|
||||
},
|
||||
],
|
||||
} as TableOptions;
|
||||
|
@ -1,690 +1,55 @@
|
||||
import { TableOptions } from '@nocobase/database';
|
||||
import { types, getOptions } from '../interfaces';
|
||||
import { DataTypes } from 'sequelize';
|
||||
|
||||
export default {
|
||||
name: 'fields',
|
||||
title: '字段配置',
|
||||
internal: true,
|
||||
draggable: true,
|
||||
model: 'FieldModel',
|
||||
developerMode: true,
|
||||
model: 'Field',
|
||||
fields: [
|
||||
{
|
||||
interface: 'sort',
|
||||
type: 'sort',
|
||||
name: 'sort',
|
||||
scope: ['collection'],
|
||||
title: '排序',
|
||||
component: {
|
||||
type: 'sort',
|
||||
className: 'drag-visible',
|
||||
width: 60,
|
||||
showInTable: true,
|
||||
},
|
||||
type: 'uid',
|
||||
name: 'key',
|
||||
primaryKey: true,
|
||||
prefix: 'f_',
|
||||
// autoIncrement: false,
|
||||
// defaultValue: DataTypes.UUIDV4,
|
||||
},
|
||||
{
|
||||
interface: 'string',
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
title: '字段名称',
|
||||
required: true,
|
||||
component: {
|
||||
type: 'string',
|
||||
className: 'drag-visible',
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'string',
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
title: '标识',
|
||||
required: true,
|
||||
createOnly: true,
|
||||
developerMode: true,
|
||||
component: {
|
||||
type: 'string',
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'select',
|
||||
type: 'string',
|
||||
name: 'interface',
|
||||
title: '字段类型',
|
||||
required: true,
|
||||
dataSource: getOptions(),
|
||||
createOnly: true,
|
||||
component: {
|
||||
type: 'select',
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
"x-linkages": [
|
||||
// TODO(draft): 统一解决字段类型和配置参数联动的一种方式
|
||||
// {
|
||||
// type: 'value:schema',
|
||||
// target: 'options',
|
||||
// schema: {
|
||||
// 'x-component-props': {
|
||||
// fields: '{{ $self.values[1].fields || [] }}'
|
||||
// }
|
||||
// },
|
||||
// condition: '{{ !!$self.value }}'
|
||||
// },
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "precision",
|
||||
"condition": "{{ ['number', 'percent'].indexOf($self.value) !== -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "dataSource",
|
||||
"condition": "{{ ['select', 'multipleSelect', 'radio', 'checkboxes'].indexOf($self.value) !== -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "dateFormat",
|
||||
"condition": "{{ ['datetime', 'createdAt', 'updatedAt'].indexOf($self.value) !== -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "showTime",
|
||||
"condition": "{{ ['datetime', 'createdAt', 'updatedAt'].indexOf($self.value) !== -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "timeFormat",
|
||||
"condition": "{{ ['time'].indexOf($self.value) !== -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "multiple",
|
||||
"condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "target",
|
||||
"condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
|
||||
},
|
||||
// {
|
||||
// "type": "value:visible",
|
||||
// "target": "labelField",
|
||||
// "condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
|
||||
// },
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "createable",
|
||||
"condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "children",
|
||||
"condition": "{{ ['subTable'].indexOf($self.value) !== -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "component.showInTable",
|
||||
"condition": "{{ ['subTable', 'description'].indexOf($self.value) === -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "component.showInForm",
|
||||
"condition": "{{ ['createdAt', 'updatedAt', 'createdBy', 'updatedBy'].indexOf($self.value) === -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "required",
|
||||
"condition": "{{ ['createdAt', 'updatedAt', 'createdBy', 'updatedBy'].indexOf($self.value) === -1 }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "maxLevel",
|
||||
"condition": "{{ ['chinaRegion'].includes($self.value) }}"
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "incompletely",
|
||||
"condition": "{{ ['chinaRegion'].includes($self.value) }}"
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// TODO(draft): 将 options 作为集合字段开放出来,可以动态的解决字段参数的配置表单联动问题
|
||||
// {
|
||||
// interface: 'json',
|
||||
// type: 'json',
|
||||
// name: 'options',
|
||||
// title: '配置信息',
|
||||
// defaultValue: {},
|
||||
// component: {
|
||||
// type: 'subFields',
|
||||
// showInForm: true,
|
||||
// },
|
||||
// },
|
||||
{
|
||||
interface: 'subTable',
|
||||
type: 'virtual',
|
||||
name: 'dataSource',
|
||||
title: '可选项',
|
||||
component: {
|
||||
type: 'table',
|
||||
default: [{}],
|
||||
// showInTable: true,
|
||||
// showInDetail: true,
|
||||
showInForm: true,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
value: {
|
||||
type: "string",
|
||||
title: "值",
|
||||
required: true
|
||||
},
|
||||
label: {
|
||||
type: "string",
|
||||
title: "选项",
|
||||
required: true
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'string',
|
||||
type: 'string',
|
||||
name: 'type',
|
||||
title: '数据类型',
|
||||
developerMode: true,
|
||||
component: {
|
||||
type: 'string',
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
},
|
||||
name: 'dataType',
|
||||
},
|
||||
{
|
||||
interface: 'number',
|
||||
type: 'integer',
|
||||
name: 'parent_id',
|
||||
title: '所属分组',
|
||||
component: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'select',
|
||||
type: 'virtual',
|
||||
name: 'precision',
|
||||
title: '精度',
|
||||
dataSource: [
|
||||
{ value: 0, label: '1' },
|
||||
{ value: 1, label: '1.0' },
|
||||
{ value: 2, label: '1.00' },
|
||||
{ value: 3, label: '1.000' },
|
||||
{ value: 4, label: '1.0000' },
|
||||
],
|
||||
component: {
|
||||
type: 'number',
|
||||
showInForm: true,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'select',
|
||||
type: 'virtual',
|
||||
name: 'dateFormat',
|
||||
title: '日期格式',
|
||||
dataSource: [
|
||||
{ value: 'YYYY/MM/DD', label: '年/月/日' },
|
||||
{ value: 'YYYY-MM-DD', label: '年-月-日' },
|
||||
{ value: 'DD/MM/YYYY', label: '日/月/年' },
|
||||
],
|
||||
component: {
|
||||
type: 'string',
|
||||
showInForm: true,
|
||||
default: 'YYYY-MM-DD',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'virtual',
|
||||
name: 'showTime',
|
||||
title: '显示时间',
|
||||
component: {
|
||||
type: 'boolean',
|
||||
showInForm: true,
|
||||
default: false,
|
||||
"x-linkages": [
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "timeFormat",
|
||||
"condition": "{{ ($form.values && $form.values.interface === 'time') || $self.value === true }}"
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'select',
|
||||
type: 'virtual',
|
||||
name: 'timeFormat',
|
||||
title: '时间格式',
|
||||
dataSource: [
|
||||
{ value: 'HH:mm:ss', label: '24小时制' },
|
||||
{ value: 'hh:mm:ss a', label: '12小时制' },
|
||||
],
|
||||
component: {
|
||||
type: 'string',
|
||||
showInForm: true,
|
||||
default: 'HH:mm:ss',
|
||||
},
|
||||
},
|
||||
// TODO(refactor): 此部分类型相关的参数,后期应拆分出去
|
||||
{
|
||||
name: 'maxLevel',
|
||||
title: '可选层级',
|
||||
interface: 'radio',
|
||||
type: 'virtual',
|
||||
dataSource: [
|
||||
{ value: 1, label: '省' },
|
||||
{ value: 2, label: '市' },
|
||||
{ value: 3, label: '区/县' },
|
||||
{ value: 4, label: '乡镇/街道' },
|
||||
{ value: 5, label: '村/居委会' },
|
||||
],
|
||||
component: {
|
||||
showInForm: true,
|
||||
default: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'incompletely',
|
||||
title: '可部分选择',
|
||||
interface: 'boolean',
|
||||
type: 'virtual',
|
||||
component: {
|
||||
showInForm: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
interface: 'linkTo',
|
||||
multiple: false,
|
||||
type: 'belongsTo',
|
||||
name: 'parent',
|
||||
title: '所属分组',
|
||||
target: 'fields',
|
||||
foreignKey: 'parent_id',
|
||||
targetKey: 'id',
|
||||
component: {
|
||||
type: 'drawerSelect',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'string',
|
||||
type: 'virtual',
|
||||
name: 'target',
|
||||
title: '要关联的数据表',
|
||||
required: true,
|
||||
createOnly: true,
|
||||
component: {
|
||||
type: 'remoteSelect',
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
'x-component-props': {
|
||||
mode: 'simple',
|
||||
resourceName: 'collections',
|
||||
labelField: 'title',
|
||||
valueField: 'name',
|
||||
},
|
||||
"x-linkages": [
|
||||
{
|
||||
type: "value:state",
|
||||
target: "labelField",
|
||||
condition: "{{ $self.inputed }}",
|
||||
state: {
|
||||
value: null,
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "value:visible",
|
||||
"target": "labelField",
|
||||
"condition": "{{ !!$self.value }}"
|
||||
},
|
||||
{
|
||||
type: "value:schema",
|
||||
target: "labelField",
|
||||
// condition: "{{ $self.value }}",
|
||||
schema: {
|
||||
"x-component-props": {
|
||||
"associatedKey": "{{ $self.value }}"
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value:visible',
|
||||
target: 'component.x-component-props.filter',
|
||||
condition: '{{ !!$self.value }}'
|
||||
},
|
||||
{
|
||||
type: "value:schema",
|
||||
target: "component.x-component-props.filter",
|
||||
schema: {
|
||||
"x-component-props": {
|
||||
"associatedKey": "{{ $self.value }}"
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'string',
|
||||
type: 'virtual',
|
||||
name: 'labelField',
|
||||
title: '要显示的字段',
|
||||
required: true,
|
||||
component: {
|
||||
type: 'remoteSelect',
|
||||
'x-component-props': {
|
||||
mode: 'simple',
|
||||
resourceName: 'collections.fields',
|
||||
labelField: 'title',
|
||||
valueField: 'name',
|
||||
},
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'virtual',
|
||||
name: 'multiple',
|
||||
title: '允许添加多条记录',
|
||||
component: {
|
||||
type: 'checkbox',
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'component.x-component-props.filter',
|
||||
interface: 'json',
|
||||
type: 'virtual',
|
||||
title: '数据范围',
|
||||
component: {
|
||||
type: 'filter',
|
||||
'x-component-props': {
|
||||
resourceName: 'collections.fields',
|
||||
},
|
||||
showInForm: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'virtual',
|
||||
name: 'createable',
|
||||
title: '允许直接在关联的数据表内新建数据',
|
||||
component: {
|
||||
type: 'checkbox',
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'subTable',
|
||||
type: 'hasMany',
|
||||
name: 'children',
|
||||
target: 'fields',
|
||||
sourceKey: 'id',
|
||||
foreignKey: 'parent_id',
|
||||
title: '子表格字段',
|
||||
viewName: 'table2',
|
||||
// visible: true,
|
||||
component: {
|
||||
type: 'subTable',
|
||||
default: [],
|
||||
// showInTable: true,
|
||||
// showInDetail: true,
|
||||
showInForm: true,
|
||||
'x-linkages': [
|
||||
{
|
||||
type: 'value:schema',
|
||||
target: 'children',
|
||||
schema: {
|
||||
'x-component-props': {
|
||||
associatedKey: "{{ $form.values && $form.values.id }}"
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// {
|
||||
// interface: 'linkTo',
|
||||
// multiple: true,
|
||||
// type: 'hasMany',
|
||||
// name: 'children',
|
||||
// title: '子字段',
|
||||
// target: 'fields',
|
||||
// foreignKey: 'parent_id',
|
||||
// sourceKey: 'id',
|
||||
// component: {
|
||||
// type: 'drawerSelect',
|
||||
// },
|
||||
// },
|
||||
{
|
||||
interface: 'textarea',
|
||||
type: 'virtual',
|
||||
name: 'component.tooltip',
|
||||
title: '提示信息',
|
||||
component: {
|
||||
type: 'textarea',
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'boolean',
|
||||
name: 'required',
|
||||
title: '必填项',
|
||||
component: {
|
||||
type: 'checkbox',
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'virtual',
|
||||
name: 'component.showInTable',
|
||||
title: '显示在表格中',
|
||||
component: {
|
||||
type: 'checkbox',
|
||||
tooltip: '若勾选,该字段将作为一列显示在数据表里',
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'virtual',
|
||||
name: 'component.showInForm',
|
||||
title: '显示在表单中',
|
||||
component: {
|
||||
type: 'checkbox',
|
||||
tooltip: '若勾选,该字段将出现在表单中',
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'virtual',
|
||||
name: 'component.showInDetail',
|
||||
title: '显示在详情中',
|
||||
component: {
|
||||
type: 'checkbox',
|
||||
tooltip: '若勾选,该字段将出现在详情中',
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
default: true,
|
||||
},
|
||||
sourceKey: 'key',
|
||||
foreignKey: 'parentKey',
|
||||
},
|
||||
{
|
||||
interface: 'linkTo',
|
||||
type: 'belongsTo',
|
||||
name: 'collection',
|
||||
title: '所属数据表',
|
||||
target: 'collections',
|
||||
targetKey: 'name',
|
||||
labelField: 'title',
|
||||
component: {
|
||||
type: 'drawerSelect',
|
||||
// showInTable: true,
|
||||
'x-component-props': {
|
||||
resourceName: 'collections.fields',
|
||||
labelField: 'title',
|
||||
valueField: 'name',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
type: 'boolean',
|
||||
name: 'developerMode',
|
||||
title: '开发者模式',
|
||||
defaultValue: false,
|
||||
component: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'json',
|
||||
type: 'json',
|
||||
name: 'component',
|
||||
title: '前端组件',
|
||||
type: 'belongsTo',
|
||||
name: 'uiSchema',
|
||||
target: 'ui_schemas',
|
||||
defaultValue: {},
|
||||
component: {
|
||||
type: 'hidden',
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'json',
|
||||
type: 'json',
|
||||
name: 'options',
|
||||
title: '配置信息',
|
||||
defaultValue: {},
|
||||
component: {
|
||||
type: 'hidden',
|
||||
},
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: 'list',
|
||||
name: 'list',
|
||||
title: '查看',
|
||||
},
|
||||
{
|
||||
type: 'destroy',
|
||||
name: 'destroy',
|
||||
title: '删除',
|
||||
},
|
||||
{
|
||||
type: 'create',
|
||||
name: 'create',
|
||||
title: '新增',
|
||||
viewName: 'form',
|
||||
},
|
||||
{
|
||||
type: 'update',
|
||||
name: 'update',
|
||||
title: '编辑',
|
||||
viewName: 'form',
|
||||
},
|
||||
],
|
||||
views_v2: [
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'table',
|
||||
name: 'table',
|
||||
title: '关联的字段',
|
||||
labelField: 'title',
|
||||
actions: [
|
||||
{
|
||||
name: 'create',
|
||||
type: 'create',
|
||||
title: '新增',
|
||||
viewName: 'form',
|
||||
},
|
||||
{
|
||||
name: 'destroy',
|
||||
type: 'destroy',
|
||||
title: '删除',
|
||||
},
|
||||
],
|
||||
fields: ['title', 'interface'],
|
||||
detailsOpenMode: 'drawer', // window
|
||||
details: ['form'],
|
||||
sort: ['sort'],
|
||||
},
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'table',
|
||||
name: 'table2',
|
||||
title: '表格',
|
||||
labelField: 'title',
|
||||
actions: [
|
||||
{
|
||||
name: 'create',
|
||||
type: 'create',
|
||||
title: '新增',
|
||||
viewName: 'form',
|
||||
},
|
||||
{
|
||||
name: 'destroy',
|
||||
type: 'destroy',
|
||||
title: '删除',
|
||||
},
|
||||
],
|
||||
fields: ['sort', 'title', 'interface'],
|
||||
detailsOpenMode: 'drawer', // window
|
||||
details: ['form'],
|
||||
sort: ['sort'],
|
||||
},
|
||||
{
|
||||
developerMode: true,
|
||||
type: 'form',
|
||||
name: 'form',
|
||||
title: '表单',
|
||||
fields: [
|
||||
'title',
|
||||
'interface',
|
||||
'dataSource',
|
||||
'precision',
|
||||
'dateFormat',
|
||||
'showTime',
|
||||
'timeFormat',
|
||||
'maxLevel',
|
||||
'incompletely',
|
||||
'target',
|
||||
'labelField',
|
||||
'children',
|
||||
'multiple',
|
||||
// 'required',
|
||||
],
|
||||
},
|
||||
],
|
||||
} as TableOptions;
|
||||
|
@ -1,241 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import BaseModel from './base';
|
||||
import Field from './field';
|
||||
import { TableOptions } from '@nocobase/database';
|
||||
import { SaveOptions, Op } from 'sequelize';
|
||||
import { Model } from '@nocobase/database';
|
||||
|
||||
export class Collection extends Model {
|
||||
|
||||
/**
|
||||
* 生成随机数据库表名
|
||||
*
|
||||
* 策略:暂时使用 3+2
|
||||
* 1. 自增 id
|
||||
* 2. 随机字母
|
||||
* 3. 时间戳
|
||||
* 4. 转拼音
|
||||
* 5. 常见词翻译
|
||||
*
|
||||
* @param title 显示的名称
|
||||
*/
|
||||
export function generateCollectionName(title?: string): string {
|
||||
return `t_${Date.now().toString(36)}_${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
|
||||
}
|
||||
|
||||
export interface LoadOptions {
|
||||
reset?: boolean;
|
||||
where?: any;
|
||||
skipExisting?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface MigrateOptions {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class CollectionModel extends BaseModel {
|
||||
|
||||
generateName() {
|
||||
this.set('name', generateCollectionName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 name 获取 collection
|
||||
*
|
||||
* @param name
|
||||
*/
|
||||
static async findByName(name: string) {
|
||||
return this.findOne({ where: { name } });
|
||||
}
|
||||
|
||||
/**
|
||||
* DOTO:
|
||||
* - database.table 初始化可能存在一些缺陷
|
||||
* - 是否需要考虑关系数据的重载?
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
async loadTableOptions(opts: any = {}) {
|
||||
const options = await this.getOptions();
|
||||
// const prevTable = this.database.getTable(this.get('name'));
|
||||
// const prevOptions = prevTable ? prevTable.getOptions() : {};
|
||||
// table 是初始化和重新初始化
|
||||
const table = this.database.table(options);
|
||||
// console.log({options, actions: table.getOptions()['actions']})
|
||||
|
||||
// 如果关系表未加载,一起处理
|
||||
// const associationTableNames = [];
|
||||
// for (const [key, association] of table.getAssociations()) {
|
||||
// // TODO:是否需要考虑重载的情况?(暂时是跳过处理)
|
||||
// if (!this.database.isDefined(association.options.target)) {
|
||||
// continue;
|
||||
// }
|
||||
// associationTableNames.push(association.options.target);
|
||||
// }
|
||||
// console.log({associationTableNames});
|
||||
// if (associationTableNames.length) {
|
||||
// await CollectionModel.load({
|
||||
// ...opts,
|
||||
// where: {
|
||||
// name: {
|
||||
// [Op.in]: associationTableNames,
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
return table;
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移
|
||||
*/
|
||||
async migrate(options: MigrateOptions = {}) {
|
||||
const { isNewRecord } = options;
|
||||
const table = await this.loadTableOptions(options);
|
||||
// 如果不是新增数据,force 必须为 false
|
||||
if (!isNewRecord) {
|
||||
return await table.sync({
|
||||
force: false,
|
||||
alter: {
|
||||
drop: false,
|
||||
}
|
||||
});
|
||||
}
|
||||
// TODO: 暂时加了个 collectionSync 解决 collection.create 的数据不清空问题
|
||||
// @ts-ignore
|
||||
const sync = this.sequelize.options.collectionSync;
|
||||
return await table.sync(sync || {
|
||||
force: false,
|
||||
alter: {
|
||||
drop: false,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getFieldsOptions() {
|
||||
const fieldsOptions = [];
|
||||
const fields = await this.getFields();
|
||||
for (const field of fields) {
|
||||
fieldsOptions.push(await field.getOptions());
|
||||
}
|
||||
return fieldsOptions;
|
||||
}
|
||||
|
||||
async getOptions(): Promise<TableOptions> {
|
||||
const options: any = {
|
||||
...this.get(),
|
||||
actions: await this.getActions(),
|
||||
fields: await this.getFieldsOptions(),
|
||||
}
|
||||
// @ts-ignore
|
||||
// console.log(this.constructor.associations);
|
||||
// @ts-ignore
|
||||
if (this.constructor.hasAlias('views_v2')) {
|
||||
options.views_v2 = await this.getViews_v2();
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO:需要考虑是初次加载还是重载
|
||||
*
|
||||
* @param options
|
||||
*/
|
||||
static async load(options: LoadOptions = {}) {
|
||||
const { skipExisting = false, reset = false, where = {}, transaction } = options;
|
||||
const collections = await this.findAll({
|
||||
transaction,
|
||||
where,
|
||||
});
|
||||
for (const collection of collections) {
|
||||
if (skipExisting && this.database.isDefined(collection.get('name'))) {
|
||||
continue;
|
||||
}
|
||||
await collection.loadTableOptions({
|
||||
transaction,
|
||||
reset,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async import(data: TableOptions, options: SaveOptions = {}): Promise<CollectionModel> {
|
||||
data = _.cloneDeep(data);
|
||||
// @ts-ignore
|
||||
const { update } = options;
|
||||
let collection: CollectionModel;
|
||||
if (data.name) {
|
||||
collection = await this.findOne({
|
||||
...options,
|
||||
where: {
|
||||
name: data.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (collection) {
|
||||
// @ts-ignore
|
||||
await collection.update(data, options);
|
||||
}
|
||||
if (!collection) {
|
||||
// @ts-ignore
|
||||
collection = await this.create(data, options);
|
||||
}
|
||||
|
||||
const associations = ['fields', 'actions', 'views_v2'];
|
||||
for (const key of associations) {
|
||||
if (!Array.isArray(data[key])) {
|
||||
continue;
|
||||
}
|
||||
const Model = this.database.getModel(key);
|
||||
if (!Model) {
|
||||
continue;
|
||||
}
|
||||
let ids = [];
|
||||
for (const index in data[key]) {
|
||||
if (key === 'fields') {
|
||||
ids = await Model.import(data[key], {
|
||||
...options,
|
||||
collectionName: collection.name,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let model;
|
||||
const item = data[key][index];
|
||||
if (item.name) {
|
||||
model = await Model.findOne({
|
||||
...options,
|
||||
where: {
|
||||
collection_name: collection.name,
|
||||
name: item.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (model) {
|
||||
await model.update({
|
||||
...item,
|
||||
// sort: index+1
|
||||
}, options);
|
||||
}
|
||||
if (!model) {
|
||||
model = await Model.create(
|
||||
{
|
||||
...item,
|
||||
// sort: index+1,
|
||||
collection_name: collection.name,
|
||||
},
|
||||
// @ts-ignore
|
||||
options
|
||||
);
|
||||
}
|
||||
if (model) {
|
||||
ids.push(model.id);
|
||||
}
|
||||
}
|
||||
if (ids.length && collection.get('internal')) {
|
||||
await collection.updateAssociations({
|
||||
[key]: ids,
|
||||
});
|
||||
}
|
||||
}
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
|
||||
export default CollectionModel;
|
||||
|
@ -1,263 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import BaseModel from './base';
|
||||
import { FieldOptions, BELONGSTO, BELONGSTOMANY, HASMANY } from '@nocobase/database';
|
||||
import * as types from '../interfaces/types';
|
||||
import { merge } from '../utils';
|
||||
import { BuildOptions } from 'sequelize';
|
||||
import { SaveOptions, Utils } from 'sequelize';
|
||||
import { generateCollectionName } from './collection';
|
||||
import { Model } from '@nocobase/database';
|
||||
|
||||
export class Field extends Model {
|
||||
|
||||
interface FieldImportOptions extends SaveOptions {
|
||||
parentId?: number;
|
||||
collectionName?: string;
|
||||
}
|
||||
|
||||
export function generateValueName(title?: string): string {
|
||||
return `${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
|
||||
}
|
||||
|
||||
export function generateFieldName(title?: string): string {
|
||||
return `f_${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
|
||||
}
|
||||
|
||||
export class FieldModel extends BaseModel {
|
||||
|
||||
constructor(values: any = {}, options: any = {}) {
|
||||
let data = {
|
||||
...(values.options || {}),
|
||||
...values,
|
||||
// ..._.omit(values, 'options'),
|
||||
};
|
||||
const interfaceType = data.interface;
|
||||
if (interfaceType) {
|
||||
const { options } = types[interfaceType];
|
||||
let args = [options, data];
|
||||
// @ts-ignore
|
||||
data = merge(...args);
|
||||
if (['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(data.type)) {
|
||||
// 关系字段如果没有 name,相关参数都随机生成
|
||||
if (!data.name) {
|
||||
data.name = generateFieldName();
|
||||
data.paired = true;
|
||||
// 通用,关系表
|
||||
if (!data.target) {
|
||||
data.target = generateCollectionName();
|
||||
}
|
||||
// 通用,外键
|
||||
if (!data.foreignKey) {
|
||||
data.foreignKey = generateFieldName();
|
||||
}
|
||||
if (data.type !== 'belongsTo' && !data.sourceKey) {
|
||||
data.sourceKey = 'id';
|
||||
}
|
||||
if (['belongsTo', 'belongsToMany'].includes(data.type) && !data.targetKey) {
|
||||
data.targetKey = 'id';
|
||||
}
|
||||
// 多对多关联
|
||||
if (data.type === 'belongsToMany') {
|
||||
if (!data.through) {
|
||||
data.through = generateCollectionName();
|
||||
}
|
||||
if (!data.otherKey) {
|
||||
data.otherKey = generateFieldName();
|
||||
}
|
||||
}
|
||||
}
|
||||
// 有 name,但是没有 target
|
||||
if (!data.target) {
|
||||
data.target = ['hasOne', 'belongsTo'].includes(data.type) ? Utils.pluralize(data.name) : data.name;
|
||||
}
|
||||
}
|
||||
if (!data.name) {
|
||||
data.name = generateFieldName();
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
super(data, options);
|
||||
}
|
||||
|
||||
generateName() {
|
||||
this.set('name', generateFieldName());
|
||||
}
|
||||
|
||||
async generatePairField(options) {
|
||||
const { interface: control, paired, type, target, sourceKey, targetKey, foreignKey, otherKey, through, collection_name } = this.get();
|
||||
if (control !== 'linkTo' || type !== 'belongsToMany' || !collection_name || !paired) {
|
||||
return;
|
||||
}
|
||||
if (!this.database.isDefined(target)) {
|
||||
return;
|
||||
}
|
||||
const targetTable = this.database.getTable(target);
|
||||
const Field = FieldModel;
|
||||
let labelField = 'id';
|
||||
const targetField = await Field.findOne({
|
||||
...options,
|
||||
where: {
|
||||
type: 'string',
|
||||
collection_name: target,
|
||||
},
|
||||
order: [['sort', 'asc']],
|
||||
});
|
||||
if (targetField) {
|
||||
labelField = targetField.get('name');
|
||||
}
|
||||
const collection = await this.getCollection(options);
|
||||
let targetOptions: any = {
|
||||
...types.linkTo.options,
|
||||
interface: 'linkTo',
|
||||
title: collection.get('title'),
|
||||
collection_name: target,
|
||||
options: {
|
||||
paired: true,
|
||||
target: collection_name,
|
||||
labelField,
|
||||
},
|
||||
component: {
|
||||
showInTable: true,
|
||||
showInForm: true,
|
||||
showInDetail: true,
|
||||
},
|
||||
};
|
||||
// 暂时不处理 hasone
|
||||
switch (type) {
|
||||
case 'hasMany':
|
||||
targetOptions.type = 'belongsTo';
|
||||
targetOptions.options.targetKey = sourceKey;
|
||||
targetOptions.options.foreignKey = foreignKey;
|
||||
break;
|
||||
case 'belongsTo':
|
||||
targetOptions.type = 'hasMany';
|
||||
targetOptions.options.sourceKey = targetKey;
|
||||
targetOptions.options.foreignKey = foreignKey;
|
||||
break;
|
||||
case 'belongsToMany':
|
||||
targetOptions.type = 'belongsToMany';
|
||||
targetOptions.options.sourceKey = targetKey;
|
||||
targetOptions.options.foreignKey = otherKey;
|
||||
targetOptions.options.targetKey = sourceKey;
|
||||
targetOptions.options.otherKey = foreignKey;
|
||||
targetOptions.options.through = through;
|
||||
break;
|
||||
}
|
||||
const associations = targetTable.getAssociations();
|
||||
// console.log(associations);
|
||||
for (const association of associations.values()) {
|
||||
if (association instanceof BELONGSTOMANY) {
|
||||
if (
|
||||
association.options.foreignKey === otherKey
|
||||
&& association.options.sourceKey === targetKey
|
||||
&& association.options.otherKey === foreignKey
|
||||
&& association.options.targetKey === sourceKey
|
||||
&& association.options.through === through
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// if (association instanceof BELONGSTO) {
|
||||
// continue;
|
||||
// }
|
||||
// if (association instanceof HASMANY) {
|
||||
// continue;
|
||||
// }
|
||||
}
|
||||
const f = await Field.create(targetOptions, options);
|
||||
// console.log({targetOptions}, f.get('options'));
|
||||
}
|
||||
|
||||
setInterface(value) {
|
||||
const { options } = types[value];
|
||||
let args = [];
|
||||
// 如果是新数据或 interface 不相等,interface options 放后
|
||||
if (this.isNewRecord || this.get('interface') !== value) {
|
||||
args = [this.get(), options];
|
||||
} else {
|
||||
// 已存在的数据更新,不相等,interface options 放前面
|
||||
args = [options, this.get()];
|
||||
}
|
||||
// @ts-ignore
|
||||
const values = merge(...args);
|
||||
this.set(values);
|
||||
}
|
||||
|
||||
async getOptions(): Promise<FieldOptions> {
|
||||
return this.get();
|
||||
}
|
||||
|
||||
async migrate(options: any = {}) {
|
||||
const collectionName = this.get('collection_name');
|
||||
if (!collectionName) {
|
||||
return false;
|
||||
}
|
||||
if (!this.database.isDefined(collectionName)) {
|
||||
throw new Error(`${collectionName} is not defined`);
|
||||
}
|
||||
const table = this.database.getTable(collectionName);
|
||||
table.addField(await this.getOptions());
|
||||
await table.sync({
|
||||
force: false,
|
||||
alter: {
|
||||
drop: false,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async import(items: any, options: FieldImportOptions = {}): Promise<any> {
|
||||
const { parentId, collectionName } = options;
|
||||
if (!Array.isArray(items)) {
|
||||
items = [items];
|
||||
}
|
||||
const ids = [];
|
||||
for (const index in items) {
|
||||
const item = items[index];
|
||||
let model;
|
||||
const where: any = {};
|
||||
if (parentId) {
|
||||
where.parent_id = parentId
|
||||
} else {
|
||||
where.collection_name = collectionName;
|
||||
}
|
||||
if (item.name) {
|
||||
model = await this.findOne({
|
||||
...options,
|
||||
where: {
|
||||
...where,
|
||||
name: item.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!model) {
|
||||
const tmp: any = {};
|
||||
if (parentId) {
|
||||
tmp.parent_id = parentId
|
||||
} else {
|
||||
tmp.collection_name = collectionName;
|
||||
}
|
||||
model = await this.create(
|
||||
{
|
||||
...item,
|
||||
...tmp,
|
||||
},
|
||||
//@ts-ignore
|
||||
options
|
||||
);
|
||||
} else {
|
||||
//@ts-ignore
|
||||
await model.update(item, options);
|
||||
}
|
||||
if (Array.isArray(item.children)) {
|
||||
const childrenIds = await this.import(item.children, {
|
||||
...options,
|
||||
parentId: model.id,
|
||||
collectionName,
|
||||
});
|
||||
await model.updateAssociations({
|
||||
children: childrenIds,
|
||||
}, options);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
|
||||
export default FieldModel;
|
||||
|
@ -1,7 +1,2 @@
|
||||
export * from './base';
|
||||
export * from './action';
|
||||
export * from './collection';
|
||||
export * from './field';
|
||||
export * from './tab';
|
||||
export * from './view';
|
||||
export * from './page';
|
||||
|
@ -1,77 +1,19 @@
|
||||
import path from 'path';
|
||||
import { Application } from '@nocobase/server';
|
||||
import hooks from './hooks';
|
||||
import { registerModels, Table } from '@nocobase/database';
|
||||
import * as models from './models';
|
||||
|
||||
export default async function (this: Application, options = {}) {
|
||||
const database = this.database;
|
||||
const resourcer = this.resourcer;
|
||||
// 提供全局的 models 注册机制
|
||||
registerModels(models);
|
||||
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
database.addHook('afterUpdateAssociations', async function (model, options) {
|
||||
if (model instanceof models.FieldModel) {
|
||||
if (model.get('interface') === 'subTable') {
|
||||
const { migrate = true } = options;
|
||||
const Collection = model.database.getModel('collections');
|
||||
await Collection.load({ ...options, where: { name: model.get('collection_name') } });
|
||||
migrate && await model.migrate(options);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(hooks).forEach(modelName => {
|
||||
const Model = database.getModel(modelName);
|
||||
Object.keys(hooks[modelName]).forEach(hookKey => {
|
||||
// TODO(types): 多层 map 映射类型定义较为复杂,暂时忽略
|
||||
// @ts-ignore
|
||||
Model.addHook(hookKey, hooks[modelName][hookKey]);
|
||||
});
|
||||
});
|
||||
|
||||
const Collection = database.getModel('collections');
|
||||
Collection.addHook('afterCreate', async (model: any, options) => {
|
||||
if (model.get('developerMode')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.get('statusable') === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("model.get('developerMode')", model.get('name'));
|
||||
|
||||
const { transaction = await model.sequelize.transaction() } = options;
|
||||
|
||||
await model.createField({
|
||||
interface: 'radio',
|
||||
name: 'status',
|
||||
type: 'string',
|
||||
filterable: true,
|
||||
title: '状态',
|
||||
// index: true,
|
||||
dataSource: [
|
||||
{
|
||||
label: '已发布',
|
||||
value: 'publish',
|
||||
},
|
||||
{
|
||||
label: '草稿',
|
||||
value: 'draft',
|
||||
}
|
||||
],
|
||||
component: {
|
||||
type: 'radio',
|
||||
},
|
||||
}, { transaction });
|
||||
|
||||
if (!options.transaction) {
|
||||
await transaction.commit();
|
||||
database.getModel('fields').beforeCreate((model) => {
|
||||
if (!model.get('name')) {
|
||||
model.set('name', model.get('key'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
7
packages/plugin-routes/.npmignore
Normal file
7
packages/plugin-routes/.npmignore
Normal file
@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
*.log
|
||||
docs
|
||||
__tests__
|
||||
tsconfig.json
|
||||
src
|
||||
.fatherrc.ts
|
16
packages/plugin-routes/package.json
Normal file
16
packages/plugin-routes/package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-routes",
|
||||
"version": "0.4.0-alpha.7",
|
||||
"main": "lib/index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nocobase/actions": "^0.4.0-alpha.7"
|
||||
},
|
||||
"gitHead": "f0b335ac30f29f25c95d7d137655fa64d8d67f1e"
|
||||
}
|
144
packages/plugin-routes/src/__tests__/index.ts
Normal file
144
packages/plugin-routes/src/__tests__/index.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import qs from 'qs';
|
||||
import plugin from '../server';
|
||||
import supertest from 'supertest';
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import { Dialect } from 'sequelize';
|
||||
import Database from '@nocobase/database';
|
||||
import { actions, middlewares } from '@nocobase/actions';
|
||||
import { Application, middleware } from '@nocobase/server';
|
||||
|
||||
function getTestKey() {
|
||||
const { id } = require.main;
|
||||
const key = id
|
||||
.replace(`${process.env.PWD}/packages`, '')
|
||||
.replace(/src\/__tests__/g, '')
|
||||
.replace('.test.ts', '')
|
||||
.replace(/[^\w]/g, '_')
|
||||
.replace(/_+/g, '_');
|
||||
return key
|
||||
}
|
||||
|
||||
const config = {
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_DATABASE,
|
||||
host: process.env.DB_HOST,
|
||||
port: Number.parseInt(process.env.DB_PORT, 10),
|
||||
dialect: process.env.DB_DIALECT as Dialect,
|
||||
define: {
|
||||
hooks: {
|
||||
beforeCreate(model, options) {
|
||||
|
||||
},
|
||||
},
|
||||
},
|
||||
logging: false,
|
||||
sync: {
|
||||
force: true,
|
||||
alter: {
|
||||
drop: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export async function getApp() {
|
||||
const app = new Application({
|
||||
database: {
|
||||
...config,
|
||||
hooks: {
|
||||
beforeDefine(columns, model) {
|
||||
model.tableName = `${getTestKey()}_${model.tableName || model.name.plural}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
resourcer: {
|
||||
prefix: '/api',
|
||||
},
|
||||
});
|
||||
app.resourcer.use(middlewares.associated);
|
||||
app.resourcer.registerActionHandlers({ ...actions.associate, ...actions.common });
|
||||
app.registerPlugin('collections', [plugin]);
|
||||
await app.loadPlugins();
|
||||
await app.database.sync();
|
||||
// 表配置信息存到数据库里
|
||||
// const tables = app.database.getTables([]);
|
||||
// for (const table of tables) {
|
||||
// const Collection = app.database.getModel('collections');
|
||||
// await Collection.import(table.getOptions(), { hooks: false });
|
||||
// }
|
||||
app.use(async (ctx, next) => {
|
||||
ctx.db = app.database;
|
||||
await next();
|
||||
});
|
||||
app.use(bodyParser());
|
||||
app.use(middleware({
|
||||
prefix: '/api',
|
||||
resourcer: app.resourcer,
|
||||
database: app.database,
|
||||
}));
|
||||
return app;
|
||||
}
|
||||
|
||||
interface ActionParams {
|
||||
resourceKey?: string | number;
|
||||
// resourceName?: string;
|
||||
// associatedName?: string;
|
||||
associatedKey?: string | number;
|
||||
fields?: any;
|
||||
filter?: any;
|
||||
values?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface Handler {
|
||||
get: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
list: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
create: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
update: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
destroy: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
[name: string]: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
}
|
||||
|
||||
export interface Agent {
|
||||
resource: (name: string) => Handler;
|
||||
}
|
||||
|
||||
export function getAgent(app: Application): Agent {
|
||||
const agent = supertest.agent(app.callback());
|
||||
return {
|
||||
resource(name: string): any {
|
||||
return new Proxy({}, {
|
||||
get(target, method, receiver) {
|
||||
return (params: ActionParams = {}) => {
|
||||
const { associatedKey, resourceKey, values = {}, ...restParams } = params;
|
||||
let url = `/api/${name}`;
|
||||
if (associatedKey) {
|
||||
url = `/api/${name.split('.').join(`/${associatedKey}/`)}`;
|
||||
}
|
||||
url += `:${method as string}`;
|
||||
if (resourceKey) {
|
||||
url += `/${resourceKey}`;
|
||||
}
|
||||
console.log(url);
|
||||
if (['list', 'get'].indexOf(method as string) !== -1) {
|
||||
return agent.get(`${url}?${qs.stringify(restParams)}`);
|
||||
} else {
|
||||
return agent.post(`${url}?${qs.stringify(restParams)}`).send(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getDatabase() {
|
||||
return new Database({
|
||||
...config,
|
||||
hooks: {
|
||||
beforeDefine(columns, model) {
|
||||
model.tableName = `${getTestKey()}_${model.tableName || model.name.plural}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
82
packages/plugin-routes/src/__tests__/routes.test.ts
Normal file
82
packages/plugin-routes/src/__tests__/routes.test.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { Agent, getAgent, getApp } from '.';
|
||||
import { Application } from '@nocobase/server';
|
||||
import Database from '@nocobase/database';
|
||||
|
||||
describe('routes', () => {
|
||||
let app: Application;
|
||||
let agent: Agent;
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await getApp();
|
||||
db = app.database;
|
||||
agent = getAgent(app);
|
||||
});
|
||||
|
||||
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('create route', async () => {
|
||||
const Route = db.getModel('routes');
|
||||
const data = [
|
||||
{
|
||||
type: 'redirect',
|
||||
from: '/',
|
||||
to: '/admin',
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: '/admin/:name(.+)?',
|
||||
component: 'AdminLayout',
|
||||
title: `后台`,
|
||||
uiSchema: {
|
||||
key: 'qqzzjakwkwl',
|
||||
name: 'qqzzjakwkwl',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'AuthLayout',
|
||||
children: [
|
||||
{
|
||||
name: 'login',
|
||||
path: '/login',
|
||||
component: 'DefaultPage',
|
||||
title: `登录`,
|
||||
uiSchema: {
|
||||
key: 'dtf9j0b8p9u',
|
||||
name: 'dtf9j0b8p9u',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'register',
|
||||
path: '/register',
|
||||
component: 'DefaultPage',
|
||||
title: `注册`,
|
||||
uiSchema: {
|
||||
key: '46qlxqam3xk',
|
||||
name: '46qlxqam3xk',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
for (const item of data) {
|
||||
const route = await Route.create(item);
|
||||
await route.updateAssociations(item);
|
||||
}
|
||||
});
|
||||
});
|
35
packages/plugin-routes/src/collections/routes.ts
Normal file
35
packages/plugin-routes/src/collections/routes.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { TableOptions } from '@nocobase/database';
|
||||
|
||||
export default {
|
||||
name: 'routes',
|
||||
title: '路由表',
|
||||
fields: [
|
||||
{
|
||||
type: 'uid',
|
||||
name: 'key',
|
||||
prefix: 'r_',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'type',
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'children',
|
||||
target: 'routes',
|
||||
sourceKey: 'key',
|
||||
foreignKey: 'parentKey',
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'uiSchema',
|
||||
target: 'ui_schemas',
|
||||
},
|
||||
{
|
||||
type: 'json',
|
||||
name: 'options',
|
||||
defaultValue: {},
|
||||
},
|
||||
],
|
||||
} as TableOptions;
|
1
packages/plugin-routes/src/models/index.ts
Normal file
1
packages/plugin-routes/src/models/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './route';
|
6
packages/plugin-routes/src/models/route.ts
Normal file
6
packages/plugin-routes/src/models/route.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import { Model } from '@nocobase/database';
|
||||
|
||||
export class Route extends Model {
|
||||
|
||||
}
|
13
packages/plugin-routes/src/server.ts
Normal file
13
packages/plugin-routes/src/server.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import path from 'path';
|
||||
import { Application } from '@nocobase/server';
|
||||
import { registerModels } from '@nocobase/database';
|
||||
import * as models from './models';
|
||||
|
||||
export default async function (this: Application, options = {}) {
|
||||
const database = this.database;
|
||||
registerModels(models);
|
||||
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
}
|
9
packages/plugin-routes/src/utils.ts
Normal file
9
packages/plugin-routes/src/utils.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
|
||||
const overwriteMerge = (destinationArray, sourceArray, options) => sourceArray
|
||||
|
||||
export function merge(obj1: any, obj2: any) {
|
||||
return deepmerge(obj1, obj2, {
|
||||
arrayMerge: overwriteMerge,
|
||||
});
|
||||
}
|
7
packages/plugin-ui-schema/.npmignore
Normal file
7
packages/plugin-ui-schema/.npmignore
Normal file
@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
*.log
|
||||
docs
|
||||
__tests__
|
||||
tsconfig.json
|
||||
src
|
||||
.fatherrc.ts
|
16
packages/plugin-ui-schema/package.json
Normal file
16
packages/plugin-ui-schema/package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-ui-schema",
|
||||
"version": "0.4.0-alpha.7",
|
||||
"main": "lib/index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nocobase/actions": "^0.4.0-alpha.7"
|
||||
},
|
||||
"gitHead": "f0b335ac30f29f25c95d7d137655fa64d8d67f1e"
|
||||
}
|
144
packages/plugin-ui-schema/src/__tests__/index.ts
Normal file
144
packages/plugin-ui-schema/src/__tests__/index.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import qs from 'qs';
|
||||
import plugin from '../server';
|
||||
import supertest from 'supertest';
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import { Dialect } from 'sequelize';
|
||||
import Database from '@nocobase/database';
|
||||
import { actions, middlewares } from '@nocobase/actions';
|
||||
import { Application, middleware } from '@nocobase/server';
|
||||
|
||||
function getTestKey() {
|
||||
const { id } = require.main;
|
||||
const key = id
|
||||
.replace(`${process.env.PWD}/packages`, '')
|
||||
.replace(/src\/__tests__/g, '')
|
||||
.replace('.test.ts', '')
|
||||
.replace(/[^\w]/g, '_')
|
||||
.replace(/_+/g, '_');
|
||||
return key
|
||||
}
|
||||
|
||||
const config = {
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_DATABASE,
|
||||
host: process.env.DB_HOST,
|
||||
port: Number.parseInt(process.env.DB_PORT, 10),
|
||||
dialect: process.env.DB_DIALECT as Dialect,
|
||||
define: {
|
||||
hooks: {
|
||||
beforeCreate(model, options) {
|
||||
|
||||
},
|
||||
},
|
||||
},
|
||||
logging: false,
|
||||
sync: {
|
||||
force: true,
|
||||
alter: {
|
||||
drop: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export async function getApp() {
|
||||
const app = new Application({
|
||||
database: {
|
||||
...config,
|
||||
hooks: {
|
||||
beforeDefine(columns, model) {
|
||||
model.tableName = `${getTestKey()}_${model.tableName || model.name.plural}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
resourcer: {
|
||||
prefix: '/api',
|
||||
},
|
||||
});
|
||||
app.resourcer.use(middlewares.associated);
|
||||
app.resourcer.registerActionHandlers({ ...actions.associate, ...actions.common });
|
||||
app.registerPlugin('collections', [plugin]);
|
||||
await app.loadPlugins();
|
||||
await app.database.sync();
|
||||
// 表配置信息存到数据库里
|
||||
// const tables = app.database.getTables([]);
|
||||
// for (const table of tables) {
|
||||
// const Collection = app.database.getModel('collections');
|
||||
// await Collection.import(table.getOptions(), { hooks: false });
|
||||
// }
|
||||
app.use(async (ctx, next) => {
|
||||
ctx.db = app.database;
|
||||
await next();
|
||||
});
|
||||
app.use(bodyParser());
|
||||
app.use(middleware({
|
||||
prefix: '/api',
|
||||
resourcer: app.resourcer,
|
||||
database: app.database,
|
||||
}));
|
||||
return app;
|
||||
}
|
||||
|
||||
interface ActionParams {
|
||||
resourceKey?: string | number;
|
||||
// resourceName?: string;
|
||||
// associatedName?: string;
|
||||
associatedKey?: string | number;
|
||||
fields?: any;
|
||||
filter?: any;
|
||||
values?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface Handler {
|
||||
get: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
list: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
create: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
update: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
destroy: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
[name: string]: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
}
|
||||
|
||||
export interface Agent {
|
||||
resource: (name: string) => Handler;
|
||||
}
|
||||
|
||||
export function getAgent(app: Application): Agent {
|
||||
const agent = supertest.agent(app.callback());
|
||||
return {
|
||||
resource(name: string): any {
|
||||
return new Proxy({}, {
|
||||
get(target, method, receiver) {
|
||||
return (params: ActionParams = {}) => {
|
||||
const { associatedKey, resourceKey, values = {}, ...restParams } = params;
|
||||
let url = `/api/${name}`;
|
||||
if (associatedKey) {
|
||||
url = `/api/${name.split('.').join(`/${associatedKey}/`)}`;
|
||||
}
|
||||
url += `:${method as string}`;
|
||||
if (resourceKey) {
|
||||
url += `/${resourceKey}`;
|
||||
}
|
||||
console.log(url);
|
||||
if (['list', 'get'].indexOf(method as string) !== -1) {
|
||||
return agent.get(`${url}?${qs.stringify(restParams)}`);
|
||||
} else {
|
||||
return agent.post(`${url}?${qs.stringify(restParams)}`).send(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getDatabase() {
|
||||
return new Database({
|
||||
...config,
|
||||
hooks: {
|
||||
beforeDefine(columns, model) {
|
||||
model.tableName = `${getTestKey()}_${model.tableName || model.name.plural}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
50
packages/plugin-ui-schema/src/__tests__/ui_schemas.test.ts
Normal file
50
packages/plugin-ui-schema/src/__tests__/ui_schemas.test.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { Agent, getAgent, getApp } from '.';
|
||||
import { Application } from '@nocobase/server';
|
||||
import Database from '@nocobase/database';
|
||||
|
||||
describe('ui_schemas', () => {
|
||||
let app: Application;
|
||||
let agent: Agent;
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await getApp();
|
||||
db = app.database;
|
||||
agent = getAgent(app);
|
||||
});
|
||||
|
||||
afterEach(() => app.database.close());
|
||||
|
||||
it('create ui_schemas', async () => {
|
||||
const UISchema = db.getModel('ui_schemas');
|
||||
const schema = await UISchema.create({
|
||||
type: 'void',
|
||||
name: 'abc',
|
||||
properties: {
|
||||
field1: {
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
field2: {
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
field3: {
|
||||
type: 'array',
|
||||
properties: {
|
||||
field11: {
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
field12: {
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const properties = await schema.getProperties();
|
||||
console.log(JSON.stringify(properties, null, 2));
|
||||
});
|
||||
});
|
38
packages/plugin-ui-schema/src/collections/ui_schemas.ts
Normal file
38
packages/plugin-ui-schema/src/collections/ui_schemas.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { TableOptions } from '@nocobase/database';
|
||||
|
||||
export default {
|
||||
name: 'ui_schemas',
|
||||
title: '字段配置',
|
||||
model: 'UISchema',
|
||||
fields: [
|
||||
{
|
||||
type: 'uid',
|
||||
name: 'key',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'type',
|
||||
},
|
||||
{
|
||||
type: 'json',
|
||||
name: 'options',
|
||||
defaultValue: {},
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'children',
|
||||
target: 'ui_schemas',
|
||||
sourceKey: 'key',
|
||||
foreignKey: 'parentKey',
|
||||
},
|
||||
],
|
||||
} as TableOptions;
|
1
packages/plugin-ui-schema/src/models/index.ts
Normal file
1
packages/plugin-ui-schema/src/models/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './ui-schema';
|
62
packages/plugin-ui-schema/src/models/ui-schema.ts
Normal file
62
packages/plugin-ui-schema/src/models/ui-schema.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import _ from 'lodash';
|
||||
import { Model } from '@nocobase/database';
|
||||
|
||||
export class UISchema 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);
|
||||
if (!attributes.children) {
|
||||
attributes.children = this.properties2children(attributes.properties);
|
||||
await model.updateAssociation('children', attributes.children, options);
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
static toAttributes(value = {}): any {
|
||||
const data = _.cloneDeep(value);
|
||||
const keys = [
|
||||
'properties',
|
||||
...Object.keys(this.rawAttributes),
|
||||
...Object.keys(this.associations),
|
||||
];
|
||||
const attrs = _.pick(data, keys);
|
||||
const options = _.omit(data, keys);
|
||||
return { ...attrs, options };
|
||||
}
|
||||
|
||||
static properties2children(properties = []) {
|
||||
const children = [];
|
||||
for (const [name, property] of Object.entries(properties)) {
|
||||
if (property.properties) {
|
||||
property.children = this.properties2children(property.properties);
|
||||
}
|
||||
children.push({
|
||||
name,
|
||||
...property,
|
||||
});
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
toProperty() {
|
||||
const options = this.get('options') || {};
|
||||
const data = _.omit(this.toJSON(), ['created_at', 'updated_at', 'options', 'parentKey']);
|
||||
return { ...data, ...options };
|
||||
}
|
||||
|
||||
async getProperties() {
|
||||
const properties = {};
|
||||
const children: UISchema[] = await this.getChildren();
|
||||
for (const child of children) {
|
||||
const property = child.toProperty();
|
||||
const childProperties = await child.getProperties();
|
||||
if (Object.keys(childProperties).length) {
|
||||
property['properties'] = childProperties;
|
||||
}
|
||||
properties[child.name] = property;
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
}
|
19
packages/plugin-ui-schema/src/server.ts
Normal file
19
packages/plugin-ui-schema/src/server.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import path from 'path';
|
||||
import { Application } from '@nocobase/server';
|
||||
import { registerModels, Table } from '@nocobase/database';
|
||||
import * as models from './models';
|
||||
|
||||
export default async function (this: Application, options = {}) {
|
||||
const database = this.database;
|
||||
registerModels(models);
|
||||
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
database.getModel('ui_schemas').beforeCreate((model) => {
|
||||
if (!model.get('name')) {
|
||||
model.set('name', model.get('key'));
|
||||
}
|
||||
});
|
||||
}
|
9
packages/plugin-ui-schema/src/utils.ts
Normal file
9
packages/plugin-ui-schema/src/utils.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
|
||||
const overwriteMerge = (destinationArray, sourceArray, options) => sourceArray
|
||||
|
||||
export function merge(obj1: any, obj2: any) {
|
||||
return deepmerge(obj1, obj2, {
|
||||
arrayMerge: overwriteMerge,
|
||||
});
|
||||
}
|
13
uid.js
Normal file
13
uid.js
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
let IDX = 36,
|
||||
HEX = ''
|
||||
while (IDX--) HEX += IDX.toString(36)
|
||||
|
||||
function uid(len = 11) {
|
||||
let str = '',
|
||||
num = len || 11
|
||||
while (num--) str += HEX[(Math.random() * 36) | 0]
|
||||
return str
|
||||
}
|
||||
|
||||
console.log(uid());
|
161
yarn.lock
161
yarn.lock
@ -4136,7 +4136,7 @@
|
||||
"@umijs/deps" "3.4.25"
|
||||
"@umijs/utils" "3.4.25"
|
||||
|
||||
"@umijs/test@^3.0.7", "@umijs/test@^3.4.15", "@umijs/test@^3.4.25":
|
||||
"@umijs/test@^3.0.7", "@umijs/test@^3.4.15":
|
||||
version "3.4.25"
|
||||
resolved "https://registry.npmjs.org/@umijs/test/-/test-3.4.25.tgz#9df95dc25cecaee9f45cdb62a227634defb5ae87"
|
||||
integrity sha512-VfaQyvF3vkHkHROS3LYQjRvx3dwk1Lv56gfgDIHGCX2n2ARSGyaFTbXsv/QafrKENDTDp3FTcTIUG6oY6mHH2g==
|
||||
@ -4319,14 +4319,6 @@ agentkeepalive@^3.4.1:
|
||||
dependencies:
|
||||
humanize-ms "^1.2.1"
|
||||
|
||||
aggregate-error@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
|
||||
integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==
|
||||
dependencies:
|
||||
clean-stack "^2.0.0"
|
||||
indent-string "^4.0.0"
|
||||
|
||||
ahooks@^2.10.2:
|
||||
version "2.10.6"
|
||||
resolved "https://registry.npmjs.org/ahooks/-/ahooks-2.10.6.tgz#05fad82f77a308886c174a61b7e1232cb16516ac"
|
||||
@ -4479,7 +4471,7 @@ ansi-escapes@^3.0.0, ansi-escapes@^3.2.0:
|
||||
resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
|
||||
integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
|
||||
|
||||
ansi-escapes@^4.2.1, ansi-escapes@^4.3.0:
|
||||
ansi-escapes@^4.2.1:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
|
||||
integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
|
||||
@ -5874,11 +5866,6 @@ classnames@2.x, classnames@^2.2.0, classnames@^2.2.1, classnames@^2.2.3, classna
|
||||
resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
|
||||
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
|
||||
|
||||
clean-stack@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
|
||||
integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
|
||||
|
||||
cli-boxes@^2.2.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f"
|
||||
@ -5891,21 +5878,6 @@ cli-cursor@^2.1.0:
|
||||
dependencies:
|
||||
restore-cursor "^2.0.0"
|
||||
|
||||
cli-cursor@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
|
||||
integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
|
||||
dependencies:
|
||||
restore-cursor "^3.1.0"
|
||||
|
||||
cli-truncate@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7"
|
||||
integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==
|
||||
dependencies:
|
||||
slice-ansi "^3.0.0"
|
||||
string-width "^4.2.0"
|
||||
|
||||
cli-width@^2.0.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
|
||||
@ -6128,11 +6100,6 @@ commander@^2.19.0, commander@^2.20.0:
|
||||
resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
||||
|
||||
commander@^6.2.0:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
|
||||
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
|
||||
|
||||
commondir@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
||||
@ -6918,7 +6885,7 @@ debug@3.X, debug@^3.1.0, debug@^3.2.7:
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1:
|
||||
debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
|
||||
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
|
||||
@ -7513,7 +7480,7 @@ enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.1:
|
||||
memory-fs "^0.5.0"
|
||||
tapable "^1.0.0"
|
||||
|
||||
enquirer@^2.3.5, enquirer@^2.3.6:
|
||||
enquirer@^2.3.5:
|
||||
version "2.3.6"
|
||||
resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
|
||||
integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
|
||||
@ -8056,7 +8023,7 @@ execa@^1.0.0:
|
||||
signal-exit "^3.0.0"
|
||||
strip-eof "^1.0.0"
|
||||
|
||||
execa@^4.0.0, execa@^4.1.0:
|
||||
execa@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a"
|
||||
integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==
|
||||
@ -10224,11 +10191,6 @@ is-unc-path@^1.0.0:
|
||||
dependencies:
|
||||
unc-path-regex "^0.1.2"
|
||||
|
||||
is-unicode-supported@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
|
||||
integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
|
||||
|
||||
is-url@1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52"
|
||||
@ -11660,40 +11622,6 @@ lines-and-columns@^1.1.6:
|
||||
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
|
||||
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
|
||||
|
||||
lint-staged@^10.0.7:
|
||||
version "10.5.4"
|
||||
resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-10.5.4.tgz#cd153b5f0987d2371fc1d2847a409a2fe705b665"
|
||||
integrity sha512-EechC3DdFic/TdOPgj/RB3FicqE6932LTHCUm0Y2fsD9KGlLB+RwJl2q1IYBIvEsKzDOgn0D4gll+YxG5RsrKg==
|
||||
dependencies:
|
||||
chalk "^4.1.0"
|
||||
cli-truncate "^2.1.0"
|
||||
commander "^6.2.0"
|
||||
cosmiconfig "^7.0.0"
|
||||
debug "^4.2.0"
|
||||
dedent "^0.7.0"
|
||||
enquirer "^2.3.6"
|
||||
execa "^4.1.0"
|
||||
listr2 "^3.2.2"
|
||||
log-symbols "^4.0.0"
|
||||
micromatch "^4.0.2"
|
||||
normalize-path "^3.0.0"
|
||||
please-upgrade-node "^3.2.0"
|
||||
string-argv "0.3.1"
|
||||
stringify-object "^3.3.0"
|
||||
|
||||
listr2@^3.2.2:
|
||||
version "3.10.0"
|
||||
resolved "https://registry.npmjs.org/listr2/-/listr2-3.10.0.tgz#58105a53ed7fa1430d1b738c6055ef7bb006160f"
|
||||
integrity sha512-eP40ZHihu70sSmqFNbNy2NL1YwImmlMmPh9WO5sLmPDleurMHt3n+SwEWNu2kzKScexZnkyFtc1VI0z/TGlmpw==
|
||||
dependencies:
|
||||
cli-truncate "^2.1.0"
|
||||
colorette "^1.2.2"
|
||||
log-update "^4.0.0"
|
||||
p-map "^4.0.0"
|
||||
rxjs "^6.6.7"
|
||||
through "^2.3.8"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
load-json-file@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
|
||||
@ -11897,24 +11825,6 @@ lodash@4.x, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.15, lodash@^4.17.19,
|
||||
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
log-symbols@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
|
||||
integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
|
||||
dependencies:
|
||||
chalk "^4.1.0"
|
||||
is-unicode-supported "^0.1.0"
|
||||
|
||||
log-update@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1"
|
||||
integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==
|
||||
dependencies:
|
||||
ansi-escapes "^4.3.0"
|
||||
cli-cursor "^3.1.0"
|
||||
slice-ansi "^4.0.0"
|
||||
wrap-ansi "^6.2.0"
|
||||
|
||||
long-timeout@0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514"
|
||||
@ -13467,13 +13377,6 @@ p-map@^2.1.0:
|
||||
resolved "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
|
||||
integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
|
||||
|
||||
p-map@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
|
||||
integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==
|
||||
dependencies:
|
||||
aggregate-error "^3.0.0"
|
||||
|
||||
p-pipe@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz#4b1a11399a11520a67790ee5a0c1d5881d6befe9"
|
||||
@ -13965,13 +13868,6 @@ platform@^1.3.1:
|
||||
resolved "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7"
|
||||
integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==
|
||||
|
||||
please-upgrade-node@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942"
|
||||
integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==
|
||||
dependencies:
|
||||
semver-compare "^1.0.0"
|
||||
|
||||
plugin-error@^0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz#3b9bb3335ccf00f425e07437e19276967da47ace"
|
||||
@ -16383,14 +16279,6 @@ restore-cursor@^2.0.0:
|
||||
onetime "^2.0.0"
|
||||
signal-exit "^3.0.2"
|
||||
|
||||
restore-cursor@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
|
||||
integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
|
||||
dependencies:
|
||||
onetime "^5.1.0"
|
||||
signal-exit "^3.0.2"
|
||||
|
||||
ret@~0.1.10:
|
||||
version "0.1.15"
|
||||
resolved "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
|
||||
@ -16754,7 +16642,7 @@ run-queue@^1.0.0, run-queue@^1.0.3:
|
||||
dependencies:
|
||||
aproba "^1.1.1"
|
||||
|
||||
rxjs@^6.4.0, rxjs@^6.6.7:
|
||||
rxjs@^6.4.0:
|
||||
version "6.6.7"
|
||||
resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
|
||||
integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
|
||||
@ -16876,11 +16764,6 @@ select@^1.1.2:
|
||||
resolved "https://registry.npmjs.org/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
|
||||
integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=
|
||||
|
||||
semver-compare@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
|
||||
integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
|
||||
|
||||
semver-diff@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"
|
||||
@ -17095,15 +16978,6 @@ slash@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
||||
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
|
||||
|
||||
slice-ansi@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787"
|
||||
integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
astral-regex "^2.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
|
||||
slice-ansi@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
|
||||
@ -17459,11 +17333,6 @@ strict-uri-encode@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
|
||||
integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
|
||||
|
||||
string-argv@0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
|
||||
integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==
|
||||
|
||||
string-convert@^0.2.0:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97"
|
||||
@ -18021,7 +17890,7 @@ through2@^4.0.0:
|
||||
dependencies:
|
||||
readable-stream "3"
|
||||
|
||||
through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.8, through@~2.3:
|
||||
through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@~2.3:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
|
||||
@ -18398,11 +18267,6 @@ typescript@^3.7.2:
|
||||
resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8"
|
||||
integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==
|
||||
|
||||
typescript@^4.1.2:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz#3f85b986945bcf31071decdd96cf8bfa65f9dcbc"
|
||||
integrity sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==
|
||||
|
||||
ua-parser-js@^0.7.18:
|
||||
version "0.7.28"
|
||||
resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
|
||||
@ -18513,7 +18377,7 @@ umi-utils@1.7.2:
|
||||
prettier "1.15.3"
|
||||
slash2 "2.0.0"
|
||||
|
||||
umi@^3.0.0, umi@^3.4.25:
|
||||
umi@^3.0.0:
|
||||
version "3.4.25"
|
||||
resolved "https://registry.npmjs.org/umi/-/umi-3.4.25.tgz#e1d53378ef8fa89011a8f54e7c5ff6984875b2de"
|
||||
integrity sha512-67GMhP40Tz2sbyAzB4at7IUjXEIlG4j3zADfRhepLRWYbhUa55ZHnWyvuws3/dtUfifpraz4ckVcJJESoJ7Xaw==
|
||||
@ -19284,15 +19148,6 @@ wrap-ansi@^6.2.0:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
|
Loading…
Reference in New Issue
Block a user