feat: actions & views (#18)

* feat: 视图新增支持 associated 的情况

* fix: 解决关系数据的视图问题

* 表单数据初始化

* feat: sortable table

* 排序

* 表格细节调整

* feat: field actions
This commit is contained in:
chenos 2020-11-19 21:12:15 +08:00 committed by GitHub
parent b0ba1472bb
commit eb93f0734f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 595 additions and 289 deletions

View File

@ -33,12 +33,14 @@
"@nocobase/server": "^0.3.0-alpha.0", "@nocobase/server": "^0.3.0-alpha.0",
"@umijs/preset-react": "1.x", "@umijs/preset-react": "1.x",
"@umijs/test": "^3.2.23", "@umijs/test": "^3.2.23",
"array-move": "^3.0.1",
"concurrently": "^5.3.0", "concurrently": "^5.3.0",
"lint-staged": "^10.0.7", "lint-staged": "^10.0.7",
"nodemon": "^2.0.6", "nodemon": "^2.0.6",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"react": "^16.12.0", "react": "^16.12.0",
"react-dom": "^16.12.0", "react-dom": "^16.12.0",
"react-sortable-hoc": "^1.11.0",
"styled-components": "^5.2.1", "styled-components": "^5.2.1",
"umi": "^3.2.23", "umi": "^3.2.23",
"yorkie": "^2.0.0" "yorkie": "^2.0.0"

View File

@ -43,7 +43,7 @@ const data = {
path: '/', path: '/',
type: 'layout', type: 'layout',
template: 'TopMenuLayout', template: 'TopMenuLayout',
order: 1, sort: 10,
children: [ children: [
{ {
title: '仪表盘', title: '仪表盘',
@ -51,7 +51,7 @@ const data = {
path: '/dashboard', path: '/dashboard',
icon: 'dashboard', icon: 'dashboard',
template: 'page1', template: 'page1',
order: 2, sort: 20,
}, },
{ {
title: '数据', title: '数据',
@ -59,7 +59,7 @@ const data = {
path: '/collections', path: '/collections',
icon: 'dashboard', icon: 'dashboard',
template: 'SideMenuLayout', template: 'SideMenuLayout',
order: 3, sort: 30,
children: [ children: [
{ {
title: '页面3', title: '页面3',
@ -67,7 +67,7 @@ const data = {
path: '/collections/page3', path: '/collections/page3',
icon: 'dashboard', icon: 'dashboard',
template: 'page3', template: 'page3',
order: 5, sort: 40,
}, },
{ {
title: '页面4', title: '页面4',
@ -75,17 +75,17 @@ const data = {
path: '/collections/page4', path: '/collections/page4',
icon: 'dashboard', icon: 'dashboard',
template: 'page4', template: 'page4',
order: 6, sort: 50,
},
{
title: '页面5',
type: 'collection',
path: '/collections/collection1',
icon: 'dashboard',
template: 'collection',
collection: 'collection1',
order: 7,
}, },
// {
// title: '页面5',
// type: 'collection',
// path: '/collections/collection1',
// icon: 'dashboard',
// template: 'collection',
// collection: 'collection1',
// sort: 60,
// },
] ]
}, },
{ {
@ -94,7 +94,7 @@ const data = {
path: '/users', path: '/users',
icon: 'dashboard', icon: 'dashboard',
template: 'SideMenuLayout', template: 'SideMenuLayout',
order: 3, sort: 70,
children: [ children: [
{ {
title: '用户管理', title: '用户管理',
@ -103,7 +103,7 @@ const data = {
icon: 'dashboard', icon: 'dashboard',
template: 'collection', template: 'collection',
collection: 'users', collection: 'users',
order: 1, sort: 80,
}, },
] ]
}, },
@ -113,7 +113,7 @@ const data = {
path: '/settings', path: '/settings',
icon: 'dashboard', icon: 'dashboard',
template: 'SideMenuLayout', template: 'SideMenuLayout',
order: 4, sort: 90,
children: [ children: [
{ {
title: '页面与菜单', title: '页面与菜单',
@ -121,7 +121,7 @@ const data = {
collection: 'pages', collection: 'pages',
path: '/settings/pages', path: '/settings/pages',
icon: 'dashboard', icon: 'dashboard',
order: 1, sort: 100,
}, },
{ {
title: '数据表配置', title: '数据表配置',
@ -129,7 +129,7 @@ const data = {
collection: 'collections', collection: 'collections',
path: '/settings/collections', path: '/settings/collections',
icon: 'dashboard', icon: 'dashboard',
order: 2, sort: 110,
}, },
{ {
title: '权限配置', title: '权限配置',
@ -137,7 +137,7 @@ const data = {
collection: 'roles', collection: 'roles',
path: '/settings/roles', path: '/settings/roles',
icon: 'dashboard', icon: 'dashboard',
order: 3, sort: 120,
}, },
] ]
}, },
@ -173,117 +173,121 @@ const data = {
// console.log(options); // console.log(options);
const associations: any = {}; const associations: any = {};
if (options.fields) { if (options.fields) {
associations['fields'] = options.fields.map(item => ({ associations['fields'] = options.fields.map((item, sort) => ({
...item, ...item,
options: item, options: item,
sort,
})) }))
} }
if (options.tabs) { if (options.tabs) {
associations['tabs'] = options.tabs.map(item => ({ associations['tabs'] = options.tabs.map((item, sort) => ({
...item, ...item,
options: item, options: item,
sort,
})) }))
} }
if (options.actions) { if (options.actions) {
associations['actions'] = options.actions.map(item => ({ associations['actions'] = options.actions.map((item, sort) => ({
...item, ...item,
options: item, options: item,
sort,
})) }))
} }
if (options.views) { if (options.views) {
associations['views'] = options.views.map(item => ({ associations['views'] = options.views.map((item, sort) => ({
...item, ...item,
options: item, options: item,
sort,
})) }))
} }
await collection.updateAssociations(associations); await collection.updateAssociations(associations);
} }
const actions = await Action.findAll(); // const actions = await Action.findAll();
for (const action of actions) { // for (const action of actions) {
const viewName = action.options.viewName; // const viewName = action.options.viewName;
console.log({viewName}); // console.log({viewName});
if (viewName) { // if (viewName) {
const view = await View.findOne({ // const view = await View.findOne({
where: { // where: {
name: viewName, // name: viewName,
collection_name: action.collection_name // collection_name: action.collection_name
}, // },
}); // });
if (view) { // if (view) {
action.options.viewId = view.id; // action.options.viewId = view.id;
console.log(action.options); // console.log(action.options);
action.setDataValue('options', action.options); // action.setDataValue('options', action.options);
action.changed('options', true); // action.changed('options', true);
await action.save(); // await action.save();
} // }
} // }
} // }
const tabs = await Tab.findAll(); // const tabs = await Tab.findAll();
for (const tab of tabs) { // for (const tab of tabs) {
const viewName = tab.options.viewName; // const viewName = tab.options.viewName;
if (!viewName) { // if (!viewName) {
continue; // continue;
} // }
let view: any; // let view: any;
if (tab.type === 'association') { // if (tab.type === 'association') {
view = await View.findOne({ // view = await View.findOne({
where: { // where: {
name: viewName, // name: viewName,
collection_name: tab.options.association, // collection_name: tab.options.association,
}, // },
}); // });
} else { // } else {
view = await View.findOne({ // view = await View.findOne({
where: { // where: {
name: viewName, // name: viewName,
collection_name: tab.collection_name, // collection_name: tab.collection_name,
}, // },
}); // });
} // }
if (view) { // if (view) {
tab.options.viewId = view.id; // tab.options.viewId = view.id;
tab.setDataValue('options', tab.options); // tab.setDataValue('options', tab.options);
tab.changed('options', true); // tab.changed('options', true);
await tab.save(); // await tab.save();
} // }
} // }
const views = await View.findAll(); // const views = await View.findAll();
for (const view of views) { // for (const view of views) {
const detailsViewName = view.options.detailsViewName; // const detailsViewName = view.options.detailsViewName;
if (detailsViewName) { // if (detailsViewName) {
const v = await View.findOne({ // const v = await View.findOne({
where: { // where: {
name: detailsViewName, // name: detailsViewName,
collection_name: view.collection_name // collection_name: view.collection_name
}, // },
}); // });
if (v) { // if (v) {
view.options.detailsViewId = v.id; // view.options.detailsViewId = v.id;
view.setDataValue('options', view.options); // view.setDataValue('options', view.options);
view.changed('options', true); // view.changed('options', true);
await view.save(); // await view.save();
} // }
} // }
const updateViewName = view.options.updateViewName; // const updateViewName = view.options.updateViewName;
if (updateViewName) { // if (updateViewName) {
const v = await View.findOne({ // const v = await View.findOne({
where: { // where: {
name: updateViewName, // name: updateViewName,
collection_name: view.collection_name // collection_name: view.collection_name
}, // },
}); // });
if (v) { // if (v) {
view.options.updateViewId = v.id; // view.options.updateViewId = v.id;
view.setDataValue('options', view.options); // view.setDataValue('options', view.options);
view.changed('options', true); // view.changed('options', true);
await view.save(); // await view.save();
} // }
} // }
console.log({detailsViewName, updateViewName}); // console.log({detailsViewName, updateViewName});
} // }
// for (let table of tables) { // for (let table of tables) {
// const options = table.getOptions(); // const options = table.getOptions();

View File

@ -4,28 +4,29 @@ import ViewFactory from '@/components/views';
export function Create(props) { export function Create(props) {
console.log(props); console.log(props);
const { title, viewCollectionName, viewName } = props.schema; const { title, viewName, collection_name } = props.schema;
const { activeTab = {}, item = {} } = props; const { activeTab = {}, item = {} } = props;
const { association, collection_name } = activeTab; const { association } = activeTab;
const params = {}; const params = {};
if (association) { if (association) {
params['resourceName'] = association; params['resourceName'] = association;
params['associatedName'] = collection_name; params['associatedName'] = activeTab.collection_name;
params['associatedKey'] = item.itemId; params['associatedKey'] = item.itemId;
} else { } else {
params['resourceName'] = collection_name; params['resourceName'] = collection_name;
params['resourceKey'] = item.itemId; params['resourceKey'] = item.itemId;
} }
console.log(params);
const drawerRef = useRef<any>(); const drawerRef = useRef<any>();
return ( return (
<> <>
<ViewFactory <ViewFactory
{...props} {...props}
reference={drawerRef} reference={drawerRef}
viewCollectionName={viewCollectionName}
viewName={viewName} viewName={viewName}
{...params} {...params}
/> />

View File

@ -4,15 +4,15 @@ import ViewFactory from '@/components/views';
export function Update(props) { export function Update(props) {
console.log(props); console.log(props);
const { title, viewCollectionName, viewName } = props.schema; const { title, viewName, resourceName, collection_name } = props.schema;
const { activeTab = {}, item = {} } = props; const { activeTab = {}, item = {} } = props;
const { association, collection_name } = activeTab; const { association } = activeTab;
const params = {}; const params = {};
if (association) { if (association) {
params['resourceName'] = association; params['resourceName'] = association;
params['associatedName'] = collection_name; params['associatedName'] = resourceName;
params['associatedKey'] = item.itemId; params['associatedKey'] = item.itemId;
} else { } else {
params['resourceName'] = collection_name; params['resourceName'] = collection_name;
@ -25,12 +25,12 @@ export function Update(props) {
<ViewFactory <ViewFactory
{...props} {...props}
reference={drawerRef} reference={drawerRef}
viewCollectionName={viewCollectionName}
viewName={viewName} viewName={viewName}
{...params} {...params}
/> />
<Button type={'primary'} onClick={() => { <Button type={'primary'} onClick={() => {
drawerRef.current.setVisible(true); drawerRef.current.setVisible(true);
drawerRef.current.getData(item.itemId);
}}>{title}</Button> }}>{title}</Button>
</> </>
) )

View File

@ -29,8 +29,8 @@ export function CollectionIndex(props) {
// } // }
/> />
<div className={'collection-content'}> <div className={'collection-content'}>
<ViewFactory {...props} <ViewFactory
viewCollectionName={collection} {...props}
viewName={viewName||defaultViewName} viewName={viewName||defaultViewName}
resourceName={collection} resourceName={collection}
/> />

View File

@ -4,6 +4,7 @@ import { CollectionTabPane } from './CollectionTabPane';
import { getPathName, redirectTo } from './utils'; import { getPathName, redirectTo } from './utils';
import api from '@/api-client'; import api from '@/api-client';
import { useRequest } from 'umi'; import { useRequest } from 'umi';
import { Spin } from '@nocobase/client';
export function CollectionSingle(props) { export function CollectionSingle(props) {
console.log(props); console.log(props);
@ -11,13 +12,16 @@ export function CollectionSingle(props) {
const { tabs = [] } = props.collection; const { tabs = [] } = props.collection;
const activeTab = tabs.find(tab => tab.name == item.tabName)||{}; const activeTab = tabs.find(tab => tab.name == item.tabName)||{};
console.log(activeTab); console.log(activeTab);
const { data = {} } = useRequest(() => activeTab && api.resource(activeTab.collection_name).getPageInfo({ const { data = {}, loading } = useRequest(() => activeTab && api.resource(activeTab.collection_name).getPageInfo({
resourceKey: item.itemId, resourceKey: item.itemId,
})); }));
console.log(data); console.log(data);
if (!activeTab) { if (!activeTab) {
return null; return null;
} }
if (loading) {
return <Spin/>;
}
return ( return (
<div> <div>
<PageHeader <PageHeader

View File

@ -5,7 +5,7 @@ import { useRequest, request, Spin } from '@nocobase/client';
export function CollectionTabPane(props) { export function CollectionTabPane(props) {
const { pageInfo = {}, activeTab = {}, item = {} } = props; const { pageInfo = {}, activeTab = {}, item = {} } = props;
const { viewCollectionName, viewName, association, collection_name, field = {} } = activeTab; const { viewName, association, collection_name, field = {} } = activeTab;
const { sourceKey = 'id' } = field; const { sourceKey = 'id' } = field;
const params = {}; const params = {};
@ -25,7 +25,6 @@ export function CollectionTabPane(props) {
<div> <div>
<ViewFactory <ViewFactory
{...props} {...props}
viewCollectionName={viewCollectionName}
viewName={viewName} viewName={viewName}
{...params} {...params}
/> />

View File

@ -34,7 +34,7 @@ export function CollectionLoader(props: any) {
return ( return (
<div className={'collection'}> <div className={'collection'}>
<div className={'collection-index'}> <div className={'collection-index'}>
<CollectionIndex collection={data.data||{}} {...props}/> { items.length === 0 && <CollectionIndex collection={data.data||{}} {...props}/> }
</div> </div>
{items.length > 0 && ( {items.length > 0 && (
<div className={'collection-item'}> <div className={'collection-item'}>

View File

@ -0,0 +1,20 @@
import React from 'react';
import { PageHeader } from 'antd';
import './style.less';
export function Page(props: any) {
const { children, ...restProps } = props;
return (
<div>
<PageHeader
ghost={false}
{...restProps}
/>
<div className={'page-content'}>
{children}
</div>
</div>
);
};
export default Page;

View File

@ -4,3 +4,4 @@ export { CollectionLoader } from './CollectionLoader';
export { PageLoader as default } from './PageLoader'; export { PageLoader as default } from './PageLoader';
export { SideMenuLayout } from './SideMenuLayout/index'; export { SideMenuLayout } from './SideMenuLayout/index';
export { TopMenuLayout } from './TopMenuLayout/index'; export { TopMenuLayout } from './TopMenuLayout/index';
export { Page } from './Page';

View File

@ -0,0 +1,3 @@
.page-content {
margin: 24px;
}

View File

@ -0,0 +1,39 @@
import React from 'react';
import { Card, Descriptions, Button } from 'antd';
import { Actions } from '@/components/actions';
import api from '@/api-client';
import { useRequest } from 'umi';
import { Spin } from '@nocobase/client';
export function Details(props: any) {
const {
activeTab = {},
pageInfo = {},
schema,
resourceName,
associatedName,
associatedKey,
resourceKey,
} = props;
const { data = {}, loading } = useRequest(() => {
const name = associatedName ? `${associatedName}.${resourceName}` : resourceName;
return api.resource(name).get({
resourceKey,
associatedKey,
});
});
console.log(props);
const { actions = [], fields = [] } = props.schema;
return (
<Card bordered={false}>
<Actions {...props} style={{ marginBottom: 14 }} actions={actions}/>
{loading ? <Spin/> : (
<Descriptions bordered column={1}>
{fields.map((field: any) => (
<Descriptions.Item label={field.title||field.name}>{data[field.name]}</Descriptions.Item>
))}
</Descriptions>
)}
</Card>
);
}

View File

@ -14,15 +14,37 @@ import {
setValidationLanguage, setValidationLanguage,
} from '@formily/antd'; } from '@formily/antd';
import { QuestionCircleOutlined } from '@ant-design/icons'; import { QuestionCircleOutlined } from '@ant-design/icons';
import { useRequest } from 'umi';
import api from '@/api-client';
import { Spin } from '@nocobase/client';
export const DrawerForm = forwardRef((props: any, ref) => { export const DrawerForm = forwardRef((props: any, ref) => {
console.log(props); console.log(props);
const {
activeTab = {},
pageInfo = {},
schema,
resourceName,
associatedName,
associatedKey,
} = props;
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { data, run, loading } = useRequest((resourceKey) => {
const name = associatedName ? `${associatedName}.${resourceName}` : resourceName;
return api.resource(name).get({
resourceKey,
associatedKey,
});
}, {
manual: true,
});
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
setVisible, setVisible,
getData: run,
})); }));
const actions = createAsyncFormActions(); const actions = createAsyncFormActions();
const { title } = props.schema||{}; const { title, fields: properties ={} } = props.schema||{};
console.log({properties});
return ( return (
<Drawer <Drawer
{...props} {...props}
@ -39,37 +61,33 @@ export const DrawerForm = forwardRef((props: any, ref) => {
}}></Button> }}></Button>
]} ]}
> >
<SchemaForm {loading ? <Spin/> : (
colon={true} <SchemaForm
layout={'vertical'} colon={true}
initialValues={{}} layout={'vertical'}
actions={actions} initialValues={data}
schema={{ actions={actions}
type: 'object', schema={{
properties: { type: 'object',
title: { properties,
type: 'string', }}
title: '标题', expressionScope={{
required: true, text(...args: any[]) {
return React.createElement('span', {}, ...args)
}, },
}, tooltip(title: string, offset = 3) {
}} return (
expressionScope={{ <Tooltip title={title}>
text(...args: any[]) { <QuestionCircleOutlined
return React.createElement('span', {}, ...args) style={{ margin: '0 3px', cursor: 'default', marginLeft: offset }}
}, />
tooltip(title: string, offset = 3) { </Tooltip>
return ( );
<Tooltip title={title}> },
<QuestionCircleOutlined }}
style={{ margin: '0 3px', cursor: 'default', marginLeft: offset }} >
/> </SchemaForm>
</Tooltip> )}
);
},
}}
>
</SchemaForm>
</Drawer> </Drawer>
); );
}); });

View File

@ -1,38 +1,11 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { Table as AntdTable, Card } from 'antd'; import { Table as AntdTable, Card } from 'antd';
import { redirectTo } from '@/components/pages/CollectionLoader/utils';
import { Actions } from '@/components/actions'; import { Actions } from '@/components/actions';
import ViewFactory from '@/components/views'; import ViewFactory from '@/components/views';
import { request, useRequest } from 'umi'; import { useRequest } from 'umi';
import api from '@/api-client'; import api from '@/api-client';
import get from 'lodash/get';
const dataSource = []; import { components, fields2columns } from './SortableTable';
for (let i = 0; i < 46; i++) {
dataSource.push({
id: i,
name: `Edward King ${i}`,
age: 32,
address: `London, Park Lane no. ${i}`,
});
}
const columns = [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '年龄',
dataIndex: 'age',
key: 'age',
},
{
title: '住址',
dataIndex: 'address',
key: 'address',
},
];
export interface SimpleTableProps { export interface SimpleTableProps {
schema?: any; schema?: any;
@ -53,23 +26,46 @@ export function SimpleTable(props: SimpleTableProps) {
associatedName, associatedName,
associatedKey, associatedKey,
} = props; } = props;
const { fields, viewCollectionName, rowViewName, actions = [] } = schema; const { rowKey = 'id', fields = [], rowViewName, actions = [] } = schema;
const { sourceKey = 'id' } = activeTab.field||{}; const { sourceKey = 'id' } = activeTab.field||{};
const drawerRef = useRef<any>(); const drawerRef = useRef<any>();
const name = associatedName ? `${associatedName}.${resourceName}` : resourceName; const name = associatedName ? `${associatedName}.${resourceName}` : resourceName;
const { data } = useRequest(() => api.resource(name).list({ const { data, mutate } = useRequest(() => api.resource(name).list({
associatedKey, associatedKey,
})); }));
console.log(activeTab); console.log(activeTab);
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const onChange = (selectedRowKeys: React.ReactText[]) => {
setSelectedRowKeys(selectedRowKeys);
}
const tableProps: any = {};
if (actions.length) {
tableProps.rowSelection = {
selectedRowKeys,
onChange,
}
}
return ( return (
<Card bordered={false}> <Card bordered={false}>
<Actions {...props} style={{ marginBottom: 14 }} actions={actions}/> <Actions {...props} style={{ marginBottom: 14 }} actions={actions}/>
<ViewFactory reference={drawerRef} viewCollectionName={viewCollectionName} viewName={rowViewName}/> <ViewFactory
<AntdTable dataSource={data} onRow={(data) => ({ {...props}
onClick: () => { viewName={rowViewName}
drawerRef.current.setVisible(true); reference={drawerRef}
}, />
})} columns={fields} /> <AntdTable
columns={fields2columns(fields)}
dataSource={data}
rowKey={rowKey}
components={components({data, mutate})}
onRow={(record) => ({
onClick: () => {
drawerRef.current.setVisible(true);
drawerRef.current.getData(record[rowKey]);
}
})}
{...tableProps}
/>
</Card> </Card>
); );
} }

View File

@ -0,0 +1,54 @@
import React from 'react';
// @ts-ignore
import { sortableContainer, sortableElement, sortableHandle } from 'react-sortable-hoc';
import { MenuOutlined } from '@ant-design/icons';
import arrayMove from 'array-move';
import findIndex from 'lodash/findIndex';
import get from 'lodash/get';
import './style.less';
export const SortableItem = sortableElement(props => <tr {...props} />);
export const SortableContainer = sortableContainer(props => <tbody {...props} />);
export const DragHandle = sortableHandle(() => (
<MenuOutlined style={{ cursor: 'pointer', color: '#999' }} />
));
export const components = ({data, mutate}) => {
return {
body: {
wrapper: props => (
<SortableContainer
useDragHandle
helperClass="row-dragging"
onSortEnd={({ oldIndex, newIndex }) => {
if (oldIndex !== newIndex) {
const newData = arrayMove([].concat(data), oldIndex, newIndex).filter(el => !!el);
mutate(newData);
}
}}
{...props}
/>
),
row: ({ className, style, ...restProps }) => {
// function findIndex base on Table rowKey props and should always be a right array index
const index = findIndex(data, (x: any) => x.id === restProps['data-row-key']);
return <SortableItem index={index} {...restProps} />;
},
},
};
};
export function fields2columns(fields) {
const columns: any[] = fields.map(field => {
const type = get(field, 'component.type');
if (type === 'sort') {
field.render = () => <DragHandle/>;
}
return {
...field,
...(field.component||{}),
}
});
return columns;
}

View File

@ -0,0 +1,14 @@
.row-dragging {
background: #fafafa;
border: 1px solid #ccc;
z-index: 99999;
}
.row-dragging td {
padding: 16px;
visibility: hidden;
}
.row-dragging .drag-visible {
visibility: visible;
}

View File

@ -1,9 +1,10 @@
import React from 'react'; import React, { useState } from 'react';
import { Table as AntdTable, Card } from 'antd'; import { Table as AntdTable, Card } from 'antd';
import { redirectTo } from '@/components/pages/CollectionLoader/utils'; import { redirectTo } from '@/components/pages/CollectionLoader/utils';
import { Actions } from '@/components/actions'; import { Actions } from '@/components/actions';
import { request, useRequest } from 'umi'; import { request, useRequest } from 'umi';
import api from '@/api-client'; import api from '@/api-client';
import { components, fields2columns } from './SortableTable';
const columns = [ const columns = [
{ {
@ -40,28 +41,45 @@ export function Table(props: TableProps) {
associatedName, associatedName,
associatedKey, associatedKey,
} = props; } = props;
const { defaultTabId, fields, defaultTabName, rowKey = 'id', actions = [] } = schema; const { fields, defaultTabName, rowKey = 'id', actions = [] } = schema;
const name = associatedName ? `${associatedName}.${resourceName}` : resourceName; const name = associatedName ? `${associatedName}.${resourceName}` : resourceName;
const { data } = useRequest(() => api.resource(name).list({ const { data, mutate } = useRequest(() => api.resource(name).list({
associatedKey, associatedKey,
})); }));
const { sourceKey = 'id' } = activeTab.field||{}; const { sourceKey = 'id' } = activeTab.field||{};
console.log(data); console.log(props);
console.log(activeTab); const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const onChange = (selectedRowKeys: React.ReactText[]) => {
setSelectedRowKeys(selectedRowKeys);
}
const tableProps: any = {};
if (actions.length) {
tableProps.rowSelection = {
selectedRowKeys,
onChange,
}
}
return ( return (
<Card bordered={false}> <Card bordered={false}>
<Actions {...props} style={{ marginBottom: 14 }} actions={actions}/> <Actions {...props} style={{ marginBottom: 14 }} actions={actions}/>
<AntdTable dataSource={data} onRow={(data) => ({ <AntdTable
onClick: () => { dataSource={data}
redirectTo({ rowKey={rowKey}
...props.match.params, columns={fields2columns(fields)}
[activeTab ? 'newItem' : 'lastItem']: { components={components({data, mutate})}
itemId: data[rowKey]||data.id, onRow={(data) => ({
tabName: defaultTabName, onClick: () => {
}, redirectTo({
}); ...props.match.params,
}, [activeTab ? 'newItem' : 'lastItem']: {
})} columns={fields} /> itemId: data[rowKey]||data.id,
tabName: defaultTabName,
},
});
},
})}
{...tableProps}
/>
</Card> </Card>
); );
} }

View File

@ -1,38 +0,0 @@
import React from 'react';
import { Card, Descriptions, Button } from 'antd';
import { Actions } from '@/components/actions';
export function Details(props: any) {
console.log(props);
const { actions = [], fields = [] } = props.schema;
return (
<Card bordered={false}>
<Actions {...props} style={{ marginBottom: 14 }} actions={actions}/>
<Descriptions bordered column={1}>
<Descriptions.Item label="Product">Cloud Database</Descriptions.Item>
<Descriptions.Item label="Billing Mode">Prepaid</Descriptions.Item>
<Descriptions.Item label="Automatic Renewal">YES</Descriptions.Item>
<Descriptions.Item label="Order time">2018-04-24 18:00:00</Descriptions.Item>
<Descriptions.Item label="Usage Time" >
2019-04-24 18:00:00
</Descriptions.Item>
<Descriptions.Item label="Negotiated Amount">$80.00</Descriptions.Item>
<Descriptions.Item label="Discount">$20.00</Descriptions.Item>
<Descriptions.Item label="Official Receipts">$60.00</Descriptions.Item>
<Descriptions.Item label="Config Info">
Data disk type: MongoDB
<br />
Database version: 3.4
<br />
Package: dds.mongo.mid
<br />
Storage space: 10 GB
<br />
Replication factor: 3
<br />
Region: East China 1<br />
</Descriptions.Item>
</Descriptions>
</Card>
);
}

View File

@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import { Form, DrawerForm } from './Form/index';
import { Table } from './Table';
import { Details } from './Details';
import { useRequest, request, Spin } from '@nocobase/client';
import { SimpleTable } from './SimpleTable';
import api from '@/api-client'; import api from '@/api-client';
import { useRequest } from 'umi';
import { Spin } from '@nocobase/client';
import { SimpleTable } from './SimpleTable';
import { Table } from './Table';
import { Form, DrawerForm } from './Form/index';
import { Details } from './Details';
const TEMPLATES = new Map<string, any>(); const TEMPLATES = new Map<string, any>();
@ -24,19 +25,37 @@ registerView('Table', Table);
registerView('SimpleTable', SimpleTable); registerView('SimpleTable', SimpleTable);
registerView('Details', Details); registerView('Details', Details);
export default function ViewFactory(props) { export interface ViewProps {
const { activeTab, viewCollectionName, viewName, reference } = props; resourceName: string;
console.log({viewCollectionName, viewName}); resourceKey?: string | number;
const { data = {}, error, loading, run } = useRequest(() => api.resource(viewCollectionName).getView({ associatedName?: string;
resourceKey: viewName, associatedKey?: string | number;
}), { viewName?: string;
refreshDeps: [viewCollectionName, viewName], [key: string]: any;
}
export default function ViewFactory(props: ViewProps) {
const {
associatedName,
associatedKey,
resourceName,
viewName,
reference,
} = props;
const { data = {}, loading } = useRequest(() => {
const params = {
resourceKey: viewName,
associatedName: associatedName,
};
return api.resource(resourceName).getView(params);
}, {
refreshDeps: [associatedName, resourceName, viewName],
}); });
console.log(activeTab); console.log(data);
if (loading) { if (loading) {
return <Spin/>; return <Spin/>;
} }
const { template } = data.data; const { template } = data;
const Template = getViewTemplate(template); const Template = getViewTemplate(template);
return Template && <Template {...props} ref={reference} schema={data.data}/>; return Template && <Template {...props} ref={reference} schema={data}/>;
} }

View File

@ -1,11 +1,38 @@
import React from 'react'; import React from 'react';
import { Link } from 'umi'; import { Link } from 'umi';
import { Statistic, Card, Row, Col } from 'antd';
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
import { Page } from '@/components/pages';
export default (props: any) => { export default (props: any) => {
return ( return (
<div> <Page title={'页面标题'}>
Page1 <Row gutter={16}>
<Link to={'/page2'}>Page2</Link> <Col span={12}>
</div> <Card>
<Statistic
title="Active"
value={11.28}
precision={2}
valueStyle={{ color: '#3f8600' }}
prefix={<ArrowUpOutlined />}
suffix="%"
/>
</Card>
</Col>
<Col span={12}>
<Card>
<Statistic
title="Idle"
value={9.3}
precision={2}
valueStyle={{ color: '#cf1322' }}
prefix={<ArrowDownOutlined />}
suffix="%"
/>
</Card>
</Col>
</Row>
</Page>
); );
}; };

View File

@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import { Link } from 'umi'; import { Link } from 'umi';
import { Page } from '@/components/pages';
export default (props: any) => { export default (props: any) => {
return ( return (
<div> <Page title={'页面标题'}>
Page2 Page2
<Link to={'/page1'}>Page1</Link> <Link to={'/page1'}>Page1</Link>
</div> </Page>
); );
}; };

View File

@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import { Link } from 'umi'; import { Link } from 'umi';
import { Page } from '@/components/pages';
export default (props: any) => { export default (props: any) => {
return ( return (
<div> <Page title={'页面标题'}>
Page3 Page3
<Link to={'/page1'}>Page1</Link> <Link to={'/page1'}>Page1</Link>
</div> </Page>
); );
}; };

View File

@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import { Link } from 'umi'; import { Link } from 'umi';
import { Page } from '@/components/pages';
export default (props: any) => { export default (props: any) => {
return ( return (
<div> <Page title={'页面标题'}>
Page4 Page4
<Link to={'/page1'}>Page1</Link> <Link to={'/page1'}>Page1</Link>
</div> </Page>
); );
}; };

View File

@ -4,10 +4,24 @@ export default {
name: 'actions', name: 'actions',
title: '操作配置', title: '操作配置',
fields: [ fields: [
{
type: 'integer',
name: 'sort',
title: '排序',
component: {
type: 'sort',
className: 'drag-visible',
width: 60,
},
},
{ {
type: 'string', type: 'string',
name: 'type', name: 'type',
title: '类型', title: '类型',
component: {
type: 'string',
className: 'drag-visible',
},
}, },
{ {
type: 'string', type: 'string',

View File

@ -6,6 +6,16 @@ export default {
title: '数据表配置', title: '数据表配置',
model: CollectionModel, model: CollectionModel,
fields: [ fields: [
{
type: 'integer',
name: 'sort',
title: '排序',
component: {
type: 'sort',
className: 'drag-visible',
width: 60,
},
},
{ {
type: 'string', type: 'string',
name: 'title', name: 'title',
@ -14,6 +24,7 @@ export default {
required: true, required: true,
component: { component: {
type: 'string', type: 'string',
className: 'drag-visible',
}, },
}, },
{ {
@ -48,21 +59,41 @@ export default {
type: 'hasMany', type: 'hasMany',
name: 'fields', name: 'fields',
sourceKey: 'name', sourceKey: 'name',
actions: {
list: {
sort: 'sort',
},
},
}, },
{ {
type: 'hasMany', type: 'hasMany',
name: 'actions', name: 'actions',
sourceKey: 'name', sourceKey: 'name',
actions: {
list: {
sort: 'sort',
},
},
}, },
{ {
type: 'hasMany', type: 'hasMany',
name: 'tabs', name: 'tabs',
sourceKey: 'name', sourceKey: 'name',
actions: {
list: {
sort: 'sort',
},
},
}, },
{ {
type: 'hasMany', type: 'hasMany',
name: 'views', name: 'views',
sourceKey: 'name', sourceKey: 'name',
actions: {
list: {
sort: 'sort',
},
},
}, },
], ],
actions: [ actions: [

View File

@ -4,10 +4,24 @@ export default {
name: 'fields', name: 'fields',
title: '字段配置', title: '字段配置',
fields: [ fields: [
{
type: 'integer',
name: 'sort',
title: '排序',
component: {
type: 'sort',
className: 'drag-visible',
width: 60,
},
},
{ {
type: 'string', type: 'string',
name: 'type', name: 'type',
title: '类型', title: '类型',
component: {
type: 'string',
className: 'drag-visible',
},
}, },
{ {
type: 'string', type: 'string',
@ -19,6 +33,16 @@ export default {
name: 'title', name: 'title',
title: '名称', title: '名称',
}, },
{
type: 'boolean',
name: 'showInListAction',
title: '显示在表格里',
},
{
type: 'boolean',
name: 'showInGetAction',
title: '显示在详情里',
},
{ {
type: 'json', type: 'json',
name: 'options', name: 'options',
@ -26,6 +50,7 @@ export default {
{ {
type: 'belongsTo', type: 'belongsTo',
name: 'collection', name: 'collection',
title: '所属数据表',
target: 'collections', target: 'collections',
targetKey: 'name', targetKey: 'name',
}, },

View File

@ -4,10 +4,24 @@ export default {
name: 'tabs', name: 'tabs',
title: '标签配置', title: '标签配置',
fields: [ fields: [
{
type: 'integer',
name: 'sort',
title: '排序',
component: {
type: 'sort',
className: 'drag-visible',
width: 60,
},
},
{ {
type: 'string', type: 'string',
name: 'type', name: 'type',
title: '类型', title: '类型',
component: {
type: 'string',
className: 'drag-visible',
},
}, },
{ {
type: 'string', type: 'string',

View File

@ -4,10 +4,24 @@ export default {
name: 'views', name: 'views',
title: '视图配置', title: '视图配置',
fields: [ fields: [
{
type: 'integer',
name: 'sort',
title: '排序',
component: {
type: 'sort',
className: 'drag-visible',
width: 60,
},
},
{ {
type: 'string', type: 'string',
name: 'type', name: 'type',
title: '类型', title: '类型',
component: {
type: 'string',
className: 'drag-visible',
},
}, },
{ {
type: 'string', type: 'string',

View File

@ -20,7 +20,9 @@ export default async (ctx, next) => {
}); });
collection.setDataValue('defaultViewId', get(views, [0, 'id'])); collection.setDataValue('defaultViewId', get(views, [0, 'id']));
collection.setDataValue('defaultViewName', get(views, [0, 'name'])); collection.setDataValue('defaultViewName', get(views, [0, 'name']));
const tabs = await collection.getTabs() as Model[]; const tabs = await collection.getTabs({
order: [['sort', 'asc']],
}) as Model[];
const tabItems = []; const tabItems = [];
for (const tab of tabs) { for (const tab of tabs) {
const itemTab = { const itemTab = {

View File

@ -34,7 +34,9 @@ function pages2routes(pages: Array<any>) {
export default async function getRoutes(ctx, next) { export default async function getRoutes(ctx, next) {
const database: Database = ctx.database; const database: Database = ctx.database;
const Page = database.getModel('pages'); const Page = database.getModel('pages');
let pages = await Page.findAll({}); let pages = await Page.findAll({
order: [['sort', 'asc']],
});
const data = flatToTree(pages.map(row => row.toJSON()), { const data = flatToTree(pages.map(row => row.toJSON()), {
id: 'id', id: 'id',
parentId: 'parent_id', parentId: 'parent_id',

View File

@ -15,15 +15,14 @@ const transforms = {
return arr; return arr;
}, },
form: async (fields: Model[]) => { form: async (fields: Model[]) => {
const arr = []; const schema = {};
for (const field of fields) { for (const field of fields) {
arr.push({ schema[field.name] = {
...field.toJSON(), type: 'string',
...field.options, title: field.title||field.name,
dataIndex: field.name, };
});
} }
return arr; return schema;
}, },
details: async (fields: Model[]) => { details: async (fields: Model[]) => {
const arr = []; const arr = [];
@ -50,8 +49,16 @@ export default async (ctx, next) => {
// }, // },
})); }));
const collection = await view.getCollection(); const collection = await view.getCollection();
const fields = await collection.getFields(); const fields = await collection.getFields({
const actions = await collection.getActions(); order: [
['sort', 'asc'],
]
});
const actions = await collection.getActions({
order: [
['sort', 'asc'],
]
});
const actionNames = view.options.actionNames||[]; const actionNames = view.options.actionNames||[];
console.log(view.options); console.log(view.options);
if (view.type === 'table') { if (view.type === 'table') {

View File

@ -6,10 +6,12 @@ export default {
fields: [ fields: [
{ {
type: 'integer', type: 'integer',
name: 'parent_id', name: 'sort',
title: '父级页面', title: '排序',
component: { component: {
type: 'number', type: 'sort',
className: 'drag-visible',
width: 60,
}, },
}, },
{ {
@ -20,6 +22,15 @@ export default {
isMainTitle: true, isMainTitle: true,
component: { component: {
type: 'string', type: 'string',
className: 'drag-visible',
},
},
{
type: 'integer',
name: 'parent_id',
title: '父级页面',
component: {
type: 'number',
}, },
}, },
{ {
@ -140,10 +151,6 @@ export default {
type: 'boolean', type: 'boolean',
}, },
}, },
{
type: 'integer',
name: 'order',
},
{ {
type: 'hasMany', type: 'hasMany',
name: 'children', name: 'children',

View File

@ -7,6 +7,7 @@ export default {
{ {
type: 'string', type: 'string',
name: 'username', name: 'username',
title: '用户名',
}, },
], ],
actions: [ actions: [

View File

@ -44,6 +44,7 @@ export function middleware(options: MiddlewareOptions = {}) {
const field = table.getField(names[0]) as BelongsTo | HasMany | BelongsToMany | HasOne; const field = table.getField(names[0]) as BelongsTo | HasMany | BelongsToMany | HasOne;
if (names.length == 0 || field) { if (names.length == 0 || field) {
let resourceType = 'single'; let resourceType = 'single';
let actions = {};
if (field) { if (field) {
if (field instanceof HasOne) { if (field instanceof HasOne) {
resourceType = 'hasOne'; resourceType = 'hasOne';
@ -54,10 +55,14 @@ export function middleware(options: MiddlewareOptions = {}) {
} else if (field instanceof BelongsToMany) { } else if (field instanceof BelongsToMany) {
resourceType = 'belongsToMany'; resourceType = 'belongsToMany';
} }
if (field.options.actions) {
actions = field.options.actions;
}
} }
resourcer.define({ resourcer.define({
type: resourceType as any, type: resourceType as any,
name: resourceName, name: resourceName,
actions,
}); });
} }
} }