* refactor: fields/views/pages...

* update

* update

* update

* updates

* updates

* add yarn.lock

* updates

* updates

* updates

* updates

* updates

* updates

* updates

* updates

* updates

* developerMode

* 一大波更新

* bugfix

* fix: hide the sorting settings

* fix: reload menu when menu is updated

* 页面重构

* modify text

* 补充细节

* system settings

* 继续更新补充

* fix: 多级菜单支持

* 无限嵌套

* fix: icon

* 省市区参数调整

* 表单描述、文案调整

* 支持草稿

* 邮箱登录

* 细节补充

* 菜单页面权限初步

* 详情页打开方式

* 菜单父级、草稿问题

* 描述文字

* 详情分组显示

* 状态改为 radio

* 菜单权限

* 跳过省市区 api

* 修复权限数据范围

* onDraft

* 页面跳转

* 修改文案

* 注册、登录

* fix: 权限过滤问题

* 微调上传组件样式

* 0.4.0-alpha.0

* father-build

* remove father-build

* 细节调整
This commit is contained in:
chenos 2021-03-16 14:31:54 +08:00 committed by GitHub
parent 068dde29f4
commit 6c39ac3538
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
503 changed files with 22828 additions and 4178 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@ lib/
.DS_Store
package-lock.json
yarn.lock
!/yarn.lock
yarn-error.log
lerna-debug.log
packages/database/package-lock.json

View File

@ -7,8 +7,7 @@
"start:app:server": "cd packages/app && nodemon",
"start-server": "yarn nodemon",
"bootstrap": "lerna bootstrap --no-ci",
"build": "npm run build-father-build && node packages/father-build/bin/father-build.js",
"build-father-build": "cd packages/father-build && npm run build",
"build": "father-build",
"clean": "lerna clean",
"db:start": "docker-compose up -d",
"lint": "eslint --ext .ts,.tsx,.js \"packages/*/src/**.@(ts|tsx|js)\" --fix",
@ -36,7 +35,7 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"father-build": "^1.18.5",
"father-build": "^1.19.2",
"jest": "^26.1.0",
"koa": "^2.13.0",
"koa-bodyparser": "^4.3.0",

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/actions",
"version": "0.3.0-alpha.0",
"version": "0.4.0-alpha.0",
"description": "",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
@ -8,8 +8,8 @@
},
"license": "MIT",
"dependencies": {
"@nocobase/database": "^0.3.0-alpha.0",
"@nocobase/resourcer": "^0.3.0-alpha.0"
"@nocobase/database": "^0.4.0-alpha.0",
"@nocobase/resourcer": "^0.4.0-alpha.0"
},
"devDependencies": {
"koa": "^2.13.0",

View File

@ -286,6 +286,7 @@ export async function get(ctx: Context, next: Next) {
resourceKeyAttribute,
fields = []
} = ctx.action.params;
console.log({associated, resourceField})
if (associated && resourceField) {
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
@ -297,7 +298,16 @@ export async function get(ctx: Context, next: Next) {
fields,
});
if (resourceField instanceof HASONE || resourceField instanceof BELONGSTO) {
const model: Model = await associated[getAccessor]({ ...options, context: ctx });
let model: Model = await associated[getAccessor]({ context: ctx });
if (model) {
model = await TargetModel.findOne({
...options,
context: ctx,
where: {
[TargetModel.primaryKeyAttribute]: model[TargetModel.primaryKeyAttribute],
},
});
}
ctx.body = model;
} else if (resourceField instanceof HASMANY || resourceField instanceof BELONGSTOMANY) {
const [model]: Model[] = await associated[getAccessor]({

View File

@ -1,12 +1,12 @@
{
"name": "@nocobase/app",
"version": "0.3.0-alpha.0",
"version": "0.4.0-alpha.0",
"private": true,
"scripts": {
"start": "concurrently \"nodemon\" \"umi dev\"",
"start-api": "nodemon",
"db-migrate": "ts-node ./src/api/migrate.ts",
"build": "father-build && umi build",
"build": "umi build",
"postinstall": "umi generate tmp",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"test": "umi-test",
@ -26,18 +26,18 @@
"dependencies": {
"@ant-design/pro-layout": "^5.0.12",
"@formily/antd-components": "^1.3.6",
"@nocobase/client": "^0.3.0-alpha.0",
"@nocobase/database": "^0.3.0-alpha.0",
"@nocobase/father-build": "^0.3.0-alpha.0",
"@nocobase/plugin-action-logs": "^0.3.0-alpha.0",
"@nocobase/plugin-china-region": "^0.3.0-alpha.0",
"@nocobase/plugin-collections": "^0.3.0-alpha.0",
"@nocobase/plugin-pages": "^0.3.0-alpha.0",
"@nocobase/server": "^0.3.0-alpha.0",
"@nocobase/client": "^0.4.0-alpha.0",
"@nocobase/database": "^0.4.0-alpha.0",
"@nocobase/plugin-action-logs": "^0.4.0-alpha.0",
"@nocobase/plugin-china-region": "^0.4.0-alpha.0",
"@nocobase/plugin-collections": "^0.4.0-alpha.0",
"@nocobase/plugin-pages": "^0.4.0-alpha.0",
"@nocobase/server": "^0.4.0-alpha.0",
"@types/react-big-calendar": "^0.24.8",
"@umijs/preset-react": "1.x",
"@umijs/test": "^3.2.23",
"ahooks": "^2.9.3",
"antd": "^4.13.0",
"array-move": "^3.0.1",
"clean-deep": "^3.4.0",
"concurrently": "^5.3.0",
@ -48,6 +48,7 @@
"react": "16.14.0",
"react-big-calendar": "^0.30.0",
"react-dom": "16.14.0",
"react-drag-listview": "^0.1.8",
"react-image-lightbox": "^5.1.1",
"react-sortable-hoc": "^1.11.0",
"styled-components": "^5.2.1",

View File

@ -40,6 +40,7 @@ class ApiClient {
options.params = restParams;
} else {
options.method = 'post';
options.params = restParams;
options.data = values;
}
if (associatedKey) {

View File

@ -0,0 +1,181 @@
import api from '../app';
import Database from '@nocobase/database';
import views_v2 from '@nocobase/plugin-pages/src/collections/views_v2';
import { Op } from 'sequelize';
(async () => {
await api.loadPlugins();
const database: Database = api.database;
await api.database.sync();
await api.database.getModel('collections').load({skipExisting: true});
const [Collection, Field, Page, Menu, View] = database.getModels(['collections', 'fields', 'pages_v2', 'menus', 'views_v2']);
// await Collection.import(require('./collections/authors').default, { update: true });
// await Collection.import(require('./collections/books').default, { update: true });
await Menu.truncate();
await Page.destroy({
where: {
id: {
[Op.not]: null,
},
}
});
await View.destroy({
where: {
id: {
[Op.not]: null,
},
}
});
const collection = await Collection.findOne({
where: {
name: 'views_v2',
}
});
await collection.updateAssociations({
fields: views_v2.fields,
views_v2: views_v2.views_v2,
});
// const authors = require('./collections/authors').default;
console.log('views_v2.fields', views_v2.fields.map(field=>field.name));
// await collection.updateAssociations({
// views_v2: authors.views_v2,
// });
const tables = database.getTables([
'collections',
'fields',
'menus',
'pages_v2',
'views_v2',
'pages_views_v2',
'automations',
'automations_jobs',
'action_logs',
'users',
'roles',
'scopes',
'views_actions_v2',
'views_pages_v2',
'views_fields_v2',
]);
for (let table of tables) {
console.log(table.getName());
await Collection.import(table.getOptions(), { update: true, migrate: false });
}
const menus = [
{
title: '仪表盘',
icon: 'DashboardOutlined',
type: 'group',
children: [
{
title: '欢迎光临',
icon: 'DatabaseOutlined',
type: 'page',
pageName: 'welcome'
},
],
},
{
title: '数据',
icon: 'DatabaseOutlined',
type: 'group',
children: [
{
title: '作者',
icon: 'DatabaseOutlined',
type: 'page',
pageName: 'authors.all',
},
{
title: '申请表单',
icon: 'DatabaseOutlined',
type: 'page',
pageName: 'authors.form',
},
],
},
{
title: '用户',
icon: 'TeamOutlined',
type: 'group',
children: [
{
title: '用户管理',
icon: 'DatabaseOutlined',
type: 'page',
pageName: 'users.all',
},
],
},
{
title: '动态',
icon: 'NotificationOutlined',
type: 'group',
children: [
{
title: '操作日志',
icon: 'DatabaseOutlined',
type: 'page',
pageName: 'action_logs.all',
},
],
},
{
title: '配置',
icon: 'SettingOutlined',
type: 'group',
children: [
{
title: '数据表配置',
icon: 'DatabaseOutlined',
type: 'page',
pageName: 'collections.all',
},
{
title: '菜单配置',
icon: 'MenuOutlined',
type: 'page',
pageName: 'menus.all',
},
{
title: '页面配置',
icon: 'MenuOutlined',
type: 'page',
pageName: 'pages_v2.globals',
},
{
title: '权限配置',
icon: 'MenuOutlined',
type: 'page',
pageName: 'roles.all',
},
{
title: '自动化配置',
icon: 'MenuOutlined',
type: 'page',
pageName: 'automations.all',
},
],
},
];
for (const item of menus) {
const menu = await Menu.create(item);
await menu.updateAssociations(item);
}
})();

View File

@ -39,17 +39,19 @@ export default {
showInForm: true,
},
},
// {
// interface: 'linkTo',
// title: '书籍',
// target: 'books',
// labelField: 'name',
// component: {
// showInTable: true,
// showInDetail: true,
// showInForm: true,
// },
// },
{
interface: 'linkTo',
type: 'belongsToMany',
title: '书籍',
name: 'books',
target: 'books',
labelField: 'name',
component: {
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'createdBy',
title: '创建人',
@ -87,4 +89,87 @@ export default {
},
}
],
views_v2: [
{
type: 'table',
name: 'table',
title: '全部数据',
labelField: 'name',
actions: [
{
name: 'filter',
type: 'filter',
title: '过滤',
fields: [
'name',
],
// viewName: 'form',
},
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
fields: ['name'],
detailsOpenMode: 'drawer', // window
details: ['descriptions', 'books'],
sort: ['id'],
},
{
type: 'descriptions',
name: 'descriptions',
title: '详情',
fields: ['name'],
actions: [
{
name: 'update',
type: 'update',
title: '编辑',
viewName: 'form',
},
],
},
{
type: 'form',
name: 'form',
title: '表单',
fields: ['name'],
},
{
type: 'association',
name: 'books',
title: '书籍',
targetViewName: 'table',
targetFieldName: 'books',
},
],
pages_v2: [
{
title: '全部数据',
name: 'all',
views: ['table'],
},
{
title: '详情',
name: 'descriptions',
views: ['descriptions'],
},
{
title: '表单',
name: 'form',
views: ['form'],
},
{
title: '书籍',
name: 'books',
views: ['books'],
},
],
};

View File

@ -74,4 +74,75 @@ export default {
},
}
],
views_v2: [
{
type: 'table',
name: 'table',
title: '全部数据',
labelField: 'name',
actions: [
{
name: 'filter',
type: 'filter',
title: '过滤',
fields: [
'name',
],
// viewName: 'form',
},
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
fields: ['name'],
detailsOpenMode: 'drawer', // window
details: ['descriptions', 'form'],
sort: ['id'],
},
{
type: 'descriptions',
name: 'descriptions',
title: '详情',
fields: ['name'],
actions: [
{
name: 'update',
type: 'update',
title: '编辑',
viewName: 'form',
},
],
},
{
type: 'form',
name: 'form',
title: '表单',
fields: ['name'],
},
],
pages_v2: [
{
title: '全部数据',
name: 'all',
views: ['table'],
},
{
title: '详情',
name: 'descriptions',
views: ['descriptions'],
},
{
title: '表单',
name: 'form',
views: ['form'],
},
],
};

View File

@ -17,134 +17,15 @@ const data = [
type: 'layout',
template: 'TopMenuLayout',
sort: 10,
children: [
{
title: '仪表盘',
type: 'page',
path: '/dashboard',
icon: 'DashboardOutlined',
template: 'page1',
sort: 20,
showInMenu: true,
},
{
title: '数据',
type: 'layout',
path: '/collections',
icon: 'DatabaseOutlined',
template: 'SideMenuLayout',
sort: 30,
showInMenu: true,
children: [
// {
// title: '页面3',
// type: 'page',
// path: '/collections/page3',
// icon: 'dashboard',
// template: 'page3',
// sort: 40,
// },
// {
// title: '页面4',
// type: 'page',
// path: '/collections/page4',
// icon: 'dashboard',
// template: 'page4',
// sort: 50,
// },
]
},
{
title: '用户',
type: 'layout',
path: '/users',
icon: 'TeamOutlined',
template: 'SideMenuLayout',
sort: 70,
showInMenu: true,
children: [
{
title: '用户管理',
type: 'collection',
path: '/users/users',
icon: 'UserOutlined',
template: 'collection',
collection: 'users',
sort: 80,
showInMenu: true,
},
]
},
{
title: '动态',
type: 'layout',
path: '/activity',
icon: 'NotificationOutlined',
template: 'SideMenuLayout',
sort: 85,
showInMenu: true,
children: [
{
title: '操作记录',
type: 'collection',
path: '/activity/logs',
icon: 'HistoryOutlined',
template: 'collection',
collection: 'action_logs',
sort: 80,
showInMenu: true,
},
]
},
{
title: '配置',
type: 'layout',
path: '/settings',
icon: 'SettingOutlined',
template: 'SideMenuLayout',
sort: 90,
showInMenu: true,
children: [
{
title: '页面与菜单',
type: 'collection',
collection: 'pages',
path: '/settings/pages',
icon: 'MenuOutlined',
sort: 100,
developerMode: true,
showInMenu: true,
},
{
title: '数据表配置',
type: 'collection',
collection: 'collections',
path: '/settings/collections',
icon: 'TableOutlined',
sort: 110,
showInMenu: true,
},
{
title: '权限组配置',
type: 'collection',
collection: 'roles',
path: '/settings/roles',
icon: 'TableOutlined',
sort: 120,
showInMenu: true,
},
{
title: '自动化配置',
type: 'collection',
collection: 'automations',
path: '/settings/automations',
icon: 'TableOutlined',
sort: 130,
showInMenu: true,
},
]
},
],
redirect: '/admin',
},
{
title: '后台',
path: '/admin',
type: 'page',
inherit: false,
template: 'AdminLoader',
order: 230,
},
{
title: '登录页面',
@ -161,7 +42,7 @@ const data = [
inherit: false,
template: 'register',
order: 130,
}
},
];
(async () => {
@ -190,6 +71,7 @@ const data = [
nickname: "超级管理员",
password: "admin",
username: "admin",
email: 'dev@nocobase.com',
token: "38979f07e1fca68fb3d2",
});
}
@ -221,14 +103,16 @@ const data = [
});
}
const Role = database.getModel('roles');
const roles = await Role.bulkCreate([
{ title: '系统开发组', type: -1 },
{ title: '匿名用户组', type: 0 },
{ title: '普通用户组', default: true },
]);
await roles[0].updateAssociations({
users: user
});
if (Role) {
const roles = await Role.bulkCreate([
{ title: '系统开发组', type: -1 },
// { title: '匿名用户组', type: 0 },
{ title: '普通用户组', default: true },
]);
await roles[0].updateAssociations({
users: user
});
}
const Action = database.getModel('actions');
// 全局
@ -238,8 +122,116 @@ const data = [
// 导入地域数据
await chinaRegionSeederInit(api);
await database.getModel('collections').import(require('./collections/example').default);
await database.getModel('collections').import(require('./collections/authors').default);
await database.getModel('collections').import(require('./collections/books').default);
// await database.getModel('collections').import(require('./collections/example').default);
// await database.getModel('collections').import(require('./collections/authors').default);
// await database.getModel('collections').import(require('./collections/books').default);
const Menu = database.getModel('menus');
const menus = [
{
title: '仪表盘',
icon: 'DashboardOutlined',
type: 'group',
children: [
{
title: '欢迎光临',
icon: 'DatabaseOutlined',
type: 'page',
views: [],
name: 'welcome',
},
],
},
{
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: 'page',
views: ['action_logs.table'],
developerMode: true,
name: 'auditing',
},
],
},
{
title: '配置',
icon: 'SettingOutlined',
type: 'group',
developerMode: true,
children: [
{
name: 'system_settings',
title: '系统配置',
icon: 'DatabaseOutlined',
type: 'page',
views: ['system_settings.descriptions'],
developerMode: true,
},
{
name: 'collections',
title: '数据表配置',
icon: 'DatabaseOutlined',
type: 'page',
views: ['collections.table'],
developerMode: true,
},
{
name: 'menus',
title: '菜单和页面配置',
icon: 'MenuOutlined',
type: 'page',
views: ['menus.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,
},
],
},
];
for (const item of menus) {
const menu = await Menu.create(item);
await menu.updateAssociations(item);
}
await database.close();
})();

View File

@ -35,6 +35,9 @@ export const request: RequestConfig = {
export async function getInitialState() {
const { pathname, search } = location;
console.log(location);
const { data: systemSettings = {} } = await umiRequest('/system_settings:get?fields[appends]=logo,logo.storage', {
method: 'get',
});
let redirect = '';
// if (href.includes('?')) {
redirect = `?redirect=${pathname}${search}`;
@ -49,11 +52,13 @@ export async function getInitialState() {
if (!data.id) {
history.push('/login' + redirect);
return {
systemSettings,
currentUser: {},
};
}
return {
systemSettings,
currentUser: data,
};
} catch (error) {
@ -62,5 +67,8 @@ export async function getInitialState() {
}
}
return {};
return {
systemSettings,
currentUser: {},
};
}

View File

@ -1,12 +1,11 @@
.action-buttons {
display: flex;
justify-content: flex-end;
// justify-content: flex-end;
min-height: 32px;
flex-direction: row-reverse;
align-items: flex-start;
.action-button {
margin-right: 8px;
&:last-child {
margin-right: 0;
}
margin-left: 8px;
}
.filter-action-button {
position: absolute;

View File

@ -24,17 +24,40 @@ export const Cascader = connect({
})(function (props) {
const {
disabled,
target,
labelField,
valueField = 'id',
parentField,
maxLevel,
changeOnSelect,
// target,
// labelField,
// valueField = 'id',
// parentField,
// maxLevel,
// changeOnSelect,
value = [],
onChange,
schema = {},
// TODO(feature): 增加静态数据支持
// dataSource: []
} = props;
const {
target,
targetKey: valueField,
// 值字段
// valueField: 'code',
// 名称字段
labelField,
// TODO(refactor): 等 toWhere 重构完成后要改成 parent
// 上级字段名
parentField,
maxLevel,
// valueField = 'id',
// 深度限制,默认:-1代表不控制即如果是数据表则无限加载
// limit: -1,
// 可选层级,默认:-1代表可选的最深层级
// maxLevel: null,
// 是否可以不选择到最深一级
// 'x-component-props': { changeOnSelect: true }
incompletely: changeOnSelect,
} = schema;
const fieldNames = {
label: labelField,
value: valueField,

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react'
import { connect } from '@formily/react-schema-renderer'
import { Select, Drawer, Button, Space } from 'antd'
import { Select, Button, Space } from 'antd'
import {
mapStyledProps,
mapTextComponent,
@ -9,13 +9,15 @@ import {
isArr
} from '../shared'
import ViewFactory from '@/components/views'
import Drawer from '@/components/pages/AdminLoader/Drawer';
import View from '@/components/pages/AdminLoader/View';
function transform({value, multiple, labelField, valueField = 'id'}) {
let selectedKeys = [];
let selectedValue = [];
const values = Array.isArray(value) ? value : [];
selectedKeys = values.map(item => item[valueField]);
selectedValue = values.map(item => {
const values = Array.isArray(value) ? value : [value];
selectedKeys = values.filter(item => item).map(item => item[valueField]);
selectedValue = values.filter(item => item).map(item => {
return {
value: item[valueField],
label: item[labelField],
@ -27,14 +29,13 @@ function transform({value, multiple, labelField, valueField = 'id'}) {
return [selectedKeys, selectedValue];
}
function DrawerSelectComponent(props) {
const { disabled, target, multiple, filter, resourceName, associatedKey, labelField, valueField = 'id', value, onChange } = props;
export function DrawerSelectComponent(props) {
const { __parent, size, schema = {}, disabled, viewName, target, multiple, filter, resourceName, associatedKey, labelField, valueField = 'id', value, onChange } = props;
const [selectedKeys, selectedValue] = transform({value, multiple, labelField, valueField });
const [visible, setVisible] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState(multiple ? selectedKeys : [selectedKeys]);
const [selectedRows, setSelectedRows] = useState(selectedValue);
const [options, setOptions] = useState(selectedValue);
// console.log('valuevaluevaluevaluevaluevalue', value);
const { title = '' } = schema;
return (
<>
<Select
@ -42,6 +43,7 @@ function DrawerSelectComponent(props) {
open={false}
mode={multiple ? 'tags' : undefined}
labelInValue
size={size}
allowClear={true}
value={options}
notFoundContent={''}
@ -63,58 +65,54 @@ function DrawerSelectComponent(props) {
}}
onClick={() => {
if (!disabled) {
setVisible(true);
Drawer.open({
title: `选择要关联的${title}数据`,
content: ({resolve}) => {
console.log('valuevaluevaluevaluevaluevalue', selectedRowKeys, selectedRows, options);
const [rows, setRows] = useState(selectedRows);
const [rowKeys, setRowKeys] = useState(selectedRowKeys)
const [selected, setSelected] = useState(Array.isArray(value) ? value : [value]);
console.log({selectedRowKeys});
return (
<>
<View
__parent={__parent}
associatedKey={associatedKey}
multiple={multiple}
defaultFilter={filter}
defaultSelectedRowKeys={selectedRowKeys}
onSelected={(values) => {
setSelected(values);
const [selectedKeys, selectedValue] = transform({value: values, multiple: true, labelField, valueField });
setSelectedRows(selectedValue);
setRows(selectedValue);
setSelectedRowKeys(selectedKeys);
setRowKeys(selectedKeys);
console.log({ values, selectedValue, selectedKeys });
console.log({selectedRows, selectedRowKeys});
}}
viewName={viewName || `${target}.table`}
/>
<Drawer.Footer>
<Space>
<Button onClick={resolve}></Button>
<Button onClick={() => {
setOptions(rows);
// console.log('valuevaluevaluevaluevaluevalue', {selectedRowKeys});
onChange(multiple ? selected : selected.shift());
// console.log({rows, rowKeys});
resolve();
}} type={'primary'}></Button>
</Space>
</Drawer.Footer>
</>
)
},
})
// setVisible(true);
}
}}
></Select>
<Drawer
width={'40%'}
className={'noco-drawer'}
title={'关联数据'}
visible={visible}
bodyStyle={{padding: 0}}
onClose={() => {
setVisible(false);
}}
footer={[
<div
style={{
textAlign: 'right',
}}
>
<Space>
<Button onClick={() => setVisible(false)}></Button>
<Button type={'primary'} onClick={() => {
setOptions(selectedRows);
// console.log('valuevaluevaluevaluevaluevalue', {selectedRowKeys});
onChange(multiple ? selectedRowKeys : selectedRowKeys.shift());
setVisible(false);
}}></Button>
</Space>
</div>
]}
>
<ViewFactory
defaultFilter={filter}
multiple={multiple}
resourceTarget={target}
resourceName={associatedKey ? resourceName : target}
associatedKey={associatedKey}
isFieldComponent={true}
selectedRowKeys={selectedRowKeys}
onSelected={(values) => {
// 需要返回的是 array
const [selectedKeys, selectedValue] = transform({value: values, multiple: true, labelField, valueField });
setSelectedRows(selectedValue);
setSelectedRowKeys(selectedKeys);
// console.log('valuevaluevaluevaluevaluevalue', {values, selectedKeys, selectedValue});
}}
// associatedKey={}
// associatedName={associatedName}
viewName={'table'}
/>
</Drawer>
</>
);
}

View File

@ -480,7 +480,7 @@ export const Filter = connect({
],
};
const { value, onChange, associatedKey, filter = {}, sourceName, sourceFilter = {}, fields = [], ...restProps } = props;
console.log('filter', {associatedKey})
const { data = [], loading = true } = useRequest(() => {
return associatedKey ? api.resource(`collections.fields`).list({
associatedKey,

View File

@ -6,12 +6,14 @@ import { markdown } from '@/components/views/Field'
export const FormDescription = createVirtualBox(
'description',
styled(({ children, className, ...props }) => {
styled(({ schema = {}, children, className, ...props }) => {
const { title, tooltip } = schema as any;
console.log({schema})
return (
<Card size={'small'} headStyle={{padding: 0}} bodyStyle={{
<Card title={title} size={'small'} headStyle={{padding: 0}} bodyStyle={{
padding: 0,
}} className={className} {...props}>
{typeof children === 'string' && children && <div dangerouslySetInnerHTML={{__html: markdown(children)}}></div>}
{typeof tooltip === 'string' && tooltip && <div dangerouslySetInnerHTML={{__html: markdown(tooltip)}}></div>}
</Card>
)
})`

View File

@ -9,6 +9,7 @@ import findIndex from 'lodash/findIndex';
import get from 'lodash/get';
import set from 'lodash/set';
import { Scope } from './Scope';
import { DrawerSelectComponent } from '../drawer-select';
export const Permissions = {} as {Actions: any, Fields: any, Tabs: any};
@ -70,11 +71,15 @@ Permissions.Actions = connect({
}
const values = [...value||[]];
const index = findIndex(values, (item: any) => item && item.name === `${resourceKey}:${record.name}`);
console.log(values, index, `${resourceKey}:${record.name}`, get(values, [index, 'scope']));
return (
<Scope
resourceTarget={'scopes'}
associatedName={'collections'}
<DrawerSelectComponent
schema={{
title: '选择可操作的数据范围',
}}
size={'small'}
associatedKey={resourceKey}
viewName={'collections.scopes.table'}
target={'scopes'}
multiple={false}
labelField={'title'}
@ -86,16 +91,41 @@ Permissions.Actions = connect({
if (index === -1) {
values.push({
name: `${resourceKey}:${record.name}`,
scope_id: data,
scope_id: data.id,
});
} else {
set(values, [index, 'scope_id'], data);
set(values, [index, 'scope_id'], data.id);
}
console.log('valvalvalvalval', {values})
onChange(values);
console.log('valvalvalvalval', data);
}}
/>
// <Scope
// resourceTarget={'scopes'}
// associatedName={'collections'}
// associatedKey={resourceKey}
// target={'scopes'}
// multiple={false}
// labelField={'title'}
// valueField={'id'}
// value={get(values, [index, 'scope'])}
// onChange={(data) => {
// const values = [...value||[]];
// const index = findIndex(values, (item: any) => item && item.name === `${resourceKey}:${record.name}`);
// if (index === -1) {
// values.push({
// name: `${resourceKey}:${record.name}`,
// scope_id: data,
// });
// } else {
// set(values, [index, 'scope_id'], data);
// }
// console.log('valvalvalvalval', {values})
// onChange(values);
// console.log('valvalvalvalval', data);
// }}
// />
)
}
},

View File

@ -24,6 +24,8 @@ import { Permissions } from './permissions'
import { DraggableTable } from './draggable-table'
import { Values } from './values'
import { Automations } from './automations'
import { VirtualTable } from './virtual-table'
import { Wysiwyg } from './wysiwyg'
export const setup = () => {
registerFormFields({
@ -44,6 +46,7 @@ export const setup = () => {
month: DatePicker.MonthPicker,
week: DatePicker.WeekPicker,
string: Input,
select: Input,
icon: Icon,
textarea: Input.TextArea,
number: NumberPicker,
@ -60,11 +63,13 @@ export const setup = () => {
subTable: SubTable,
draggableTable: DraggableTable,
values: Values,
wysiwyg: Wysiwyg,
'permissions.actions': Permissions.Actions,
'permissions.fields': Permissions.Fields,
'permissions.tabs': Permissions.Tabs,
'automations.datetime': Automations.DateTime,
'automations.endmode': Automations.EndMode,
'automations.cron': Automations.Cron,
'virtualTable': VirtualTable,
});
}

View File

@ -12,9 +12,26 @@ import {
import { useRequest } from 'umi';
import api from '@/api-client';
import { Spin } from '@nocobase/client'
import get from 'lodash/get';
function RemoteSelectComponent(props) {
const { value, onChange, disabled, resourceName, associatedKey, filter, labelField, valueField = 'id', objectValue, placeholder, multiple } = props;
let { schema = {}, value, onChange, disabled, resourceName, associatedKey, filter, labelField, valueField, objectValue, placeholder, multiple } = props;
console.log({schema});
if (!resourceName) {
resourceName = get(schema, 'component.resourceName');
}
if (!filter) {
filter = get(schema, 'component.filter');
}
if (!labelField) {
labelField = get(schema, 'component.labelField');
}
if (!valueField) {
valueField = get(schema, 'component.valueField');
}
if (!valueField) {
valueField = 'id';
}
const { data = [], loading = true } = useRequest(() => {
return api.resource(resourceName).list({
associatedKey,
@ -27,6 +44,7 @@ function RemoteSelectComponent(props) {
if (multiple) {
selectProps.mode = 'multiple'
}
console.log({ data, props, associatedKey })
return (
<>
<Select

View File

@ -9,6 +9,7 @@ import { components, fields2columns } from '@/components/views/SortableTable';
import Form from './Form';
import { Spin } from '@nocobase/client';
import maxBy from 'lodash/maxBy';
import View from '@/components/pages/AdminLoader/View';
export interface SimpleTableProps {
schema?: any;
@ -24,102 +25,20 @@ export function generateIndex(): string {
}
export default function Table(props: SimpleTableProps) {
console.log(props);
const drawerRef = useRef<any>();
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const onTableChange = (selectedRowKeys: React.ReactText[]) => {
setSelectedRowKeys(selectedRowKeys);
}
const tableProps: any = {};
tableProps.rowSelection = {
selectedRowKeys,
onChange: onTableChange,
}
const { rowKey = '__id', fields = [] } = props;
const { target = 'fields', value = [], onChange } = props;
const { data: schema = {}, loading } = useRequest(() => api.resource(target).getView({
resourceKey: 'simple'
}));
const [dataSource, setDataSource] = useState(value.map((item, index) => {
if (item.__id) {
return item;
};
return {...item, __id: generateIndex()};
}))
if (loading) {
return <Spin/>
}
// console.log('dataSource', dataSource);
const { schema = {}, associatedKey, value, onChange, __parent } = props;
console.log({props, associatedKey, schema, __parent})
const { collection_name, name } = schema;
const viewName = `${collection_name}.${name}.table`;
return (
<div>
<div>
<Space style={{marginBottom: 14, position: 'absolute', right: 0, top: -31}}>
<Popconfirm title="确认删除吗?" onConfirm={() => {
console.log({selectedRowKeys})
const newValues = dataSource.filter(item => selectedRowKeys.indexOf(item.__id) === -1);
setDataSource(newValues);
onChange(newValues);
}}>
<Button size={'small'} type={'ghost'} danger></Button>
</Popconfirm>
<Button size={'small'} type={'primary'} onClick={() => {
drawerRef.current.setVisible(true);
drawerRef.current.setIndex(undefined);
drawerRef.current.setData({});
drawerRef.current.setTitle('新增子字段');
}}></Button>
</Space>
</div>
<Form target={target} onFinish={(values, index: number) => {
console.log(values);
const newVaules = [...dataSource];
if (typeof index === 'undefined') {
newVaules.push({...values, __id: generateIndex()})
} else {
newVaules[index] = values;
}
setDataSource(newVaules);
onChange(newVaules);
// console.log(newVaules);
}} ref={drawerRef}/>
<AntdTable
size={'small'}
rowKey={rowKey}
// loading={loading}
columns={fields2columns(schema.fields||[])}
dataSource={dataSource}
onChange={(pagination, filters, sorter, extra) => {
}}
components={components({
data: {
list: dataSource,
},
mutate: (values) => {
onChange(values.list);
setDataSource(values.list);
console.log('mutate', values);
},
rowKey,
onMoved: async ({resourceKey, target}) => {
}
})}
onRow={(record, index) => ({
onClick: () => {
console.log(record);
drawerRef.current.setVisible(true);
drawerRef.current.setIndex(index);
drawerRef.current.setData(record);
drawerRef.current.setTitle('编辑子字段');
}
})}
pagination={false}
{...tableProps}
<>
<View
__parent={__parent}
data={value}
onChange={onChange}
associatedKey={associatedKey}
viewName={viewName}
type={'subTable'}
/>
</div>
);
</>
)
}

View File

@ -184,7 +184,7 @@ export function getImgUrls(value) {
export const Upload = connect({
getProps: mapStyledProps
})((props) => {
const { value, onChange } = props;
const { multiple = true, value, onChange } = props;
const [visible, setVisible] = useState(false);
const [imgIndex, setImgIndex] = useState(0);
const [fileList, setFileList] = useState(toFileList(value));
@ -194,15 +194,16 @@ export const Upload = connect({
onChange({ fileList }) {
console.log(fileList);
setFileList((fileList));
onChange(toValues(fileList));
const list = toValues(fileList);
onChange(multiple ? list : (list.shift()||null));
},
};
const images = getImgUrls(fileList);
// console.log(images);
console.log({fileList});
return (
<div>
<AntdUpload
listType={'picture-card'}
listType={'picture'}
{...uploadProps}
fileList={fileList}
multiple={true}
@ -230,8 +231,11 @@ export const Upload = connect({
// }}
>
<PlusOutlined />
<div style={{marginTop: 5}}></div>
{(multiple || fileList.length < 1) && (
<>
<Button icon={<UploadOutlined />}></Button>
</>
)}
</AntdUpload>
{visible && <Lightbox
mainSrc={get(images, [imgIndex, 'url'])}

View File

@ -0,0 +1,52 @@
import React, { useState } from 'react';
import { Tooltip, Button } from 'antd';
import {
SchemaForm,
SchemaMarkupField as Field,
createFormActions,
createAsyncFormActions,
Submit,
Reset,
FormButtonGroup,
registerFormFields,
FormValidator,
setValidationLanguage,
} from '@formily/antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
import scopes from '@/components/views/Form/scopes';
import { fields2properties } from '@/components/pages/AdminLoader/View/Form';
import Drawer from '@/components/pages/AdminLoader/Drawer';
export function Form(props: any) {
const { data, onFinish } = props;
const { fields = [] } = props.schema||{};
return (
<SchemaForm
colon={true}
layout={'vertical'}
initialValues={data}
// actions={actions}
onReset={async () => {
// setData({filter: {}});
onFinish && await onFinish(null);
}}
onSubmit={async (values) => {
if (onFinish) {
await onFinish(values);
}
}}
schema={{
type: 'object',
properties: fields2properties(fields),
}}
expressionScope={scopes}
>
<FormButtonGroup align={'end'}>
<Reset></Reset>
<Submit></Submit>
</FormButtonGroup>
</SchemaForm>
);
}
export default Form;

View File

@ -0,0 +1,254 @@
import React, { useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import { Table as AntdTable, Button, Space, Popconfirm } from 'antd';
import { Actions } from '@/components/actions';
import ViewFactory from '@/components/views';
import { useRequest } from 'umi';
import api from '@/api-client';
import { components, fields2columns } from '@/components/views/SortableTable';
import Form from './Form';
import { Spin } from '@nocobase/client';
import maxBy from 'lodash/maxBy';
import Drawer from '@/components/pages/AdminLoader/Drawer';
import ReactDragListView from 'react-drag-listview';
import arrayMove from 'array-move';
import get from 'lodash/get';
import findIndex from 'lodash/findIndex';
import { FilterOutlined, PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
export interface SimpleTableProps {
schema?: any;
activeTab?: any;
resourceName: string;
associatedName?: string;
associatedKey?: string;
[key: string]: any;
}
const schema = {
talbe: {
fields: [
{
interface: 'sort',
type: 'sort',
name: 'sort',
title: '排序',
component: {},
},
// {
// interface: 'string',
// type: 'string',
// name: 'name',
// title: '视图',
// component: {
// type: 'string',
// },
// },
{
interface: 'linkTo',
type: 'string',
name: 'view',
target: 'views_v2',
// foreignKey: 'pageName',
// targetKey: 'id',
title: '视图',
labelField: 'title',
valueField: 'name',
multiple: false,
component: {
type: 'drawerSelect',
'x-component-props': {
viewName: 'views_v2.table',
resourceName: 'views_v2',
labelField: 'title',
valueField: 'name',
},
},
},
{
interface: 'radio',
type: 'string',
name: 'width',
title: '宽度',
dataSource: [
{ label: '50%', value: '50%' },
{ label: '100%', value: '100%' },
],
component: {
type: 'radio',
},
}
]
},
form: {
fields: [
// {
// interface: 'string',
// type: 'string',
// name: 'name',
// title: '视图',
// component: {
// type: 'string',
// },
// },
{
interface: 'linkTo',
type: 'belongsTo',
name: 'view',
target: 'views_v2',
// foreignKey: 'pageName',
// targetKey: 'id',
title: '视图',
labelField: 'title',
valueField: 'name',
multiple: false,
component: {
type: 'drawerSelect',
'x-component-props': {
multiple: false,
viewName: 'views_v2.table',
resourceName: 'views_v2',
labelField: 'title',
valueField: 'name',
},
},
},
{
interface: 'radio',
type: 'string',
name: 'width',
title: '宽度',
dataSource: [
{ label: '50%', value: '50%' },
{ label: '100%', value: '100%' },
],
component: {
type: 'radio',
},
}
],
},
};
export function generateIndex(): string {
return `${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
}
export default function Table(props: SimpleTableProps) {
console.log({props});
const { associatedKey, rowKey = '__index', value, onChange } = props;
const [dataSource, setDataSource] = useState(() => {
if (!Array.isArray(value)) {
return [];
}
return value.map((item: any) => {
if (typeof item === 'string') {
item = { name: item };
}
if (!item.__index) {
item.__index = generateIndex();
}
return item;
});
});
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const columns = fields2columns(schema.talbe.fields);
console.log({value});
const dragProps = {
async onDragEnd(fromIndex, toIndex) {
const list = dataSource;
const newList = arrayMove(list, fromIndex, toIndex);
setDataSource(newList);
console.log({fromIndex, toIndex, newList});
},
handleSelector: ".drag-handle",
ignoreSelector: "tr.ant-table-expanded-row",
nodeSelector: "tr.ant-table-row"
};
return (
<div>
<Popconfirm title="确认删除吗?" onConfirm={async () => {
const data = dataSource.filter(item => !selectedRowKeys.includes(item.__index));
setDataSource(data);
}}>
<Button
danger
type={'ghost'}
icon={<DeleteOutlined/>}
></Button>
</Popconfirm>
<Button icon={<PlusOutlined/>} type={'primary'} onClick={() => {
Drawer.open({
title: 'xx',
content: ({ resolve }) => {
return (
<>
<Form onFinish={(values) => {
const data = [...dataSource];
data.push({
...values,
__index: generateIndex(),
});
setDataSource(data);
onChange(data);
resolve();
}} schema={{
fields: schema.form.fields,
}}/>
</>
)
}
})
}}></Button>
<ReactDragListView {...dragProps}>
<AntdTable
rowKey={rowKey}
pagination={false}
size={'small'}
columns={columns}
dataSource={dataSource}
rowSelection={{
selectedRowKeys,
onChange(selectedRowKeys) {
setSelectedRowKeys(selectedRowKeys);
}
}}
onRow={(record) => {
return {
onClick() {
Drawer.open({
title: 'xx',
content: ({ resolve }) => {
return (
<>
<Form onFinish={(values) => {
const index = findIndex(dataSource, item => item.__index === values.__index);
const data = [...dataSource];
if (index === -1) {
return;
}
data[index] = values;
console.log({values, index, data})
setDataSource(data);
onChange(data);
// if (index >= 0) {
// dataSource[index] = values;
// setDataSource({...dataSource});
// }
resolve();
}} data={record} schema={{
fields: schema.form.fields,
}}/>
</>
)
}
})
}
}
}}
/>
</ReactDragListView>
</div>
);
}

View File

@ -0,0 +1,20 @@
import React, { useRef } from 'react'
import { connect } from '@formily/react-schema-renderer'
import moment from 'moment'
import { Select, Button, Table as AntdTable } from 'antd'
import {
mapStyledProps,
mapTextComponent,
compose,
isStr,
isArr
} from '../shared'
import ViewFactory from '@/components/views';
import Table from './Table';
export const VirtualTable = connect({
getProps: mapStyledProps,
getComponent: mapTextComponent,
})(Table)
export default VirtualTable

View File

@ -0,0 +1,11 @@
import { connect } from '@formily/react-schema-renderer'
import React from 'react';
import { Input as AntdInput } from 'antd'
import { acceptEnum, mapStyledProps, mapTextComponent } from '../shared'
export const Wysiwyg = connect({
getProps: mapStyledProps,
getComponent: mapTextComponent
})(acceptEnum((props) => <AntdInput.TextArea autoSize={{minRows: 2, maxRows: 12}} {...props}/>))
export default Wysiwyg

View File

@ -15,6 +15,9 @@ export default (props: any) => {
const { items = [], hideChildren, ...restProps } = props;
const location = useLocation();
let paths = items.map(item => item.path);
if (items.length === 0) {
return null;
}
return (
<Menu
defaultSelectedKeys={paths.filter(path => pathcamp(location.pathname, path)).concat(location.pathname)}

View File

@ -0,0 +1,274 @@
import React, { useState } from 'react';
import { Space, Button, Popconfirm, Popover } from 'antd';
import { FilterOutlined, PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import Drawer from '@/components/pages/AdminLoader/Drawer';
import View from '@/components/pages/AdminLoader/View';
import get from 'lodash/get';
import set from 'lodash/set';
export function Create(props) {
const { size, onFinish, schema = {}, associatedKey, ...restProps } = props;
const { title, viewName } = schema;
return (
<>
<Button
size={size}
onClick={() => {
Drawer.open({
title: title,
content: ({resolve}) => (
<div>
<View
{...restProps}
associatedKey={associatedKey}
viewName={viewName}
onReset={resolve}
onDraft={async (values) => {
await resolve();
console.log('onFinish', values);
onFinish && await onFinish(values);
}}
onFinish={async (values) => {
await resolve();
console.log('onFinish', values);
onFinish && await onFinish(values);
}}
/>
</div>
),
});
}}
icon={<PlusOutlined />}
type={'primary'}
>{ title }</Button>
</>
)
}
export function Update(props) {
const { onFinish, data, schema = {}, associatedKey, ...restProps } = props;
const { title, viewName } = schema;
return (
<>
<Button
onClick={() => {
Drawer.open({
title: title,
content: ({resolve}) => (
<div>
<View
{...restProps}
associatedKey={associatedKey}
data={data}
viewName={viewName}
onReset={resolve}
onDraft={async (values) => {
await resolve();
onFinish && await onFinish(values);
}}
onFinish={async (values) => {
await resolve();
onFinish && await onFinish(values);
}}
/>
</div>
),
});
}}
icon={<PlusOutlined />}
type={'primary'}
>{ title }</Button>
</>
)
}
export function Add(props) {
const { size, onFinish, schema = {}, associatedKey, ...restProps } = props;
console.log({associatedKey}, 'add');
const { filter, title, viewName, transform } = schema;
return (
<>
<Button
size={size}
onClick={() => {
Drawer.open({
title: title,
content: ({resolve}) => {
const [selectedRows, setSelectedRows] = useState([]);
return (
<div>
<View
{...restProps}
defaultFilter={filter}
viewName={viewName}
associatedKey={associatedKey}
onSelected={(values) => {
console.log(values);
setSelectedRows(values.map( item => {
if (!transform) {
return;
}
const data = {};
for (const [sourceKey, targetKey] of Object.entries<string>(transform)) {
const value = get({ data: item }, sourceKey);
set(data, targetKey, value);
}
return data;
}));
}}
/>
<Drawer.Footer>
<Space>
<Button onClick={resolve}></Button>
<Button type={'primary'} onClick={async () => {
console.log({schema, onFinish});
onFinish && await onFinish(selectedRows);
resolve();
}}></Button>
</Space>
</Drawer.Footer>
</div>
);
},
});
}}
icon={<PlusOutlined />}
type={'primary'}
>{ title }</Button>
</>
)
}
export function Destroy(props) {
const { size, schema = {}, onFinish } = props;
const { title } = schema;
return (
<Popconfirm title="确认删除吗?" onConfirm={async () => {
onFinish && await onFinish();
}}>
<Button
size={size}
danger
type={'ghost'}
icon={<DeleteOutlined />}
>{ title }</Button>
</Popconfirm>
)
}
export function Filter(props) {
const { schema = {}, onFinish } = props;
const { title, fields = [] } = schema;
const [visible, setVisible] = useState(false);
const [data, setData] = useState({});
const [filterCount, setFilterCount] = useState(0);
console.log('Filter', { visible, data });
return (
<>
{visible && (
<div
style={{
height: '100vh',
width: '100vw',
zIndex: 1000,
position: 'fixed',
background: 'rgba(0, 0, 0, 0)',
top: 0,
left: 0,
}}
onClick={() => setVisible(false)}
></div>
)}
<Popover
// title="设置筛选"
trigger="click"
visible={visible}
defaultVisible={visible}
placement={'bottomLeft'}
destroyTooltipOnHide
onVisibleChange={(visible) => {
setVisible(visible);
}}
className={'filters-popover'}
style={{
}}
overlayStyle={{
minWidth: 500
}}
content={(
<>
<View data={data} onFinish={async (values) => {
if (values) {
const items = values.filter.and || values.filter.or;
setFilterCount(Object.keys(items).length);
setData(values);
onFinish && await onFinish(values);
}
setVisible(false);
}} schema={{
"type": "filterForm",
"fields": [{
"dataIndex": ["filter"],
"name": "filter",
"interface": "json",
"type": "json",
"component": {
"type": "filter",
'x-component-props': {
fields,
}
},
}],
}}/>
</>
)}
>
<Button icon={<FilterOutlined />} >{filterCount ? `${filterCount}${title}` : title}</Button>
</Popover>
</>
)
}
export function Actions(props) {
const { onTrigger = {}, actions = [], style, ...restProps } = props;
return actions.length > 0 && (
<div className={'action-buttons'} style={style}>
{actions.map(action => (
<div className={`${action.type}-action-button action-button`}>
<Action
{...restProps}
onFinish={onTrigger[action.type]}
schema={action}
/>
</div>
))}
</div>
);
}
export default Actions;
const ACTIONS = new Map<string, any>();
export function registerAction(type: string, Action: any) {
ACTIONS.set(type, Action);
}
export function getAction(type: string) {
return ACTIONS.get(type);
}
export function Action(props) {
const { schema = {} } = props;
// cnsole.log(schema);
const { type } = schema;
const Component = getAction(type);
return Component && <Component {...props}/>;
}
registerAction('add', Add);
registerAction('update', Update);
registerAction('create', Create);
registerAction('destroy', Destroy);
registerAction('filter', Filter);

View File

@ -0,0 +1,177 @@
import React, { Fragment, useLayoutEffect, useRef, useState } from 'react'
import ReactDOM, { createPortal } from 'react-dom'
import { createForm } from '@formily/core'
// import { FormProvider } from '@formily/react'
import { isNum, isStr, isBool, isFn } from '@formily/shared'
import { Drawer } from 'antd'
import { DrawerProps } from 'antd/lib/drawer'
import { useContext } from 'react'
import { ConfigProvider } from 'antd'
import zhCN from 'antd/lib/locale/zh_CN';
export const usePrefixCls = (
tag?: string,
props?: {
prefixCls?: string
}
) => {
const { getPrefixCls } = useContext(ConfigProvider.ConfigContext)
return getPrefixCls(tag, props?.prefixCls)
}
type DrawerTitle = string | number | React.ReactElement
const isDrawerTitle = (props: any): props is DrawerTitle => {
return (
isNum(props) || isStr(props) || isBool(props) || React.isValidElement(props)
)
}
const getDrawerProps = (props: any): DrawerProps => {
if (isDrawerTitle(props)) {
return {
title: props,
}
} else {
return props
}
}
const createElement = (content, props?: any) => {
if (!content) {
return null;
}
if (typeof content === 'string') {
return content;
}
if (React.isValidElement(content)) {
return content;
}
return React.createElement(content, props);
}
export interface IFormDrawer {
open(props?: any): void
close(): void
}
export function FormDrawer(
title: DrawerProps,
content: any,
): IFormDrawer
export function FormDrawer(
title: DrawerTitle,
content: any,
): IFormDrawer
export function FormDrawer(title: any, content: any): IFormDrawer {
document.querySelectorAll('.env-root').forEach(el => {
el.className = 'env-root env-root-push';
});
const env = {
root: document.createElement('div'),
promise: null,
}
env.root.className = 'env-root';
const props = getDrawerProps(title)
const drawer = {
width: '50%',
...props,
onClose: (e: any) => {
props?.onClose?.(e)
formDrawer.close()
},
afterVisibleChange: (visible: boolean) => {
props?.afterVisibleChange?.(visible)
if (visible) return
ReactDOM.unmountComponentAtNode(env.root)
env.root?.parentNode?.removeChild(env.root)
env.root = undefined
},
}
const render = (visible = true, resolve?: () => any, reject?: () => any) => {
ReactDOM.render(
<ConfigProvider locale={zhCN}>
<Drawer {...drawer} className={'nb-drawer'} visible={visible}>
{createElement(content, {
resolve,
reject,
})}
</Drawer>
</ConfigProvider>,
env.root
)
}
document.body.appendChild(env.root)
const formDrawer = {
open: (props: any) => {
render(
false,
() => {
formDrawer.close()
},
() => {
formDrawer.close()
}
)
setTimeout(() => {
render(
true,
() => {
formDrawer.close()
},
() => {
formDrawer.close()
}
)
})
},
close: () => {
if (!env.root) return
const els = document.querySelectorAll('.env-root-push');
if (els.length) {
const last = els[els.length-1];
last.className = 'env-root';
}
render(false)
},
}
return formDrawer
}
const DrawerFooter: React.FC = (props) => {
const ref = useRef<HTMLDivElement>()
const [footer, setFooter] = useState<HTMLDivElement>()
const footerRef = useRef<HTMLDivElement>()
const prefixCls = usePrefixCls('drawer')
useLayoutEffect(() => {
const content = ref.current?.closest(`.${prefixCls}-wrapper-body`)
if (content) {
if (!footerRef.current) {
footerRef.current = content.querySelector(`.${prefixCls}-footer`)
if (!footerRef.current) {
footerRef.current = document.createElement('div')
footerRef.current.classList.add(`${prefixCls}-footer`)
content.appendChild(footerRef.current)
}
}
setFooter(footerRef.current)
}
})
footerRef.current = footer
return (
<div ref={ref} style={{ display: 'none' }}>
{footer && createPortal(props.children, footer)}
</div>
)
}
FormDrawer.open = (props) => {
const { content, visible, ...rest } = props;
return FormDrawer(rest, content).open({visible});
}
FormDrawer.Footer = DrawerFooter
export default FormDrawer

View File

@ -0,0 +1,89 @@
import React from 'react';
import { PageHeader, Card, Row, Col, Modal, message } from 'antd';
import './style.less';
import { Helmet, useHistory } from 'umi';
import { Spin } from '@nocobase/client';
import { useRequest, useLocation } from 'umi';
import api from '@/api-client';
import View from '../View';
import get from 'lodash/get';
import { markdown } from '@/components/views/Field';
export function Page(props: any) {
const { currentRowId, pageName, children, ...restProps } = props;
const { data = {}, loading, error } = useRequest(() => api.resource('menus').getInfo({
resourceKey: pageName,
}), {
refreshDeps: [pageName],
});
const history = useHistory();
if (error) {
return null;
}
if (loading) {
return <Spin/>
}
const views = data.views || [];
return (
<div>
<Helmet>
<title>{data.title}</title>
</Helmet>
<PageHeader
title={data.title}
ghost={false}
{...restProps}
/>
<div className={'page-content'}>
<Row className={'nb-row'} gutter={24}>
{views.map(view => {
let viewName: string;
let span = 24;
if (typeof view === 'string') {
viewName = view;
} if (typeof view === 'object') {
viewName = `${view.name}`;
if (view.width === '50%') {
span = 12;
} else if (view.width === '100%') {
span = 24;
}
}
return (
<Col style={{marginBottom: 24}} span={span}>
<Card bordered={false}>
<View
currentRowId={currentRowId}
onDraft={() => {
message.success('草稿保存成功');
}}
onFinish={() => {
if (view.returnType === 'message' && view.message) {
Modal.success({
title: '提交成功',
content: <div dangerouslySetInnerHTML={{__html: markdown(view.message)}}/>,
});
} else if (view.returnType === 'redirect') {
const path = get(view, 'redirect.name');
path && history.push(`${path}`);
}
}}
viewName={viewName}
/>
</Card>
</Col>
);
})}
</Row>
</div>
</div>
);
};
export default Page;

View File

@ -0,0 +1,32 @@
.page-content {
margin: 24px;
}
.env-root-push + style + div > div {
transform: translateX(-10%);
}
.env-root-push + div > div {
transform: translateX(-10%);
}
.nb-row {
> .ant-col:first-child:last-child {
display: block;
flex: 0 0 100%;
max-width: 100%;
}
}
@media only screen and (max-width: 800px) {
.ant-row.nb-row {
margin: 0 !important;
.ant-col {
display: block;
flex: 0 0 100%;
max-width: 100%;
padding: 0 !important;
margin-bottom: 1px !important;
}
}
}

View File

@ -0,0 +1,59 @@
import React, { useState } from 'react';
import { Layout, Breadcrumb, Drawer } from 'antd';
import { Link } from 'umi';
import './style.less';
import Menu from '../menu';
import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons';
import { useLocalStorageState } from 'ahooks';
import { useResponsive } from 'ahooks';
export function SideMenuLayout(props: any) {
const { currentPageName, menu = [], menuId } = props;
const [visible, setVisible] = useState(false);
// console.log(menu);
const [collapsed, setCollapsed] = useLocalStorageState(`nocobase-menu-collapsed-${menuId}`, false);
const responsive = useResponsive();
const isMobile = responsive.small && !responsive.middle && !responsive.large;
document.body.className = collapsed ? 'collapsed' : '';
return (
<Layout style={{height: 'calc(100vh - 48px)'}}>
{!isMobile && <Layout.Sider className={`nb-sider${collapsed ? ' collapsed' : ''}`} theme={'light'}>
<Menu menuId={menuId} currentPageName={currentPageName} items={menu} mode={'inline'}/>
<div onClick={() => {
setCollapsed(!collapsed);
setVisible(true);
document.body.className = collapsed ? 'collapsed' : '';
}} className={'menu-toggle'}>
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
style: { fontSize: 16 },
})}
</div>
</Layout.Sider>}
<Layout.Content id={'content'}>
{props.children}
{isMobile && <Drawer visible={visible} onClose={() => {
setCollapsed(!collapsed);
setVisible(false);
document.body.className = collapsed ? 'collapsed' : '';
}} placement={'left'} closable={false} bodyStyle={{padding: 0}}>
<Menu onSelect={() => {
setCollapsed(false);
setVisible(false);
document.body.className = collapsed ? 'collapsed' : '';
}} currentPageName={currentPageName} menuId={menuId} items={menu} mode={'inline'}/>
</Drawer>}
{isMobile && <div onClick={() => {
setCollapsed(!collapsed);
setVisible(true);
document.body.className = collapsed ? 'collapsed' : '';
}} className={'menu-toggle'}>
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
style: { fontSize: 16 },
})}
</div>}
</Layout.Content>
</Layout>
);
};
export default SideMenuLayout;

View File

@ -0,0 +1,21 @@
.nb-sider {
position: relative;
left: 0;
z-index: 100;
box-shadow: 2px 0 8px 0 rgba(29,35,41,.05);
&.collapsed {
margin-left: -200px;
}
.ant-menu-light {
border-right-color: transparent !important;
}
}
.menu-toggle {
position: fixed;
bottom: 12px;
left: 12px;
line-height: 1;
cursor: pointer;
z-index: 1000;
}

View File

@ -0,0 +1,55 @@
import React, { useState } from 'react';
import { Layout, Dropdown, Avatar, Drawer } from 'antd';
import './style.less';
import { history, Link, request, useModel } from 'umi';
import { UserOutlined, CodeOutlined, MenuOutlined } from '@ant-design/icons';
import AvatarDropdown from '@/components/pages/AvatarDropdown';
import Menu from '../menu';
import { ReactComponent as Logo } from './logo-white.svg';
import { useResponsive, useLocalStorageState } from 'ahooks';
import get from 'lodash/get';
export function TopMenuLayout(props: any) {
const { currentPageName, menu = [] } = props;
console.log({menu})
// const [visible, setVisible] = useState(false);
const [visible, setVisible] = useLocalStorageState(`nocobase-nav-visible`, false);
const responsive = useResponsive();
const isMobile = responsive.small && !responsive.middle && !responsive.large;
const { initialState = {}, loading, error, refresh, setInitialState } = useModel('@@initialState');
const logoUrl = get(initialState, 'systemSettings.logo.url');
console.log({logoUrl});
return (
<Layout style={{ height: '100vh' }}>
<Layout.Header style={{height: 48, lineHeight: '48px', padding: 0}} className="nb-header">
<div className="logo" style={{width: 200, height: 24, float: 'left'}}>
{!logoUrl ? <Logo/> : <img src={logoUrl}/>}
</div>
{!isMobile && <Menu currentPageName={currentPageName} hideChildren={true} items={menu} className={'noco-top-menu'} style={{float: 'left'}} theme="dark" mode="horizontal"/>}
{!isMobile && <AvatarDropdown/>}
{isMobile && <MenuOutlined onClick={() => {
setVisible(true);
}} style={{
fontSize: 16,
color: '#fff',
position: 'absolute',
right: 16,
top: 16,
}}/>}
{isMobile && <Drawer visible={visible} onClose={() => {
setVisible(false);
}} placement={'right'} closable={false} bodyStyle={{background: '#001529', padding: 0}}>
<Menu currentPageName={currentPageName} onSelect={() => {
setVisible(false);
}} mode={'inline'} hideChildren={true} items={menu} className={'noco-top-menu'} style={{float: 'left'}} theme="dark"/>
<AvatarDropdown/>
</Drawer>}
</Layout.Header>
<Layout.Content>
{props.children}
</Layout.Content>
</Layout>
);
};
export default TopMenuLayout;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,29 @@
.logo {
// font-family: 'Michroma', sans-serif;
color: rgba(255, 255, 255, 0.8);
font-size: 18px;
letter-spacing: 2px;
font-weight: 300;
padding: 0 15px;
img {
height: 100%;
}
svg {
margin-top: 12px;
}
}
body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.noco-top-menu.ant-menu-dark.ant-menu-horizontal > .ant-menu-item:hover,
.noco-top-menu.ant-menu.ant-menu-dark .ant-menu-item-selected,
.noco-top-menu.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected {
background-color: rgba(255, 255, 255, 0.1);
}
.ant-layout-content {
overflow: auto;
}

View File

@ -0,0 +1,27 @@
import React, { useState, useEffect } from 'react';
import './style.less';
import { Helmet } from 'umi';
import { Spin } from '@nocobase/client';
import { useRequest, useLocation } from 'umi';
import api from '@/api-client';
import { Actions } from '../Actions';
import { Table as AntdTable, Card, Pagination, Button, Tabs, Descriptions, Tooltip } from 'antd';
import { LoadingOutlined } from '@ant-design/icons';
import { components, fields2columns } from '@/components/views/SortableTable';
import ReactDragListView from 'react-drag-listview';
import arrayMove from 'array-move';
import get from 'lodash/get';
import Drawer from '@/components/pages/AdminLoader/Drawer';
import Field from '@/components/views/Field';
import { Form } from './Form';
import { View } from './';
export function Association(props) {
const { schema = {}, ...restProps } = props;
const { targetViewName } = schema;
return (
<div>
<View {...restProps} associationSchema={schema} viewName={targetViewName}/>
</div>
);
}

View File

@ -0,0 +1,115 @@
import React, { useState, useEffect } from 'react';
import './style.less';
import { Helmet } from 'umi';
import { Spin } from '@nocobase/client';
import { useRequest, useLocation } from 'umi';
import api from '@/api-client';
import { Actions } from '../Actions';
import { Table as AntdTable, Card, Pagination, Button, Tabs, Descriptions as AntdDescriptions, Tooltip } from 'antd';
import { LoadingOutlined } from '@ant-design/icons';
import { components, fields2columns } from '@/components/views/SortableTable';
import ReactDragListView from 'react-drag-listview';
import arrayMove from 'array-move';
import get from 'lodash/get';
import Drawer from '@/components/pages/AdminLoader/Drawer';
import Field from '@/components/views/Field';
import { Form } from './Form';
import { configResponsive, useResponsive } from 'ahooks';
import { InfoCircleOutlined } from '@ant-design/icons';
configResponsive({
small: 0,
middle: 800,
large: 1200,
});
function toGroups(fields: any[]) {
const groups = [];
let group = {
title: undefined,
tooltip: undefined,
children: [],
};
fields.forEach(field => {
if (field.interface === 'description') {
if (group.children.length) {
groups.push(group);
}
group = {
title: field.title,
tooltip: field.tooltip,
children: [],
};
} else {
group.children.push(field);
}
});
if (group.children.length) {
groups.push(group);
}
return groups;
}
export function Descriptions(props) {
const { data: record = {}, schema = {}, onDataChange } = props;
const { rowKey = 'id', resourceName, fields = [], actions = [], appends = [], associationField = {} } = schema;
const responsive = useResponsive();
const resourceKey = props.resourceKey || record[associationField.targetKey||rowKey];
const associatedKey = props.associatedKey || record[associationField.sourceKey||'id'];
console.log({resourceKey, data: record, associatedKey, associationField})
const { data = {}, loading, refresh } = useRequest(() => {
return api.resource(resourceName).get({
resourceKey,
associatedKey,
'fields[appends]': appends,
});
});
if (loading) {
return <Spin/>;
}
let descriptionsProps: any = {
size: 'middle',
bordered: true,
}
if (responsive.small && !responsive.middle && !responsive.large) {
descriptionsProps = {
layout: 'vertical'
}
}
const groups = toGroups(fields);
return (
<div>
<Actions
onTrigger={{
async update(values) {
refresh();
onDataChange && onDataChange(values);
},
}}
associatedKey={associatedKey}
data={data}
actions={actions}
style={{ marginBottom: 14 }}
/>
{groups.map(group => (
<AntdDescriptions
// layout={'vertical'}
// size={'middle'}
// bordered
{...descriptionsProps}
title={group.title && <span>{group.title} {group.tooltip && <Tooltip title={group.tooltip}><InfoCircleOutlined /></Tooltip>}</span>}
column={1}>
{group.children.map((field: any) => {
return (
<AntdDescriptions.Item label={field.title||field.name}>
<Field data={data} viewType={'descriptions'} schema={field} value={get(data, field.name)}/>
</AntdDescriptions.Item>
)
})}
</AntdDescriptions>
))}
</div>
);
}

View File

@ -0,0 +1,49 @@
import React, { useState } from 'react';
import { Tooltip, Button } from 'antd';
import {
SchemaForm,
SchemaMarkupField as Field,
createFormActions,
createAsyncFormActions,
Submit,
Reset,
FormButtonGroup,
registerFormFields,
FormValidator,
setValidationLanguage,
} from '@formily/antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
import scopes from '@/components/views/Form/scopes';
import { fields2properties } from './Form';
export function FilterForm(props: any) {
const { data, onFinish } = props;
const { fields = [] } = props.schema||{};
return (
<SchemaForm
colon={true}
layout={'vertical'}
initialValues={data}
// actions={actions}
onReset={async () => {
// setData({filter: {}});
onFinish && await onFinish(null);
}}
onSubmit={async (values) => {
if (onFinish) {
await onFinish(values);
}
}}
schema={{
type: 'object',
properties: fields2properties(fields),
}}
expressionScope={scopes}
>
<FormButtonGroup align={'end'}>
<Reset></Reset>
<Submit></Submit>
</FormButtonGroup>
</SchemaForm>
);
}

View File

@ -0,0 +1,245 @@
import React, { useState } from 'react';
import { Tooltip, Card, Button, message } from 'antd';
import {
SchemaForm,
SchemaMarkupField as Field,
createFormActions,
createAsyncFormActions,
Submit,
Reset,
FormButtonGroup,
registerFormFields,
FormValidator,
setValidationLanguage,
FormSpy,
LifeCycleTypes,
} from '@formily/antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
import api from '@/api-client';
import { useRequest, useLocation } from 'umi';
import Drawer from '@/components/pages/AdminLoader/Drawer';
import set from 'lodash/set';
import cloneDeep from 'lodash/cloneDeep'
import { Spin } from '@nocobase/client';
import { markdown } from '@/components/views/Field';
export function fields2properties(fields = []) {
const properties = {};
fields.forEach(field => {
const data = {
...field.component,
title: field.title,
required: field.required,
};
const linkages = field.linkages;
delete field.linkages;
set(data, 'x-component-props.schema', cloneDeep(field));
if (field.dataSource) {
data.enum = field.dataSource;
}
if (field.interface === 'boolean') {
set(data, 'x-component-props.children', data.title);
delete data.title;
}
properties[field.name] = data;
if (field.interface === 'linkTo') {
set(data, 'x-component-props.target', field.target);
set(data, 'x-component-props.multiple', field.multiple);
}
if (field.name === 'dataSource') {
set(data, 'x-component-props.operationsWidth', 'auto');
set(data, 'x-component-props.bordered', true);
set(data, 'x-component-props.className', 'data-source-table');
const property = {};
Object.assign(property, {
label: {
type: "string",
title: "选项",
required: true,
'x-component-props': {
bordered: false,
},
},
color: {
type: "colorSelect",
title: "颜色",
'x-component-props': {
bordered: false,
},
},
});
set(data, 'items.properties', property);
}
if (linkages) {
data['x-linkages'] = linkages;
}
if (field.defaultValue !== null) {
data.default = field.defaultValue;
}
if (field.tooltip) {
data.description = <div className={'markdown-content'} dangerouslySetInnerHTML={{__html: markdown(field.tooltip)}}></div>;
}
});
console.log({properties});
return properties;
}
const actions = createFormActions();
export function Form(props: any) {
const { onReset, __parent, noRequest = false, onFinish, onDraft, resolve, data: record = {}, associatedKey, schema = {} } = props;
console.log({ noRequest, record, associatedKey, __parent });
const { statusable, resourceName, rowKey = 'id', fields = [], appends = [], associationField = {} } = schema;
const resourceKey = props.resourceKey || record[associationField.targetKey||rowKey];
const { data = {}, loading, refresh } = useRequest(() => {
return (!noRequest && resourceKey) ? api.resource(resourceName).get({
associatedKey,
resourceKey,
'fields[appends]': appends,
}) : Promise.resolve({data: record});
});
const [status, setStatus] = useState('publish');
if (loading) {
return <Spin/>;
}
return (
<SchemaForm
colon={true}
layout={'vertical'}
initialValues={{
...data,
associatedKey,
resourceKey,
}}
effects={($, { setFieldState }) => {
$(LifeCycleTypes.ON_FORM_INIT).subscribe(() => {
setFieldState('*', state => {
set(state.props, 'x-component-props.__parent', __parent);
})
})
}}
// actions={actions}
schema={{
type: 'object',
properties: fields2properties(fields),
}}
onReset={async () => {
onReset && await onReset();
}}
onSubmit={async (values) => {
console.log({status});
if (!noRequest) {
resourceKey
? await api.resource(resourceName).update({
associatedKey,
resourceKey,
values: {
...values,
status,
},
})
: await api.resource(resourceName).create({
associatedKey,
values: {
...values,
status,
},
});
}
onFinish && await onFinish(values);
}}
expressionScope={{
text(...args: any[]) {
return React.createElement('span', {}, ...args)
},
tooltip(title: string, offset = 3) {
return (
<Tooltip title={title}>
<QuestionCircleOutlined
style={{ margin: '0 3px', cursor: 'default', marginLeft: offset }}
/>
</Tooltip>
);
},
}}
>
<FormButtonGroup className={'form-button-group'} align={'end'}>
<Reset></Reset>
{statusable && (
<FormSpy
selector={[
LifeCycleTypes.ON_FORM_MOUNT,
LifeCycleTypes.ON_FORM_SUBMIT_START,
LifeCycleTypes.ON_FORM_SUBMIT_END
]}
reducer={(state, action) => {
switch (action.type) {
case LifeCycleTypes.ON_FORM_SUBMIT_START:
return {
...state,
submitting: true
}
case LifeCycleTypes.ON_FORM_SUBMIT_END:
return {
...state,
submitting: false
}
default:
return state
}
}}
>
{({ state, form }) => {
const [submitting, setSubmitting] = useState(false);
return (
<Button
onClick={e => {
setSubmitting(true);
form.getFormState(state => {
(async () => {
resourceKey
? await api.resource(resourceName).update({
associatedKey,
resourceKey,
values: {
...state.values,
status: 'draft',
},
})
: await api.resource(resourceName).create({
associatedKey,
values: {
...state.values,
status: 'draft',
},
});
await form.reset({
validate: false,
});
onDraft && await onDraft({
...state.values,
status: 'draft',
});
setSubmitting(false);
})();
});
}}
{...props}
htmlType={'button'}
loading={submitting}
>
{'保存草稿'}
</Button>
)
}}
</FormSpy>
)}
<Submit></Submit>
</FormButtonGroup>
</SchemaForm>
);
}

View File

@ -0,0 +1,291 @@
import React, { useState, useEffect, useRef, createRef } from 'react';
import './style.less';
import { Helmet } from 'umi';
import { Spin } from '@nocobase/client';
import { useRequest, useLocation } from 'umi';
import api from '@/api-client';
import { Actions } from '../Actions';
import { Table as AntdTable, Card, Pagination, Button, Tabs, Descriptions, Tooltip } from 'antd';
import { LoadingOutlined } from '@ant-design/icons';
import { components, fields2columns } from '@/components/views/SortableTable';
import ReactDragListView from 'react-drag-listview';
import arrayMove from 'array-move';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import some from 'lodash/some';
import Drawer from '@/components/pages/AdminLoader/Drawer';
import Field from '@/components/views/Field';
import { Form } from './Form';
import { View } from './';
export function Details(props) {
const { onReset, __parent, noRequest, associatedKey, resourceName, onFinish, onDataChange, data, items = [], resolve } = props;
if (!items || items.length === 0) {
return null;
}
const [currentTabIndex, setCurrentTabIndex] = useState('0');
return (
<div className={'page-tabs'}>
{ items.length > 1 && (
<div className={'tabs-wrap'}>
<Tabs size={'small'} activeKey={`${currentTabIndex}`} onChange={(activeKey) => {
setCurrentTabIndex(activeKey);
}}>
{items.map((page, index) => (
<Tabs.TabPane tab={page.title} key={`${index}`}/>
))}
</Tabs>
</div>
) }
{(get(items, [currentTabIndex, 'views'])||[]).map(view => {
let viewName: string;
if (typeof view === 'string') {
viewName = `${resourceName}.${view}`;
} if (typeof view === 'object') {
viewName = `${resourceName}.${view.name}`;
}
return (
<View
onReset={onReset}
__parent={__parent}
noRequest={noRequest}
associatedKey={associatedKey}
onFinish={onFinish}
onDataChange={onDataChange}
data={data}
viewName={viewName}/>
);
})}
</div>
);
}
export function generateIndex(): string {
return `${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
}
export function SubTable(props: any) {
const {
__parent,
schema = {},
associatedKey,
onChange,
size = 'middle'
} = props;
const {
fields = [],
actions = [],
details = [],
paginated = true,
defaultPerPage = 10,
// rowKey = 'id',
labelField = 'id',
sort,
resourceName,
associationField = {},
appends = [],
expandable,
filter: schemaFilter = {},
} = schema;
const cloneFields = cloneDeep(fields) as any[];
let draggable = !!schema.draggable;
let sortField: string;
for (const field of cloneFields) {
if (field.type === 'sort') {
sortField = field.name;
}
}
if (draggable && !sortField) {
sortField = 'sort',
cloneFields.unshift({
"dataIndex": [
"sort"
],
"title": "排序",
"name": "sort",
"interface": "sort",
"type": "sort",
"required": true,
"developerMode": false,
"component": {
"type": "sort",
"showInTable": true,
"width": 60,
"className": "drag-visible"
},
});
}
if (!sortField) {
sortField = 'sort';
}
const { type } = associationField;
const { data = [], loading, mutate, refresh, run, params } = useRequest((params = {}, ...args) => {
return !associatedKey || type === 'virtual' || type === 'json' ? Promise.resolve({
data: (props.data||[]).map(item => {
if (!item[rowKey]) {
item[rowKey] = generateIndex();
}
return item;
})
}) : api.resource(resourceName).list({
associatedKey,
perPage: -1,
'fields[appends]': appends,
})
.then(({ data = [] }) => {
if (!Array.isArray(data)) {
return {
data: [],
}
}
return {
data: data.map(item => {
if (!item[rowKey]) {
item[rowKey] = generateIndex();
}
return item;
})
}
});
}, {
paginated: false,
});
const dataSource = data as any;
const rowKey = '__index';
const dragProps = {
async onDragEnd(fromIndex, toIndex) {
let data = arrayMove(dataSource, fromIndex, toIndex);
data = data.map((v: any, i) => {
return {...v, [sortField]: i};
});
mutate(data);
onChange && await onChange(data);
},
handleSelector: ".drag-handle",
ignoreSelector: "tr.ant-table-expanded-row",
nodeSelector: "tr.ant-table-row"
};
const tableProps: any = {};
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
if (actions.length) {
tableProps.rowSelection = {
type: 'checkbox',
selectedRowKeys,
onChange: (selectedRowKeys: React.ReactText[], selectedRows: React.ReactText[]) => {
setSelectedRowKeys(selectedRowKeys);
},
}
}
return (
<div>
<Actions size={size} __parent={__parent} associatedKey={associatedKey} noRequest={true} onTrigger={{
async create(values) {
values[rowKey] = generateIndex();
let data = [...dataSource];
data.push(values);
data = data.map((v: any, i) => {
return {...v, [sortField]: i};
});
mutate(data);
onChange && await onChange(data);
},
async add(items = []) {
let data = [...dataSource];
data.push(...items);
data = data.map((v: any, i) => {
if (!v[rowKey]) {
v[rowKey] = generateIndex();
}
return {...v, [sortField]: i};
});
mutate(data);
onChange && await onChange(data);
},
async destroy() {
let data = dataSource.filter(item => !selectedRowKeys.includes(item[rowKey]));
data = data.map((v: any, i) => {
return {...v, [sortField]: i};
});
mutate(data);
onChange && await onChange(data);
},
}} actions={actions} style={{ marginBottom: 14, marginTop: -31 }}/>
<ReactDragListView {...dragProps}>
<AntdTable
rowKey={rowKey}
dataSource={dataSource}
size={size}
columns={fields2columns(cloneFields)}
pagination={false}
onChange={(pagination, filters, sorter, extra) => {
}}
expandable={expandable}
onRow={(data, index) => ({
onClick: (e) => {
const className = (e.target as HTMLElement).className;
if (typeof className === 'string' &&
(className.includes('ant-table-selection-column')
|| className.includes('ant-checkbox')
|| className.includes('ant-radio')
)
) {
return;
}
Drawer.open({
title: details.length > 1 ? undefined : data[labelField],
bodyStyle: {
// padding: 0,
},
content: ({resolve}) => (
<div>
<Details
// __parent={__parent}
associatedKey={associatedKey}
resourceName={resourceName}
onFinish={async (values) => {
let data = [...dataSource];
data[index] = values;
data = data.map((v: any, i) => {
return {...v, [sortField]: i};
});
mutate(data);
onChange && await onChange(data);
resolve();
}}
onReset={resolve}
onDataChange={() => {
}}
noRequest={true}
data={data}
resolve={resolve}
items={details}
/>
</div>
),
});
},
})}
{...tableProps}
/>
</ReactDragListView>
</div>
);
}

View File

@ -0,0 +1,470 @@
import React, { useState, useEffect, useRef, createRef } from 'react';
import './style.less';
import { Helmet } from 'umi';
import { Spin } from '@nocobase/client';
import { useRequest, useHistory } from 'umi';
import api from '@/api-client';
import { Actions } from '../Actions';
import { PageHeader, Table as AntdTable, Card, Pagination, Button, Tabs, Descriptions, Tooltip } from 'antd';
import { LoadingOutlined } from '@ant-design/icons';
import { components, fields2columns } from '@/components/views/SortableTable';
import ReactDragListView from 'react-drag-listview';
import arrayMove from 'array-move';
import get from 'lodash/get';
import find from 'lodash/find';
import Drawer from '@/components/pages/AdminLoader/Drawer';
import Field from '@/components/views/Field';
import { Form } from './Form';
import { View } from './';
import pathToRegexp from 'path-to-regexp'
export const icon = <LoadingOutlined style={{ fontSize: 36 }} spin />;
export function Details(props) {
const { __parent, associatedKey, resourceName, onFinish, onReset, onDataChange, data, items = [], resolve } = props;
if (!items || items.length === 0) {
return null;
}
const [currentTabIndex, setCurrentTabIndex] = useState('0');
return (
<div className={'page-tabs'}>
{ items.length > 1 && (
<div className={'tabs-wrap'}>
<Tabs size={'small'} activeKey={`${currentTabIndex}`} onChange={(activeKey) => {
setCurrentTabIndex(activeKey);
}}>
{items.map((page, index) => (
<Tabs.TabPane tab={page.title} key={`${index}`}/>
))}
</Tabs>
</div>
) }
{(get(items, [currentTabIndex, 'views'])||[]).map(view => {
let viewName: string;
if (typeof view === 'string') {
viewName = `${resourceName}.${view}`;
} if (typeof view === 'object') {
viewName = `${resourceName}.${view.name}`;
}
return (
<View __parent={__parent} associatedKey={associatedKey} onReset={onReset} onFinish={onFinish} onDataChange={onDataChange} data={data} viewName={viewName}/>
);
})}
</div>
);
}
export function DetailsPage(props) {
const { currentRowId, title, __parent, associatedKey, resourceName, onFinish, onReset, onDataChange, data, items = [], resolve } = props;
if (!items || items.length === 0) {
return null;
}
const history = useHistory();
const paths = history.location.pathname.split('/');
const index = parseInt(paths[4]);
const [currentTabIndex, setCurrentTabIndex] = useState(items.length > index ? paths[4] : '0');
return (
<div>
<PageHeader
title={title}
ghost={false}
onBack={() => {
history.push(`/admin/${paths[2]}`);
}}
footer={<Tabs size={'small'} activeKey={`${currentTabIndex}`} onChange={(activeKey) => {
setCurrentTabIndex(activeKey);
history.push(`/admin/${paths[2]}/${currentRowId}/${activeKey}`);
}}>
{items.map((page, index) => (
<Tabs.TabPane tab={page.title} key={`${index}`}/>
))}
</Tabs>}
/>
<div style={{margin: 24}}>
<Card bordered={false}>
{(get(items, [currentTabIndex, 'views'])||[]).map(view => {
let viewName: string;
if (typeof view === 'string') {
viewName = `${resourceName}.${view}`;
} if (typeof view === 'object') {
viewName = `${resourceName}.${view.name}`;
}
return (
<View __parent={__parent} associatedKey={associatedKey} onReset={onReset} onFinish={onFinish} onDataChange={onDataChange} data={data} viewName={viewName}/>
);
})}
</Card>
</div>
</div>
);
}
export function Table(props: any) {
const {
onSelected,
multiple = true,
isFieldComponent,
schema = {},
data: record = {},
defaultFilter,
defaultSelectedRowKeys,
noRequest = false,
__parent,
currentRowId,
} = props;
const content = document.getElementById('content');
const {
fields = [],
actions = [],
details = [],
paginated = true,
defaultPerPage = 10,
rowKey = 'id',
labelField = 'id',
sort,
resourceName,
associationField = {},
appends = [],
expandable,
detailsOpenMode = 'drawer',
filter: schemaFilter = {},
} = schema;
const history = useHistory();
const associatedKey = props.associatedKey || record[associationField.sourceKey||'id'];
console.log({associatedKey, record, associationField, __parent})
async function reloadMenu() {
if (resourceName !== 'menus') {
return;
}
(window as any).reloadMenu && await (window as any).reloadMenu();
}
const { data, loading, pagination, mutate, refresh, run, params } = useRequest((params = {}, ...args) => {
const { current, pageSize, sorter, filter, ...restParams } = params;
console.log('paramsparamsparamsparamsparams', params, args);
return api.resource(resourceName).list({
associatedKey,
page: paginated ? current : 1,
perPage: paginated ? pageSize : -1,
sorter,
sort,
'fields[appends]': appends,
// filter,
// ...actionDefaultParams,
filter: {
and: [
defaultFilter,
schemaFilter,
filter,
// __parent ? {
// collection_name: __parent,
// } : null,
].filter(obj => obj && Object.keys(obj).length)
}
// ...args2,
})
.then(({data = [], meta = {}}) => {
return {
data: {
list: data,
total: meta.count||data.length,
},
};
});
}, {
paginated,
defaultPageSize: defaultPerPage,
});
const currentRow = find(data && data.list, item => item[rowKey] == currentRowId)
console.log({currentRow});
function getExpandedRowKeys(items: Array<any>) {
if (!Array.isArray(items)) {
return [];
}
console.log({items})
let rowKeys = [];
items.forEach(item => {
if (item.children && item.children.length) {
rowKeys.push(item[rowKey]);
rowKeys = rowKeys.concat(getExpandedRowKeys(item.children));
}
});
return rowKeys;
}
const [expandedRowKeys, setExpandedRowKeys] = useState(() => {
if (expandable) {
return getExpandedRowKeys(data?.list);
}
return [];
});
useEffect(() => {
setExpandedRowKeys(getExpandedRowKeys(data?.list));
}, [data]);
if (expandable) {
// expandable.expandIconColumnIndex = 4;
expandable.onExpand = (expanded, record) => {
if (!expanded) {
const index = expandedRowKeys.indexOf(record[rowKey]);
if (index >= 0) {
expandedRowKeys.splice(index, 1);
}
} else {
expandedRowKeys.push(record[rowKey]);
}
setExpandedRowKeys(expandedRowKeys);
}
expandable.expandedRowKeys = expandedRowKeys;
console.log({expandable, data});
}
// const { data, loading, pagination, mutate, refresh, run, params } = useRequest((params = {}, ...args) => {
// const { current, pageSize, sorter, filter, ...restParams } = params;
// return api.resource(resourceName).list({
// associatedKey,
// sort,
// }).then(({data = [], meta = {}}) => {
// return {
// data: {
// list: data,
// total: meta.count||data.length,
// },
// };
// });
// }, {
// paginated,
// defaultPageSize: defaultPerPage,
// });
const [selectedRowKeys, setSelectedRowKeys] = useState(defaultSelectedRowKeys||[]);
const onChange = (selectedRowKeys: React.ReactText[], selectedRows: React.ReactText[]) => {
setSelectedRowKeys(selectedRowKeys);
onSelected && onSelected(selectedRows);
}
// useEffect(() => {
// setSelectedRowKeys(srk);
// }, [srk]);
// console.log(srk);
const tableProps: any = {};
if (actions.length || defaultSelectedRowKeys) {
tableProps.rowSelection = {
type: multiple ? 'checkbox' : 'radio',
selectedRowKeys,
onChange,
}
}
const ref = createRef<HTMLDivElement>();
const dragProps = {
async onDragEnd(fromIndex, toIndex) {
const list = data?.list||(data as any);
const nodes = ref.current.querySelectorAll('.ant-table-row');
const resourceKey = nodes[fromIndex].getAttribute('data-row-key');
const targetIndex = nodes[toIndex].getAttribute('data-row-key');
// const newList = arrayMove(list, fromIndex, toIndex);
// const item = list.splice(fromIndex, 1)[0];
// list.splice(toIndex, 0, item);
// mutate({
// ...data,
// list: newList,
// });
await api.resource(resourceName).sort({
associatedKey,
resourceKey,
values: {
field: 'sort',
target: {
[rowKey]: targetIndex,
},
},
});
await refresh();
await reloadMenu();
// console.log(nodes[fromIndex].getAttribute('data-row-key'), nodes[toIndex])
console.log({
// ref: ref.current.querySelectorAll('.ant-table-row'),
// fromIndex, toIndex, newList,
values: {
field: 'sort',
target: {
[rowKey]: targetIndex,
},
},
});
},
handleSelector: ".drag-handle",
ignoreSelector: "tr.ant-table-expanded-row",
nodeSelector: "tr.ant-table-row"
};
return (
<div>
<div ref={ref}>
<Actions __parent={__parent} associatedKey={associatedKey} onTrigger={{
async create(values) {
await refresh();
await reloadMenu();
},
async add(values = []) {
await api.resource(resourceName).add({
associatedKey,
values,
});
},
async update(values) {
await refresh();
await reloadMenu();
},
async filter(values) {
const items = values.filter.and || values.filter.or;
// @ts-ignore
run({...params[0], filter: values.filter});
// refresh();
},
async destroy() {
if (selectedRowKeys.length) {
await api.resource(resourceName).destroy({
associatedKey,
filter: {
[`${rowKey}.in`]: selectedRowKeys,
},
});
}
refresh();
await reloadMenu();
},
}} actions={actions} style={{ marginBottom: 14 }}/>
<ReactDragListView {...dragProps}>
<AntdTable
rowKey={rowKey}
loading={{
spinning: loading,
size: 'large',
indicator: icon,
}}
components={{
body: {
row: ({className, ...others}) => {
if (!detailsOpenMode) {
return <tr className={className} {...others}/>
}
return <tr className={className ? `${className} row-clickable` : 'row-clickable'} {...others}/>
},
}
}}
dataSource={data?.list||(data as any)}
size={'middle'}
columns={fields2columns(fields, {associatedKey, refresh})}
pagination={false}
onChange={(pagination, filters, sorter, extra) => {
run({...params[0], sorter});
}}
expandable={expandable}
onRow={(data) => ({
onClick: (e) => {
const className = (e.target as HTMLElement).className;
console.log({className});
if (typeof className === 'string' &&
(className.includes('ant-table-selection-column')
|| className.includes('ant-checkbox')
|| className.includes('ant-radio')
)
) {
return;
}
if (!detailsOpenMode) {
return;
}
if (detailsOpenMode === 'window') {
const paths = history.location.pathname.split('/');
history.push(`/admin/${paths[2]}/${data[rowKey]}/0`);
} else {
Drawer.open({
headerStyle: details.length > 1 ? {
paddingBottom: 0,
borderBottom: 0,
// paddingTop: 16,
// marginBottom: -4,
} : {},
// title: details.length > 1 ? undefined : data[labelField],
title: data[labelField],
bodyStyle: {
// padding: 0,
},
content: ({resolve}) => (
<div>
<Details
__parent={__parent}
associatedKey={associatedKey}
resourceName={resourceName}
onFinish={async () => {
await refresh();
resolve();
await reloadMenu();
}}
onDraft={async () => {
await refresh();
resolve();
await reloadMenu();
}}
onReset={resolve}
onDataChange={async () => {
await refresh();
await reloadMenu();
}}
data={data}
resolve={resolve}
items={details}
/>
</div>
),
});
}
},
})}
{...tableProps}
/>
</ReactDragListView>
{paginated && (
<div className={'table-pagination'}>
<Pagination {...pagination} showTotal={(total)=> `${total} 条记录`} showQuickJumper showSizeChanger size={'small'}/>
</div>
)}
</div>
{currentRow && <div className={'details-page'}>
<DetailsPage
title={get(currentRow, labelField)}
__parent={__parent}
associatedKey={associatedKey}
resourceName={resourceName}
onFinish={async () => {
await refresh();
await reloadMenu();
}}
onReset={() => {
}}
onDataChange={async () => {
await refresh();
await reloadMenu();
}}
currentRowId={currentRowId}
data={currentRow}
items={details}
/>
</div>}
</div>
);
}

View File

@ -0,0 +1,26 @@
import React, { useState, useEffect } from 'react';
import './style.less';
import { Helmet } from 'umi';
import { Spin } from '@nocobase/client';
import { useRequest, useLocation } from 'umi';
import api from '@/api-client';
import { Actions } from '../Actions';
import { Table as AntdTable, Card, Pagination, Button, Tabs, Descriptions as AntdDescriptions, Tooltip } from 'antd';
import { LoadingOutlined } from '@ant-design/icons';
import { components, fields2columns } from '@/components/views/SortableTable';
import ReactDragListView from 'react-drag-listview';
import arrayMove from 'array-move';
import get from 'lodash/get';
import Drawer from '@/components/pages/AdminLoader/Drawer';
import Field, { markdown } from '@/components/views/Field';
import { Form } from './Form';
export function Wysiwyg(props) {
const { data: record = {}, schema = {}, onDataChange } = props;
const { html = '' } = schema;
return (
<div dangerouslySetInnerHTML={{__html: markdown(html)}}></div>
);
}

View File

@ -0,0 +1,68 @@
import React, { useState, useEffect } from 'react';
import './style.less';
import { Helmet } from 'umi';
import { Spin } from '@nocobase/client';
import { useRequest, useLocation } from 'umi';
import api from '@/api-client';
import { Actions } from '../Actions';
import { Table as AntdTable, Card, Pagination, Button, Tabs, Tooltip } from 'antd';
import { LoadingOutlined } from '@ant-design/icons';
import { components, fields2columns } from '@/components/views/SortableTable';
import ReactDragListView from 'react-drag-listview';
import arrayMove from 'array-move';
import get from 'lodash/get';
import Drawer from '@/components/pages/AdminLoader/Drawer';
import Field from '@/components/views/Field';
import { Form } from './Form';
import { Table } from './Table';
import { Association } from './Association';
import { Descriptions } from './Descriptions';
import { FilterForm } from './FilterForm';
import { SubTable } from './SubTable';
import { Wysiwyg } from './Wysiwyg';
const VIEWS = new Map();
export function registerView(type, view) {
VIEWS.set(type, view);
}
export function getView(type) {
return VIEWS.get(type);
}
export const icon = <LoadingOutlined style={{ fontSize: 36 }} spin />;
export function View(props: any) {
const { schema, viewName, children, ...restProps } = props;
const { data = {}, loading } = useRequest(() => {
return schema ? Promise.resolve({ data: schema }) : api.resource('views_v2').getInfo({
resourceKey: viewName,
});
}, {
refreshDeps: [viewName, schema],
});
if (loading) {
return <Spin/>
}
const type = props.type || data.type;
const Component = getView(type);
return (
<Component {...restProps} schema={data}/>
);
};
registerView('table', Table);
registerView('subTable', SubTable);
registerView('form', Form);
registerView('filterForm', FilterForm);
registerView('descriptions', Descriptions);
registerView('association', Association);
registerView('wysiwyg', Wysiwyg);
export default View;

View File

@ -0,0 +1,67 @@
.ant-drawer-body {
padding-bottom: 50px;
.form-button-group {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 10px 16px;
border-top: 1px solid #f0f0f0;
background-color: #fff;
}
}
.ant-drawer-footer {
text-align: right;
}
.page-tabs {
.ant-tabs {
margin: -24px -24px 8px;
position: relative;
// position: relative;
// // border-bottom: 1px solid #f0f0f0;
// padding: 0 24px;
// &::after {
// position: absolute;
// right: -24px;
// left: -24px;
// border-bottom: 1px solid #f0f0f0;
// content: '';
// bottom: 16px;
// z-index: 11;
// }
.ant-tabs-nav {
&::before {
right: -24px;
left: -24px;
}
.ant-tabs-tab {
margin: 0 0 0 24px;
}
// margin-bottom: -1px;
}
}
}
.markdown-content {
*:last-child {
margin-bottom: 0;
}
}
.details-page {
background: #f0f2f5;
position: fixed;
top: 48px;
left: 200px;
width: calc(100% - 200px);
height: 100%;
overflow: auto;
transition: all 0.2s;
}
body.collapsed {
.details-page {
left: 0;
width: 100%;
}
}

View File

@ -0,0 +1,57 @@
import React from 'react';
import api from '@/api-client';
import { useRequest, useLocation, useHistory, Redirect } from 'umi';
import get from 'lodash/get';
import { TopMenuLayout } from './TopMenuLayout';
import { SideMenuLayout } from './SideMenuLayout';
import Page from './Page';
import pathToRegexp from 'path-to-regexp'
import { Spin } from '@nocobase/client';
export function AdminLoader(props: any) {
const { data = [], error, loading, run } = useRequest(() => api.resource('menus').getTree());
(window as any).reloadMenu = async () => {
await run();
};
const { lastPage: { path } } = props;
const location = useLocation();
const match = pathToRegexp(`${path}/:path?/:rowId?/:tabId?`).exec(location.pathname);
const pageName = match[1]||null;
const history = useHistory();
const currentRowId = match[2]||null;
const items = data
// .filter(item => item.type !== 'group' || (item.children && item.children.length))
;
const sideMenu = items.find(item => {
if (item.paths && item.paths.includes(pageName)) {
return true;
}
return false;
});
console.log({pageName, sideMenu})
if (loading) {
return <Spin/>
}
if (!pageName) {
return <Redirect to={`/admin/${get(items, [0, 'path'])}`}/>;
}
return (
<>
<TopMenuLayout currentPageName={pageName} {...props} menu={items}>
{sideMenu ? (
<SideMenuLayout currentPageName={pageName} {...props} menuId={sideMenu.id} menu={sideMenu.children}>
<Page currentRowId={currentRowId} pageName={pageName}></Page>
</SideMenuLayout>
) : (
<Page pageName={pageName}></Page>
)}
</TopMenuLayout>
</>
);
}
export default AdminLoader;

View File

@ -0,0 +1,91 @@
import React, { useEffect, useState } from 'react';
import { Layout, Menu, Breadcrumb } from 'antd';
import { Link as UmiLink, useLocation } from 'umi';
import Icon from '@/components/icons';
import './style.less';
function pathcamp(path1: string, path2: string) {
return true;
if (path1 === path2) {
return true;
}
return path1.indexOf(`${path2}/`) === 0;
}
function Link(props: any) {
const { to, children } = props;
if (/^http/.test(to)) {
return <a target={'_blank'} href={to}>{children}</a>
}
return <UmiLink {...props} to={`/admin/${to}`}/>
}
export default (props: any) => {
const { menuId, currentPageName, items = [], hideChildren, ...restProps } = props;
if (items.length === 0) {
return null;
}
const toPaths = (data) => {
const paths = [];
data.forEach(item => {
if (item.path && item.path === currentPageName) {
paths.push(`${item.name}`);
}
if (item.paths && item.paths.includes(currentPageName)) {
paths.push(`${item.name}`);
}
paths.push(...toPaths(item.children||[]));
});
return paths;
}
const keys = toPaths(items);
console.log({menuId, currentPageName, items, keys});
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
setTimeout(() => {
setLoading(false);
});
}, [menuId]);
if (loading) {
return null;
}
const renderChildren = (items) => {
return items.map(item => {
const { children = [] } = item;
// const subItems = children.filter(child => child.showInMenu);
if (!hideChildren && children.length) {
return (
<Menu.SubMenu key={`${item.name}`} icon={item.icon && <Icon type={item.icon}/>} title={<>{item.title}</>}>
{renderChildren(children)}
</Menu.SubMenu>
)
}
return (
<Menu.Item icon={item.icon && <Icon type={item.icon}/>} key={`${item.name}`}>
<Link to={item.path}>{item.title}</Link>
</Menu.Item>
)
})
}
return (
<Menu
defaultSelectedKeys={keys}
defaultOpenKeys={keys}
// selectedKeys={keys}
// openKeys={keys}
onOpenChange={(openKeys) => {
console.log({openKeys});
}}
onSelect={(info) => {
console.log({info});
}}
onDeselect={(info) => {
console.log({info});
}}
{...restProps}
>
{renderChildren(items)}
</Menu>
);
};

View File

@ -0,0 +1,7 @@
.ant-menu-sub.ant-menu-inline > .ant-menu-item,
.ant-menu-sub.ant-menu-inline > .ant-menu-submenu > .ant-menu-submenu-title {
height: 32px;
line-height: 32px;
margin: 4px 0;
font-size: 13px;
}

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Layout, Dropdown, Avatar, Drawer } from 'antd';
import './style.less';
import { history, Link, request, useModel } from 'umi';
import { history, Link, request, useModel, Redirect } from 'umi';
import { UserOutlined, CodeOutlined, MenuOutlined } from '@ant-design/icons';
import AvatarDropdown from '../AvatarDropdown';
import Menu from '@/components/menu';
@ -14,10 +14,13 @@ export function TopMenuLayout(props: any) {
const [visible, setVisible] = useLocalStorageState(`nocobase-nav-visible`, false);
const responsive = useResponsive();
const isMobile = responsive.small && !responsive.middle && !responsive.large;
return <Redirect to={'/admin'}/>
return (
<Layout style={{ height: '100vh' }}>
<Layout.Header style={{height: 48, lineHeight: '48px', padding: 0}} className="nb-header">
<div className="logo" style={{width: 200, height: 20, float: 'left'}}><Logo/></div>
<div className="logo" style={{width: 200, height: 20, float: 'left'}}>
<Logo/>
</div>
{!isMobile && <Menu hideChildren={true} items={menu} className={'noco-top-menu'} style={{float: 'left'}} theme="dark" mode="horizontal"/>}
{!isMobile && <AvatarDropdown/>}
{isMobile && <MenuOutlined onClick={() => {

View File

@ -1,3 +1,20 @@
.page-content {
margin: 24px;
}
.popover-button-mask {
height: 100vh;
width: 100vw;
z-index: -1;
position: fixed;
background-color: rgba(0, 0, 0, 0.45);
top: 0px;
left: 0px;
}
@media only screen and (max-width: 800px) {
.page-content {
margin: 0px;
margin-top: 1px;
}
}

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import moment from 'moment';
import { Tag, Popover, Table, Drawer, Modal, Checkbox, message } from 'antd';
import { Tag, Popover, Table, Modal, Checkbox, message } from 'antd';
import Icon from '@/components/icons';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
@ -16,6 +16,8 @@ import Lightbox from 'react-image-lightbox';
import 'react-image-lightbox/style.css';
import api from '@/api-client';
import { useRequest } from 'umi';
import Drawer from '@/components/pages/AdminLoader/Drawer';
import View from '@/components/pages/AdminLoader/View';
marked.setOptions({
gfm: true,
@ -97,17 +99,12 @@ export function TextareaField(props: any) {
// refreshDeps: [resourceKey]
// });
export function BooleanField(props: any) {
const { data = {}, value, schema: { name, editable, resource } } = props;
const { data = {}, value, schema: { name, editable, resourceName } } = props;
if (editable) {
return <Checkbox defaultChecked={value} onChange={async (e) => {
await api.resource(resource).update({
await api.resource(resourceName).toggle({
associatedKey: data.associatedKey,
resourceKey: data.id,
tableName: data.tableName||'pages',
values: {
tableName: data.tableName||'pages',
[name]: e.target.checked,
},
});
message.success('保存成功');
// console.log(props);
@ -234,29 +231,45 @@ export function SubTableField(props: any) {
}
export function LinkToField(props: any) {
const { schema, value } = props;
const { data, schema, value } = props;
if (!value) {
return null;
}
console.log({props});
const values = Array.isArray(value) ? value : [value];
return (
<div className={'link-to-field'}>
{values.map(item => <LinkToFieldLink data={item} schema={schema}/>)}
{values.map(item => <LinkToFieldLink parent={data} data={item} schema={schema}/>)}
</div>
);
}
export function LinkToFieldLink(props) {
const { data, schema, schema: { title, labelField } } = props;
const { parent, schema, schema: { title, labelField, viewName, name, target, collection_name } } = props;
const [visible, setVisible] = useState(false);
// console.log(schema);
const [data, setData] = useState(props.data||{});
return (
<span className={'link-to-field-tag'}>
<a onClick={(e) => {
e.stopPropagation();
setVisible(true);
// setVisible(true);
Drawer.open({
title: data[labelField],
content: ({resolve}) => {
console.log({parent, data, props, schema});
return (
<div>
<View onFinish={(values) => {
setData(values);
resolve();
console.log({data, values});
}} associatedKey={parent.id} data={data} viewName={viewName || `${collection_name}.${name}.descriptions`}/>
</div>
);
}
});
}}>{data[labelField]}</a>
<Drawer
{/* <Drawer
// @ts-ignore
onClick={(e) => {
e.stopPropagation();
@ -273,7 +286,7 @@ export function LinkToFieldLink(props) {
viewName={'details'}
resourceKey={data.id}
/>
</Drawer>
</Drawer> */}
</span>
);
}
@ -394,6 +407,7 @@ registerFieldComponents({
textarea: TextareaField,
boolean: BooleanField,
select: DataSourceField,
status: DataSourceField,
multipleSelect: DataSourceField,
radio: DataSourceField,
checkboxes: DataSourceField,

View File

@ -20,9 +20,19 @@ export function Login(props: any) {
const { initialState = {}, loading, error, refresh, setInitialState } = useModel('@@initialState');
const { redirect } = props.location.query;
if (loading) {
return null;
}
const { systemSettings = {} } = initialState as any;
console.log({systemSettings});
const { title } = systemSettings || {};
return (
<div className={'users-form'}>
<h1>NocoBase</h1>
<h1>{title||'NocoBase'}</h1>
<h2></h2>
<SchemaForm onSubmit={async (values) => {
console.log(values);
@ -32,20 +42,24 @@ export function Login(props: any) {
});
if (data.data && data.data.token) {
localStorage.setItem('NOCOBASE_TOKEN', data.data.token);
setInitialState({currentUser :data.data});
// @ts-ignore
setInitialState({
...initialState,
currentUser: data.data,
});
await (window as any).routesReload();
history.push(redirect||'/');
}
}} actions={actions} schema={{
type: 'object',
properties: {
username: {
email: {
type: 'string',
title: '',
required: true,
'x-component-props': {
size: 'large',
placeholder: '用户名',
placeholder: '邮箱',
}
},
password: {

View File

@ -44,10 +44,21 @@ const useLinkageValidateEffects = () => {
export function Register(props: any) {
const actions = createFormActions();
const { initialState = {}, loading, error, refresh, setInitialState } = useModel('@@initialState');
if (loading) {
return null;
}
const { systemSettings = {} } = initialState as any;
console.log({systemSettings});
const { title } = systemSettings || {};
return (
<div className={'users-form'}>
<h1>NocoBase</h1>
<h1>{title || 'NocoBase'}</h1>
<h2></h2>
<SchemaForm
effects={() => {
@ -70,13 +81,13 @@ export function Register(props: any) {
}} actions={actions} schema={{
type: 'object',
properties: {
username: {
email: {
type: 'string',
title: '',
required: true,
'x-component-props': {
size: 'large',
placeholder: '用户名',
placeholder: '邮箱',
}
},
nickname: {
@ -113,7 +124,7 @@ export function Register(props: any) {
},
}
}}>
<FormButtonGroup>
<FormButtonGroup align={'start'}>
<Submit size={'large'}></Submit>
<Button size={'large'} onClick={() => {
history.push('/login');

View File

@ -6,4 +6,10 @@
.ant-form-item-with-help {
margin-bottom: 24px;
}
.users-form {
.button-group {
justify-content: flex-start;
}
}

View File

@ -15,7 +15,7 @@ export const SortableItem = sortableElement(props => <tr {...props} />);
export const SortableContainer = sortableContainer(props => <tbody {...props} />);
export const DragHandle = sortableHandle(() => (
<MenuOutlined style={{ cursor: 'pointer', color: '#999' }} />
<MenuOutlined className="drag-handle" style={{ cursor: 'pointer', color: '#999' }} />
));
interface Props {
@ -60,9 +60,12 @@ export const components = ({data = {}, rowKey, mutate, onMoved, isFieldComponent
};
};
export function fields2columns(fields, ctx: any = {}) {
export function fields2columns(fields = [], ctx: any = {}) {
const columns: any[] = fields.map(item => {
const field = cloneDeep(item);
if (!field.dataIndex) {
field.dataIndex = field.name.split('.');
}
field.render = (value, record) => field.interface === 'sort' ? <DragHandle/> : <Field data={record} viewType={'table'} schema={field} value={value}/>;
field.className = `${field.className||''} noco-field-${field.interface}`;
if (field.editable && field.interface === 'boolean') {

View File

@ -55,6 +55,9 @@
}
@media only screen and (max-width: 800px) {
.ant-drawer.nb-drawer .ant-drawer-content-wrapper {
width: 100% !important;
}
.ant-drawer.noco-drawer {
.ant-drawer-content-wrapper {
width: 100% !important;

View File

@ -7,6 +7,7 @@
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<title>loading...</title>
<link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAAiCAYAAAA6RwvCAAAC0UlEQVRYR83XT6hVVRTH8Y+FIJUE6UADaZA0KoW0gfivwokDDSobFARWSJYhKFKKmpUiGApiikJpDYxE+zeKiLSaSGJJSM2bSEIoTZqF8bvsA9f7zn3n3Puu8ha8wbt77bW+e/3b+0wxSWTKJOEwapB9uBvH8csghxwVyAocxQv4Gf/gBN7B9TZAEwWZgR9wpjjt9rkYn2EXPmqCmQjIRqzBqoZTH8CreBwX+gENA7IWR/AiTjedtKzHz2FcrYlcR2UQkKRhL+7B8y0BovYcTuEJLJ8oyFfF0CL80RLiYfyI37GspGZokKThZXyDPS0BpuHNUqSP4WLZlxoZCuTtUogLWwJELe27BZ/jvZ59Q4EklKn27fgUB/vltjh7FK9gPtK2dRKQBdhft9hbrCnIROKNopwCy5xIjcTZU7jUY+gQlpQuujxO9AIyFx82gbyE3ZjdpViB5Kd5+K5MztXIX+bI+dLOTRkMyMwy/MboJiIJZfKa0/ZKN0i19joShR0DFHD2BmRqOUwtyI1xjlIHEvVzpZ2bolCtP1MikYtwE37q3ZiI3EqQpOILLK2pq3fxd/X7rQR5rYz1flG7UtKbJ0NnxI86Iqm5GH+oZd6+xvtNIL+VQk63dEtdjcwqbb+tJUC32o0mkEo5J4yD3J51xZrWzwC8dwiIbGkNEuU/y+17DGfxZLk7NuDZIQGqbQOBVJvyBkk35JGTy20UMhRIHF/DfaMgKDY6IOnxrVg5QsODmuqAVJKL7i3cP6iVEejfBBJ7D5bo5DF0O2UMSOU8d0PSlffD7ZC+IHF+V0lV5sedLWky+L7EB7ij5Z5/MzTbvOLzYE508v3ST/4q0J8Uhen4GE83wJzE5gzKNiCVrfUFaE6P8XxqZqj9V+M0g24nHulZ+7U8whK9jgwCEv0Hyqhfh+9zWeHbFinIN3AeX7lg86Ge/2+SQUFa+BxOZdKA/A+Y7oq2wGtDDgAAAABJRU5ErkJggg==" class="jsx-1062430909 jsx-1773464493">
</head>
<body>

View File

@ -12,4 +12,5 @@ export default {
'page4': require('@/pages/page4').default,
login: require('@/pages/login').default,
register: require('@/pages/register').default,
AdminLoader: require('@/components/pages/AdminLoader').default,
};

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/client",
"version": "0.3.0-alpha.0",
"version": "0.4.0-alpha.0",
"main": "lib/index.js",
"license": "MIT",
"resolutions": {

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/create-nocobase-app",
"version": "0.3.0-alpha.0",
"version": "0.4.0-alpha.0",
"description": "create-nocobase-app",
"main": "lib/index.js",
"types": "lib/index.d.ts",

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/database",
"version": "0.3.0-alpha.0",
"version": "0.4.0-alpha.0",
"description": "",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1 +0,0 @@
Used in bin/father.js to determine if it is in the local debug state.

View File

@ -1,4 +0,0 @@
/src/fixtures
/src/**/*.test.ts
/lib/**/*.test.js
/.local

View File

@ -1,4 +0,0 @@
# father-build
See our [main repo](https://github.com/umijs/father) for more information.

View File

@ -1,61 +0,0 @@
#!/usr/bin/env node
const { existsSync } = require('fs');
const { join } = require('path');
const yParser = require('yargs-parser');
const chalk = require('chalk');
const signale = require('signale');
// print version and @local
const args = yParser(process.argv.slice(2));
if (args.v || args.version) {
console.log(require('../package').version);
if (existsSync(join(__dirname, '../.local'))) {
console.log(chalk.cyan('@local'));
}
process.exit(0);
}
// Notify update when process exits
const updater = require('update-notifier');
const pkg = require('../package.json');
updater({ pkg }).notify({ defer: true });
function stripEmptyKeys(obj) {
Object.keys(obj).forEach((key) => {
if (!obj[key] || (Array.isArray(obj[key]) && !obj[key].length)) {
delete obj[key];
}
});
return obj;
}
function build() {
// Parse buildArgs from cli
const buildArgs = stripEmptyKeys({
esm: args.esm && { type: args.esm === true ? 'rollup' : args.esm },
cjs: args.cjs && { type: args.cjs === true ? 'rollup' : args.cjs },
umd: args.umd && { name: args.umd === true ? undefined : args.umd },
file: args.file,
target: args.target,
entry: args._,
});
if (buildArgs.file && buildArgs.entry && buildArgs.entry.length > 1) {
signale.error(new Error(
`Cannot specify file when have multiple entries (${buildArgs.entry.join(', ')})`
));
process.exit(1);
}
require('../lib/build').default({
cwd: args.root || process.cwd(),
watch: args.w || args.watch,
buildArgs,
}).catch(e => {
signale.error(e);
process.exit(1);
});
}
build();

View File

@ -1,80 +0,0 @@
{
"name": "@nocobase/father-build",
"version": "0.3.0-alpha.0",
"description": "Library build tool based on rollup.",
"main": "lib/index.js",
"bin": {
"father-build": "./bin/father-build.js"
},
"scripts": {
"build": "umi-tools build"
},
"typings": "./index.d.ts",
"dependencies": {
"@babel/core": "7.4.5",
"@babel/plugin-proposal-class-properties": "7.4.4",
"@babel/plugin-proposal-decorators": "7.4.4",
"@babel/plugin-proposal-do-expressions": "7.2.0",
"@babel/plugin-proposal-export-default-from": "7.2.0",
"@babel/plugin-proposal-export-namespace-from": "7.2.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.7.4",
"@babel/plugin-proposal-optional-chaining": "7.7.4",
"@babel/plugin-syntax-dynamic-import": "7.2.0",
"@babel/plugin-transform-modules-commonjs": "7.5.0",
"@babel/plugin-transform-runtime": "7.4.4",
"@babel/preset-env": "7.4.5",
"@babel/preset-react": "7.0.0",
"@babel/preset-typescript": "7.3.3",
"@babel/register": "7.4.4",
"@svgr/rollup": "^4.3.0",
"ajv": "6.10.0",
"autoprefixer": "9.6.0",
"babel-plugin-istanbul": "^5.2.0",
"babel-plugin-react-require": "3.1.1",
"chalk": "2.4.2",
"chokidar": "^3.0.2",
"glob": "^7.1.4",
"gulp-if": "2.0.2",
"gulp-less": "^4.0.1",
"gulp-plumber": "^1.2.1",
"gulp-typescript": "5.0.1",
"less": "3.9.0",
"less-plugin-npm-import": "2.1.0",
"lodash": "4.17.19",
"rimraf": "2.6.3",
"rollup": "1.27.8",
"rollup-plugin-babel": "4.3.3",
"rollup-plugin-commonjs": "10.0.0",
"rollup-plugin-inject": "3.0.1",
"rollup-plugin-json": "4.0.0",
"rollup-plugin-node-resolve": "5.0.1",
"rollup-plugin-postcss-umi": "2.0.3",
"rollup-plugin-replace": "2.2.0",
"rollup-plugin-terser": "5.1.3",
"rollup-plugin-typescript2": "0.25.3",
"rollup-plugin-url": "^2.2.2",
"signale": "1.4.0",
"slash2": "2.0.0",
"temp-dir": "2.0.0",
"through2": "3.0.1",
"ts-loader": "^6.0.2",
"typescript": "^3.7.3",
"update-notifier": "3.0.0",
"vinyl-fs": "3.0.3",
"yargs-parser": "13.1.2"
},
"repository": {
"type": "git",
"url": "http://github.com/umijs/father"
},
"homepage": "http://github.com/umijs/father",
"bugs": "http://github.com/umijs/father/issues",
"authors": [
"chencheng <sorrycc@gmail.com> (https://github.com/sorrycc)"
],
"license": "MIT",
"devDependencies": {
"@types/gulp-plumber": "^0.0.32",
"umi-tools": "^0.4.0"
}
}

View File

@ -1,238 +0,0 @@
import { join, extname, relative } from "path";
import { existsSync, readFileSync, statSync } from "fs";
import vfs from "vinyl-fs";
import signale from "signale";
import lodash from "lodash";
import rimraf from "rimraf";
import through from "through2";
import slash from "slash2";
import * as chokidar from "chokidar";
import * as babel from "@babel/core";
import gulpTs from "gulp-typescript";
import gulpLess from "gulp-less";
import gulpPlumber from 'gulp-plumber';
import gulpIf from "gulp-if";
import chalk from "chalk";
import getBabelConfig from "./getBabelConfig";
import { IBundleOptions } from "./types";
import * as ts from "typescript";
interface IBabelOpts {
cwd: string;
rootPath?: string;
type: "esm" | "cjs";
target?: "browser" | "node";
log?: (string) => void;
watch?: boolean;
importLibToEs?: boolean;
bundleOpts: IBundleOptions;
}
interface ITransformOpts {
file: {
contents: string;
path: string;
};
type: "esm" | "cjs";
}
export default async function(opts: IBabelOpts) {
const {
cwd,
rootPath,
type,
watch,
importLibToEs,
log,
bundleOpts: {
target = "browser",
runtimeHelpers,
extraBabelPresets = [],
extraBabelPlugins = [],
browserFiles = [],
nodeFiles = [],
nodeVersion,
disableTypeCheck,
cjs,
include,
lessInBabelMode
}
} = opts;
const srcPath = join(cwd, "src");
const targetDir = type === "esm" ? "es" : "lib";
const targetPath = join(cwd, targetDir);
log(chalk.gray(`Clean ${targetDir} directory`));
rimraf.sync(targetPath);
function transform(opts: ITransformOpts) {
const { file, type } = opts;
const { opts: babelOpts, isBrowser } = getBabelConfig({
target,
type,
typescript: true,
runtimeHelpers,
filePath: slash(relative(cwd, file.path)),
browserFiles,
nodeFiles,
nodeVersion,
lazy: cjs && (cjs as any).lazy,
lessInBabelMode
});
if (importLibToEs && type === "esm") {
babelOpts.plugins.push(require.resolve("../lib/importLibToEs"));
}
babelOpts.presets.push(...extraBabelPresets);
babelOpts.plugins.push(...extraBabelPlugins);
const relFile = slash(file.path).replace(`${cwd}/`, "");
log(
`Transform to ${type} for ${chalk[isBrowser ? "yellow" : "blue"](
relFile
)}`
);
return babel.transform(file.contents, {
...babelOpts,
filename: file.path,
// 不读取外部的babel.config.js配置文件全采用babelOpts中的babel配置来构建
configFile: false,
}).code;
}
/**
* tsconfig.json is not valid json file
* https://github.com/Microsoft/TypeScript/issues/20384
*/
function parseTsconfig(path: string) {
const readFile = (path: string) => readFileSync(path, "utf-8");
const result = ts.readConfigFile(path, readFile);
if (result.error) {
return;
}
return result.config;
}
function getTsconfigCompilerOptions(path: string) {
const config = parseTsconfig(path);
return config ? config.compilerOptions : undefined;
}
function getTSConfig() {
const tsconfigPath = join(cwd, "tsconfig.json");
const templateTsconfigPath = join(__dirname, "../template/tsconfig.json");
if (existsSync(tsconfigPath)) {
return getTsconfigCompilerOptions(tsconfigPath) || {};
}
if (rootPath && existsSync(join(rootPath, "tsconfig.json"))) {
return getTsconfigCompilerOptions(join(rootPath, "tsconfig.json")) || {};
}
return getTsconfigCompilerOptions(templateTsconfigPath) || {};
}
function createStream(src) {
const tsConfig = getTSConfig();
const babelTransformRegexp = disableTypeCheck ? /\.(t|j)sx?$/ : /\.jsx?$/;
function isTsFile(path) {
return /\.tsx?$/.test(path) && !path.endsWith(".d.ts");
}
function isTransform(path) {
return babelTransformRegexp.test(path) && !path.endsWith(".d.ts");
}
return vfs
.src(src, {
allowEmpty: true,
base: srcPath
})
.pipe(watch ? gulpPlumber() : through.obj())
.pipe(
gulpIf(f => !disableTypeCheck && isTsFile(f.path), gulpTs(tsConfig))
)
.pipe(
gulpIf(
f => lessInBabelMode && /\.less$/.test(f.path),
gulpLess(lessInBabelMode || {})
)
)
.pipe(
gulpIf(
f => isTransform(f.path),
through.obj((file, env, cb) => {
try {
file.contents = Buffer.from(
transform({
file,
type
})
);
// .jsx -> .js
file.path = file.path.replace(extname(file.path), ".js");
cb(null, file);
} catch (e) {
signale.error(`Compiled faild: ${file.path}`);
console.log(e);
cb(null);
}
})
)
)
.pipe(vfs.dest(targetPath));
}
return new Promise(resolve => {
const patterns = include ? [
join(srcPath, include as any),
] : [
join(srcPath, "**/*"),
`!${join(srcPath, "**/fixtures{,/**}")}`,
`!${join(srcPath, "**/demos{,/**}")}`,
`!${join(srcPath, "**/__test__{,/**}")}`,
`!${join(srcPath, "**/*.mdx")}`,
`!${join(srcPath, "**/*.md")}`,
`!${join(srcPath, "**/*.+(test|e2e|spec).+(js|jsx|ts|tsx)")}`
];
createStream(patterns).on("end", () => {
if (watch) {
log(
chalk.magenta(
`Start watching ${slash(srcPath).replace(
`${cwd}/`,
""
)} directory...`
)
);
const watcher = chokidar.watch(patterns, {
ignoreInitial: true
});
const files = [];
function compileFiles() {
while (files.length) {
createStream(files.pop());
}
}
const debouncedCompileFiles = lodash.debounce(compileFiles, 1000);
watcher.on("all", (event, fullPath) => {
const relPath = fullPath.replace(srcPath, "");
log(
`[${event}] ${slash(join(srcPath, relPath)).replace(`${cwd}/`, "")}`
);
if (!existsSync(fullPath)) return;
if (statSync(fullPath).isFile()) {
if (!files.includes(fullPath)) files.push(fullPath);
debouncedCompileFiles();
}
});
process.once("SIGINT", () => {
watcher.close();
});
}
resolve();
});
});
}

View File

@ -1,54 +0,0 @@
import { join, basename } from 'path';
import { existsSync, readdirSync, renameSync, statSync } from 'fs';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import build from './build';
function moveEsLibToDist(cwd) {
['es', 'lib'].forEach(dir => {
const absDirPath = join(cwd, dir);
const absDistPath = join(cwd, 'dist');
if (existsSync(absDirPath)) {
mkdirp.sync(absDistPath);
renameSync(absDirPath, join(absDistPath, dir));
}
});
}
describe('father build', () => {
const rootConfigMapping = {
'lerna-root-config-override': { cjs: 'rollup', esm: false },
};
require('test-build-result')({
root: join(__dirname, './fixtures/build'),
build({ cwd }) {
process.chdir(cwd);
rimraf.sync(join(cwd, 'dist'));
return build({ cwd, rootConfig: rootConfigMapping[basename(cwd)] }).then(() => {
// babel
moveEsLibToDist(cwd);
// lerna
if (existsSync(join(cwd, 'lerna.json'))) {
mkdirp.sync(join(cwd, 'dist'));
const pkgs = readdirSync(join(cwd, 'packages'));
for (let pkg of pkgs) {
// TODO: hard code
if (pkg === '@hoo') {
pkg = '@hoo/bar';
}
const pkgPath = join(cwd, 'packages', pkg);
if (!statSync(pkgPath).isDirectory()) continue;
moveEsLibToDist(pkgPath);
renameSync(
join(pkgPath, 'dist'),
// @foo/bar -> bar
join(cwd, 'dist', pkg.split('/').slice(-1).join(''))
);
}
}
});
},
});
});

View File

@ -1,238 +0,0 @@
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
import { join } from 'path';
import rimraf from 'rimraf';
import * as assert from 'assert';
import { merge } from 'lodash';
import signale from 'signale';
import chalk from 'chalk';
import { IOpts, IBundleOptions, IBundleTypeOutput, ICjs, IEsm } from './types';
import babel from './babel';
import rollup from './rollup';
import registerBabel from './registerBabel';
import { getExistFile } from './utils';
import getUserConfig, { CONFIG_FILES } from './getUserConfig';
import randomColor from "./randomColor";
export function getBundleOpts(opts: IOpts): IBundleOptions[] {
const { cwd, buildArgs = {}, rootConfig = {} } = opts;
const entry = getExistFile({
cwd,
files: ['src/index.tsx', 'src/index.ts', 'src/index.jsx', 'src/index.js'],
returnRelative: true,
});
const userConfig = getUserConfig({ cwd });
const userConfigs = Array.isArray(userConfig) ? userConfig : [userConfig];
return (userConfigs as any).map(userConfig => {
const bundleOpts = merge(
{
entry,
},
rootConfig,
userConfig,
buildArgs,
);
// Support config esm: 'rollup' and cjs: 'rollup'
if (typeof bundleOpts.esm === 'string') {
bundleOpts.esm = { type: bundleOpts.esm };
}
if (typeof bundleOpts.cjs === 'string') {
bundleOpts.cjs = { type: bundleOpts.cjs };
}
return bundleOpts;
});
}
function validateBundleOpts(bundleOpts: IBundleOptions, { cwd, rootPath }) {
if (bundleOpts.runtimeHelpers) {
const pkgPath = join(cwd, 'package.json');
assert.ok(existsSync(pkgPath), `@babel/runtime dependency is required to use runtimeHelpers`);
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
assert.ok(
(pkg.dependencies || {})['@babel/runtime'],
`@babel/runtime dependency is required to use runtimeHelpers`,
);
}
if (bundleOpts.cjs && (bundleOpts.cjs as ICjs).lazy && (bundleOpts.cjs as ICjs).type === 'rollup') {
throw new Error(`
cjs.lazy don't support rollup.
`.trim());
}
if (!bundleOpts.esm && !bundleOpts.cjs && !bundleOpts.umd) {
throw new Error(
`
None format of ${chalk.cyan(
'cjs | esm | umd',
)} is configured, checkout https://github.com/umijs/father for usage details.
`.trim(),
);
}
if (bundleOpts.entry) {
const tsConfigPath = join(cwd, 'tsconfig.json');
const tsConfig = existsSync(tsConfigPath)
|| (rootPath && existsSync(join(rootPath, 'tsconfig.json')));
if (
!tsConfig && (
(Array.isArray(bundleOpts.entry) && bundleOpts.entry.some(isTypescriptFile)) ||
(!Array.isArray(bundleOpts.entry) && isTypescriptFile(bundleOpts.entry))
)
) {
signale.info(
`Project using ${chalk.cyan('typescript')} but tsconfig.json not exists. Use default config.`
);
}
}
}
function isTypescriptFile(filePath) {
return filePath.endsWith('.ts') || filePath.endsWith('.tsx')
}
interface IExtraBuildOpts {
pkg?: string;
}
export async function build(opts: IOpts, extraOpts: IExtraBuildOpts = {}) {
const { cwd, rootPath, watch } = opts;
const { pkg } = extraOpts;
// register babel for config files
registerBabel({
cwd,
only: CONFIG_FILES,
});
function log(msg) {
console.log(`${pkg ? `${randomColor(`${pkg}`)}: ` : ''}${msg}`);
}
// Get user config
const bundleOptsArray = getBundleOpts(opts);
for (const bundleOpts of bundleOptsArray) {
validateBundleOpts(bundleOpts, { cwd, rootPath });
// Clean dist
log(chalk.gray(`Clean dist directory`));
rimraf.sync(join(cwd, 'dist'));
// Build umd
if (bundleOpts.umd) {
log(`Build umd`);
await rollup({
cwd,
log,
type: 'umd',
entry: bundleOpts.entry,
watch,
bundleOpts,
});
}
// Build cjs
if (bundleOpts.cjs) {
const cjs = bundleOpts.cjs as IBundleTypeOutput;
log(`Build cjs with ${cjs.type}`);
if (cjs.type === 'babel') {
await babel({ cwd, rootPath, watch, type: 'cjs', log, bundleOpts });
} else {
await rollup({
cwd,
log,
type: 'cjs',
entry: bundleOpts.entry,
watch,
bundleOpts,
});
}
}
// Build esm
if (bundleOpts.esm) {
const esm = bundleOpts.esm as IEsm;
log(`Build esm with ${esm.type}`);
const importLibToEs = esm && esm.importLibToEs;
if (esm && esm.type === 'babel') {
await babel({ cwd, rootPath, watch, type: 'esm', importLibToEs, log, bundleOpts });
} else {
await rollup({
cwd,
log,
type: 'esm',
entry: bundleOpts.entry,
importLibToEs,
watch,
bundleOpts,
});
}
}
}
}
export async function buildForLerna(opts: IOpts) {
const { cwd } = opts;
// register babel for config files
registerBabel({
cwd,
only: CONFIG_FILES,
});
const userConfig = merge(getUserConfig({ cwd }), opts.rootConfig || {});
let pkgs = readdirSync(join(cwd, 'packages'));
// support define pkgs in lerna
if (userConfig.pkgs) {
pkgs = userConfig.pkgs;
}
// 支持 scope
pkgs = pkgs.reduce((memo, pkg) => {
const pkgPath = join(cwd, 'packages', pkg);
if (statSync(pkgPath).isDirectory()) {
if (pkg.startsWith('@')) {
readdirSync(join(cwd, 'packages', pkg)).filter(subPkg => {
if (statSync(join(cwd, 'packages', pkg, subPkg)).isDirectory()) {
memo = memo.concat(`${pkg}/${subPkg}`);
}
});
} else {
memo = memo.concat(pkg);
}
}
return memo;
}, []);
for (const pkg of pkgs) {
if (process.env.PACKAGE && pkg !== process.env.PACKAGE) continue;
// build error when .DS_Store includes in packages root
const pkgPath = join(cwd, 'packages', pkg);
assert.ok(
existsSync(join(pkgPath, 'package.json')),
`package.json not found in packages/${pkg}`,
);
process.chdir(pkgPath);
await build(
{
// eslint-disable-line
...opts,
buildArgs: opts.buildArgs,
rootConfig: userConfig,
cwd: pkgPath,
rootPath: cwd,
},
{
pkg,
},
);
}
}
export default async function(opts: IOpts) {
const useLerna = existsSync(join(opts.cwd, 'lerna.json'));
if (useLerna && process.env.LERNA !== 'none') {
await buildForLerna(opts);
} else {
await build(opts);
}
}

View File

@ -1,8 +0,0 @@
export default {
esm: 'babel',
target: 'node',
browserFiles: [
'src/browser.js',
],
}

View File

@ -1,8 +0,0 @@
export default {
target: 'node',
cjs: { type: 'babel', lazy: true },
browserFiles: [
'src/foo.js',
],
};

View File

@ -1,3 +0,0 @@
import bar from 'bar';
bar();

View File

@ -1,3 +0,0 @@
import bar from 'bar';
bar();

View File

@ -1,10 +0,0 @@
export default {
esm: { type: 'babel' },
extraBabelPresets: [
require.resolve('./preset'),
],
extraBabelPlugins: [
require.resolve('./p2'),
],
};

View File

@ -1,2 +0,0 @@
console.log("p1", "p2", 1);
alert(2);

View File

@ -1,20 +0,0 @@
module.exports = function ({ types: t }) {
function isConsoleLog(node) {
const { callee, callee: { object, property } } = node;
return t.isMemberExpression(callee)
&& t.isIdentifier(object) && object.name === 'console'
&& t.isIdentifier(property) && property.name === 'log';
}
return {
visitor: {
CallExpression(path, state) {
const { node, node: { callee, callee: { object, property } } } = path;
if (isConsoleLog(node)) {
node.arguments.unshift(t.stringLiteral('p1'));
}
},
},
};
}

View File

@ -1,20 +0,0 @@
module.exports = function ({ types: t }) {
function isConsoleLog(node) {
const { callee, callee: { object, property } } = node;
return t.isMemberExpression(callee)
&& t.isIdentifier(object) && object.name === 'console'
&& t.isIdentifier(property) && property.name === 'log';
}
return {
visitor: {
CallExpression(path, state) {
const { node, node: { callee, callee: { object, property } } } = path;
if (isConsoleLog(node)) {
node.arguments.unshift(t.stringLiteral('p2'));
}
},
},
};
}

View File

@ -1,8 +0,0 @@
module.exports = function () {
return {
plugins: [
require.resolve('./p1'),
],
};
}

View File

@ -1,5 +0,0 @@
export default {
cjs: { type: 'babel' },
esm: { type: 'babel', importLibToEs: true },
};

View File

@ -1,2 +0,0 @@
import foo from "foo/es/foo";
console.log(foo());

View File

@ -1,3 +0,0 @@
import foo from 'foo/lib/foo';
console.log(foo());

View File

@ -1,6 +0,0 @@
export default {
cjs: { type: 'babel' },
esm: { type: 'babel' },
lessInBabelMode: false,
};

View File

@ -1,6 +0,0 @@
import "./index.less";
import "./foo.module.less";
export default function () {
return 'foo';
}

View File

@ -1,6 +0,0 @@
@link-color: green;
.foo {
color: @link-color;
}

View File

@ -1,6 +0,0 @@
import "./index.less";
import "./foo.module.less";
export default function () {
return 'foo';
}

View File

@ -1,6 +0,0 @@
@link-color: green;
.foo {
color: @link-color;
}

View File

@ -1,6 +0,0 @@
export default {
cjs: { type: 'babel' },
esm: { type: 'babel' },
lessInBabelMode: true,
};

View File

@ -1,6 +0,0 @@
import "./index.css";
import "./foo.module.css";
export default function () {
return 'foo';
}

View File

@ -1,6 +0,0 @@
import "./index.less";
import "./foo.module.less";
export default function () {
return 'foo';
}

View File

@ -1,3 +0,0 @@
.foo {
color: green;
}

Some files were not shown because too many files have changed in this diff Show More