This commit is contained in:
chenos 2021-07-27 10:34:52 +08:00
parent 06fbd8c51b
commit 8bfb5cf85c
39 changed files with 656 additions and 391 deletions

View File

@ -23,7 +23,7 @@ jobs:
- run: npm install
- run: npm run bootstrap
- run: echo "API_URL=${{secrets.API_URL}}" >> .env
- run: echo "API_URL=${{ secrets.API_URL }}" >> .env
- run: npm run build-docs
- name: Deploy

View File

@ -43,7 +43,7 @@ const plugins = [
'@nocobase/plugin-ui-schema',
// '@nocobase/plugin-action-logs',
// '@nocobase/plugin-pages',
// '@nocobase/plugin-users',
'@nocobase/plugin-users',
// '@nocobase/plugin-file-manager',
// '@nocobase/plugin-permissions',
// '@nocobase/plugin-automations',

View File

@ -17,6 +17,11 @@ import * as uiSchema from './ui-schema';
// tables: ['collections', 'fields', 'actions', 'views', 'tabs'],
});
const config = require('@nocobase/plugin-users/src/collections/users').default;
const Collection = database.getModel('collections');
const collection = await Collection.create(config);
await collection.updateAssociations(config);
const Route = database.getModel('routes');
const data = [

0
packages/app/src/app.ts Normal file
View File

View File

@ -1,10 +1,8 @@
import 'antd/dist/antd.css'
import 'antd/dist/antd.css';
import { useRequest } from 'ahooks';
import { Spin } from 'antd';
import React, { useMemo } from 'react';
import {
MemoryRouter as Router,
} from 'react-router-dom';
import { MemoryRouter as Router } from 'react-router-dom';
import {
createRouteSwitch,
AdminLayout,
@ -19,8 +17,6 @@ const request = extend({
timeout: 1000,
});
// console.log = () => {}
const RouteSwitch = createRouteSwitch({
components: {
AdminLayout,
@ -35,7 +31,7 @@ const App = () => {
});
if (loading) {
return <Spin/>
return <Spin />;
}
return (

View File

@ -1,8 +1,29 @@
import { SchemaRenderer } from '../../';
import React from 'react';
import { FormItem } from '@formily/antd';
import { useCollectionContext } from '../../schemas';
import { action } from '@formily/reactive';
const useAsyncDataSource = (service: any) => (field: any) => {
field.loading = true;
service(field).then(
action((data: any) => {
field.dataSource = data;
field.loading = false;
}),
);
};
export default () => {
const { data, loading } = useCollectionContext();
const loadCollections = async (field: any) => {
return data.map((item: any) => ({
label: item.title,
value: item.name,
}));
};
const schema = {
type: 'array',
name: 'collections',
@ -33,5 +54,11 @@ export default () => {
},
},
};
return <SchemaRenderer components={{ FormItem }} schema={schema} />;
return (
<SchemaRenderer
scope={{ loadCollections, useAsyncDataSource }}
components={{ FormItem }}
schema={schema}
/>
);
};

View File

@ -29,7 +29,7 @@ import { useRequest } from 'ahooks';
import './style.less';
import { uid } from '@formily/shared';
import { ISchema } from '@formily/react';
import { ISchema, Schema } from '@formily/react';
import Database from './datatable';
import { HighlightOutlined } from '@ant-design/icons';
import { useCookieState } from 'ahooks';
@ -51,14 +51,20 @@ function DesignableToggle() {
);
}
function LayoutWithMenu({ schema }) {
interface LayoutWithMenuProps {
schema: Schema;
[key: string]: any;
}
function LayoutWithMenu(props: LayoutWithMenuProps) {
const { schema, defaultSelectedKeys } = props;
const match = useRouteMatch<any>();
const location = useLocation();
const sideMenuRef = useRef();
const history = useHistory();
const [activeKey, setActiveKey] = useState(match.params.name);
const [, setPageTitle] = usePageTitleContext();
const onSelect = (info) => {
console.log('LayoutWithMenu', info);
if (!info.schema) {
setActiveKey(null);
} else if (info.schema['x-component'] === 'Menu.SubMenu') {
@ -66,12 +72,13 @@ function LayoutWithMenu({ schema }) {
setActiveKey(null);
} else {
setActiveKey(info.schema.key);
history.push(`/admin/${info.schema.key}`);
if (info.schema.title) {
setPageTitle(info.schema.title);
}
}
};
console.log({ match });
return (
<Layout>
<Layout.Header style={{ display: 'flex' }}>
@ -80,7 +87,7 @@ function LayoutWithMenu({ schema }) {
scope={{
sideMenuRef,
onSelect,
selectedKeys: [activeKey].filter(Boolean),
selectedKeys: defaultSelectedKeys.filter(Boolean),
}}
/>
<Database />
@ -124,16 +131,42 @@ export function AdminLayout({ route }: any) {
formatResult: (result) => result?.data,
},
);
const match = useRouteMatch<any>();
if (loading) {
return <Spin />;
}
const findProperties = (schema: Schema): Schema[] => {
if (!schema) {
return [];
}
return schema.reduceProperties((items, current) => {
if (current['key'] == match.params.name) {
return [...items, current];
}
return [...items, ...findProperties(current)];
}, []);
}
const current = findProperties(new Schema(data)).shift();
const defaultSelectedKeys = [current?.name];
let parent = current?.parent;
while(parent) {
if (parent['x-component'] === 'Menu') {
break;
}
defaultSelectedKeys.unshift(parent.name);
parent = parent.parent;
}
console.log('current?.title', current, current?.title, defaultSelectedKeys);
return (
<SwithDesignableContextProvider>
<CollectionContextProvider>
<PageTitleContextProvider>
<LayoutWithMenu schema={data} />
{/* @ts-ignore */}
<PageTitleContextProvider defaultPageTitle={current?.title}>
<LayoutWithMenu defaultSelectedKeys={defaultSelectedKeys} current={current} schema={data} />
</PageTitleContextProvider>
</CollectionContextProvider>
</SwithDesignableContextProvider>

View File

@ -51,7 +51,7 @@ import { CardItem } from '../../schemas/card-item';
import { DragAndDrop } from '../../schemas/drag-and-drop';
import { TreeSelect } from '../../schemas/tree-select';
import { Page } from '../../schemas/page';
import { Chart } from '../../schemas/chart';
// import { Chart } from '../../schemas/chart';
import { useCollectionContext, useSwithDesignableContext } from '../../schemas';
export const BlockContext = createContext({ dragRef: null });
@ -72,7 +72,7 @@ export const SchemaField = createSchemaField({
Div,
Space,
Page,
Chart,
// Chart,
ArrayCollapse,
ArrayTable,

View File

@ -882,8 +882,9 @@ AddNew.PaneItem = observer((props: any) => {
placement={'bottomCenter'}
overlay={
<Menu>
<Menu.SubMenu title={'新建卡片'}>
<Menu.ItemGroup title={'数据区块'}>
<Menu.Item
icon={<IconPicker type={'FileOutlined'} />}
onClick={async () => {
let data: ISchema = {
type: 'void',
@ -927,9 +928,10 @@ AddNew.PaneItem = observer((props: any) => {
}}
style={{ minWidth: 150 }}
>
</Menu.Item>
<Menu.Item
icon={<IconPicker type={'FormOutlined'} />}
onClick={async () => {
let data: ISchema = {
type: 'void',
@ -972,47 +974,50 @@ AddNew.PaneItem = observer((props: any) => {
setVisible(false);
}}
>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu title={'要展示的相关数据'}>
<Menu.Item style={{ minWidth: 150 }}></Menu.Item>
<Menu.Item></Menu.Item>
</Menu.SubMenu>
<Menu.Item
onClick={async () => {
let data: ISchema = {
key: uid(),
type: 'void',
default: '这是一段演示文字,**支持使用 Markdown 语法**',
'x-designable-bar': 'Markdown.Void.DesignableBar',
'x-decorator': 'CardItem',
'x-read-pretty': true,
'x-component': 'Markdown.Void',
};
if (isGridBlock(schema)) {
path.pop();
path.pop();
data = generateGridBlock(data);
} else if (isGrid(schema)) {
data = generateGridBlock(data);
}
if (data) {
let s;
if (isGrid(schema)) {
s = appendChild(data, [...path]);
} else if (defaultAction === 'insertAfter') {
s = insertAfter(data, [...path]);
} else {
s = insertBefore(data, [...path]);
</Menu.ItemGroup>
<Menu.ItemGroup title={'相关数据区块'}>
<Menu.Item style={{ minWidth: 150 }} icon={<IconPicker type={'HistoryOutlined'} />}></Menu.Item>
<Menu.Item icon={<IconPicker type={'CommentOutlined'} />}></Menu.Item>
</Menu.ItemGroup>
<Menu.ItemGroup title={'多媒体区块'}>
<Menu.Item
icon={<IconPicker type={'FileMarkdownOutlined'} />}
onClick={async () => {
let data: ISchema = {
key: uid(),
type: 'void',
default: '这是一段演示文字,**支持使用 Markdown 语法**',
'x-designable-bar': 'Markdown.Void.DesignableBar',
'x-decorator': 'CardItem',
'x-read-pretty': true,
'x-component': 'Markdown.Void',
};
if (isGridBlock(schema)) {
path.pop();
path.pop();
data = generateGridBlock(data);
} else if (isGrid(schema)) {
data = generateGridBlock(data);
}
await createSchema(s);
}
setVisible(false);
}}
>
</Menu.Item>
if (data) {
let s;
if (isGrid(schema)) {
s = appendChild(data, [...path]);
} else if (defaultAction === 'insertAfter') {
s = insertAfter(data, [...path]);
} else {
s = insertBefore(data, [...path]);
}
await createSchema(s);
}
setVisible(false);
}}
>
Markdown
</Menu.Item>
</Menu.ItemGroup>
</Menu>
}
>

View File

@ -1,111 +1,111 @@
import React, { useContext } from 'react';
import {
Column,
ColumnConfig,
Line,
LineConfig,
Pie,
PieConfig,
Bar,
BarConfig,
} from '@ant-design/charts';
import {
connect,
mapProps,
observer,
useField,
useFieldSchema,
mapReadPretty,
} from '@formily/react';
import { Button, Dropdown, Input as AntdInput, Menu, Space } from 'antd';
import { InputProps, TextAreaProps } from 'antd/lib/input';
import { Display } from '../display';
import { LoadingOutlined, MenuOutlined, DragOutlined } from '@ant-design/icons';
import micromark from 'micromark';
import { useDesignable } from '../../components/schema-renderer';
import { useState } from 'react';
import AddNew from '../add-new';
import cls from 'classnames';
import { DraggableBlockContext } from '../../components/drag-and-drop';
import { uid } from '@formily/shared';
import { removeSchema, updateSchema } from '..';
import { isGridRowOrCol } from '../grid';
// import React, { useContext } from 'react';
// import {
// Column,
// ColumnConfig,
// Line,
// LineConfig,
// Pie,
// PieConfig,
// Bar,
// BarConfig,
// } from '@ant-design/charts';
// import {
// connect,
// mapProps,
// observer,
// useField,
// useFieldSchema,
// mapReadPretty,
// } from '@formily/react';
// import { Button, Dropdown, Input as AntdInput, Menu, Space } from 'antd';
// import { InputProps, TextAreaProps } from 'antd/lib/input';
// import { Display } from '../display';
// import { LoadingOutlined, MenuOutlined, DragOutlined } from '@ant-design/icons';
// import micromark from 'micromark';
// import { useDesignable } from '../../components/schema-renderer';
// import { useState } from 'react';
// import AddNew from '../add-new';
// import cls from 'classnames';
// import { DraggableBlockContext } from '../../components/drag-and-drop';
// import { uid } from '@formily/shared';
// import { removeSchema, updateSchema } from '..';
// import { isGridRowOrCol } from '../grid';
export const Chart: any = {};
// export const Chart: any = {};
Chart.Column = observer((props: any) => {
return <Column {...props.config} />;
});
// Chart.Column = observer((props: any) => {
// return <Column {...props.config} />;
// });
Chart.Line = observer((props: any) => {
return <Line {...props.config} />;
});
// Chart.Line = observer((props: any) => {
// return <Line {...props.config} />;
// });
Chart.Pie = observer((props: any) => {
return <Pie {...props.config} />;
});
// Chart.Pie = observer((props: any) => {
// return <Pie {...props.config} />;
// });
Chart.Bar = observer((props: any) => {
return <Bar {...props.config} />;
});
// Chart.Bar = observer((props: any) => {
// return <Bar {...props.config} />;
// });
Chart.DesignableBar = observer((props) => {
const field = useField();
const { designable, schema, refresh, deepRemove } = useDesignable();
const [visible, setVisible] = useState(false);
const { dragRef } = useContext(DraggableBlockContext);
if (!designable) {
return null;
}
return (
<div className={cls('designable-bar', { active: visible })}>
<span
onClick={(e) => {
e.stopPropagation();
}}
className={cls('designable-bar-actions', { active: visible })}
>
<Space size={'small'}>
<AddNew.CardItem defaultAction={'insertAfter'} ghost />
{dragRef && <DragOutlined ref={dragRef} />}
<Dropdown
trigger={['click']}
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
}}
overlay={
<Menu>
<Menu.Item
key={'update'}
onClick={() => {
field.readPretty = false;
setVisible(false);
}}
>
</Menu.Item>
<Menu.Divider />
<Menu.Item
key={'delete'}
onClick={async () => {
const removed = deepRemove();
// console.log({ removed })
const last = removed.pop();
if (isGridRowOrCol(last)) {
await removeSchema(last);
}
}}
>
</Menu.Item>
</Menu>
}
>
<MenuOutlined />
</Dropdown>
</Space>
</span>
</div>
);
});
// Chart.DesignableBar = observer((props) => {
// const field = useField();
// const { designable, schema, refresh, deepRemove } = useDesignable();
// const [visible, setVisible] = useState(false);
// const { dragRef } = useContext(DraggableBlockContext);
// if (!designable) {
// return null;
// }
// return (
// <div className={cls('designable-bar', { active: visible })}>
// <span
// onClick={(e) => {
// e.stopPropagation();
// }}
// className={cls('designable-bar-actions', { active: visible })}
// >
// <Space size={'small'}>
// <AddNew.CardItem defaultAction={'insertAfter'} ghost />
// {dragRef && <DragOutlined ref={dragRef} />}
// <Dropdown
// trigger={['click']}
// visible={visible}
// onVisibleChange={(visible) => {
// setVisible(visible);
// }}
// overlay={
// <Menu>
// <Menu.Item
// key={'update'}
// onClick={() => {
// field.readPretty = false;
// setVisible(false);
// }}
// >
// 修改文本段
// </Menu.Item>
// <Menu.Divider />
// <Menu.Item
// key={'delete'}
// onClick={async () => {
// const removed = deepRemove();
// // console.log({ removed })
// const last = removed.pop();
// if (isGridRowOrCol(last)) {
// await removeSchema(last);
// }
// }}
// >
// 删除当前文本
// </Menu.Item>
// </Menu>
// }
// >
// <MenuOutlined />
// </Dropdown>
// </Space>
// </span>
// </div>
// );
// });

View File

@ -26,7 +26,7 @@ import {
message,
Spin,
} from 'antd';
import { options, interfaces } from './interfaces';
import { options, interfaces, getDefaultFields } from './interfaces';
import {
DeleteOutlined,
DatabaseOutlined,
@ -53,16 +53,21 @@ export const DatabaseCollection = observer((props) => {
const [activeIndex, setActiveIndex] = useState(0);
const form = useForm();
const [newValue, setNewValue] = useState('');
const { refresh } = useCollectionContext();
const { loading, refresh, data } = useCollectionContext();
const { run, loading } = useRequest('collections:findAll', {
formatResult: (result) => result?.data,
onSuccess(data) {
field.setValue(data);
console.log('onSuccess', data);
},
manual: true,
});
useEffect(() => {
field.setValue(data);
console.log('onSuccess', data);
}, [data]);
// const { run, loading } = useRequest('collections:findAll', {
// formatResult: (result) => result?.data,
// onSuccess(data) {
// // field.setValue(data);
// // console.log('onSuccess', data);
// },
// manual: true,
// });
return (
<div>
@ -74,12 +79,12 @@ export const DatabaseCollection = observer((props) => {
}}
onClick={async () => {
setVisible(true);
await run();
// await run();
if (field.value?.length === 0) {
field.push({
name: `t_${uid()}`,
unsaved: true,
fields: [],
fields: getDefaultFields(),
});
}
}}
@ -127,7 +132,7 @@ export const DatabaseCollection = observer((props) => {
const data = {
name: `t_${uid()}`,
title: value,
fields: [],
fields: getDefaultFields(),
};
field.push(data);
setActiveIndex(field.value.length - 1);
@ -160,28 +165,30 @@ export const DatabaseCollection = observer((props) => {
>
{item.title || '未命名'}{' '}
{item.unsaved ? '(未保存)' : ''}
<DeleteOutlined
onClick={async (e) => {
e.stopPropagation();
field.remove(index);
if (field.value?.length === 0) {
field.push({
name: `t_${uid()}`,
unsaved: true,
fields: [],
});
}
if (activeIndex === index) {
setActiveIndex(0);
} else if (activeIndex > index) {
setActiveIndex(activeIndex - 1);
}
if (item.name) {
await deleteCollection(item.name);
await refresh();
}
}}
/>
{item.privilege !== 'undelete' && (
<DeleteOutlined
onClick={async (e) => {
e.stopPropagation();
field.remove(index);
if (field.value?.length === 0) {
field.push({
name: `t_${uid()}`,
unsaved: true,
fields: getDefaultFields(),
});
}
if (activeIndex === index) {
setActiveIndex(0);
} else if (activeIndex > index) {
setActiveIndex(activeIndex - 1);
}
if (item.name) {
await deleteCollection(item.name);
await refresh();
}
}}
/>
)}
</div>
</Select.Option>
);
@ -206,24 +213,20 @@ export const DatabaseCollection = observer((props) => {
} catch (error) {}
}}
>
{loading ? (
<Spin />
) : (
<FormLayout layout={'vertical'}>
<RecursionField
name={activeIndex}
schema={
new Schema({
type: 'object',
properties: schema.properties,
})
}
/>
{/* <FormConsumer>
<FormLayout layout={'vertical'}>
<RecursionField
name={activeIndex}
schema={
new Schema({
type: 'object',
properties: schema.properties,
})
}
/>
{/* <FormConsumer>
{(form) => <pre>{JSON.stringify(form.values, null, 2)}</pre>}
</FormConsumer> */}
</FormLayout>
)}
</FormLayout>
</Modal>
</div>
);
@ -237,7 +240,6 @@ export const DatabaseField: any = observer((props) => {
}
}, []);
const [activeKey, setActiveKey] = useState(null);
console.log('DatabaseField', field);
return (
<div>
<Collapse
@ -249,13 +251,19 @@ export const DatabaseField: any = observer((props) => {
accordion
>
{field.value?.map((item, index) => {
if (!item.interface) {
return;
}
const schema = cloneDeep(interfaces.get(item.interface));
if (!schema) {
console.error('schema invalid');
return;
}
const path = field.address.concat(index);
const errors = field.form.queryFeedbacks({
type: 'error',
address: `*(${path},${path}.*)`,
});
console.log('item.key', item.key);
return (
<Collapse.Panel
header={
@ -263,22 +271,30 @@ export const DatabaseField: any = observer((props) => {
{(item.uiSchema && item.uiSchema.title) || (
<i style={{ color: 'rgba(0, 0, 0, 0.25)' }}></i>
)}{' '}
<Tag>{schema.title}</Tag>
<Tag
className={item.privilege ? cls(item.privilege) : undefined}
>
{schema.title}
</Tag>
<span style={{ color: 'rgba(0, 0, 0, 0.25)', fontSize: 14 }}>
{item.name}
</span>
</>
}
extra={[
<Badge key={'1'} count={errors.length} />,
<DeleteOutlined
key={'2'}
onClick={(e) => {
e.stopPropagation();
field.remove(index);
}}
/>,
]}
extra={
item.privilege === 'undelete'
? []
: [
<Badge key={'1'} count={errors.length} />,
<DeleteOutlined
key={'2'}
onClick={(e) => {
e.stopPropagation();
field.remove(index);
}}
/>,
]
}
key={item.key}
>
<RecursionField
@ -325,6 +341,9 @@ export const DatabaseField: any = observer((props) => {
name: `f_${uid()}`,
interface: info.key,
};
if (schema.initialize) {
schema.initialize(data);
}
field.push(data);
setActiveKey(data.key);
console.log('info.key', field.value);

View File

@ -1,4 +1,5 @@
import { ISchema } from '@formily/react';
import { omit } from 'lodash';
import { defaultProps } from './properties';
export const checkbox: ISchema = {

View File

@ -1,5 +1,5 @@
import { ISchema } from '@formily/react';
import { defaultProps } from './properties';
import { dateTimeProps, defaultProps } from './properties';
export const createdAt: ISchema = {
name: 'createdAt',
@ -13,7 +13,7 @@ export const createdAt: ISchema = {
// name,
uiSchema: {
type: 'datetime',
// title,
title: '创建时间',
'x-component': 'DatePicker',
'x-component-props': {},
'x-read-pretty': true,
@ -23,6 +23,7 @@ export const createdAt: ISchema = {
},
properties: {
...defaultProps,
...dateTimeProps,
},
operations: [
{ label: '等于', value: 'eq' },

View File

@ -14,7 +14,7 @@ export const createdBy: ISchema = {
// name,
uiSchema: {
type: 'object',
// title,
title: '创建人',
'x-component': 'Select.Drawer',
'x-component-props': {},
'x-decorator': 'FormItem',

View File

@ -1,5 +1,5 @@
import { ISchema } from '@formily/react';
import { defaultProps } from './properties';
import { dateTimeProps, defaultProps } from './properties';
export const datetime: ISchema = {
name: 'datetime',
@ -14,13 +14,16 @@ export const datetime: ISchema = {
type: 'datetime',
// title,
'x-component': 'DatePicker',
'x-component-props': {},
'x-component-props': {
showTime: false,
},
'x-decorator': 'FormItem',
'x-designable-bar': 'DatePicker.DesignableBar',
} as ISchema,
},
properties: {
...defaultProps,
...dateTimeProps,
},
operations: [
{ label: '等于', value: 'eq' },

View File

@ -1,12 +1,26 @@
import { ISchema } from '@formily/react';
import { set } from 'lodash';
import { cloneDeep, set } from 'lodash';
import * as types from './types';
import { uid } from '@formily/shared';
export const interfaces = new Map<string, ISchema>();
const fields = {};
const groupLabels = {};
export function getDefaultFields() {
const defaults = ['createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
return defaults.map(key => {
return {
interface: key,
key: uid(),
name: uid(),
privilege: 'undelete',
...cloneDeep(interfaces.get(key)?.default),
}
});
}
export function registerField(group: string, type: string, schema) {
fields[group] = fields[group] || {};
set(fields, [group, type], schema);
@ -25,6 +39,7 @@ Object.keys(types).forEach((type) => {
registerGroupLabel('basic', '基本类型');
registerGroupLabel('choices', '选择类型');
registerGroupLabel('media', '多媒体类型');
registerGroupLabel('datetime', '日期和时间');
registerGroupLabel('relation', '关系类型');
registerGroupLabel('systemInfo', '系统信息');
registerGroupLabel('others', '其他类型');

View File

@ -1,5 +1,6 @@
import { ISchema } from '@formily/react';
import { defaultProps } from './properties';
import { uid } from '@formily/shared';
export const linkTo: ISchema = {
name: 'linkTo',
@ -19,8 +20,41 @@ export const linkTo: ISchema = {
'x-designable-bar': 'Select.Drawer.DesignableBar',
} as ISchema,
},
initialize: (values: any) => {
if (values.dataType === 'belongsToMany') {
if (!values.through) {
values.through = `t_${uid()}`;
}
if (!values.foreignKey) {
values.foreignKey = `f_${uid()}`;
}
if (!values.otherKey) {
values.otherKey = `f_${uid()}`;
}
if (!values.sourceKey) {
values.sourceKey = 'id';
}
if (!values.targetKey) {
values.targetKey = 'id';
}
}
},
properties: {
...defaultProps,
target: {
type: 'string',
title: '要关联的数据表',
required: true,
'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'],
'x-decorator': 'FormItem',
'x-component': 'Select',
},
'uiSchema.x-component-props.multiple': {
type: 'boolean',
'x-content': '允许关联多条记录',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
},
},
operations: [
{ label: '等于', value: 'eq' },

View File

@ -14,7 +14,9 @@ export const multipleSelect: ISchema = {
type: 'array',
// title,
'x-component': 'Select',
'x-component-props': {},
'x-component-props': {
mode: 'multiple',
},
'x-decorator': 'FormItem',
'x-designable-bar': 'Select.DesignableBar',
enum: [],

View File

@ -14,12 +14,31 @@ export const number: ISchema = {
type: 'number',
// title,
'x-component': 'InputNumber',
'x-component-props': {
stringMode: true,
step: '0',
},
'x-decorator': 'FormItem',
'x-designable-bar': 'InputNumber.DesignableBar',
} as ISchema,
},
properties: {
...defaultProps,
'uiSchema.x-component-props.step': {
type: 'string',
title: '精度',
'x-component': 'Select',
'x-decorator': 'FormItem',
default: '0',
enum: [
{ value: '0', label: '1' },
{ value: '0.1', label: '1.0' },
{ value: '0.01', label: '1.00' },
{ value: '0.001', label: '1.000' },
{ value: '0.0001', label: '1.0000' },
{ value: '0.00001', label: '1.00000' },
],
},
},
operations: [
{ label: '等于', value: 'eq' },

View File

@ -14,12 +14,31 @@ export const percent: ISchema = {
type: 'string',
// title,
'x-component': 'InputNumber',
'x-component-props': {
stringMode: true,
step: '0',
},
'x-decorator': 'FormItem',
'x-designable-bar': 'InputNumber.DesignableBar',
} as ISchema,
},
properties: {
...defaultProps
...defaultProps,
'uiSchema.x-component-props.step': {
type: 'string',
title: '精度',
'x-component': 'Select',
'x-decorator': 'FormItem',
default: '0',
enum: [
{ value: '0', label: '1' },
{ value: '0.1', label: '1.0' },
{ value: '0.01', label: '1.00' },
{ value: '0.001', label: '1.000' },
{ value: '0.0001', label: '1.0000' },
{ value: '0.00001', label: '1.00000' },
]
},
},
operations: [
{ label: '等于', value: 'eq' },

View File

@ -1,9 +1,10 @@
import { ISchema } from "@formily/react";
import { ISchema } from '@formily/react';
export const dataType: ISchema = {
type: 'string',
title: '数据类型',
required: true,
'x-disabled': true,
'x-decorator': 'FormItem',
'x-component': 'Select',
enum: [
@ -24,7 +25,61 @@ export const dataType: ISchema = {
{ label: 'BelongsTo', value: 'belongsTo' },
{ label: 'BelongsToMany', value: 'belongsToMany' },
],
}
};
export const dateTimeProps: { [key: string]: ISchema } = {
dateFormat: {
type: 'string',
title: '日期格式',
'x-component': 'Radio.Group',
'x-decorator': 'FormItem',
default: 'YYYY-MM-DD',
enum: [
{
label: '年/月/日',
value: 'YYYY/MM/DD',
},
{
label: '年-月-日',
value: 'YYYY-MM-DD',
},
{
label: '日/月/年',
value: 'DD/MM/YYYY',
},
],
},
showTime: {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '显示时间',
'x-reactions': [
`{{(field) => {
field.query('..[].timeFormat').take(f => {
f.display = field.value ? 'visible' : 'none';
});
}}}`,
],
},
timeFormat: {
type: 'string',
title: '时间格式',
'x-component': 'Radio.Group',
'x-decorator': 'FormItem',
default: 'HH:mm:ss',
enum: [
{
label: '24小时制',
value: 'HH:mm:ss',
},
{
label: '12小时制',
value: 'hh:mm:ss a',
},
],
},
};
export const dataSource: ISchema = {
type: 'array',
@ -53,7 +108,7 @@ export const dataSource: ISchema = {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': { title: '选项值' },
"x-hidden": true,
'x-hidden': true,
properties: {
value: {
type: 'string',

View File

@ -1,5 +1,6 @@
import { ISchema } from '@formily/react';
import { defaultProps } from './properties';
import { uid } from '@formily/shared';
export const subTable: ISchema = {
name: 'subTable',
@ -20,6 +21,14 @@ export const subTable: ISchema = {
enum: [],
} as ISchema,
},
initialize: (values: any) => {
if (!values.target) {
values.target = `t_${uid()}`;
}
if (!values.foreignKey) {
values.foreignKey = `f_${uid()}`;
}
},
properties: {
...defaultProps,
'children': {

View File

@ -20,6 +20,23 @@ export const time: ISchema = {
},
properties: {
...defaultProps,
'uiSchema.x-component-props.format': {
type: 'string',
title: '时间格式',
'x-component': 'Radio.Group',
'x-decorator': 'FormItem',
default: 'HH:mm:ss',
enum: [
{
label: '24小时制',
value: 'HH:mm:ss',
},
{
label: '12小时制',
value: 'hh:mm:ss a',
},
],
},
},
operations: [
{ label: '等于', value: 'eq' },

View File

@ -1,5 +1,5 @@
import { ISchema } from '@formily/react';
import { defaultProps } from './properties';
import { dateTimeProps, defaultProps } from './properties';
export const updatedAt: ISchema = {
name: 'updatedAt',
@ -13,7 +13,7 @@ export const updatedAt: ISchema = {
// name,
uiSchema: {
type: 'datetime',
// title,
title: '最后更新时间',
'x-component': 'DatePicker',
'x-component-props': {},
'x-read-pretty': true,
@ -23,6 +23,7 @@ export const updatedAt: ISchema = {
},
properties: {
...defaultProps,
...dateTimeProps,
},
operations: [
{ label: '等于', value: 'eq' },

View File

@ -14,7 +14,7 @@ export const updatedBy: ISchema = {
// name,
uiSchema: {
type: 'object',
// title,
title: '最后修改人',
'x-component': 'Select.Drawer',
'x-component-props': {},
'x-decorator': 'FormItem',

View File

@ -21,3 +21,8 @@
overflow: auto;
}
}
.ant-tag.disabled {
color: rgba(0, 0, 0, 0.25);
cursor: not-allowed;
}

View File

@ -55,7 +55,7 @@ export const FieldDesignableBar = observer((props) => {
className={cls('designable-bar-actions', { active: visible })}
>
<Space size={'small'}>
<AddNew.CardItem defaultAction={'insertAfter'} ghost />
<AddNew.FormItem defaultAction={'insertAfter'} ghost />
{dragRef && <DragOutlined ref={dragRef} />}
<Dropdown
trigger={['click']}

View File

@ -49,8 +49,8 @@ export function useVisible(name, defaultValue = false) {
export const DesignableBarContext = createContext(null);
const [PageTitleContextProvider, usePageTitleContext] = constate(() => {
return useState(null);
const [PageTitleContextProvider, usePageTitleContext] = constate(({ defaultPageTitle }) => {
return useState(defaultPageTitle);
});
export { PageTitleContextProvider, usePageTitleContext };

View File

@ -72,7 +72,7 @@ import { onFieldChange } from '@formily/core';
export const MenuModeContext = createContext(null);
const SideMenu = (props: any) => {
const { selectedKey, onSelect, path } = props;
const { selectedKey, defaultSelectedKeys, onSelect, path } = props;
const { schema } = useDesignable();
if (!selectedKey) {
return null;
@ -101,11 +101,11 @@ const SideMenu = (props: any) => {
};
export const Menu: any = observer((props: any) => {
const { mode, onSelect, sideMenuRef, ...others } = props;
const { mode, onSelect, sideMenuRef, defaultSelectedKeys = [], ...others } = props;
const { designable, schema } = useDesignable();
const fieldSchema = useFieldSchema();
console.log('Menu.schema', schema, fieldSchema);
const [selectedKey, setSelectedKey] = useState(null);
const [selectedKey, setSelectedKey] = useState(defaultSelectedKeys[0]||null);
const ref = useRef();
const path = useSchemaPath();
const child = schema.properties && schema.properties[selectedKey];
@ -133,6 +133,7 @@ export const Menu: any = observer((props: any) => {
return (
<MenuModeContext.Provider value={mode}>
<AntdMenu
defaultSelectedKeys={defaultSelectedKeys}
{...others}
mode={mode === 'mix' ? 'horizontal' : mode}
onSelect={(info) => {
@ -163,12 +164,13 @@ export const Menu: any = observer((props: any) => {
<SideMenu
path={path}
onSelect={(info) => {
const keyPath = [selectedKey, ...info.keyPath];
const keyPath = [selectedKey, ...[...info.keyPath].reverse()];
const selectedSchema = findPropertyByPath(schema, keyPath);
console.log('keyPath', keyPath, selectedSchema);
onSelect &&
onSelect({ ...info, keyPath, schema: selectedSchema });
}}
defaultSelectedKeys={defaultSelectedKeys||[]}
selectedKey={selectedKey}
sideMenuRef={sideMenuRef}
/>

View File

@ -423,10 +423,11 @@ export abstract class Relation extends Field {
public targetTableInit() {
const { target, fields = [] } = this.options;
if (target && fields.length) {
const children = fields.concat(this.options.children || []);
if (target && children.length) {
this.context.database.table({
name: target,
fields,
fields: children,
});
}
}

View File

@ -7,6 +7,6 @@ export const create = async (ctx: actions.Context, next: actions.Next) => {
await actions.common.create(ctx, async () => {});
const { associated } = ctx.action.params;
await associated.migrate();
console.log('associated.migrate');
// console.log('associated.migrate');
await next();
}

View File

@ -32,7 +32,7 @@ export const createOrUpdate = async (ctx: actions.Context, next: actions.Next) =
await collection.updateAssociations(values);
await collection.migrate();
} catch (error) {
console.log('error.errors', error.errors)
// console.log('error.errors', error.errors)
throw error;
}
ctx.body = collection;

View File

@ -20,6 +20,10 @@ export default {
name: 'title',
required: true,
},
{
type: 'string',
name: 'privilege',
},
{
type: 'json',
name: 'options',
@ -30,5 +34,10 @@ export default {
name: 'fields',
sourceKey: 'name',
},
{
type: 'belongsTo',
name: 'uiSchema',
target: 'ui_schemas',
},
],
} as TableOptions;

View File

@ -31,6 +31,10 @@ export default {
type: 'string',
name: 'dataType',
},
{
type: 'string',
name: 'privilege',
},
{
type: 'hasMany',
name: 'children',
@ -49,7 +53,6 @@ export default {
type: 'belongsTo',
name: 'uiSchema',
target: 'ui_schemas',
defaultValue: {},
},
{
type: 'json',

View File

@ -41,7 +41,9 @@ export class Collection extends Model {
}
async getNestedFields() {
const fields = await this.getFields();
const fields = await this.getFields({
order: [['sort', 'asc']],
});
const items = [];
for (const field of fields) {
items.push(await field.toProps());
@ -58,7 +60,7 @@ export class Collection extends Model {
*/
async loadTableOptions(opts: any = {}) {
const options = await this.toProps();
console.log(JSON.stringify(options, null, 2));
// console.log(JSON.stringify(options, null, 2));
const table = this.database.table(options);
return table;
}

View File

@ -3,7 +3,6 @@ import { Model } from '@nocobase/database';
export class Field extends Model {
static async create(value?: any, options?: any): Promise<any> {
// console.log({ value });
const attributes = this.toAttributes(value);
// @ts-ignore
const model: Model = await super.create(attributes, options);
@ -11,13 +10,16 @@ export class Field extends Model {
}
static toAttributes(value = {}): any {
const data = _.cloneDeep(value);
const data: any = _.cloneDeep(value);
const keys = [
...Object.keys(this.rawAttributes),
...Object.keys(this.associations),
];
if (!data.dataType && data.type) {
data.dataType = data.type;
}
const attrs = _.pick(data, keys);
const options = _.omit(data, keys);
const options = _.omit(data, [...keys, 'type']);
return { ...attrs, options };
}

View File

@ -11,10 +11,86 @@ export default async function (this: Application, options = {}) {
database.import({
directory: path.resolve(__dirname, 'collections'),
});
database.getModel('fields').beforeCreate((model) => {
const [Collection, Field] = database.getModels(['collections', 'fields']);
Field.beforeCreate(async (model) => {
if (!model.get('name')) {
model.set('name', model.get('key'));
}
if (!model.get('collection_name') && model.get('parentKey')) {
const field = await Field.findByPk(model.get('parentKey'));
if (field) {
const { target } = field.get('options') || {};
if (target) {
model.set('collection_name', target);
}
}
}
});
Field.beforeUpdate(async (model) => {
if (!model.get('collection_name') && model.get('parentKey')) {
const field = await Field.findByPk(model.get('parentKey'));
if (field) {
const { target } = field.get('options') || {};
if (target) {
model.set('collection_name', target);
}
}
}
});
Field.afterCreate(async (model) => {
console.log('afterCreate');
if (model.get('interface') !== 'subTable') {
return;
}
const { target } = model.get('options') || {};
// const uiSchemaKey = model.get('ui_schema_key');
// console.log({ uiSchemaKey })
try {
let collection = await Collection.findOne({
where: {
name: target,
},
});
if (!collection) {
collection = await Collection.create({
name: target,
// ui_schema_key: uiSchemaKey,
});
}
// if (model.get('ui_schema_key')) {
// collection.set('ui_schema_key', model.get('ui_schema_key'));
// await collection.save({ hooks: false });
// }
await collection.migrate();
} catch (error) {
throw error;
}
});
Field.afterUpdate(async (model) => {
console.log('afterUpdate');
if (model.get('interface') !== 'subTable') {
return;
}
const { target } = model.get('options') || {};
try {
let collection = await Collection.findOne({
where: {
name: target,
},
});
if (!collection) {
collection = await Collection.create({
name: target,
});
}
// if (model.get('ui_schema_key')) {
// collection.set('ui_schema_key', model.get('ui_schema_key'));
// await collection.save({ hooks: false });
// }
await collection.migrate();
} catch (error) {
throw error;
}
});
this.resourcer.registerActionHandler('collections.fields:create', create);
this.resourcer.registerActionHandler('collections:findAll', findAll);

View File

@ -1,12 +1,12 @@
import _ from 'lodash';
import { Model } from '@nocobase/database';
import deepmerge from 'deepmerge';
import { merge } from '../utils';
export class UISchema extends Model {
static async create(value?: any, options?: any): Promise<any> {
// console.log({ value });
const attributes = this.toAttributes(_.cloneDeep(value));
console.log({ attributes })
// console.log({ attributes })
// @ts-ignore
const model: Model = await super.create(attributes, options);
if (!attributes.children) {
@ -34,7 +34,9 @@ export class UISchema extends Model {
];
const attrs = _.pick(data, keys);
const options = _.omit(data, keys);
return { ...attrs, options: deepmerge(opts, options) };
return {
...attrs, options: merge(opts, options)
};
}
static properties2children(properties = []) {

View File

@ -7,165 +7,67 @@ export default {
// internal: true,
createdBy: false,
updatedBy: false,
privilege: 'undelete',
fields: [
{
interface: 'string',
type: 'string',
name: 'username',
title: '用户名',
unique: true,
required: true,
createOnly: true,
component: {
name: 'nickname',
uiSchema: {
type: 'string',
showInTable: true,
showInDetail: true,
showInForm: true,
title: '昵称',
'x-component': 'Input',
},
},
{
interface: 'email',
type: 'string',
name: 'email',
title: '邮箱',
unique: true,
required: true,
createOnly: true,
component: {
privilege: 'undelete',
uiSchema: {
type: 'string',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'string',
type: 'string',
name: 'nickname',
title: '昵称',
required: true,
component: {
type: 'string',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'phone',
type: 'string',
name: 'phone',
unique: true,
title: '手机号',
component: {
type: 'string',
showInTable: true,
showInDetail: true,
showInForm: true,
title: '邮箱',
'x-component': 'Input',
require: true,
},
},
{
interface: 'password',
type: 'password',
name: 'password',
title: '密码',
hidden: true,
component: {
type: 'password',
showInForm: true,
privilege: 'undelete',
uiSchema: {
type: 'string',
title: '密码',
'x-component': 'Password',
},
},
{
interface: 'string',
interface: 'password',
type: 'string',
name: 'token',
title: 'Token',
unique: true,
hidden: true,
filterable: false,
developerMode: true,
privilege: 'undelete',
uiSchema: {
type: 'string',
title: 'Token',
'x-component': 'Password',
},
},
{
interface: 'string',
interface: 'password',
type: 'string',
name: 'reset_token',
title: 'Reset Token',
unique: true,
hidden: true,
filterable: false,
developerMode: true,
},
],
actions: [
{
type: 'list',
name: 'list',
title: '查看',
},
{
type: 'destroy',
name: 'destroy',
title: '删除',
},
{
type: 'create',
name: 'create',
title: '新增',
viewName: 'form',
},
{
type: 'update',
name: 'update',
title: '编辑',
viewName: 'form',
},
],
views_v2: [
{
developerMode: true,
type: 'table',
name: 'table',
title: '全部数据',
labelField: 'nickname',
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
fields: ['email', 'nickname', 'phone', 'roles'],
detailsOpenMode: 'drawer', // window
details: ['form'],
sort: ['id'],
},
{
developerMode: true,
type: 'descriptions',
name: 'descriptions',
title: '详情',
fields: ['email', 'nickname', 'phone', 'roles'],
actions: [
{
name: 'update',
type: 'update',
title: '编辑',
viewName: 'form',
},
],
},
{
developerMode: true,
type: 'form',
name: 'form',
title: '表单',
fields: ['email', 'nickname', 'phone', 'password', 'roles'],
privilege: 'undelete',
uiSchema: {
type: 'string',
title: 'Reset Token',
'x-component': 'Password',
},
},
],
} as TableOptions;