diff --git a/.github/workflows/vercel.yml b/.github/workflows/vercel.yml index 44e2c53d16..e9d22e2b7c 100644 --- a/.github/workflows/vercel.yml +++ b/.github/workflows/vercel.yml @@ -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 diff --git a/packages/api/src/app.ts b/packages/api/src/app.ts index dedd4e86f4..4f56813818 100644 --- a/packages/api/src/app.ts +++ b/packages/api/src/app.ts @@ -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', diff --git a/packages/api/src/migrations/init.ts b/packages/api/src/migrations/init.ts index 64e596464f..cd1a377db5 100644 --- a/packages/api/src/migrations/init.ts +++ b/packages/api/src/migrations/init.ts @@ -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 = [ diff --git a/packages/app/src/app.ts b/packages/app/src/app.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/app/src/pages/index.tsx b/packages/app/src/pages/index.tsx index 294732e558..3cfbd710ff 100644 --- a/packages/app/src/pages/index.tsx +++ b/packages/app/src/pages/index.tsx @@ -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 + return ; } return ( diff --git a/packages/client/src/components/admin-layout/datatable.tsx b/packages/client/src/components/admin-layout/datatable.tsx index 2ead62ee20..fa1fae5fd3 100644 --- a/packages/client/src/components/admin-layout/datatable.tsx +++ b/packages/client/src/components/admin-layout/datatable.tsx @@ -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 ; + return ( + + ); }; diff --git a/packages/client/src/components/admin-layout/index.tsx b/packages/client/src/components/admin-layout/index.tsx index c320081b13..896cee3e23 100644 --- a/packages/client/src/components/admin-layout/index.tsx +++ b/packages/client/src/components/admin-layout/index.tsx @@ -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(); 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 ( @@ -80,7 +87,7 @@ function LayoutWithMenu({ schema }) { scope={{ sideMenuRef, onSelect, - selectedKeys: [activeKey].filter(Boolean), + selectedKeys: defaultSelectedKeys.filter(Boolean), }} /> @@ -124,16 +131,42 @@ export function AdminLayout({ route }: any) { formatResult: (result) => result?.data, }, ); + const match = useRouteMatch(); if (loading) { return ; } + 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 ( - - + {/* @ts-ignore */} + + diff --git a/packages/client/src/components/schema-renderer/index.tsx b/packages/client/src/components/schema-renderer/index.tsx index de9c67b3f1..a5547afa9a 100644 --- a/packages/client/src/components/schema-renderer/index.tsx +++ b/packages/client/src/components/schema-renderer/index.tsx @@ -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, diff --git a/packages/client/src/schemas/add-new/index.tsx b/packages/client/src/schemas/add-new/index.tsx index 0232c7f272..18b626f0ee 100644 --- a/packages/client/src/schemas/add-new/index.tsx +++ b/packages/client/src/schemas/add-new/index.tsx @@ -882,8 +882,9 @@ AddNew.PaneItem = observer((props: any) => { placement={'bottomCenter'} overlay={ - + } onClick={async () => { let data: ISchema = { type: 'void', @@ -927,9 +928,10 @@ AddNew.PaneItem = observer((props: any) => { }} style={{ minWidth: 150 }} > - 阅读模式 + 详情 } onClick={async () => { let data: ISchema = { type: 'void', @@ -972,47 +974,50 @@ AddNew.PaneItem = observer((props: any) => { setVisible(false); }} > - 编辑模式 + 表单 - - - 日志 - 评论 - - { - 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]); + + + }>日志 + }>评论 + + + } + 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); - }} - > - 添加说明文字 - + 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 + + } > diff --git a/packages/client/src/schemas/chart/index.tsx b/packages/client/src/schemas/chart/index.tsx index 3d91cc8403..72b2be7008 100644 --- a/packages/client/src/schemas/chart/index.tsx +++ b/packages/client/src/schemas/chart/index.tsx @@ -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 ; -}); +// Chart.Column = observer((props: any) => { +// return ; +// }); -Chart.Line = observer((props: any) => { - return ; -}); +// Chart.Line = observer((props: any) => { +// return ; +// }); -Chart.Pie = observer((props: any) => { - return ; -}); +// Chart.Pie = observer((props: any) => { +// return ; +// }); -Chart.Bar = observer((props: any) => { - return ; -}); +// Chart.Bar = observer((props: any) => { +// return ; +// }); -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 ( -
- { - e.stopPropagation(); - }} - className={cls('designable-bar-actions', { active: visible })} - > - - - {dragRef && } - { - setVisible(visible); - }} - overlay={ - - { - field.readPretty = false; - setVisible(false); - }} - > - 修改文本段 - - - { - const removed = deepRemove(); - // console.log({ removed }) - const last = removed.pop(); - if (isGridRowOrCol(last)) { - await removeSchema(last); - } - }} - > - 删除当前文本 - - - } - > - - - - -
- ); -}); \ No newline at end of file +// 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 ( +//
+// { +// e.stopPropagation(); +// }} +// className={cls('designable-bar-actions', { active: visible })} +// > +// +// +// {dragRef && } +// { +// setVisible(visible); +// }} +// overlay={ +// +// { +// field.readPretty = false; +// setVisible(false); +// }} +// > +// 修改文本段 +// +// +// { +// const removed = deepRemove(); +// // console.log({ removed }) +// const last = removed.pop(); +// if (isGridRowOrCol(last)) { +// await removeSchema(last); +// } +// }} +// > +// 删除当前文本 +// +// +// } +// > +// +// +// +// +//
+// ); +// }); \ No newline at end of file diff --git a/packages/client/src/schemas/database-field/index.tsx b/packages/client/src/schemas/database-field/index.tsx index 08395d3a01..6970077ed4 100644 --- a/packages/client/src/schemas/database-field/index.tsx +++ b/packages/client/src/schemas/database-field/index.tsx @@ -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 (
@@ -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 ? '(未保存)' : ''} - { - 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' && ( + { + 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(); + } + }} + /> + )}
); @@ -206,24 +213,20 @@ export const DatabaseCollection = observer((props) => { } catch (error) {} }} > - {loading ? ( - - ) : ( - - - {/* + + + {/* {(form) =>
{JSON.stringify(form.values, null, 2)}
}
*/} -
- )} +
); @@ -237,7 +240,6 @@ export const DatabaseField: any = observer((props) => { } }, []); const [activeKey, setActiveKey] = useState(null); - console.log('DatabaseField', field); return (
{ 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 ( { {(item.uiSchema && item.uiSchema.title) || ( 未命名 )}{' '} - {schema.title} + + {schema.title} + {item.name} } - extra={[ - , - { - e.stopPropagation(); - field.remove(index); - }} - />, - ]} + extra={ + item.privilege === 'undelete' + ? [] + : [ + , + { + e.stopPropagation(); + field.remove(index); + }} + />, + ] + } key={item.key} > { name: `f_${uid()}`, interface: info.key, }; + if (schema.initialize) { + schema.initialize(data); + } field.push(data); setActiveKey(data.key); console.log('info.key', field.value); diff --git a/packages/client/src/schemas/database-field/interfaces/checkbox.ts b/packages/client/src/schemas/database-field/interfaces/checkbox.ts index bc477cf523..396e159283 100644 --- a/packages/client/src/schemas/database-field/interfaces/checkbox.ts +++ b/packages/client/src/schemas/database-field/interfaces/checkbox.ts @@ -1,4 +1,5 @@ import { ISchema } from '@formily/react'; +import { omit } from 'lodash'; import { defaultProps } from './properties'; export const checkbox: ISchema = { diff --git a/packages/client/src/schemas/database-field/interfaces/createdAt.ts b/packages/client/src/schemas/database-field/interfaces/createdAt.ts index 0124cbac17..beea03a6ad 100644 --- a/packages/client/src/schemas/database-field/interfaces/createdAt.ts +++ b/packages/client/src/schemas/database-field/interfaces/createdAt.ts @@ -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' }, diff --git a/packages/client/src/schemas/database-field/interfaces/createdBy.ts b/packages/client/src/schemas/database-field/interfaces/createdBy.ts index 86113c5152..48e86f92c3 100644 --- a/packages/client/src/schemas/database-field/interfaces/createdBy.ts +++ b/packages/client/src/schemas/database-field/interfaces/createdBy.ts @@ -14,7 +14,7 @@ export const createdBy: ISchema = { // name, uiSchema: { type: 'object', - // title, + title: '创建人', 'x-component': 'Select.Drawer', 'x-component-props': {}, 'x-decorator': 'FormItem', diff --git a/packages/client/src/schemas/database-field/interfaces/datetime.ts b/packages/client/src/schemas/database-field/interfaces/datetime.ts index 94c87de91d..d1218e9a5f 100644 --- a/packages/client/src/schemas/database-field/interfaces/datetime.ts +++ b/packages/client/src/schemas/database-field/interfaces/datetime.ts @@ -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' }, diff --git a/packages/client/src/schemas/database-field/interfaces/index.ts b/packages/client/src/schemas/database-field/interfaces/index.ts index 8e1e8d3eda..39273b612f 100644 --- a/packages/client/src/schemas/database-field/interfaces/index.ts +++ b/packages/client/src/schemas/database-field/interfaces/index.ts @@ -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(); 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', '其他类型'); diff --git a/packages/client/src/schemas/database-field/interfaces/linkTo.ts b/packages/client/src/schemas/database-field/interfaces/linkTo.ts index d0a99901bb..b415c4f2a3 100644 --- a/packages/client/src/schemas/database-field/interfaces/linkTo.ts +++ b/packages/client/src/schemas/database-field/interfaces/linkTo.ts @@ -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' }, diff --git a/packages/client/src/schemas/database-field/interfaces/multipleSelect.ts b/packages/client/src/schemas/database-field/interfaces/multipleSelect.ts index 0430ad21b2..dc58c79d99 100644 --- a/packages/client/src/schemas/database-field/interfaces/multipleSelect.ts +++ b/packages/client/src/schemas/database-field/interfaces/multipleSelect.ts @@ -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: [], diff --git a/packages/client/src/schemas/database-field/interfaces/number.ts b/packages/client/src/schemas/database-field/interfaces/number.ts index 3224ed5692..6db0b9b449 100644 --- a/packages/client/src/schemas/database-field/interfaces/number.ts +++ b/packages/client/src/schemas/database-field/interfaces/number.ts @@ -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' }, diff --git a/packages/client/src/schemas/database-field/interfaces/percent.ts b/packages/client/src/schemas/database-field/interfaces/percent.ts index bb55a04b8b..bd82c22b89 100644 --- a/packages/client/src/schemas/database-field/interfaces/percent.ts +++ b/packages/client/src/schemas/database-field/interfaces/percent.ts @@ -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' }, diff --git a/packages/client/src/schemas/database-field/interfaces/properties/index.ts b/packages/client/src/schemas/database-field/interfaces/properties/index.ts index 8399b90697..7990af7d7e 100644 --- a/packages/client/src/schemas/database-field/interfaces/properties/index.ts +++ b/packages/client/src/schemas/database-field/interfaces/properties/index.ts @@ -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', diff --git a/packages/client/src/schemas/database-field/interfaces/subTable.ts b/packages/client/src/schemas/database-field/interfaces/subTable.ts index b85d2bb45c..1a4fc8ace9 100644 --- a/packages/client/src/schemas/database-field/interfaces/subTable.ts +++ b/packages/client/src/schemas/database-field/interfaces/subTable.ts @@ -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': { diff --git a/packages/client/src/schemas/database-field/interfaces/time.ts b/packages/client/src/schemas/database-field/interfaces/time.ts index b6929663a8..7866bdd8b2 100644 --- a/packages/client/src/schemas/database-field/interfaces/time.ts +++ b/packages/client/src/schemas/database-field/interfaces/time.ts @@ -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' }, diff --git a/packages/client/src/schemas/database-field/interfaces/updatedAt.ts b/packages/client/src/schemas/database-field/interfaces/updatedAt.ts index 593011bbc1..909e803ce2 100644 --- a/packages/client/src/schemas/database-field/interfaces/updatedAt.ts +++ b/packages/client/src/schemas/database-field/interfaces/updatedAt.ts @@ -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' }, diff --git a/packages/client/src/schemas/database-field/interfaces/updatedBy.ts b/packages/client/src/schemas/database-field/interfaces/updatedBy.ts index fae2389ac9..066a1a6ca7 100644 --- a/packages/client/src/schemas/database-field/interfaces/updatedBy.ts +++ b/packages/client/src/schemas/database-field/interfaces/updatedBy.ts @@ -14,7 +14,7 @@ export const updatedBy: ISchema = { // name, uiSchema: { type: 'object', - // title, + title: '最后修改人', 'x-component': 'Select.Drawer', 'x-component-props': {}, 'x-decorator': 'FormItem', diff --git a/packages/client/src/schemas/database-field/style.less b/packages/client/src/schemas/database-field/style.less index b8accb89dc..08ad171557 100644 --- a/packages/client/src/schemas/database-field/style.less +++ b/packages/client/src/schemas/database-field/style.less @@ -21,3 +21,8 @@ overflow: auto; } } + +.ant-tag.disabled { + color: rgba(0, 0, 0, 0.25); + cursor: not-allowed; +} \ No newline at end of file diff --git a/packages/client/src/schemas/form/Field.DesignableBar.tsx b/packages/client/src/schemas/form/Field.DesignableBar.tsx index 3bd7edd3e4..2163cade3c 100644 --- a/packages/client/src/schemas/form/Field.DesignableBar.tsx +++ b/packages/client/src/schemas/form/Field.DesignableBar.tsx @@ -55,7 +55,7 @@ export const FieldDesignableBar = observer((props) => { className={cls('designable-bar-actions', { active: visible })} > - + {dragRef && } { - return useState(null); +const [PageTitleContextProvider, usePageTitleContext] = constate(({ defaultPageTitle }) => { + return useState(defaultPageTitle); }); export { PageTitleContextProvider, usePageTitleContext }; diff --git a/packages/client/src/schemas/menu/index.tsx b/packages/client/src/schemas/menu/index.tsx index a409647156..b8171c43d9 100644 --- a/packages/client/src/schemas/menu/index.tsx +++ b/packages/client/src/schemas/menu/index.tsx @@ -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 ( { @@ -163,12 +164,13 @@ export const Menu: any = observer((props: any) => { { - 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} /> diff --git a/packages/database/src/fields/field-types.ts b/packages/database/src/fields/field-types.ts index 8e54f459fd..eb27f4d70a 100644 --- a/packages/database/src/fields/field-types.ts +++ b/packages/database/src/fields/field-types.ts @@ -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, }); } } diff --git a/packages/plugin-collections/src/actions/fields.ts b/packages/plugin-collections/src/actions/fields.ts index c418e8587b..15c0b2c016 100644 --- a/packages/plugin-collections/src/actions/fields.ts +++ b/packages/plugin-collections/src/actions/fields.ts @@ -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(); } diff --git a/packages/plugin-collections/src/actions/index.ts b/packages/plugin-collections/src/actions/index.ts index 9212d91286..c3e0cf845f 100644 --- a/packages/plugin-collections/src/actions/index.ts +++ b/packages/plugin-collections/src/actions/index.ts @@ -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; diff --git a/packages/plugin-collections/src/collections/collections.ts b/packages/plugin-collections/src/collections/collections.ts index 7237430e8f..dcfd98654f 100644 --- a/packages/plugin-collections/src/collections/collections.ts +++ b/packages/plugin-collections/src/collections/collections.ts @@ -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; diff --git a/packages/plugin-collections/src/collections/fields.ts b/packages/plugin-collections/src/collections/fields.ts index 419302ba56..6da0e297c0 100644 --- a/packages/plugin-collections/src/collections/fields.ts +++ b/packages/plugin-collections/src/collections/fields.ts @@ -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', diff --git a/packages/plugin-collections/src/models/collection.ts b/packages/plugin-collections/src/models/collection.ts index 3f3fbf9689..e6c2b50505 100644 --- a/packages/plugin-collections/src/models/collection.ts +++ b/packages/plugin-collections/src/models/collection.ts @@ -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; } diff --git a/packages/plugin-collections/src/models/field.ts b/packages/plugin-collections/src/models/field.ts index f335849f3c..793d24f531 100644 --- a/packages/plugin-collections/src/models/field.ts +++ b/packages/plugin-collections/src/models/field.ts @@ -3,7 +3,6 @@ import { Model } from '@nocobase/database'; export class Field extends Model { static async create(value?: any, options?: any): Promise { - // 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 }; } diff --git a/packages/plugin-collections/src/server.ts b/packages/plugin-collections/src/server.ts index 3cc7f1b577..b20a8f5b58 100644 --- a/packages/plugin-collections/src/server.ts +++ b/packages/plugin-collections/src/server.ts @@ -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); diff --git a/packages/plugin-ui-schema/src/models/ui-schema.ts b/packages/plugin-ui-schema/src/models/ui-schema.ts index 5d9f80b951..0c95ea6312 100644 --- a/packages/plugin-ui-schema/src/models/ui-schema.ts +++ b/packages/plugin-ui-schema/src/models/ui-schema.ts @@ -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 { // 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 = []) { diff --git a/packages/plugin-users/src/collections/users.ts b/packages/plugin-users/src/collections/users.ts index 03419fe36b..63fe72d44e 100644 --- a/packages/plugin-users/src/collections/users.ts +++ b/packages/plugin-users/src/collections/users.ts @@ -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;