feat: actions & views (#18)
* feat: 视图新增支持 associated 的情况 * fix: 解决关系数据的视图问题 * 表单数据初始化 * feat: sortable table * 排序 * 表格细节调整 * feat: field actions
This commit is contained in:
parent
b0ba1472bb
commit
eb93f0734f
@ -33,12 +33,14 @@
|
||||
"@nocobase/server": "^0.3.0-alpha.0",
|
||||
"@umijs/preset-react": "1.x",
|
||||
"@umijs/test": "^3.2.23",
|
||||
"array-move": "^3.0.1",
|
||||
"concurrently": "^5.3.0",
|
||||
"lint-staged": "^10.0.7",
|
||||
"nodemon": "^2.0.6",
|
||||
"prettier": "^1.19.1",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-sortable-hoc": "^1.11.0",
|
||||
"styled-components": "^5.2.1",
|
||||
"umi": "^3.2.23",
|
||||
"yorkie": "^2.0.0"
|
||||
|
@ -43,7 +43,7 @@ const data = {
|
||||
path: '/',
|
||||
type: 'layout',
|
||||
template: 'TopMenuLayout',
|
||||
order: 1,
|
||||
sort: 10,
|
||||
children: [
|
||||
{
|
||||
title: '仪表盘',
|
||||
@ -51,7 +51,7 @@ const data = {
|
||||
path: '/dashboard',
|
||||
icon: 'dashboard',
|
||||
template: 'page1',
|
||||
order: 2,
|
||||
sort: 20,
|
||||
},
|
||||
{
|
||||
title: '数据',
|
||||
@ -59,7 +59,7 @@ const data = {
|
||||
path: '/collections',
|
||||
icon: 'dashboard',
|
||||
template: 'SideMenuLayout',
|
||||
order: 3,
|
||||
sort: 30,
|
||||
children: [
|
||||
{
|
||||
title: '页面3',
|
||||
@ -67,7 +67,7 @@ const data = {
|
||||
path: '/collections/page3',
|
||||
icon: 'dashboard',
|
||||
template: 'page3',
|
||||
order: 5,
|
||||
sort: 40,
|
||||
},
|
||||
{
|
||||
title: '页面4',
|
||||
@ -75,17 +75,17 @@ const data = {
|
||||
path: '/collections/page4',
|
||||
icon: 'dashboard',
|
||||
template: 'page4',
|
||||
order: 6,
|
||||
},
|
||||
{
|
||||
title: '页面5',
|
||||
type: 'collection',
|
||||
path: '/collections/collection1',
|
||||
icon: 'dashboard',
|
||||
template: 'collection',
|
||||
collection: 'collection1',
|
||||
order: 7,
|
||||
sort: 50,
|
||||
},
|
||||
// {
|
||||
// title: '页面5',
|
||||
// type: 'collection',
|
||||
// path: '/collections/collection1',
|
||||
// icon: 'dashboard',
|
||||
// template: 'collection',
|
||||
// collection: 'collection1',
|
||||
// sort: 60,
|
||||
// },
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -94,7 +94,7 @@ const data = {
|
||||
path: '/users',
|
||||
icon: 'dashboard',
|
||||
template: 'SideMenuLayout',
|
||||
order: 3,
|
||||
sort: 70,
|
||||
children: [
|
||||
{
|
||||
title: '用户管理',
|
||||
@ -103,7 +103,7 @@ const data = {
|
||||
icon: 'dashboard',
|
||||
template: 'collection',
|
||||
collection: 'users',
|
||||
order: 1,
|
||||
sort: 80,
|
||||
},
|
||||
]
|
||||
},
|
||||
@ -113,7 +113,7 @@ const data = {
|
||||
path: '/settings',
|
||||
icon: 'dashboard',
|
||||
template: 'SideMenuLayout',
|
||||
order: 4,
|
||||
sort: 90,
|
||||
children: [
|
||||
{
|
||||
title: '页面与菜单',
|
||||
@ -121,7 +121,7 @@ const data = {
|
||||
collection: 'pages',
|
||||
path: '/settings/pages',
|
||||
icon: 'dashboard',
|
||||
order: 1,
|
||||
sort: 100,
|
||||
},
|
||||
{
|
||||
title: '数据表配置',
|
||||
@ -129,7 +129,7 @@ const data = {
|
||||
collection: 'collections',
|
||||
path: '/settings/collections',
|
||||
icon: 'dashboard',
|
||||
order: 2,
|
||||
sort: 110,
|
||||
},
|
||||
{
|
||||
title: '权限配置',
|
||||
@ -137,7 +137,7 @@ const data = {
|
||||
collection: 'roles',
|
||||
path: '/settings/roles',
|
||||
icon: 'dashboard',
|
||||
order: 3,
|
||||
sort: 120,
|
||||
},
|
||||
]
|
||||
},
|
||||
@ -173,117 +173,121 @@ const data = {
|
||||
// console.log(options);
|
||||
const associations: any = {};
|
||||
if (options.fields) {
|
||||
associations['fields'] = options.fields.map(item => ({
|
||||
associations['fields'] = options.fields.map((item, sort) => ({
|
||||
...item,
|
||||
options: item,
|
||||
sort,
|
||||
}))
|
||||
}
|
||||
if (options.tabs) {
|
||||
associations['tabs'] = options.tabs.map(item => ({
|
||||
associations['tabs'] = options.tabs.map((item, sort) => ({
|
||||
...item,
|
||||
options: item,
|
||||
sort,
|
||||
}))
|
||||
}
|
||||
if (options.actions) {
|
||||
associations['actions'] = options.actions.map(item => ({
|
||||
associations['actions'] = options.actions.map((item, sort) => ({
|
||||
...item,
|
||||
options: item,
|
||||
sort,
|
||||
}))
|
||||
}
|
||||
if (options.views) {
|
||||
associations['views'] = options.views.map(item => ({
|
||||
associations['views'] = options.views.map((item, sort) => ({
|
||||
...item,
|
||||
options: item,
|
||||
sort,
|
||||
}))
|
||||
}
|
||||
await collection.updateAssociations(associations);
|
||||
}
|
||||
|
||||
const actions = await Action.findAll();
|
||||
// const actions = await Action.findAll();
|
||||
|
||||
for (const action of actions) {
|
||||
const viewName = action.options.viewName;
|
||||
console.log({viewName});
|
||||
if (viewName) {
|
||||
const view = await View.findOne({
|
||||
where: {
|
||||
name: viewName,
|
||||
collection_name: action.collection_name
|
||||
},
|
||||
});
|
||||
if (view) {
|
||||
action.options.viewId = view.id;
|
||||
console.log(action.options);
|
||||
action.setDataValue('options', action.options);
|
||||
action.changed('options', true);
|
||||
await action.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
const tabs = await Tab.findAll();
|
||||
// for (const action of actions) {
|
||||
// const viewName = action.options.viewName;
|
||||
// console.log({viewName});
|
||||
// if (viewName) {
|
||||
// const view = await View.findOne({
|
||||
// where: {
|
||||
// name: viewName,
|
||||
// collection_name: action.collection_name
|
||||
// },
|
||||
// });
|
||||
// if (view) {
|
||||
// action.options.viewId = view.id;
|
||||
// console.log(action.options);
|
||||
// action.setDataValue('options', action.options);
|
||||
// action.changed('options', true);
|
||||
// await action.save();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// const tabs = await Tab.findAll();
|
||||
|
||||
for (const tab of tabs) {
|
||||
const viewName = tab.options.viewName;
|
||||
if (!viewName) {
|
||||
continue;
|
||||
}
|
||||
let view: any;
|
||||
if (tab.type === 'association') {
|
||||
view = await View.findOne({
|
||||
where: {
|
||||
name: viewName,
|
||||
collection_name: tab.options.association,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
view = await View.findOne({
|
||||
where: {
|
||||
name: viewName,
|
||||
collection_name: tab.collection_name,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (view) {
|
||||
tab.options.viewId = view.id;
|
||||
tab.setDataValue('options', tab.options);
|
||||
tab.changed('options', true);
|
||||
await tab.save();
|
||||
}
|
||||
}
|
||||
const views = await View.findAll();
|
||||
for (const view of views) {
|
||||
const detailsViewName = view.options.detailsViewName;
|
||||
if (detailsViewName) {
|
||||
const v = await View.findOne({
|
||||
where: {
|
||||
name: detailsViewName,
|
||||
collection_name: view.collection_name
|
||||
},
|
||||
});
|
||||
if (v) {
|
||||
view.options.detailsViewId = v.id;
|
||||
view.setDataValue('options', view.options);
|
||||
view.changed('options', true);
|
||||
await view.save();
|
||||
}
|
||||
}
|
||||
const updateViewName = view.options.updateViewName;
|
||||
if (updateViewName) {
|
||||
const v = await View.findOne({
|
||||
where: {
|
||||
name: updateViewName,
|
||||
collection_name: view.collection_name
|
||||
},
|
||||
});
|
||||
if (v) {
|
||||
view.options.updateViewId = v.id;
|
||||
view.setDataValue('options', view.options);
|
||||
view.changed('options', true);
|
||||
await view.save();
|
||||
}
|
||||
}
|
||||
console.log({detailsViewName, updateViewName});
|
||||
}
|
||||
// for (const tab of tabs) {
|
||||
// const viewName = tab.options.viewName;
|
||||
// if (!viewName) {
|
||||
// continue;
|
||||
// }
|
||||
// let view: any;
|
||||
// if (tab.type === 'association') {
|
||||
// view = await View.findOne({
|
||||
// where: {
|
||||
// name: viewName,
|
||||
// collection_name: tab.options.association,
|
||||
// },
|
||||
// });
|
||||
// } else {
|
||||
// view = await View.findOne({
|
||||
// where: {
|
||||
// name: viewName,
|
||||
// collection_name: tab.collection_name,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// if (view) {
|
||||
// tab.options.viewId = view.id;
|
||||
// tab.setDataValue('options', tab.options);
|
||||
// tab.changed('options', true);
|
||||
// await tab.save();
|
||||
// }
|
||||
// }
|
||||
// const views = await View.findAll();
|
||||
// for (const view of views) {
|
||||
// const detailsViewName = view.options.detailsViewName;
|
||||
// if (detailsViewName) {
|
||||
// const v = await View.findOne({
|
||||
// where: {
|
||||
// name: detailsViewName,
|
||||
// collection_name: view.collection_name
|
||||
// },
|
||||
// });
|
||||
// if (v) {
|
||||
// view.options.detailsViewId = v.id;
|
||||
// view.setDataValue('options', view.options);
|
||||
// view.changed('options', true);
|
||||
// await view.save();
|
||||
// }
|
||||
// }
|
||||
// const updateViewName = view.options.updateViewName;
|
||||
// if (updateViewName) {
|
||||
// const v = await View.findOne({
|
||||
// where: {
|
||||
// name: updateViewName,
|
||||
// collection_name: view.collection_name
|
||||
// },
|
||||
// });
|
||||
// if (v) {
|
||||
// view.options.updateViewId = v.id;
|
||||
// view.setDataValue('options', view.options);
|
||||
// view.changed('options', true);
|
||||
// await view.save();
|
||||
// }
|
||||
// }
|
||||
// console.log({detailsViewName, updateViewName});
|
||||
// }
|
||||
|
||||
// for (let table of tables) {
|
||||
// const options = table.getOptions();
|
||||
|
@ -4,28 +4,29 @@ import ViewFactory from '@/components/views';
|
||||
|
||||
export function Create(props) {
|
||||
console.log(props);
|
||||
const { title, viewCollectionName, viewName } = props.schema;
|
||||
const { title, viewName, collection_name } = props.schema;
|
||||
const { activeTab = {}, item = {} } = props;
|
||||
const { association, collection_name } = activeTab;
|
||||
const { association } = activeTab;
|
||||
|
||||
const params = {};
|
||||
|
||||
if (association) {
|
||||
params['resourceName'] = association;
|
||||
params['associatedName'] = collection_name;
|
||||
params['associatedName'] = activeTab.collection_name;
|
||||
params['associatedKey'] = item.itemId;
|
||||
} else {
|
||||
params['resourceName'] = collection_name;
|
||||
params['resourceKey'] = item.itemId;
|
||||
}
|
||||
|
||||
console.log(params);
|
||||
|
||||
const drawerRef = useRef<any>();
|
||||
return (
|
||||
<>
|
||||
<ViewFactory
|
||||
{...props}
|
||||
reference={drawerRef}
|
||||
viewCollectionName={viewCollectionName}
|
||||
viewName={viewName}
|
||||
{...params}
|
||||
/>
|
||||
|
@ -4,15 +4,15 @@ import ViewFactory from '@/components/views';
|
||||
|
||||
export function Update(props) {
|
||||
console.log(props);
|
||||
const { title, viewCollectionName, viewName } = props.schema;
|
||||
const { title, viewName, resourceName, collection_name } = props.schema;
|
||||
const { activeTab = {}, item = {} } = props;
|
||||
const { association, collection_name } = activeTab;
|
||||
const { association } = activeTab;
|
||||
|
||||
const params = {};
|
||||
|
||||
if (association) {
|
||||
params['resourceName'] = association;
|
||||
params['associatedName'] = collection_name;
|
||||
params['associatedName'] = resourceName;
|
||||
params['associatedKey'] = item.itemId;
|
||||
} else {
|
||||
params['resourceName'] = collection_name;
|
||||
@ -25,12 +25,12 @@ export function Update(props) {
|
||||
<ViewFactory
|
||||
{...props}
|
||||
reference={drawerRef}
|
||||
viewCollectionName={viewCollectionName}
|
||||
viewName={viewName}
|
||||
{...params}
|
||||
/>
|
||||
<Button type={'primary'} onClick={() => {
|
||||
drawerRef.current.setVisible(true);
|
||||
drawerRef.current.getData(item.itemId);
|
||||
}}>{title}</Button>
|
||||
</>
|
||||
)
|
||||
|
@ -29,8 +29,8 @@ export function CollectionIndex(props) {
|
||||
// }
|
||||
/>
|
||||
<div className={'collection-content'}>
|
||||
<ViewFactory {...props}
|
||||
viewCollectionName={collection}
|
||||
<ViewFactory
|
||||
{...props}
|
||||
viewName={viewName||defaultViewName}
|
||||
resourceName={collection}
|
||||
/>
|
||||
|
@ -4,6 +4,7 @@ import { CollectionTabPane } from './CollectionTabPane';
|
||||
import { getPathName, redirectTo } from './utils';
|
||||
import api from '@/api-client';
|
||||
import { useRequest } from 'umi';
|
||||
import { Spin } from '@nocobase/client';
|
||||
|
||||
export function CollectionSingle(props) {
|
||||
console.log(props);
|
||||
@ -11,13 +12,16 @@ export function CollectionSingle(props) {
|
||||
const { tabs = [] } = props.collection;
|
||||
const activeTab = tabs.find(tab => tab.name == item.tabName)||{};
|
||||
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,
|
||||
}));
|
||||
console.log(data);
|
||||
if (!activeTab) {
|
||||
return null;
|
||||
}
|
||||
if (loading) {
|
||||
return <Spin/>;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
|
@ -5,7 +5,7 @@ import { useRequest, request, Spin } from '@nocobase/client';
|
||||
|
||||
export function CollectionTabPane(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 params = {};
|
||||
@ -25,7 +25,6 @@ export function CollectionTabPane(props) {
|
||||
<div>
|
||||
<ViewFactory
|
||||
{...props}
|
||||
viewCollectionName={viewCollectionName}
|
||||
viewName={viewName}
|
||||
{...params}
|
||||
/>
|
||||
|
@ -34,7 +34,7 @@ export function CollectionLoader(props: any) {
|
||||
return (
|
||||
<div className={'collection'}>
|
||||
<div className={'collection-index'}>
|
||||
<CollectionIndex collection={data.data||{}} {...props}/>
|
||||
{ items.length === 0 && <CollectionIndex collection={data.data||{}} {...props}/> }
|
||||
</div>
|
||||
{items.length > 0 && (
|
||||
<div className={'collection-item'}>
|
||||
|
20
packages/app/src/components/pages/Page.tsx
Normal file
20
packages/app/src/components/pages/Page.tsx
Normal 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;
|
@ -4,3 +4,4 @@ export { CollectionLoader } from './CollectionLoader';
|
||||
export { PageLoader as default } from './PageLoader';
|
||||
export { SideMenuLayout } from './SideMenuLayout/index';
|
||||
export { TopMenuLayout } from './TopMenuLayout/index';
|
||||
export { Page } from './Page';
|
||||
|
3
packages/app/src/components/pages/style.less
Normal file
3
packages/app/src/components/pages/style.less
Normal file
@ -0,0 +1,3 @@
|
||||
.page-content {
|
||||
margin: 24px;
|
||||
}
|
39
packages/app/src/components/views/Details.tsx
Normal file
39
packages/app/src/components/views/Details.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -14,15 +14,37 @@ import {
|
||||
setValidationLanguage,
|
||||
} from '@formily/antd';
|
||||
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) => {
|
||||
console.log(props);
|
||||
const {
|
||||
activeTab = {},
|
||||
pageInfo = {},
|
||||
schema,
|
||||
resourceName,
|
||||
associatedName,
|
||||
associatedKey,
|
||||
} = props;
|
||||
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, () => ({
|
||||
setVisible,
|
||||
getData: run,
|
||||
}));
|
||||
const actions = createAsyncFormActions();
|
||||
const { title } = props.schema||{};
|
||||
const { title, fields: properties ={} } = props.schema||{};
|
||||
console.log({properties});
|
||||
return (
|
||||
<Drawer
|
||||
{...props}
|
||||
@ -39,20 +61,15 @@ export const DrawerForm = forwardRef((props: any, ref) => {
|
||||
}}>提交</Button>
|
||||
]}
|
||||
>
|
||||
{loading ? <Spin/> : (
|
||||
<SchemaForm
|
||||
colon={true}
|
||||
layout={'vertical'}
|
||||
initialValues={{}}
|
||||
initialValues={data}
|
||||
actions={actions}
|
||||
schema={{
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
title: '标题',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
properties,
|
||||
}}
|
||||
expressionScope={{
|
||||
text(...args: any[]) {
|
||||
@ -70,6 +87,7 @@ export const DrawerForm = forwardRef((props: any, ref) => {
|
||||
}}
|
||||
>
|
||||
</SchemaForm>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
});
|
||||
|
@ -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 { redirectTo } from '@/components/pages/CollectionLoader/utils';
|
||||
import { Actions } from '@/components/actions';
|
||||
import ViewFactory from '@/components/views';
|
||||
import { request, useRequest } from 'umi';
|
||||
import { useRequest } from 'umi';
|
||||
import api from '@/api-client';
|
||||
|
||||
const dataSource = [];
|
||||
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',
|
||||
},
|
||||
];
|
||||
import get from 'lodash/get';
|
||||
import { components, fields2columns } from './SortableTable';
|
||||
|
||||
export interface SimpleTableProps {
|
||||
schema?: any;
|
||||
@ -53,23 +26,46 @@ export function SimpleTable(props: SimpleTableProps) {
|
||||
associatedName,
|
||||
associatedKey,
|
||||
} = props;
|
||||
const { fields, viewCollectionName, rowViewName, actions = [] } = schema;
|
||||
const { rowKey = 'id', fields = [], rowViewName, actions = [] } = schema;
|
||||
const { sourceKey = 'id' } = activeTab.field||{};
|
||||
const drawerRef = useRef<any>();
|
||||
const name = associatedName ? `${associatedName}.${resourceName}` : resourceName;
|
||||
const { data } = useRequest(() => api.resource(name).list({
|
||||
const { data, mutate } = useRequest(() => api.resource(name).list({
|
||||
associatedKey,
|
||||
}));
|
||||
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 (
|
||||
<Card bordered={false}>
|
||||
<Actions {...props} style={{ marginBottom: 14 }} actions={actions}/>
|
||||
<ViewFactory reference={drawerRef} viewCollectionName={viewCollectionName} viewName={rowViewName}/>
|
||||
<AntdTable dataSource={data} onRow={(data) => ({
|
||||
<ViewFactory
|
||||
{...props}
|
||||
viewName={rowViewName}
|
||||
reference={drawerRef}
|
||||
/>
|
||||
<AntdTable
|
||||
columns={fields2columns(fields)}
|
||||
dataSource={data}
|
||||
rowKey={rowKey}
|
||||
components={components({data, mutate})}
|
||||
onRow={(record) => ({
|
||||
onClick: () => {
|
||||
drawerRef.current.setVisible(true);
|
||||
},
|
||||
})} columns={fields} />
|
||||
drawerRef.current.getData(record[rowKey]);
|
||||
}
|
||||
})}
|
||||
{...tableProps}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
54
packages/app/src/components/views/SortableTable/index.tsx
Normal file
54
packages/app/src/components/views/SortableTable/index.tsx
Normal 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;
|
||||
}
|
14
packages/app/src/components/views/SortableTable/style.less
Normal file
14
packages/app/src/components/views/SortableTable/style.less
Normal 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;
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Table as AntdTable, Card } from 'antd';
|
||||
import { redirectTo } from '@/components/pages/CollectionLoader/utils';
|
||||
import { Actions } from '@/components/actions';
|
||||
import { request, useRequest } from 'umi';
|
||||
import api from '@/api-client';
|
||||
import { components, fields2columns } from './SortableTable';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
@ -40,18 +41,33 @@ export function Table(props: TableProps) {
|
||||
associatedName,
|
||||
associatedKey,
|
||||
} = props;
|
||||
const { defaultTabId, fields, defaultTabName, rowKey = 'id', actions = [] } = schema;
|
||||
const { fields, defaultTabName, rowKey = 'id', actions = [] } = schema;
|
||||
const name = associatedName ? `${associatedName}.${resourceName}` : resourceName;
|
||||
const { data } = useRequest(() => api.resource(name).list({
|
||||
const { data, mutate } = useRequest(() => api.resource(name).list({
|
||||
associatedKey,
|
||||
}));
|
||||
const { sourceKey = 'id' } = activeTab.field||{};
|
||||
console.log(data);
|
||||
console.log(activeTab);
|
||||
console.log(props);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||
const onChange = (selectedRowKeys: React.ReactText[]) => {
|
||||
setSelectedRowKeys(selectedRowKeys);
|
||||
}
|
||||
const tableProps: any = {};
|
||||
if (actions.length) {
|
||||
tableProps.rowSelection = {
|
||||
selectedRowKeys,
|
||||
onChange,
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Card bordered={false}>
|
||||
<Actions {...props} style={{ marginBottom: 14 }} actions={actions}/>
|
||||
<AntdTable dataSource={data} onRow={(data) => ({
|
||||
<AntdTable
|
||||
dataSource={data}
|
||||
rowKey={rowKey}
|
||||
columns={fields2columns(fields)}
|
||||
components={components({data, mutate})}
|
||||
onRow={(data) => ({
|
||||
onClick: () => {
|
||||
redirectTo({
|
||||
...props.match.params,
|
||||
@ -61,7 +77,9 @@ export function Table(props: TableProps) {
|
||||
},
|
||||
});
|
||||
},
|
||||
})} columns={fields} />
|
||||
})}
|
||||
{...tableProps}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
|
||||
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 { 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>();
|
||||
|
||||
@ -24,19 +25,37 @@ registerView('Table', Table);
|
||||
registerView('SimpleTable', SimpleTable);
|
||||
registerView('Details', Details);
|
||||
|
||||
export default function ViewFactory(props) {
|
||||
const { activeTab, viewCollectionName, viewName, reference } = props;
|
||||
console.log({viewCollectionName, viewName});
|
||||
const { data = {}, error, loading, run } = useRequest(() => api.resource(viewCollectionName).getView({
|
||||
export interface ViewProps {
|
||||
resourceName: string;
|
||||
resourceKey?: string | number;
|
||||
associatedName?: string;
|
||||
associatedKey?: string | number;
|
||||
viewName?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export default function ViewFactory(props: ViewProps) {
|
||||
const {
|
||||
associatedName,
|
||||
associatedKey,
|
||||
resourceName,
|
||||
viewName,
|
||||
reference,
|
||||
} = props;
|
||||
const { data = {}, loading } = useRequest(() => {
|
||||
const params = {
|
||||
resourceKey: viewName,
|
||||
}), {
|
||||
refreshDeps: [viewCollectionName, viewName],
|
||||
associatedName: associatedName,
|
||||
};
|
||||
return api.resource(resourceName).getView(params);
|
||||
}, {
|
||||
refreshDeps: [associatedName, resourceName, viewName],
|
||||
});
|
||||
console.log(activeTab);
|
||||
console.log(data);
|
||||
if (loading) {
|
||||
return <Spin/>;
|
||||
}
|
||||
const { template } = data.data;
|
||||
const { template } = data;
|
||||
const Template = getViewTemplate(template);
|
||||
return Template && <Template {...props} ref={reference} schema={data.data}/>;
|
||||
return Template && <Template {...props} ref={reference} schema={data}/>;
|
||||
}
|
||||
|
@ -1,11 +1,38 @@
|
||||
import React from 'react';
|
||||
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) => {
|
||||
return (
|
||||
<div>
|
||||
Page1
|
||||
<Link to={'/page2'}>Page2</Link>
|
||||
</div>
|
||||
<Page title={'页面标题'}>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<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>
|
||||
);
|
||||
};
|
@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'umi';
|
||||
import { Page } from '@/components/pages';
|
||||
|
||||
export default (props: any) => {
|
||||
return (
|
||||
<div>
|
||||
<Page title={'页面标题'}>
|
||||
Page2
|
||||
<Link to={'/page1'}>Page1</Link>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
};
|
@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'umi';
|
||||
import { Page } from '@/components/pages';
|
||||
|
||||
export default (props: any) => {
|
||||
return (
|
||||
<div>
|
||||
<Page title={'页面标题'}>
|
||||
Page3
|
||||
<Link to={'/page1'}>Page1</Link>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
};
|
@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'umi';
|
||||
import { Page } from '@/components/pages';
|
||||
|
||||
export default (props: any) => {
|
||||
return (
|
||||
<div>
|
||||
<Page title={'页面标题'}>
|
||||
Page4
|
||||
<Link to={'/page1'}>Page1</Link>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
};
|
@ -4,10 +4,24 @@ export default {
|
||||
name: 'actions',
|
||||
title: '操作配置',
|
||||
fields: [
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'sort',
|
||||
title: '排序',
|
||||
component: {
|
||||
type: 'sort',
|
||||
className: 'drag-visible',
|
||||
width: 60,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'type',
|
||||
title: '类型',
|
||||
component: {
|
||||
type: 'string',
|
||||
className: 'drag-visible',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
|
@ -6,6 +6,16 @@ export default {
|
||||
title: '数据表配置',
|
||||
model: CollectionModel,
|
||||
fields: [
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'sort',
|
||||
title: '排序',
|
||||
component: {
|
||||
type: 'sort',
|
||||
className: 'drag-visible',
|
||||
width: 60,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
@ -14,6 +24,7 @@ export default {
|
||||
required: true,
|
||||
component: {
|
||||
type: 'string',
|
||||
className: 'drag-visible',
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -48,21 +59,41 @@ export default {
|
||||
type: 'hasMany',
|
||||
name: 'fields',
|
||||
sourceKey: 'name',
|
||||
actions: {
|
||||
list: {
|
||||
sort: 'sort',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'actions',
|
||||
sourceKey: 'name',
|
||||
actions: {
|
||||
list: {
|
||||
sort: 'sort',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'tabs',
|
||||
sourceKey: 'name',
|
||||
actions: {
|
||||
list: {
|
||||
sort: 'sort',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'views',
|
||||
sourceKey: 'name',
|
||||
actions: {
|
||||
list: {
|
||||
sort: 'sort',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
|
@ -4,10 +4,24 @@ export default {
|
||||
name: 'fields',
|
||||
title: '字段配置',
|
||||
fields: [
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'sort',
|
||||
title: '排序',
|
||||
component: {
|
||||
type: 'sort',
|
||||
className: 'drag-visible',
|
||||
width: 60,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'type',
|
||||
title: '类型',
|
||||
component: {
|
||||
type: 'string',
|
||||
className: 'drag-visible',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
@ -19,6 +33,16 @@ export default {
|
||||
name: 'title',
|
||||
title: '名称',
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'showInListAction',
|
||||
title: '显示在表格里',
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'showInGetAction',
|
||||
title: '显示在详情里',
|
||||
},
|
||||
{
|
||||
type: 'json',
|
||||
name: 'options',
|
||||
@ -26,6 +50,7 @@ export default {
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'collection',
|
||||
title: '所属数据表',
|
||||
target: 'collections',
|
||||
targetKey: 'name',
|
||||
},
|
||||
|
@ -4,10 +4,24 @@ export default {
|
||||
name: 'tabs',
|
||||
title: '标签配置',
|
||||
fields: [
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'sort',
|
||||
title: '排序',
|
||||
component: {
|
||||
type: 'sort',
|
||||
className: 'drag-visible',
|
||||
width: 60,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'type',
|
||||
title: '类型',
|
||||
component: {
|
||||
type: 'string',
|
||||
className: 'drag-visible',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
|
@ -4,10 +4,24 @@ export default {
|
||||
name: 'views',
|
||||
title: '视图配置',
|
||||
fields: [
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'sort',
|
||||
title: '排序',
|
||||
component: {
|
||||
type: 'sort',
|
||||
className: 'drag-visible',
|
||||
width: 60,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'type',
|
||||
title: '类型',
|
||||
component: {
|
||||
type: 'string',
|
||||
className: 'drag-visible',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
|
@ -20,7 +20,9 @@ export default async (ctx, next) => {
|
||||
});
|
||||
collection.setDataValue('defaultViewId', get(views, [0, 'id']));
|
||||
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 = [];
|
||||
for (const tab of tabs) {
|
||||
const itemTab = {
|
||||
|
@ -34,7 +34,9 @@ function pages2routes(pages: Array<any>) {
|
||||
export default async function getRoutes(ctx, next) {
|
||||
const database: Database = ctx.database;
|
||||
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()), {
|
||||
id: 'id',
|
||||
parentId: 'parent_id',
|
||||
|
@ -15,15 +15,14 @@ const transforms = {
|
||||
return arr;
|
||||
},
|
||||
form: async (fields: Model[]) => {
|
||||
const arr = [];
|
||||
const schema = {};
|
||||
for (const field of fields) {
|
||||
arr.push({
|
||||
...field.toJSON(),
|
||||
...field.options,
|
||||
dataIndex: field.name,
|
||||
});
|
||||
schema[field.name] = {
|
||||
type: 'string',
|
||||
title: field.title||field.name,
|
||||
};
|
||||
}
|
||||
return arr;
|
||||
return schema;
|
||||
},
|
||||
details: async (fields: Model[]) => {
|
||||
const arr = [];
|
||||
@ -50,8 +49,16 @@ export default async (ctx, next) => {
|
||||
// },
|
||||
}));
|
||||
const collection = await view.getCollection();
|
||||
const fields = await collection.getFields();
|
||||
const actions = await collection.getActions();
|
||||
const fields = await collection.getFields({
|
||||
order: [
|
||||
['sort', 'asc'],
|
||||
]
|
||||
});
|
||||
const actions = await collection.getActions({
|
||||
order: [
|
||||
['sort', 'asc'],
|
||||
]
|
||||
});
|
||||
const actionNames = view.options.actionNames||[];
|
||||
console.log(view.options);
|
||||
if (view.type === 'table') {
|
||||
|
@ -6,10 +6,12 @@ export default {
|
||||
fields: [
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'parent_id',
|
||||
title: '父级页面',
|
||||
name: 'sort',
|
||||
title: '排序',
|
||||
component: {
|
||||
type: 'number',
|
||||
type: 'sort',
|
||||
className: 'drag-visible',
|
||||
width: 60,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -20,6 +22,15 @@ export default {
|
||||
isMainTitle: true,
|
||||
component: {
|
||||
type: 'string',
|
||||
className: 'drag-visible',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'parent_id',
|
||||
title: '父级页面',
|
||||
component: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -140,10 +151,6 @@ export default {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'order',
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'children',
|
||||
|
@ -7,6 +7,7 @@ export default {
|
||||
{
|
||||
type: 'string',
|
||||
name: 'username',
|
||||
title: '用户名',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
|
@ -44,6 +44,7 @@ export function middleware(options: MiddlewareOptions = {}) {
|
||||
const field = table.getField(names[0]) as BelongsTo | HasMany | BelongsToMany | HasOne;
|
||||
if (names.length == 0 || field) {
|
||||
let resourceType = 'single';
|
||||
let actions = {};
|
||||
if (field) {
|
||||
if (field instanceof HasOne) {
|
||||
resourceType = 'hasOne';
|
||||
@ -54,10 +55,14 @@ export function middleware(options: MiddlewareOptions = {}) {
|
||||
} else if (field instanceof BelongsToMany) {
|
||||
resourceType = 'belongsToMany';
|
||||
}
|
||||
if (field.options.actions) {
|
||||
actions = field.options.actions;
|
||||
}
|
||||
}
|
||||
resourcer.define({
|
||||
type: resourceType as any,
|
||||
name: resourceName,
|
||||
actions,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user